/*
 * Copyright (C) 2007-2009 KenD00
 * 
 * This file is part of DumpHD.
 * 
 * DumpHD is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package dumphd.util;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.LinkedList;

/**
 * Class for reading and writing from files with automatic buffering. The read and write methodes can be used to read / write various amounts of data
 * without caring about buffering. The read, write and seek methods are all buffered.
 * 
 * This class supports ByteSource.R_MODE and ByteSource.RW_MODE.
 * 
 * TODO: More checks if FileSource is closed?
 * 
 * @author KenD00
 */
public final class FileSource implements ByteSource {

   /**
    * The size of the used buffer
    */
   public static final int bufferSize = 32 * 1024;

   /**
    * The File object this FileSource uses
    */
   private File file = null;
   /**
    * The file to use
    */
   private RandomAccessFile raf = null;
   /**
    * The FileChannel from the used file 
    */
   private FileChannel fc = null;
   /**
    * The ByteBuffer used by the FileChannel
    */
   private ByteBuffer bb = null;
   /**
    * The mode of this FileSource
    */
   private int mode = 0;
   /**
    * The list of the current windows
    */
   private LinkedList<Window> windows = new LinkedList<Window>();
   /**
    * Reference to the current active window
    */
   private Window currentWindow = null;
   /**
    * "Physical" offset of the buffer, that is position 0 of the buffer corresponds to bufferOffset in the file
    */
   private long bufferOffset = 0;
   /**
    * If true, the buffer's limit is currently also the window limit, if false, the buffer is just full when its limit is reached
    */
   private boolean bufferLimited = false;
   /**
    * The maximum position that was reached currently in the buffer
    * It is always true that 0 <= maxPos <= bb.limit() <= bb.capacity()
    */
   private int maxPos = 0;
   /**
    * If true, there is data in the buffer that has not been written to disk yet
    */
   private boolean pendingWrite = false;


   /**
    * Creates a new FileSource from the given file in the specified mode.
    * 
    * @param source The file to use
    * @param mode The mode to use
    * @throws FileNotFoundException If the file was not found
    */
   public FileSource(File source, int mode) throws FileNotFoundException {
      this.file = source;
      this.mode = mode;
      switch (mode) {
      case R_MODE:
         raf = new RandomAccessFile(source, "r");
         break;
      case RW_MODE:
         raf = new RandomAccessFile(source, "rw");
         break;
      default:
         throw new IllegalArgumentException("Invalid mode argument");
      }
      fc = raf.getChannel();
      bb = ByteBuffer.allocateDirect(bufferSize);
      currentWindow = new Window();
      windows.add(currentWindow);
   }

