/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.util.io;

import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
import com.intellij.util.Processor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.*;
import java.nio.ByteBuffer;

/**
 * Created by Maxim.Mossienko on 1/9/14.
 */
public class AppendableStorageBackedByResizableMappedFile extends ResizeableMappedFile {
  private final MyDataIS myReadStream;
  private byte[] myAppendBuffer;
  private volatile int myFileLength;
  private volatile int myBufferPosition;
  private static final int ourAppendBufferLength = 4096;

  public AppendableStorageBackedByResizableMappedFile(final File file,
                                                      int initialSize,
                                                      @Nullable PagedFileStorage.StorageLockContext lockContext,
                                                      int pageSize,
                                                      boolean valuesAreBufferAligned) throws IOException {
    super(file, initialSize, lockContext, pageSize, valuesAreBufferAligned);
    myReadStream = new MyDataIS(this);
    myFileLength = (int)length();
    myCompressedAppendableFile = /*CompressedAppendableFile.ENABLED &&*/ false ? new MyCompressedAppendableFile(file) :null;
  }

  private void flushKeyStoreBuffer() {
    if (myBufferPosition > 0) {
      put(myFileLength, myAppendBuffer, 0, myBufferPosition);
      myFileLength += myBufferPosition;
      myBufferPosition = 0;
    }
  }

  @Override
  public void force() {
    flushKeyStoreBuffer();
    if (myCompressedAppendableFile != null) myCompressedAppendableFile.force();
    super.force();
  }

  @Override
  public void close() {
    flushKeyStoreBuffer();
    if (myCompressedAppendableFile != null) myCompressedAppendableFile.dispose();
    super.close();
  }

  private final CompressedAppendableFile myCompressedAppendableFile;
  private final boolean testMode = false;

  public <Data> Data read(final int addr, KeyDescriptor<Data> descriptor) throws IOException {
    Data tempData = null;

    if (myCompressedAppendableFile != null) {
      tempData = myCompressedAppendableFile.read(addr, descriptor);
      if (!testMode) return tempData;
    }

    if (myFileLength <= addr) {
      Data data =
        descriptor.read(new DataInputStream(new UnsyncByteArrayInputStream(myAppendBuffer, addr - myFileLength, myBufferPosition)));
      if (tempData != null && !descriptor.isEqual(data, tempData)) {
        assert false;
      }
      return data;
    }
    // we do not need to flushKeyBuffer since we store complete records
    myReadStream.setup(addr, myFileLength);
    Data data = descriptor.read(myReadStream);
    if (tempData != null && !descriptor.isEqual(data, tempData)) {
      assert false;
    }
    return data;
  }

  public <Data> boolean processAll(Processor<Data> processor, KeyDescriptor<Data> descriptor) throws IOException {
    assert !isDirty();
    DataInputStream keysStream2 = myCompressedAppendableFile != null ?myCompressedAppendableFile.getStream(0) : null;
    if (!testMode && keysStream2 != null) {
      getPagedFileStorage().lock(); // todo support it inside myCompressedAppendableFile to avoid filling the cache
      try {
        try {
          while (true) {
            Data key = descriptor.read(keysStream2);
            if (!processor.process(key)) return false;
          }
        }
        catch (EOFException e) {
          // Done
        }
        return true;
      }
      finally {
        getPagedFileStorage().unlock();
        keysStream2.close();
      }
    }

    DataInputStream keysStream = new DataInputStream(new BufferedInputStream(new LimitedInputStream(new FileInputStream(getPagedFileStorage().getFile()),
                                                                                                    myFileLength) {
      @Override
      public int available() throws IOException {
        return remainingLimit();
      }
    }, 32768));

    try {
      try {
        while (true) {
          Data key = descriptor.read(keysStream);
          if (keysStream2 != null) {
            Data tempKey = descriptor.read(keysStream2);
            if (!descriptor.isEqual(key, tempKey)) {
              assert false;
            }
          }
          if (!processor.process(key)) return false;
        }
      }
      catch (EOFException e) {
        // Done
      }
      return true;
    }
    finally {
      keysStream.close();
    }
  }

  public int getCurrentLength() {
    int currentLength;
    if (myCompressedAppendableFile != null) {
      currentLength = (int)myCompressedAppendableFile.length();
      if (testMode) {
        if (currentLength != myBufferPosition + myFileLength) {
          assert false;
        }
      }
    } else {
      currentLength = myBufferPosition + myFileLength;
    }

    return currentLength;
  }

  public <Data> int append(Data value, KeyDescriptor<Data> descriptor) throws IOException {
    final BufferExposingByteArrayOutputStream bos = new BufferExposingByteArrayOutputStream();
    DataOutput out = new DataOutputStream(bos);
    descriptor.save(out, value);
    final int size = bos.size();
    final byte[] buffer = bos.getInternalBuffer();

    int currentLength = getCurrentLength();

    if (myCompressedAppendableFile != null) {
      //myCompressedAppendableFile.append(value, descriptor);
      myCompressedAppendableFile.append(buffer, size);
      if (!testMode) return currentLength;
    }

    if (size > ourAppendBufferLength) {
      flushKeyStoreBuffer();
      put(currentLength, buffer, 0, size);
      myFileLength += size;
    }
    else {
      if (size > ourAppendBufferLength - myBufferPosition) {
        flushKeyStoreBuffer();
      }
      // myAppendBuffer will contain complete records
      if (myAppendBuffer == null) {
        myAppendBuffer = new byte[ourAppendBufferLength];
      }
      System.arraycopy(buffer, 0, myAppendBuffer, myBufferPosition, size);
      myBufferPosition += size;
    }
    return currentLength;
  }

