/*
 * Copyright (C) 2008-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.bdplus;

import java.io.IOException;
import java.lang.IllegalStateException;
import java.util.ArrayList;
import java.util.Iterator;

import dumphd.util.ByteArray;
import dumphd.util.ByteSource;
import dumphd.util.Utils;

/**
 * This class represents a subtable of a BD+ conversion table.
 * 
 * @author KenD00
 */
public class SubTable {

   /**
    * Offset of this subtable from the beginning of the conversion table
    */
   private long tableOffset = 0;
   /**
    * The ID of this subtable
    */
   private long tableId = 0;
   /**
    * The segments of the subtable in the order they appear in the conversion table
    */
   private ArrayList<Segment> segments = new ArrayList<Segment>();


   /**
    * Creates an empty ConversionTable.
    */
   public SubTable() {
      // Nothing to do
   }
   
   /**
    * Creates a subtable from the given source.
    * 
    * The source read pointer must be at the location where the subtable starts, the whole conversion table must start at position 0.
    *  
    * @param source The conversion table to parse
    * @throws IOException
    */
   public SubTable(ByteSource source) throws IOException {
      tableOffset = source.getPosition();
      if (source.read(Utils.buffer, 0, 6) != 6) {
         throw new IOException("Unexpected EOF, subtable too small for header");
      }
      tableId = ByteArray.getUInt(Utils.buffer, 0);
      int segmentCount = ByteArray.getUShort(Utils.buffer, 4);
      segments.ensureCapacity(segmentCount);
      // Use the index section to jump to every segment. This isn't necessary if all segments are consecutive, it looks like they are,
      // but you can never know
      for (int i = 0; i < segmentCount; i++) {
         // Jump to the current index
         source.setPosition(tableOffset + 6 + i * 4);
         // Read it
         if (source.read(Utils.buffer, 0, 4) != 4) {
            throw new IOException("Unexpected EOF, segment index section too small");
         }
         long index = ByteArray.getUInt(Utils.buffer, 0);
         // Jump to the current segment
         source.setPosition(index);
         // Read the segment
         Segment segment = new Segment(source, i);
         segments.add(segment);
      }
   }
   
   /**
    * Parses the given byte array to create a SubTable.
    * 
    * The content of the byte array gets copied, changes of it after this method call do not affect this SubTable. 
    * 
    * @param raw The raw Conversion Table
    * @param offset Start offset, must be the start of the SubTable
    * @return The endoffset of the SubTable inside the byte array
    * @throws IndexOutOfBoundsException
    */
   public int parse(byte[] raw, int offset) throws IndexOutOfBoundsException {
      tableOffset = offset;
      tableId = ByteArray.getUInt(raw, offset);
      offset += 4;
      int segmentCount = ByteArray.getUShort(raw, offset);
      offset += 2;
      segments.ensureCapacity(segmentCount);
      for (int i = 0; i < segmentCount; i++) {
         long index = ByteArray.getUInt(raw, (int)tableOffset + 6 + i * 4);
         Segment segment = new Segment(i);
         offset = segment.parse(raw, (int)index);
         segments.add(segment);
      }
      return offset;
   }

   /**
    * @return The offset of this subtable inside the source
    */
   public long getTableOffset() {
      return tableOffset;
   }

   /**
    * @return The table id of this subtable
    */
   public long getTableId() {
      return tableId;
   }

   /**
    * @return The number of segments
    */
   public int size() {
      return segments.size();
   }

   /**
    * Returns the segment at the given position.
    * 
    * @param index Position of the segment
    * @return The segment at index
    */
   public Segment getSegment(int index) {
      return segments.get(index);
   }

   /**
    * @return An iterator over the segments
    */
   public Iterator<Segment> iterator() {
      return segments.iterator();
   }
   
   /**
    * @return An Iterator over all Patches
    */
   public PatchIterator patchIterator() {
      return new PatchIterator();
   }
   
   
   /**
    * An Iterator over all Patches of a Subtable.
    * 
    * This is more a C++ style Iterator, you don't call methods
    * to get the next element (java style), it is used more like a pointer. A call of the content retrieval
    * Methods doesn't advance the position, this has to be done manually.
    * 
    * @author KenD00
    */
   public class PatchIterator {
      
      /**
       * Iterator over all segments
       */
      private Iterator<Segment> segmentIterator = null;
      /**
       * The current selected segment
       */
      private Segment currentSegment = null;
      /**
       * Index of the current segment entry
       */
      private int currentSegmentIndex = 0;
      /**
       * If true, Patch0 gets returned, otherwise Patch1
       */
      private boolean currentSegmentFirstPatch = true;
      
      
      public PatchIterator() {
         segmentIterator = segments.iterator();
         nextSegment();
      }
      
      /**
       * @return True, if this PatchIterator is valid, that means a call of any other method does not produce an exception
       */
      public boolean isValid() {
         return (currentSegment != null);
      }
      
      /**
       * Advances to the next Patch. If there isn't any other Patch, this PatchIterator becomes invalid.
       * 
       * @throws java.lang.IllegalStateException This PatchIterator is invalid
       */
      public void increment() {
         if (currentSegment == null) {
            throw new IllegalStateException();
         }
         if (currentSegmentIndex < currentSegment.size()) {
            if (currentSegmentFirstPatch) {
               currentSegmentFirstPatch = false;
               return;
            } else {
               currentSegmentFirstPatch = true;
               currentSegmentIndex++;
            }
         }
         if (currentSegmentIndex == currentSegment.size()) {
            nextSegment();
         }
      }
      
      /**
       * @return The address of the current Patch
       * 
       * @throws java.lang.IllegalStateException This PatchIterator is invalid
       */
      public long getAddress() {
         if (currentSegment == null) {
            throw new IllegalStateException();
         }
         if (currentSegmentFirstPatch) {
            return currentSegment.getAddress0(currentSegmentIndex);
         } else {
            return currentSegment.getAddress1(currentSegmentIndex);
         }
      }

      /**
       * Copies the current Patch into the given array.
       * 
       * @param dst The destination array
       * @param offset Offset into dst where to start writing
       * 
       * @throws java.lang.IllegalStateException This PatchIterator is invalid
       */
      public void getPatch(byte[] dst, int offset) {
         if (currentSegment == null) {
            throw new IllegalStateException();
         }
         if (currentSegmentFirstPatch) {
            currentSegment.getPatch0(currentSegmentIndex, dst, offset);
         } else {
            currentSegment.getPatch1(currentSegmentIndex, dst, offset);
         }
      }
      
      /**
       * @return The length in bytes of the patch
       */
      public int getPatchLength() {
         return Segment.PATCH_LENGTH;
      }
      
      /**
       * Increments to the next non empty Segment or null (== invalid)
       */
      private void nextSegment() {
         // Reset patch identifiers
         currentSegmentIndex = 0;
         currentSegmentFirstPatch = true;
         // Navigate to the first non empty segment, or reset currentSegment to null as invalid flag
         while (segmentIterator.hasNext()) {
            currentSegment = segmentIterator.next();
            if (currentSegment.size() > 0) {
               return;
            }
         }
         currentSegment = null;
      }

   }

}