   /* (non-Javadoc)
    * @see dumphd.core.DataSource#read(byte[], int, int)
    */
   public int read(byte[] dst, int offset, int length) throws IOException {
      // Counts the number of returned bytes
      int transferred = 0;
      while (true) {
         int currentLimit = bb.limit();
         if (maxPos < currentLimit) {
            currentLimit = maxPos;
         }
         // The number of bytes remaining in the buffer for reading
         int remaining = currentLimit - bb.position();
         //System.out.println(file.getName() + " FileSource read: " + length + ", remaining: " + remaining);
         if (length <= remaining) {
            // Logical read, enough data in buffer to serve request
            //System.out.println(file.getName() + " Doing logical read , transferred: " + transferred + ", bufferOffset: " + bufferOffset + ", bufferPos: " + bb.position() + ", limit: " + bb.limit());
            bb.get(dst, offset, length);
            transferred += length;
            break;
         } else {
            // Mixed read, deliver rest of buffer, if any, fill it with a physical read
            if (remaining > 0) {
               //System.out.println(file.getName() + " Doing mixed read , transferred: " + transferred + ", bufferOffset: " + bufferOffset + ", bufferPos: " + bb.position() + ", limit: " + bb.limit());
               bb.get(dst, offset, remaining);
               offset += remaining;
               length -= remaining;
               transferred += remaining;
            }
            // Physical part
            // If we are standing on the limit, we could have reached the window limit or the buffer is just full
            if (bb.position() == bb.limit()) {
               if (bufferLimited) {
                  // Window limit reached, check if anything was read, return EOF if not
                  if (transferred == 0) {
                     transferred = -1;
                     break;
                  }
               } else {
                  // Buffer full, clear it
                  // If there is unwritten data in the buffer, write it out or it gets lost
                  if (pendingWrite) {
                     // Write the buffer, position and limit gets updated too
                     writeBuffer(false);
                  } else {
                     // No data to write, clear the buffer
                     bb.clear();
                     maxPos = 0;
                     // Move buffer just behind the current buffer length
                     bufferOffset += bufferSize;
                     // Because the buffer position has moved, update the limit status
                     updateLimit();
                     // Check if file pointer stands on the right position
                     if (fc.position() != bufferOffset) {
                        fc.position(bufferOffset);
                     }
                  }
               }
            } else {
               // Check if file pointer stands on the right position
               long readPos = bufferOffset + (long)bb.position();
               if (fc.position() != readPos) {
                  fc.position(readPos);
               }
            }
            // Now the buffer is ready to be filled (either it is partially filled, empty, or contains not written data from an incomplete write)
            // Mark current position to be able to return to it
            bb.mark();
            // If we are at EOF, break out, return EOF if nothing was read, otherwise the amount of read data
            //System.out.println(String.format("### %4$s Reading from physical position: 0x%1$010X, bufferOffset: 0x%2$010X, bufferPosition: 0x%3$04X", fc.position(), bufferOffset, bb.position(), file.getName()));
            if (fc.read(bb) == -1) {
               if (transferred == 0) {
                  transferred = -1;
               }
               // No need to update position, nothing got read
               //maxPos = bb.position();
               break;
            }
            // After a read bufferOffset and file pointer are async!
            //System.out.println(file.getName() + " Read done, new position: " + bb.position());
            maxPos = bb.position();
            bb.reset();
            //System.out.println(file.getName() + " Reset position to: " + bb.position());
         }
      }
      return transferred;
   }

   /* (non-Javadoc)
    * @see dumphd.core.DataSource#write(byte[], int, int)
    */
   public int write(byte[] src, int offset, int length) throws IOException {
      // Counts the number of returned bytes
      int transferred = 0;
      while (true) {
         int remaining = bb.remaining();
         //System.out.println(file.getName() + " FileSource write: " + length + ", remaining: " + remaining);
         if (length <= remaining) {
            // Logical write, enough space left to write data
            bb.put(src, offset, length);
            transferred += length;
            pendingWrite = true;
            // Need to check if current pos is creater than maxPos because of a seek back there can be more valid data in the buffer than we wrote
            int newPos = bb.position();
            if (newPos > maxPos) {
               maxPos = newPos;
            }
            //System.out.println(String.format("FULL write, bufferOffset: 0x%1$010X", bufferOffset));
            break;
         } else {
            // Combined write, write as much space is left, perform a physical write
            if (remaining > 0) {
               // Logical write, write as much as possible
               bb.put(src, offset, remaining);
               offset += remaining;
               length -= remaining;
               transferred += remaining;
               pendingWrite = true;
               // Need to check if current pos is creater than maxPos because of a seek back there can be more valid data in the buffer than we wrote
               int newPos = bb.position();
               if (newPos > maxPos) {
                  maxPos = newPos;
               }
               //System.out.println(String.format("%1$s FULL write, bufferOffset: 0x%2$010X, position: %3$d", file.getName(), bufferOffset, bb.position()));
            }
            // Physical part
            // Check if we are at the limit or just end of buffer
            if (bufferLimited) {
               // We are at the window limit, if nothing was transferred signal EOF
               if (transferred == 0) {
                  transferred = -1;
               }
               break;
            } else {
               // End of buffer reached, write it to storage device
               //System.out.println(file.getName() + " Writing physically!, position: " + bb.position() + ", maxPos: " + maxPos);
               writeBuffer(false);
            }
         }
      }
      return transferred;
   }

   /* (non-Javadoc)
    * @see dumphd.core.DataSource#getPosition()
    */
   public long getPosition() throws IOException {
      return bufferOffset + (long)bb.position() - currentWindow.start;
   }

