/*
 * 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.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;


/**
 * This is a wrapper class that presents a packetized stream as a contigous stream.
 * It uses a previously created list of records to find the packets and extract the payload data from the given ByteSource.
 * The given Collection of PackRecords is not modified and must contain at least one element, it must not
 * be modified after given to this class.
 * 
 * This class is not thread safe, but it is safe that the underlying ByteSource is shared among other ByteSources, the read and write methods
 * check the physical position before operation and correct it if necessary.
 * 
 * TODO: Check if PackSource is closed?
 * 
 * @author KenD00
 */
public final class PackSource implements ByteSource {

   /**
    * The underlying ByteSource used for the data access
    */
   private ByteSource bs = null;
   /**
    * The mode this PackSource uses
    */
   private int mode = 0;
   /**
    * The File this PackSource returns
    */
   private File file = null;
   /**
    * The used PackReckords to find the data segements
    */
   private Collection<PackRecord> packRecords = null;

   /**
    * Contains the maximum data position that got written
    */
   private long maxWritten = 0;

   /**
    * Traverses through the PackRecords, is always at a position so that a call to next() returns follwoing PackRecord of the current active one
    */
   private Iterator<PackRecord> recordIt = null;
   /**
    * The current active PackRecord
    */
   private PackRecord currentRecord = null;
   /**
    * The current position in the payload of the current PackRecord, 0 is the start of the payload section
    */
   private int payloadPosition = 0;
   /**
    * The maximum position in the payload of the current Packrecord (actually one greater than the maximum position that may be accessed)
    * This may be the payload size of the current PackRecord or smaller if the window limit lies in the current PackRecord 
    */
   private int recordLimit = 0;
   /**
    * If true, the window lies in the current PackRecord, otherwise not
    */
   private boolean recordLimited = false;

   /**
    * The list of all windows
    */
   private LinkedList<Window> windows = new LinkedList<Window>();
   /**
    * The current active window
    */
   private Window currentWindow = null;

   /**
    * The current position in the underlying ByteSource 
    */
   private long physicalPosition = 0;


   /**
    * Creates a new PackSource.
    * 
    * @param bs The underlying ByteSource to access
    * @param mode The mode this PackSource should use
    * @param file The File this PackSource should return
    * @param packRecords The PackRecords to use
    * @throws IOException An error accessing bs occured
    */
   public PackSource(ByteSource bs, int mode, File file, Collection<PackRecord> packRecords) throws IOException {
      this.bs = bs;
      this.mode = mode;
      this.file = file;
      this.packRecords = packRecords;
      // Traverse the complete packRecords to the last one to set the window bounds
      recordIt = packRecords.iterator();
      // There must be always at least one entry in the packRecords collection
      while (recordIt.hasNext()) {
         currentRecord = recordIt.next();
      }
      currentWindow = new Window(0, currentRecord.dataOffset + (long)currentRecord.payloadSize);
      windows.add(currentWindow);
      // Re-initialize the iterator and currentRecord
      recordIt = packRecords.iterator();
      currentRecord = recordIt.next();
      // Now set the limit
      recordLimit = currentRecord.payloadSize;
      // Set the physical postion
      physicalPosition = currentRecord.packetOffset + (long)currentRecord.payloadOffset;
      bs.setPosition(physicalPosition);
   }

   /* (non-Javadoc)
    * @see dumphd.util.ByteSource#read(byte[], int, int)
    */
   public int read(byte[] dst, int offset, int length) throws IOException {
      if (mode == ByteSource.R_MODE || mode == ByteSource.RW_MODE) {
         return readWrite(dst, offset, length, false);
      } else throw new IOException("PackSource not opened for reading");
   }

   /* (non-Javadoc)
    * @see dumphd.util.ByteSource#write(byte[], int, int)
    */
   public int write(byte[] src, int offset, int length) throws IOException {
      if (mode == ByteSource.W_MODE || mode == ByteSource.RW_MODE) {
         return readWrite(src, offset, length, true);
      } else throw new IOException("PackSource not opened for writing");
   }

   /* (non-Javadoc)
    * @see dumphd.util.ByteSource#getPosition()
    */
   public long getPosition() throws IOException {
      return currentRecord.dataOffset + (long)payloadPosition - currentWindow.start;
   }