  public <Data> boolean checkBytesAreTheSame(final int addr, Data value, KeyDescriptor<Data> descriptor) throws IOException {
    final DataInputStream compressedStream;
    final boolean[] sameValue = new boolean[1];
    OutputStream comparer;

    if (myCompressedAppendableFile != null) {
      compressedStream = myCompressedAppendableFile.getStream(addr);

      comparer = new OutputStream() {
        boolean same = true;

        @Override
        public void write(int b) throws IOException {
          if (same) {
            same = compressedStream.readByte() == (byte)b;
          }
        }

        @Override
        public void close() {
          sameValue[0] = same;
        }
      };
    } else {
      comparer = buildOldComparerStream(addr, sameValue);
    }

    DataOutput out = new DataOutputStream(comparer);
    descriptor.save(out, value);
    comparer.close();

    if (testMode) {
      final boolean[] sameValue2 = new boolean[1];
      OutputStream comparer2 = buildOldComparerStream(addr, sameValue2);
      out = new DataOutputStream(comparer2);
      descriptor.save(out, value);
      comparer2.close();
      if (sameValue[0] != sameValue2[0]) {
        assert false;
      }
    }
    return sameValue[0];
  }

  @NotNull
  private OutputStream buildOldComparerStream(final int addr, final boolean[] sameValue) {
    OutputStream comparer;
    final PagedFileStorage storage = getPagedFileStorage();

    if (myFileLength <= addr) {
      //noinspection IOResourceOpenedButNotSafelyClosed
      comparer = new OutputStream() {
        int address = addr - myFileLength;
        boolean same = true;
        @Override
        public void write(int b) {
          if (same) {
            same = address < myBufferPosition && myAppendBuffer[address++] == (byte)b;
          }
        }
        @Override
        public void close() {
          sameValue[0]  = same;
        }
      };
    }
    else {
      //noinspection IOResourceOpenedButNotSafelyClosed
      comparer = new OutputStream() {
        int base = addr;
        int address = storage.getOffsetInPage(addr);
        boolean same = true;
        ByteBuffer buffer = storage.getByteBuffer(addr, false).getCachedBuffer();
        final int myPageSize = storage.myPageSize;

        @Override
        public void write(int b) {
          if (same) {
            if (myPageSize == address && address < myFileLength) {    // reached end of current byte buffer
              base += address;
              buffer = storage.getByteBuffer(base, false).getCachedBuffer();
              address = 0;
            }
            same = address < myFileLength && buffer.get(address++) == (byte)b;
          }
        }

        @Override
        public void close() {
          sameValue[0]  = same;
        }
      };

    }
    return comparer;
  }

  private static class MyDataIS extends DataInputStream {
    private MyDataIS(ResizeableMappedFile raf) {
      super(new MyBufferedIS(new MappedFileInputStream(raf, 0, 0)));
    }

    public void setup(long pos, long limit) {
      ((MyBufferedIS)in).setup(pos, limit);
    }
  }

  private static class MyBufferedIS extends BufferedInputStream {
    public MyBufferedIS(final InputStream in) {
      super(in, 512);
    }

    public void setup(long pos, long limit) {
      this.pos = 0;
      count = 0;
      ((MappedFileInputStream)in).setup(pos, limit);
    }
  }

  private class MyCompressedAppendableFile extends CompressedAppendableFile {
    private final File myFile;
    private DataOutputStream myChunkLengthTableStream;

    public MyCompressedAppendableFile(File file) {
      super(AppendableStorageBackedByResizableMappedFile.this.getPagedFileStorage().getFile());
      myFile = file;
    }

    @NotNull
    @Override
    protected InputStream getChunkInputStream(File appendFile, long offset, int pageSize) throws IOException {
      byte[] buf = new byte[pageSize];
      get(offset, buf, 0, pageSize);

      return new ByteArrayInputStream(buf);
    }

    @Override
    protected void saveChunk(BufferExposingByteArrayOutputStream compressedChunk, long endOfFileOffset) throws IOException {
      AppendableStorageBackedByResizableMappedFile.super
        .put(endOfFileOffset, compressedChunk.getInternalBuffer(), 0, compressedChunk.size());

      if (myChunkLengthTableStream == null) {
        myChunkLengthTableStream =
          new DataOutputStream(new BufferedOutputStream(new FileOutputStream(getChunkLengthFile(), true)));
      }

      DataInputOutputUtil.writeINT(myChunkLengthTableStream, compressedChunk.size());
    }

    @Override
    public synchronized void force() {
      super.force();

      try {
        if (myChunkLengthTableStream != null) myChunkLengthTableStream.flush();
      } catch (IOException ignore) {}
    }

    @Override
    public synchronized void dispose() {
      super.dispose();
      if (myChunkLengthTableStream != null)  {
        try {
          myChunkLengthTableStream.close();
        } catch (IOException ignore) {}
      }
    }

    @NotNull
    @Override
    protected File getChunksFile() {
      return myFile;
    }
  }
}