   /* (non-Javadoc)
    * @see dumphd.core.DataSource#setPosition(long)
    */
   public void setPosition(long position) throws IOException {
      long tPosition = currentWindow.start + position;
      //System.out.println(String.format("%6$s Seek, original position: 0x%1$010X, windowed position: 0x%2$010X, bufferOffset: 0x%3$010X, bufferPosition: 0x%4$04X, bufferLimit: 0x%5$04X", position, tPosition, bufferOffset, bb.position(), bb.limit(), file.getName()));
      // Check if new position lies into current window range
      if (tPosition >= currentWindow.start && tPosition <= currentWindow.end) {
         // Position lies in current window range, now we know that the buffer limit is NOT the window limit
         long newBufferPosition = tPosition - bufferOffset;
         if (newBufferPosition >= 0 && newBufferPosition <= maxPos) {
            // New Position is inside filled buffer range, logical seek
            bb.position((int)newBufferPosition);
            //System.out.println(String.format("%3$s Logical seek, bufferOffset: 0x%1$010X, bufferPosition: 0x%2$04X", bufferOffset, bb.position(), file.getName()));
         } else {
            // New position is outside filled buffer range, physical seek
            if (pendingWrite) {
               // Requires the complete buffer to be written! Even behind a window may be data!
               //System.out.println(file.getName() + " Physical seek, write is pending, writing buffer");
               writeBuffer(true);
            } else {
               // Clear the buffer!
               bb.clear();
               maxPos = 0;
            }
            fc.position(tPosition);
            bufferOffset = tPosition;
            // Since the buffer position has moved, update the limit status 
            updateLimit();
            //System.out.println("Physical seek");
            //System.out.println(String.format("%4$s Physically seeked, bufferOffset: 0x%1$010X, bufferPosition: 0x%2$04X, bufferLimit: 0x%3$04X", bufferOffset, bb.position(), bb.limit(), file.getName()));
         }
      } else throw new IOException("Seek exceeds window limits");
   }

   /* (non-Javadoc)
    * @see dumphd.core.DataSource#addWindow(long)
    */
   public void addWindow(long size) throws IOException {
      //System.out.println(file.getName() + " Adding Window, start: " + windowStart + ", end: " + windowEnd + ", bufferPos: " + bb.position());
      long start = bufferOffset + (long)bb.position();
      long end = start + size;
      // The new window must not exceed the current one, but may be bigger as the filesize (as the current window), otherwise writing would not work
      if (end <= currentWindow.end) {
         // There may be unwritten data behind the window, but this will get written if the buffer is flushed
         currentWindow = new Window(start, end);
         windows.add(currentWindow);
         // Because the window can limit the current buffer, update the limit status
         updateLimit();
         //System.out.println(file.getName() + " Window added, start: " + windowStart + ", end: " + windowEnd + ", bufferPos: " + bb.position());
      } else throw new IOException("New window exceeds current window limits");
   }

   /* (non-Javadoc)
    * @see dumphd.core.DataSource#removeWindow()
    */
   public void removeWindow() throws IOException {
      // Because the previous window is always bigger than the current we dont need to write out pending writes
      if (windows.size() > 1) {
         windows.removeLast();
         // Set the previous window limits
         currentWindow = windows.getLast();
         // Because the windwow was removed, the limit may be obsolete
         updateLimit();
      } else throw new IOException("No window set");
   }

   /* (non-Javadoc)
    * @see dumphd.core.DataSource#windowCount()
    */
   public int windowCount() {
      return (windows.size() - 1);
   }

   /* (non-Javadoc)
    * @see dumphd.core.DataSource#size()
    */
   public long size() throws IOException {
      long fileSize = fc.size();
      if (fileSize < currentWindow.end) {
         return fileSize - currentWindow.start;
      } else {
         return currentWindow.end - currentWindow.start;
      }
   }

   /* (non-Javadoc)
    * @see dumphd.core.DataSource#getMode()
    */
   public int getMode() {
      return mode;
   }

   /* (non-Javadoc)
    * @see dumphd.core.DataSource#getFile()
    */
   public File getFile() {
      return file;
   }