   /* (non-Javadoc)
    * @see dumphd.util.ByteSource#setPosition(long)
    */
   public void setPosition(long position) throws IOException {
      // Translate from window coordinates to logical coordinates
      long tPosition = currentWindow.start + position;
      // Check if position lies into current window bounds
      //System.out.println(String.format("Seek, original position: 0x%1$010X, windowed position: 0x%2$010X, currentRecord.dataOffset: 0x%3$010X, currentRecord.dataEnd: 0x%4$010X, PackOffset: 0x%5$010X", position, tPosition, currentRecord.dataOffset, (currentRecord.dataOffset + (long)currentRecord.payloadSize), currentRecord.packetOffset));
      if (tPosition >= currentWindow.start && tPosition <= currentWindow.end) {
         // Check if new position is inside of the current packet
         if (tPosition >= currentRecord.dataOffset && tPosition <= currentRecord.dataOffset + (long)currentRecord.payloadSize) {
            // New position inside current packet, seek logically to the new position
            payloadPosition = (int)(tPosition - currentRecord.dataOffset);
            //System.out.println("Logical seek, payloadPosition: " + payloadPosition);
         } else {
            // New position outside current packet, search the corresponding packet
            recordIt = packRecords.iterator();
            currentRecord = recordIt.next();
            while (tPosition > currentRecord.dataOffset + (long)currentRecord.payloadSize) {
               if (recordIt.hasNext()) {
                  currentRecord = recordIt.next();
               } else throw new IOException("Unexpected EOF");
            }
            // Seek logically to new position, update the limit
            payloadPosition = (int)(tPosition - currentRecord.dataOffset);
            updateLimit();
            //System.out.println("Physical seek, tPosition: " + tPosition + ", dataOffset: " + currentRecord.dataOffset + ", payloadPosition: " + payloadPosition);
         }
         // Physically set the position
         physicalPosition = currentRecord.packetOffset + (long)currentRecord.payloadOffset + (long)payloadPosition;
         //System.out.println(String.format("Seeking to physical position: 0x%1$010X", physicalPosition));
         bs.setPosition(physicalPosition);
         //System.out.println(String.format("Physical position is: 0x%1$010X", bs.getPosition()));
      } else throw new IOException("Seek exceeds window limits");
   }

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

   /* (non-Javadoc)
    * @see dumphd.util.ByteSource#addWindow(long)
    */
   public void addWindow(long size) throws IOException {
      // Calculate current locigal position
      long start = currentRecord.dataOffset + (long)payloadPosition;
      long end = start + size;
      // Check if new window is inside the current window
      if (end <= currentWindow.end) {
         currentWindow = new Window(start, end);
         windows.add(currentWindow);
         updateLimit();
      } else throw new IOException("New window exceeds current window limits");
   }

   /* (non-Javadoc)
    * @see dumphd.util.ByteSource#removeWindow()
    */
   public void removeWindow() throws IOException {
      if (windows.size() > 1) {
         windows.removeLast();
         currentWindow = windows.getLast();
         updateLimit();
      } else throw new IOException("No window set");
   }

   /* (non-Javadoc)
    * @see dumphd.util.ByteSource#size()
    */
   public long size() throws IOException {
      return currentWindow.end - currentWindow.start;
   }

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

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

   /* (non-Javadoc)
    * @see dumphd.util.ByteSource#close()
    */
   public void close() throws IOException {
      // Nothing to close, release only all resources, this prevents usage of this object after it has been closed
      bs = null;
      file = null;
      currentRecord = null;
      recordIt = null;
      packRecords = null;
      currentWindow = null;
      windows = null;
   }

   /**
    * Returns the maximum position plus 1 this PackSource has written to.
    * 
    * @return The maximum position plus 1 this PackSource has written to, 0 if it has written nothing
    */
   public long maxWrittenPosition() {
      return maxWritten;
   }

