package org.infinispan.lucene.impl;

import java.io.IOException;

import org.apache.lucene.store.IndexInput;
import org.infinispan.Cache;
import org.infinispan.lucene.ChunkCacheKey;
import org.infinispan.lucene.FileCacheKey;
import org.infinispan.lucene.readlocks.SegmentReadLocker;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

/**
 * Responsible for reading from <code>InfinispanDirectory</code>
 *
 * @since 4.0
 * @author Sanne Grinovero
 * @author Davide Di Somma
 * @see org.apache.lucene.store.Directory
 * @see org.apache.lucene.store.IndexInput
 */
public class InfinispanIndexInput extends IndexInput {


   private static final Log log = LogFactory.getLog(InfinispanIndexInput.class);
   private static final boolean trace = log.isTraceEnabled();

   protected boolean isClone;

   private final Cache<ChunkCacheKey, Object> chunksCache;
   private final FileCacheKey fileKey;
   private final int chunkSize;
   private final SegmentReadLocker readLocks;
   private final String filename;
   private final long fileLength;
   private final int affinitySegmentId;

   private int currentBufferSize;
   private byte[] buffer;
   private int bufferPosition;
   private int currentLoadedChunk = -1;

   public InfinispanIndexInput(final IndexInputContext ctx) {
      super(ctx.fileKey.getFileName());
      this.chunksCache = ctx.chunksCache;
      this.fileKey = ctx.fileKey;
      this.chunkSize = ctx.fileMetadata.getBufferSize();
      this.fileLength = ctx.fileMetadata.getSize();
      this.readLocks = ctx.readLocks;
      this.affinitySegmentId = ctx.affinitySegmentId;
      this.filename = fileKey.getFileName();
      if (trace) {
         log.tracef("Opened new IndexInput for file:%s in index: %s", filename, fileKey.getIndexName());
      }
   }

   private InfinispanIndexInput(final String resourceDescription, final Cache<ChunkCacheKey, Object> chunksCache,
         FileCacheKey fileKey, int chunkSize, String filename, long fileLength, int affinitySegmentId) {
      super(resourceDescription);
      this.chunksCache = chunksCache;
      this.fileKey = fileKey;
      this.chunkSize = chunkSize;
      this.filename = filename;
      this.fileLength = fileLength;
      this.readLocks = null;//Lifecycle of this IndexInput is dependent on a parent IndexInput
      this.affinitySegmentId = affinitySegmentId;
      this.isClone = true;
   }

   @Override
   public final byte readByte() throws IOException {
      if (bufferPosition >= currentBufferSize) {
         nextChunk();
         bufferPosition = 0;
      }
      return buffer[bufferPosition++];
    }

   @Override
   public final void readBytes(final byte[] b, int offset, int bytesToRead) throws IOException {
      if (buffer == null) {
         nextChunk();
      }
      while (bytesToRead > 0) {
         int bytesToCopy = Math.min(currentBufferSize - bufferPosition, bytesToRead);
         System.arraycopy(buffer, bufferPosition, b, offset, bytesToCopy);
         offset += bytesToCopy;
         bytesToRead -= bytesToCopy;
         bufferPosition += bytesToCopy;
         if (bufferPosition >= currentBufferSize && bytesToRead > 0) {
            nextChunk();
            bufferPosition = 0;
         }
      }
   }

   @Override
   public void close() {
      currentBufferSize = 0;
      bufferPosition = 0;
      currentLoadedChunk = -1;
      buffer = null;
      if (isClone) return;
      readLocks.deleteOrReleaseReadLock(filename);
      if (trace) {
         log.tracef("Closed IndexInput for file:%s in index: %s", filename, fileKey.getIndexName());
      }
   }

   @Override
   public long getFilePointer() {
      return ((long) currentLoadedChunk) * chunkSize + bufferPosition;
   }

   @Override
   public void seek(final long pos) {
      bufferPosition = (int) (pos % chunkSize);
      final int targetChunk = (int) (pos / chunkSize);
      if (targetChunk != currentLoadedChunk) {
         currentLoadedChunk = targetChunk;
         setBufferToCurrentChunkIfPossible();
      }
   }

   private void nextChunk() throws IOException {
      currentLoadedChunk++;
      setBufferToCurrentChunk();
   }

   private void setBufferToCurrentChunk() throws IOException {
      ChunkCacheKey key = new ChunkCacheKey(fileKey.getIndexName(), filename, currentLoadedChunk, chunkSize, affinitySegmentId);
      buffer = (byte[]) chunksCache.get(key);
      if (buffer == null) {
         throw new IOException("Read past EOF: Chunk value could not be found for key " + key);
      }
      currentBufferSize = buffer.length;
   }

   // Lucene might try seek(pos) using an illegal pos value
   // RAMDirectory teaches to position the cursor to the end of previous chunk in this case
   private void setBufferToCurrentChunkIfPossible() {
      ChunkCacheKey key = new ChunkCacheKey(fileKey.getIndexName(), filename, currentLoadedChunk, chunkSize, affinitySegmentId);
      buffer = (byte[]) chunksCache.get(key);
      if (buffer == null) {
         currentLoadedChunk--;
         bufferPosition = chunkSize;
      }
      else {
         currentBufferSize = buffer.length;
      }
   }

   @Override
   public long length() {
      return this.fileLength;
   }

   @Override
   public InfinispanIndexInput clone() {
      InfinispanIndexInput clone = (InfinispanIndexInput)super.clone();
      // reference counting doesn't work properly: need to use isClone
      // as in other Directory implementations. Apparently not all clones
      // are cleaned up, but the original is (especially .tis files)
      clone.isClone = true;
      return clone;
   }

   public IndexInput slice(String sliceDescription, long offset, long length) throws IOException {
      return new SlicingInfinispanIndexInput(sliceDescription, offset, length, copyAndReset());
   }

   InfinispanIndexInput copyAndReset() {
      return new InfinispanIndexInput(filename, chunksCache, fileKey, chunkSize, filename, fileLength, affinitySegmentId);
   }

}
