/*
 * 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.FilenameFilter;
import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.zip.CRC32;


/**
 * Utility class, contains static methods for various uses.
 * 
 * @author KenD00 
 */
public final class Utils {

   /**
    * This buffer can be used by classes for temporary storage during operations.
    * It MUST NOT be used to store values for a later retrieval, this buffer can be used by everyone.
    * It MUST be ensured from outside that this buffer is used by only ONE entity at the same time.
    * The size of the buffer modulo 16 MUST be 0, otherwise the AACSDecrypter will fail
    */
   public static final byte[] buffer = new byte[4 * 1024 * 1024];
   /**
    * Lookup table to convert numbers 0 to 15 to hex digits
    */
   public static final char[] hexLut = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

   /**
    * The used MessagePrinter, by default output to console
    */
   private static MessagePrinter out = new PrintStreamPrinter(System.out);
   /**
    * Used for CRC32 calculation during file copy
    */
   private static CRC32 crc32Calc = new CRC32();


   /**
    * @return The currently used MessagePrinter
    */
   public static MessagePrinter getMessagePrinter() {
      return out;
   }

   /**
    * Sets a new MessagePrinter. By default this class outputs text to the console, set a new MessagePrinter to redirect output.
    * 
    * @param mp The new MessagePrinter, if null an IllegalArgumentException is thrown
    */
   public static void setMessagePrinter(MessagePrinter mp) {
      if (mp != null) {
         out = mp;
      } else {
         throw new IllegalArgumentException("The MessagePrinter must be non null");
      }
   }

   /**
    * Converts the given byte values byte by byte to a hex string starting at offset using length bytes.
    * 
    * @param src The byte array containing the values to get converted
    * @param offset Offset to start converting
    * @param length Number of bytes to convert
    * @return The contents of the byte array as hex string
    */
   public static String toHexString(byte[] src, int offset, int length) {
      StringBuffer sb = new StringBuffer(20);
      int endOffset = offset + length;
      for (int i = offset; i < endOffset; i++) {
         sb.append(hexLut[(src[i] >>> 4) & 0xF]);
         sb.append(hexLut[src[i] & 0xF]);
      }
      return sb.toString();
   }

   /**
    * Decodes the given hex string literally to bytes, that is using two digits and interpret them as byte value.
    * Half the length of the string number of bytes get produced.
    *
    * @param src The string to convert
    * @param dst The byte array to store the values in
    * @param offset Offset to start putting the values into the byte array
    */
   public static void decodeHexString(String src, byte[] dst, int offset) {
      int halfLength = src.length() / 2;
      int digit0 = 0;
      int digit1 = 0;
      for (int i = 0; i < halfLength; i++) {
         int pos = i * 2;
         digit1 = Character.digit(src.charAt(pos), 16);
         digit0 = Character.digit(src.charAt(++pos), 16);
         dst[offset + i] = (byte)((digit1 << 4) + digit0);
      }
   }

   /**
    * Searches the given directory for files passing the given FilenameFilter and stores its names in the given collection.
    * The filenames will be stored relative to the given baseDir.
    * 
    * If src contains the file BAR.JAVA and baseDir is FOO then the resulting filename in dst is FOO\BAR.JAVA
    * (assuming \ is the separator char).
    * 
    * @param src Source to scan for files, must be a directory, that is not verified!
    * @param baseDir Directory the filenames will be relative to
    * @param dst Destination collection where the found filenames get stored
    * @param recursive If true, subdirectories are processed too
    * @param fnf FilenameFilter files have to pass
    * @param silent If true, no output messages will be sent to the registered MessagePrinter
    */
   public static void scanForFilenames(File src, String baseDir, Collection<String> dst, boolean recursive, FilenameFilter fnf, boolean silent) {
      class DirElement {
         public String baseDir = null;
         public File dir = null;

         public DirElement(String baseDir, File dir) {
            this.baseDir = baseDir;
            this.dir = dir;
         }
      }
      LinkedList<DirElement> dirs = new LinkedList<DirElement>();
      dirs.add(new DirElement(baseDir, src));
      while (!dirs.isEmpty()) {
         DirElement dir = dirs.remove();
         if (!silent) {
            out.println("Searching " + dir.dir + " for files...");
         }
         File[] files = dir.dir.listFiles(fnf);
         if (files != null) {
            for (int i = 0; i < files.length; i++) {
               if (files[i].isDirectory()) {
                  if (recursive) {
                     dirs.add(new DirElement(dir.baseDir + File.separator + files[i].getName(), files[i]));
                  }
               } else {
                  if (!silent) {
                     out.println(files[i].toString());
                  }
                  //out.println("Debug: baseDir = " + dir.baseDir + ", filename = " + files[i].getName());
                  dst.add(dir.baseDir + File.separator + files[i].getName());
               }
            }
         }
      }
   }

   /**
    * Copies length bytes from in to out starting at their current positions.
    * Returns the number of written bytes and the crc32 of the copied data.
    * If less than requested was written then EOF was reached either during reading, during writing or during both. 
    * 
    * @param in ByteSource to copy from, starts at its current position
    * @param out ByteSource to copy to, starts at its current position
    * @param length Number of bytes to copy
    * @return Number of written bytes and their crc32 value
    * @throws IOException If an I/O error occurs
    */
   public static CopyResult copyBs(ByteSource in, ByteSource out, long length) throws IOException {
      int readResult = 0;
      int writeResult = 0;
      int writeAmount = 0;
      CopyResult returnValue = new CopyResult();
      crc32Calc.reset();
      while (length > 0) {
         if (length <= (long)buffer.length) {
            writeAmount = (int)length;
         } else {
            writeAmount = buffer.length;
         }
         // Complete requested data to copy fits into the buffer
         readResult = in.read(buffer, 0, writeAmount);
         // Check if EOF was at reading start, abort at once if that is the case
         if (readResult == -1) {
            break;
         } else {
            // If less than requested was copied, update the value of copied bytes to write the correct amount
            if (readResult != writeAmount) {
               writeAmount = readResult;
               // Set remaining length to current read data so that the loop aborts after this run
               length = (long)readResult;
            }
         }
         writeResult = out.write(buffer, 0, writeAmount);
         // Check if EOF was at writing start, abort at once if that is the case
         if (writeResult == -1) {
            break;
         } else {
            // If less than requested was written, update the value of written bytes to return the correct result
            if (writeResult != writeAmount) {
               writeAmount = writeResult;
               // Set remaining length to current written data so that the loop aborts after this run
               length = (long)writeResult;
            }
         }
         crc32Calc.update(buffer, 0, writeAmount);
         returnValue.size += (long)writeAmount;
         length -= (long)writeAmount;
      }
      returnValue.crc32 = crc32Calc.getValue();
      return returnValue;
   }
   
   /**
    * Returns the Number of a Blu-Ray Clip file.
    * 
    * @param filename The filename of the Clip. Has to be only the name without path, e.g. 00000.m2ts
    * @return The Number of the Clip or a negative value if an error occurred (-1: Filename doesn't end with m2ts, -2: Filename is not a number)
    */
   static public int getClipNumber(String filename) {
      // The number of the clip
      int clipNr = 0;
      // Offset of the extension dot
      int extOffset = filename.lastIndexOf('.');
      try {
         clipNr = Integer.parseInt(filename.substring(0, extOffset));
      }
      catch (IndexOutOfBoundsException ei) {
         // This shouldn't happen because the filename should end with .M2TS
         clipNr = -1;
      }
      catch (NumberFormatException en) {
         // This shouldn't happen because all streamfiles should be numbers (BD Spec)
         clipNr = 2;
      }
      return clipNr;
   }

}