   /**
    * Helper method that does actually the reading/writing.
    * 
    * @param data The source/destination to store/retrieve the bytes
    * @param offset Offset into data
    * @param length Number of bytes to read/write
    * @param write If true, writing is used, reading otherwise
    * @return The number of read/written bytes, -1 if EOF is reached
    * @throws IOException If an I/O error occurred 
    */
   private int readWrite(byte[] data, int offset, int length, boolean write) throws IOException {
      // The number of read/written bytes
      int transferred = 0;
      // Check if physical position is there where it should be, it could get changed if the reader and writer use the same ByteSource 
      if (bs.getPosition() != physicalPosition) {
         //System.out.println("Resetting physical position: " + physicalPosition);
         bs.setPosition(physicalPosition);
      }
      while (true) {
         // Determine the remaining bytes in the current record
         int remaining = recordLimit - payloadPosition;
         //System.out.println("Remaining: " + remaining);
         // Check if the current record has enough data remaining
         if (length <= remaining) {
            // There is enough data in the current record
            if (write) {
               // Write mode
               if (bs.write(data, offset, length) == length) {
                  // Update payloadPosition here to calculate the new maxWritten position
                  payloadPosition += length;
                  long newMaxWritten = currentRecord.dataOffset + (long)payloadPosition;
                  if (newMaxWritten > maxWritten) {
                     maxWritten = newMaxWritten;
                  }
               } else throw new IOException("Unexpected EOF");
            } else {
               // Read mode
               if (bs.read(data, offset, length) == length) {
                  payloadPosition += length;
               } else throw new IOException("Unexpected EOF");
            }
            // The physicalPositon must be updated too!
            physicalPosition += length;
            transferred += length;
            //System.out.println("Completely transferred, payloadPosition: " + payloadPosition + ", physicalPosition: " + physicalPosition + ", transferred: " + transferred);
            break;
         } else {
            // There is not enough data/free space remaining
            if (remaining > 0) {
               // There is some data/free space remaining, read/write it
               if (write) {
                  // Write mode
                  if (bs.write(data, offset, remaining) == remaining) {
                     // The position must be updated too, because we may hit the limit and then there is no new record retrieved (and the payloadPosition reset)
                     payloadPosition += remaining;
                     // Update the maximum written logical address
                     long newMaxWritten = currentRecord.dataOffset + (long)payloadPosition;
                     if (newMaxWritten > maxWritten) {
                        maxWritten = newMaxWritten;
                     }
                  } else throw new IOException("Unexpected EOF");

               } else {
                  // Read mode
                  if (bs.read(data, offset, remaining) == remaining) {
                     // The position must be updated too, because we may hit the limit and then there is no new record retrieved (and the payloadPosition reset)
                     payloadPosition += remaining;
                  } else throw new IOException("Unexpected EOF");
               }
               // The physicalPositon must be updated too!
               physicalPosition += remaining;
               offset += remaining;
               length -= remaining;
               transferred += remaining;
               //System.out.println("Partially transferred, payloadPosition: " + payloadPosition + ", physicalPosition: " + physicalPosition + ", offset: " + offset + ", length: " + length + ", transferred: " + transferred);
            }
            // If the record is limited, we hit the limit
            if (recordLimited) {
               // Limit reached, return EOF if nothing was read so far
               //System.out.println("Limit reached, transferred so far: " + transferred);
               if (transferred == 0) {
                  transferred = -1;
               }
               break;
            } else {
               // We are not at the limit, read the next record
               if (recordIt.hasNext()) {
                  // Retrive next record, reset the payloadPositon and update the limit
                  currentRecord = recordIt.next();
                  payloadPosition = 0;
                  updateLimit();
                  // Set the physical position
                  physicalPosition = currentRecord.packetOffset + (long)currentRecord.payloadOffset;
                  bs.setPosition(physicalPosition);
               } else throw new IOException("Unexpected EOF");
            }
         }
      }
      return transferred;
   }

   /**
    * Checks if the window limit lies in the current record, limits the record accordingly.
    */
   private void updateLimit() {
      long checkLimit = currentWindow.end - currentRecord.dataOffset;
      if (checkLimit <= (long)currentRecord.payloadSize) {
         recordLimited = true;
         recordLimit = (int)checkLimit;
      } else {
         recordLimited = false;
         recordLimit = currentRecord.payloadSize;
      }
   }


   /**
    * Stores information about a packet.
    * The payload must only include the bytes that belong to the desired data stream, it must not contain any headers that may be present
    * in the physical payload section of the PES Packet!
    * 
    * @author KenD00
    */
   public final static class PackRecord {
      /**
       * The first byte of the paylod of this PackRecord corresponds to this byte of the data
       */
      public long dataOffset = 0; 
      /**
       * The physical offset where the packet starts
       * This should point to the start of the section that is required to be modified to fix up the packet structure
       * (e.g. if the new payload is smaller than the present one)
       */
      public long packetOffset = 0;
      /**
       * Relative offset from the packet start to the start of the payload
       */
      public int payloadOffset = 0;
      /**
       * Size of the payload
       */
      public int payloadSize = 0;


      /**
       * Creates a PackRecord where all attributes are zero.
       */
      public PackRecord() {
         // Nothing
      }

      /**
       * Creates a PackRecord with the given values.
       * 
       * @param dataOffset The dataOffset
       * @param packetOffset The packetOffset
       * @param payloadOffset The payloadOffset
       * @param payloadSize The payloadSize
       */
      public PackRecord(long dataOffset, long packetOffset, int payloadOffset, int payloadSize) {
         this.dataOffset = dataOffset;
         this.packetOffset = packetOffset;
         this.payloadOffset = payloadOffset;
         this.payloadSize = payloadSize;
      }

      public String toString() {
         return String.format("PackRecord :: dataOffset: 0x%1$010X, packetOffset: 0x%2$010X, payloadOffset: 0x%3$04X, payloadSize: 0x%4$04X", dataOffset, packetOffset, payloadOffset, payloadSize);
      }
   }


   /**
    * 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();
      }
   }

}
