/*
 * JBoss, Home of Professional Open Source
 * Copyright 2010 Red Hat Inc. and/or its affiliates and other
 * contributors as indicated by the @author tags. All rights reserved.
 * See the copyright.txt in the distribution for a full listing of
 * individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.infinispan.lucene;

import java.io.IOException;

import org.apache.lucene.store.IndexInput;
import org.infinispan.AdvancedCache;
import org.infinispan.Cache;
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
 */
@SuppressWarnings("unchecked")
final public class InfinispanIndexInput extends IndexInput {

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

   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 int currentBufferSize;
   private byte[] buffer;
   private int bufferPosition;
   private int currentLoadedChunk = -1;

   private boolean isClone;

   public InfinispanIndexInput(final AdvancedCache<?, ?> chunksCache, final FileCacheKey fileKey, final FileMetadata fileMetadata, final SegmentReadLocker readLocks) {
      super(fileKey.getFileName());
      this.chunksCache = (Cache<ChunkCacheKey, Object>) chunksCache;
      this.fileKey = fileKey;
      this.chunkSize = fileMetadata.getBufferSize();
      this.fileLength = fileMetadata.getSize();
      this.readLocks = readLocks;
      this.filename = fileKey.getFileName();
      if (trace) {
         log.tracef("Opened new IndexInput for file:%s in index: %s", filename, fileKey.getIndexName());
      }
   }

   @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);
      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);
      buffer = (byte[]) chunksCache.get(key);
      if (buffer == null) {
         currentLoadedChunk--;
         bufferPosition = chunkSize;
      }
      else {
         currentBufferSize = buffer.length;
      }
   }

   @Override
   public long length() {
      return this.fileLength;
   }
   
   @Override
   public Object 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;
    }

}