   /* (non-Javadoc)
    * @see dumphd.core.DataSource#close()
    */
   public void close() throws IOException {
      if (pendingWrite) {
         writeBuffer(true);
      }
      if (raf != null) {
         raf.close();
      }
      // Release all used objects, this also prevents the usage of this object after it has been closed 
      bb = null;
      fc = null;
      raf = null;
      currentWindow = null;
      windows = null;
   }

   /**
    * Writes the buffer physically to disk, it is not checked if it needs to get written, that must be done from outside.
    * 
    * There are 2 modes of operation.
    * 
    * Flush-mode:
    * Flushes the whole buffer contents to disk, does not honor set windows, writes everything from 0 up to maxPos.
    * After this method call the current buffer position is destroyed, the buffer MUST be relocated! Its position / maxPos is 0, the limit is the capacity.
    * 
    * Shift-mode:
    * This must only be called when the position is at the buffer end, writes the buffer physically to disk, shifts the buffer so that it is empty.
    * Not the whole buffer may be written, thats honored during the shift.
    * 
    * @param flush If true, flush-mode is used, shift-mode otherwise
    * @throws IOException In case of an I/O error
    */
   private void writeBuffer(boolean flush) throws IOException {
      // Buffer writing starts always from bufferOffset, ensure that the file pointer is at that position
      if (fc.position() != bufferOffset) {
         fc.position(bufferOffset);
      }
      // Check if the whole buffer should be flushed or (partially) written and shifted 
      if (flush) {
         // Flush the whole buffer, this destroys its current position!
         if (bb.position() != 0) {
            bb.position(0);
         }
         if (bb.limit() != maxPos) {
            bb.limit(maxPos);
         }
         while (bb.hasRemaining()) {
            fc.write(bb);
         }
         pendingWrite = false;
         bb.clear();
         maxPos = 0;
      } else {
         // Write the buffer and shift its position
         // This part must only be called when the position is at the buffer end!
         //System.out.println(file.getName() + " Shifting write, position: " + bb.position() + ", bufferSize: " + bufferSize);
         if (bb.position() == bufferSize) {
            //Flip the buffer to write everything from its beginning to the current position
            bb.flip();
            fc.write(bb);
            if (bb.hasRemaining()) {
               // The write was partially
               pendingWrite = true;
            } else {
               // The buffer was written fully
               pendingWrite = false;
            }
            bb.compact();
            maxPos = bb.position();
            // Advance the bufferPosition to the current file position
            bufferOffset = fc.position();
            // Because the buffer was moved, update the limit
            updateLimit();
         } else throw new IllegalStateException("FileSource: Buffer cannot be written when not completely filled, position: " + bb.position() + ", bufferSize: " + bufferSize);
      }
   }

   /**
    * Updates the current limit status.
    * This method MUST be called after every movement of the buffer or adding / removement of a window!
    */
   private void updateLimit() {
      // Check if the limit lies in the current buffer range
      long checkLimit = currentWindow.end - bufferOffset;
      if (checkLimit <= (long)bufferSize) {
         // Limit lies is current buffer range, limit the buffer
         bb.limit((int)checkLimit);
         bufferLimited = true;
      } else {
         // Limit does not lie in current buffer range, reset its limit to the buffer capacity!
         bb.limit(bufferSize);
         bufferLimited = false;
      }
   }


   /**
    * Stores the logical start and end coordinates of a window.
    * 
    * @author KenD00
    */
   private final static class Window {
      public long start = 0;
      public long end = Long.MAX_VALUE;

      /**
       * Creates a window with the start coordinate 0 and the end coordinate Long.MAX_VALUE. 
       */
      public Window() {
         // Nothing
      }

      /**
       * Creates a window with the given coordinates.
       * 
       * @param start The start coordinate
       * @param end The end coordinate
       */
      public Window(long start, long end) {
         this.start = start;
         this.end = end;
      }

      public String toString() {
         StringBuffer sb = new StringBuffer(32);
         sb.append("Window :: start: ");
         sb.append(start);
         sb.append(", end: ");
         sb.append(end);
         return sb.toString();
      }
   }

}
