/*
 * 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.core;

import java.io.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;

import dumphd.aacs.AACSDecrypter;
import dumphd.aacs.AACSException;
import dumphd.util.ByteArray;
import dumphd.util.PrintStreamPrinter;
import dumphd.util.CopyResult;
import dumphd.util.ByteSource;
import dumphd.util.FileSource;
import dumphd.util.MessagePrinter;
import dumphd.util.Utils;


/**
 * A class for depacking, packing and repacking ACA-Files.
 * 
 * TODO: CRC-checking?
 * TODO: Close files in case of I/O exception? Effects only dePack and pack where new FileSources are created.
 * 
 * @author KenD00
 */
public class ACAPacker {

   /**
    * ID string of an ACA file
    */
   public final static String idString = "HDDVDACA";
   /**
    * Maximum supported ACA file version
    */
   public final static long maxVersion = 0x00100001;

   /**
    * Lookup-table for mime types. Key is the file extension in upper case including the dot 
    */
   private final static HashMap<String, Byte> mimeTypes = new HashMap<String, Byte>(); 

   /**
    * Used to output textual messages
    */
   private MessagePrinter out = null;
   /**
    * Used for detecting and processing AACS protection
    */
   private AACSDecrypter aacsDec = null;

   /**
    * Used to store the header of an ACA file
    */
   private byte[] header = new byte[32];
   /**
    * Used to store the TOC of an ACA file
    */
   private byte[][] toc = new byte[65536][];

   /**
    * If true, AACS protection is not only identified by the mime field but also by the file contents itself. Used during de- and repacking
    */
   private boolean contentCheck = false;


   /**
    * Static constructor to initialize the mime type table
    */
   static {
      mimeTypes.put(".XPL", new Byte((byte)0x01));
      mimeTypes.put(".XMF", new Byte((byte)0x02));
      mimeTypes.put(".XMU", new Byte((byte)0x03));
      mimeTypes.put(".XTS", new Byte((byte)0x04));
      mimeTypes.put(".XAS", new Byte((byte)0x05));
      mimeTypes.put(".XSS", new Byte((byte)0x06));
      mimeTypes.put(".JS",  new Byte((byte)0x07));
      mimeTypes.put(".EVO", new Byte((byte)0x08));
      mimeTypes.put(".MAP", new Byte((byte)0x09));
      mimeTypes.put(".JPG", new Byte((byte)0x0A));
      mimeTypes.put(".PNG", new Byte((byte)0x0B));
      mimeTypes.put(".MNG", new Byte((byte)0x0C));
      mimeTypes.put(".CVI", new Byte((byte)0x0D));
      mimeTypes.put(".CDW", new Byte((byte)0x0E));
      mimeTypes.put(".WAV", new Byte((byte)0x0F));
      mimeTypes.put(".OTF", new Byte((byte)0x10));
      mimeTypes.put(".TTC", new Byte((byte)0x10));
      mimeTypes.put(".TTF", new Byte((byte)0x10));
      // The mime type for encrypted and unknown file types
      mimeTypes.put(null,   new Byte((byte)0xFF));
   }


   /**
    * Creates a new ACAPacker which does not process AACS and uses the given MessagePrinter for textual output.
    *  
    * @param mp Used for textual output
    */
   public ACAPacker(MessagePrinter mp) {
      this.out = mp;
      for (int i = 0; i < 65536; i++) {
         toc[i] = null;
      }
   }

   /**
    * Creates a new ACAPacker which removes AACS and uses the given MessagePrinter for textual output.
    * 
    * @param mp Used for textual output
    * @param aacsDec Used to process AACS
    */
   public ACAPacker(MessagePrinter mp, AACSDecrypter aacsDec) {
      this.out = mp;
      this.aacsDec = aacsDec;
      for (int i = 0; i < 65536; i++) {
         toc[i] = null;
      }
   }

   /**
    * Lists the information of the ACA file header, prints out the TOC and statistics about the filesize.
    * Ignores the ACA file version and a possible filesize mismatch.
    * 
    * @param input ACE file
    * @throws ACAException There is an error with the ACA file structure
    * @throws IOException An I/O error occurred
    */
   public void list(ByteSource input) throws ACAException, IOException {
      // Data fields of the header
      String fileId = null;
      int tocEntries = 0;
      long fileSize = 0;
      // Data fields of a toc entry
      byte[] tocEntry = null;
      long offset = 0;
      long size = 0;
      long crc32 = 0;
      int mimeType = 0;
      String fileName = null;
      // Helper variables
      long tocSize = 0;
      long dataSize = 0;
      boolean versionWarning = false;
      boolean sizeWarning = false;
      out.print("Reading header... ");
      if (input.read(header, 0, header.length) == header.length) {
         fileId = new String(header, 0, idString.length(), "US-ASCII");
         if (fileId.equals(idString)) {
            if (ByteArray.getVarLong(header, 8, 4) > maxVersion) {
               versionWarning = true;
            }
            tocEntries = ByteArray.getUShort(header, 12);
            fileSize = ByteArray.getVarLong(header, 14, 4);
            if (fileSize != input.size()) {
               sizeWarning = true;
            }
            out.println("OK");
            if (versionWarning) {
               out.println("WARNING! Unsupported ACA version");
            }
            if (sizeWarning) {
               out.println("WARNING! ACA filesize does not match physical filesize");
            }
            out.println(String.format("Version         : 0x%1$08X", ByteArray.getVarLong(header, 8, 4)));
            out.println("Number of files : " + ByteArray.getUShort(header, 12));
            out.println("Filesize        : " + ByteArray.getVarLong(header, 14, 4));
            out.println("Reading TOC... ");
            out.println(String.format("%1$10s %2$10s %3$10s %4$2s %5$s", "Offset",  "Size",  "CRC32",  "Mime",  "Name"));
            // Read the toc into memory
            for (int i = 0; i < tocEntries; i++) {
               tocEntry = toc[i];
               if (tocEntry == null) {
                  tocEntry = new byte[301];
                  toc[i] = tocEntry;
               }
               // Zero the tocEntry, so that in case of a read error no old values are present
               Arrays.fill(tocEntry, 0, tocEntry.length, (byte)0);
               input.read(tocEntry, 0, 14);
               input.read(tocEntry, 14, (tocEntry[13] & 0xFF) + 32);
               offset = ByteArray.getVarLong(tocEntry, 0, 4);
               size = ByteArray.getVarLong(tocEntry, 4, 4);
               crc32 = ByteArray.getVarLong(tocEntry, 8, 4);
               mimeType = ByteArray.getUByte(tocEntry, 12);
               fileName = new String(tocEntry, 14, (tocEntry[13] & 0xFF), "US-ASCII");
               tocSize += 46 + (tocEntry[13] & 0xFF);
               dataSize += size;
               out.println(String.format("0x%1$08X 0x%2$08X 0x%3$08X 0x%4$02X %5$s", offset, size, crc32, mimeType, fileName));
            }
            long contentSize = header.length + tocSize + dataSize;
            out.println("Finished TOC reading");
            out.println();
            out.println("Header size        : " + header.length);
            out.println("TOC size           : " + tocSize);
            out.println("Data size          : " + dataSize);
            out.println("-------------------------------");
            out.println("Total content size : " + contentSize);
            out.println("Filesize           : " + fileSize);
            out.println("-------------------------------");
            out.println("Slack              : " + (fileSize - contentSize));
         } else throw new ACAException("Invalid file id");
      } else throw new ACAException("File too small to be an ACA file");
   }

   /**
    * Depacks the given input into the given output directory.
    * The input position must be at the starting position of the ACA file.
    * 
    * @param input The input ACA
    * @param outDir The output directory
    * @return The number of written bytes
    * @throws ACAException There is an error with the ACA file structure
    * @throws IOException An I/O error occurred
    */
   public long dePack(ByteSource input, File outDir) throws ACAException, IOException {
      // Data fields of the header
      String fileId = null;
      int tocEntries = 0;
      long fileSize = 0;
      // Data fields of a toc entry
      byte[] tocEntry = null;
      long offset = 0;
      long size = 0;
      long crc32 = 0;
      String fileName = null;
      // Calculated helper values
      long oldOffset = 0;
      long dataCrc32 = 0;
      long outSize = 0;
      // For storing return values from aacs-decryption and file copy
      CopyResult cr = null;
      out.print("Reading header... ");
      if (input.read(header, 0, header.length) == header.length) {
         fileId = new String(header, 0, idString.length(), "US-ASCII");
         if (fileId.equals(idString)) {
            if (ByteArray.getVarLong(header, 8, 4) <= maxVersion) {
               tocEntries = ByteArray.getUShort(header, 12);
               fileSize = ByteArray.getVarLong(header, 14, 4);
               // Allow more bytes at the end of the file
               if (fileSize <= input.size()) {
                  // Read the toc into memory
                  out.println("OK");
                  out.print("Reading TOC... ");
                  for (int i = 0; i < tocEntries; i++) {
                     tocEntry = toc[i];
                     if (tocEntry == null) {
                        tocEntry = new byte[301];
                        toc[i] = tocEntry;
                     }
                     input.read(tocEntry, 0, 14);
                     input.read(tocEntry, 14, (tocEntry[13] & 0xFF) + 32);
                  }
                  out.println("OK");
                  // OldOffset contains the end offset of the previous element
                  oldOffset = input.getPosition();
                  // Process the toc entries, depack every file
                  out.println("Depacking files...");
                  for (int i = 0; i < tocEntries; i++) {
                     tocEntry = toc[i];
                     offset = ByteArray.getVarLong(tocEntry, 0, 4);
                     size = ByteArray.getVarLong(tocEntry, 4, 4);
                     crc32 = ByteArray.getVarLong(tocEntry, 8, 4);
                     fileName = new String(tocEntry, 14, tocEntry[13], "US-ASCII");
                     // Check if this offset doesnt point into the data area of the previous dataset
                     if (offset >= oldOffset) {
                        // Update oldOffset to the end offset of the current dataset
                        oldOffset = offset + size;
                        // Check if the data does not exceed the file size
                        if (oldOffset <= fileSize) {
                           out.print(fileName);
                           FileSource output = new FileSource(new File(outDir, fileName), FileSource.RW_MODE);
                           input.setPosition(offset);
                           input.addWindow(size);
                           try {
                              // Check if AACS should be used, use only mime type or identifiy also by content
                              if (aacsDec != null && (ByteArray.getUByte(tocEntry, 12) == (mimeTypes.get(null) & 0xFF) || contentCheck)) {
                                 out.print(", searching AACS... ");
                                 cr = aacsDec.decryptArf(input, output);
                                 if (cr.size != size) {
                                    out.println("REMOVED");
                                    // Updating crc32 to avoid false crc32 error.
                                    // TODO: How to check if crc32 was correct? The ARF-Decrypter must do that, or the file must be read in just to check the crc32
                                    crc32 = cr.crc32;
                                 } else {
                                    out.println("NONE");
                                 }
                              } else {
                                 if (contentCheck) {
                                    out.print(", searching AACS... ");
                                    if (AACSDecrypter.isArfEncrypted(input)) {
                                       out.println("FOUND");
                                    } else {
                                       out.println("NONE");
                                    }
                                 } else {
                                    out.println();
                                 }
                                 // Just copy the file
                                 cr = Utils.copyBs(input, output, size);
                              }
                           }
                           catch (AACSException e) {
                              out.println("ERROR");
                              out.println(e.getMessage());
                              // Set to 0 to not update the returned outsize with the size of the error causing file
                              cr.size = 0;
                              // Update the returned crc32 value to avoid a second error message because of crc32 error
                              cr.crc32 = crc32;
                           }
                           input.removeWindow();
                           outSize += cr.size;
                           dataCrc32 = cr.crc32;
                           output.close();
                           // Check if crc is correct
                           if (dataCrc32 != crc32) {
                              // TODO: What to do in case of crc error?
                           }
                        } else throw new ACAException("Current file exceeds total file size");
                     } else throw new ACAException("Offset of current file is inside previous file");
                  }
                  out.println("Depacking finished");
                  return outSize;
               } else throw new ACAException("Physical filesize smaller than ACA filesize");
            } else throw new ACAException("Unsupported ACA file version");
         } else throw new ACAException("Invalid file id");
      } else throw new ACAException("File too small to be an ACA file");
   }

   /**
    * Packs the given input files into a new ACA file.
    * 
    * @param inFiles Collection with the input files
    * @param outFile The output ACA
    * @return The number of written bytes
    * @throws ACAException An error occurred creating the ACA file structure. Currently only happens when AACS processing failes for an input file.
    * @throws IOException An I/O error occurred
    */
   public long pack(Collection<File> inFiles, File outFile) throws ACAException, IOException {
      // Data fields of the header
      int tocEntries = 0;
      int tocSize = 0;
      // Initial size of the output file, only the size of the header
      long fileSize = header.length;
      // Data fields of a toc entry
      byte[] tocEntry = null;
      long offset = 0;
      long size = 0;
      long crc32 = 0;
      byte mimeType = 0;
      String fileName = null;
      byte[] fileNameBytes = null;
      // For storing return values from aacs-decryption and file copy
      CopyResult cr = null;
      // Parse the input file list
      out.println("Building TOC... ");
      Iterator<File> it = inFiles.iterator();
      while (it.hasNext()) {
         File inFile = it.next();
         // Only process existent files
         if (inFile.isFile()) {
            // Build the toc in memory, only set fields that are known at this point
            tocEntry = toc[tocEntries];
            if (tocEntry == null) {
               tocEntry = new byte[301];
               toc[tocEntries] = tocEntry;
            }
            fileName = inFile.getName();
            fileNameBytes = fileName.getBytes("US-ASCII");
            mimeType = mimeTypes.get(null);
            int extOffset = fileName.lastIndexOf('.');
            if (extOffset >= 0) {
               Byte mimeTypeObject = mimeTypes.get(fileName.substring(extOffset).toUpperCase());
               if (mimeTypeObject != null) {
                  mimeType = mimeTypeObject;
               }
            }
            ByteArray.setByte(tocEntry, 12, mimeType);
            ByteArray.setByte(tocEntry, 13, (byte)fileNameBytes.length);
            System.arraycopy(fileNameBytes, 0, tocEntry, 14, fileNameBytes.length);
            Arrays.fill(tocEntry, 14 + fileNameBytes.length, tocEntry.length, (byte)0);
            tocEntries += 1;
            tocSize += 46 + fileNameBytes.length;
         } else {
            // File not found / is a directory, skip it
            out.println(inFile.getPath() + ": File not found, skipping");
            it.remove();
         }
      }
      // Update file size with the size of the toc
      fileSize += tocSize;
      // Writing part, write out the aca-archive
      FileSource output = new FileSource(outFile, FileSource.RW_MODE);
      // Seek behind the toc
      //System.out.println("Seeking behind toc");
      output.setPosition(fileSize);
      // Write the every file, update the missing toc fields
      out.println("Packing files... ");
      int i = 0;
      it = inFiles.iterator();
      while (it.hasNext()) {
         File inFile = it.next();
         tocEntry = toc[i];
         out.print(inFile.getName());
         FileSource input = new FileSource(inFile, FileSource.R_MODE);
         // Set the size here so that we can check if AACS was removed
         size = input.size();
         offset = output.getPosition();
         //System.out.println("OutPos is: " + offset);
         // Setting a output window would be safer, but slower because the buffer will get flushed for every file
         output.addWindow(size);
         // If AACS should be processed, decrypt the files
         if (aacsDec != null) {
            out.print(", searching AACS... ");
            try {
               cr = aacsDec.decryptArf(input, output);
               if (cr.size != size) {
                  out.println("REMOVED");
               } else {
                  out.println("NONE");
               }
            }
            catch (AACSException e) {
               out.println("ERROR");
               out.println(e.getMessage());
               input.close();
               output.close();
               throw new ACAException("Could not add file to archive", e);
            }
         } else {
            // If content check is enabled, check if the file is encrypted
            if (contentCheck) {
               //System.out.println("Checking for encryption in src");
               out.print(", looking for AACS protection... ");
               if (AACSDecrypter.isArfEncrypted(input)) {
                  tocEntry[12] = mimeTypes.get(null);
                  out.println("FOUND");
               } else {
                  out.println("NONE");
               }
            } else {
               out.println();
            }
            //System.out.println("Size is: " + size);
            // Copy the file
            cr = Utils.copyBs(input, output, size);
         }
         output.removeWindow();
         size=cr.size;
         crc32 = cr.crc32;
         input.close();
         ByteArray.setInt(tocEntry, 0, (int)offset);
         ByteArray.setInt(tocEntry, 4, (int)size);
         ByteArray.setInt(tocEntry, 8, (int)crc32);
         fileSize += size;
         i += 1;
      }
      // Build the header and write it to the output
      // Check if i == tocEntries? Must be the case, but can it be different?
      fileNameBytes = idString.getBytes("US-ASCII");
      System.arraycopy(fileNameBytes, 0, header, 0, fileNameBytes.length);
      ByteArray.setInt(header, 8, (int)maxVersion);
      ByteArray.setShort(header, 12, (short)tocEntries);
      ByteArray.setInt(header, 14, (int)fileSize);
      Arrays.fill(header, 18, header.length, (byte)0);
      //System.out.println("Seeking begin of file");
      out.print("Writing header... ");
      output.setPosition(0);
      output.write(header, 0, header.length);
      out.println("OK");
      out.print("Writing TOC... ");
      // Write the toc to the output
      for (i = 0; i < tocEntries; i++) {
         tocEntry = toc[i];
         output.write(tocEntry, 0, 46 + tocEntry[13]);
      }
      output.close();
      out.println("OK");
      out.println("Packing finished");
      return fileSize;
   }

   /**
    * Repacks the given input ACA to a new ACA. Only useful when removing AACS.
    * The input position must be at the starting position of the ACA file, the output gets written starting at the current output position. 
    * 
    * @param input The input ACA
    * @param output The output ACA
    * @return The number of written bytes
    * @throws ACAException There is an error with the input ACA file structure, or AACS could not be removed for the output ACA
    * @throws IOException An I/O error occurred
    */
   public long rePack(ByteSource input, ByteSource output) throws ACAException, IOException {
      // Data fields of the header
      String fileId = null;
      int tocEntries = 0;
      long fileSize = 0;
      // Data fields of a toc entry
      byte[] tocEntry = null;
      long offset = 0;
      long size = 0;
      long crc32 = 0;
      // ** Change from decrypting
      byte mimeType = 0;
      // ** End of change
      String fileName = null;
      // Calculated helper values
      long oldOffset = 0;
      long dataCrc32 = 0;
      long outSize = 0;
      // For storing return values from aacs-decryption and file copy
      CopyResult cr = null;
      out.print("Reading header... ");
      if (input.read(header, 0, header.length) == header.length) {
         fileId = new String(header, 0, idString.length(), "US-ASCII");
         if (fileId.equals(idString)) {
            if (ByteArray.getVarLong(header, 8, 4) <= maxVersion) {
               tocEntries = ByteArray.getUShort(header, 12);
               fileSize = ByteArray.getVarLong(header, 14, 4);
               // Allow more bytes at the end of the file
               if (fileSize <= input.size()) {
                  // Read the toc into memory
                  out.println("OK");
                  out.print("Reading TOC... ");
                  for (int i = 0; i < tocEntries; i++) {
                     tocEntry = toc[i];
                     if (tocEntry == null) {
                        tocEntry = new byte[301];
                        toc[i] = tocEntry;
                     }
                     input.read(tocEntry, 0, 14);
                     input.read(tocEntry, 14, (tocEntry[13] & 0xFF) + 32);
                  }
                  out.println("OK");
                  // OldOffset contains the end offset of the previous element
                  oldOffset = input.getPosition();
                  // ** Change from Decrypting
                  output.setPosition(oldOffset);
                  // OutSize contains the current size of the output, that is also the offset for the current dataset!
                  outSize = oldOffset;
                  // ** End of change
                  // Process the toc entries, repack every file
                  out.println("Repacking files...");
                  for (int i = 0; i < tocEntries; i++) {
                     tocEntry = toc[i];
                     offset = ByteArray.getVarLong(tocEntry, 0, 4);
                     size = ByteArray.getVarLong(tocEntry, 4, 4);
                     crc32 = ByteArray.getVarLong(tocEntry, 8, 4);
                     fileName = new String(tocEntry, 14, tocEntry[13], "US-ASCII");
                     // Check if this offset doesnt point into the data area of the previous dataset
                     if (offset >= oldOffset) {
                        // Update oldOffset to the end offset of the current dataset
                        oldOffset = offset + size;
                        // Check if the data does not exceed the file size
                        if (oldOffset <= fileSize) {
                           out.print(fileName);
                           // ** Change from Decrypting
                           //FileSource output = new FileSource(new File(outDir, fileName), FileSource.RW_MODE);
                           // ** End of change
                           input.setPosition(offset);
                           input.addWindow(size);
                           // ** Change from Decrypting
                           // Setting a output window would be safer, but slower because the buffer will get flushed for every file
                           output.addWindow(size);
                           // ** End of change
                           // Check if AACS should be used, use only mime type or identifiy also by content
                           if (aacsDec != null && (ByteArray.getUByte(tocEntry, 12) == (mimeTypes.get(null) & 0xFF) || contentCheck)) {
                              out.print(", searching AACS... ");
                              try {
                                 cr = aacsDec.decryptArf(input, output);
                                 if (cr.size != size) {
                                    out.println("REMOVED");
                                 } else {
                                    out.println("NONE");
                                 }
                              }
                              catch (AACSException e) {
                                 out.println("ERROR");
                                 out.println(e.getMessage());
                                 throw new ACAException("Could not add file to archive", e);
                              }
                              // ** Change from decrypting
                              // Updating these values to because thay are later used to update the toc entry (but this can be done another way too)
                              // Mainly the crc32 value must be updated to avoid false crc32 errors.
                              // TODO: How to check if crc32 was correct? The ARF-Decrypter must do that, or the file must be read in just to check the crc32
                              size = cr.size;
                              crc32 = cr.crc32;
                              mimeType = mimeTypes.get(null);
                              int extOffset = fileName.lastIndexOf('.');
                              if (extOffset >= 0) {
                                 Byte mimeTypeObject = mimeTypes.get(fileName.substring(extOffset).toUpperCase());
                                 if (mimeTypeObject != null) {
                                    mimeType = mimeTypeObject;
                                 }
                              }
                              // Update offset, size, crc and mime type field in toc entry
                              ByteArray.setInt(tocEntry, 0, (int)outSize);
                              ByteArray.setInt(tocEntry, 4, (int)size);
                              ByteArray.setInt(tocEntry, 8, (int)crc32);
                              ByteArray.setByte(tocEntry, 12, mimeType);
                              // ** End of change
                           } else {
                              if (contentCheck) {
                                 out.print(", searching AACS... ");
                                 if (AACSDecrypter.isArfEncrypted(input)) {
                                    out.println("FOUND");
                                 } else {
                                    out.println("NONE");
                                 }
                              } else {
                                 out.println();
                              }
                              // Just copy the file
                              cr = Utils.copyBs(input, output, size);
                           }
                           // ** Change from Decrypting
                           output.removeWindow();
                           // ** End of change
                           input.removeWindow();
                           outSize += cr.size;
                           dataCrc32 = cr.crc32;
                           // ** Change from decrypting
                           //out.close();
                           // ** End of change
                           // Check if crc is correct
                           if (dataCrc32 != crc32) {
                              // TODO: What to do in case of crc error?
                           }
                        } else throw new ACAException("Current file exceeds total file size");
                     } else throw new ACAException("Offset of current file is inside previous file");
                  }
                  // ** Change from decrypting
                  int i = 0;
                  // ** Change from encrypting
                  //fileNameBytes = idString.getBytes("US-ASCII");
                  //System.arraycopy(fileNameBytes, 0, header, 0, fileNameBytes.length);
                  //ByteArray.setInt(header, 8, maxVersion);
                  //ByteArray.setShort(header, 12, (short)tocEntries);
                  ByteArray.setInt(header, 14, (int)outSize);
                  //Arrays.fill(header, 18, 32, (byte)0);
                  out.print("Writing header... ");
                  output.setPosition(0);
                  output.write(header, 0, header.length);
                  out.println("OK");
                  out.print("Writing TOC... ");
                  for (i = 0; i < tocEntries; i++) {
                     tocEntry = toc[i];
                     output.write(tocEntry, 0, 46 + tocEntry[13]);
                  }
                  out.println("OK");
                  // ** End of change
                  out.println("Repacking finished");
                  return outSize;
               } else throw new ACAException("Physical filesize smaller than ACA filesize");
            } else throw new ACAException("Unsupported ACA file version");
         } else throw new ACAException("Invalid file id");
      } else throw new ACAException("File too small to be an ACA file");
   }

   public void clearTocTable() {
      for (int i = 0; i < toc.length; i++) {
         toc[i] = null;
      }
   }

   public boolean isContentChecking() {
      return contentCheck;
   }

   public void setContentChecking(boolean contentCheck) {
      this.contentCheck = contentCheck;
   }


   public static void main(String[] args) {
      System.out.println("ACAPacker 0.8 by KenD00");
      System.out.println();
      int mode = -1;
      int crypto = 2;
      int currentArg = 0;
      if (args.length > 1) {
         String cli = args[currentArg];
         if (cli.startsWith("-")) {
            for (int i = 1; i < cli.length(); i++) {
               switch (cli.charAt(i)) {
               case 'd':
               case 'D':
                  mode = 0;
                  break;
               case 'p':
               case 'P':
                  mode = 1;
                  break;
               case 'r':
               case 'R':
                  mode = 2;
                  break;
               case 'l':
               case 'L':
                  mode = 3;
                  break;
               case 'c':
               case 'C':
                  if (i + 1 < cli.length()) {
                     switch (cli.charAt(i + 1)) {
                     case '0':
                        crypto = 0;
                        break;
                     case '1':
                        crypto = 1;
                        break;
                     case '2':
                        crypto = 2;
                     }
                     i += 1;
                  }
                  break;
               }
            }
            currentArg += 1;
         }
      }
      switch (mode) {
      case 0:
         if (currentArg + 2 == args.length) {
            File in = new File(args[currentArg]).getAbsoluteFile();
            File out = new File(args[currentArg + 1]).getAbsoluteFile();
            if (in.isFile()) {
               if (!out.exists()) {
                  out.mkdirs();
               }
               if (out.isDirectory()) {
                  ACAPacker packer = initACAPacker(crypto, in);
                  if (packer != null) {
                     try {
                        FileSource inSrc = new FileSource(in, FileSource.R_MODE);
                        try {
                           long written = packer.dePack(inSrc, out);
                           inSrc.close();
                           System.out.println("Written: " + written);
                        }
                        catch (ACAException e) {
                           System.out.println("Error: " + e.getMessage());
                           inSrc.close();
                           System.exit(2);
                        }
                        System.exit(0);
                     }
                     catch (IOException e) {
                        System.out.println("Error: " + e.getMessage());
                        e.printStackTrace();
                     }
                  }
               } else System.out.println("Error: output is not a directory");
            } else System.out.println("Error: input file not found");
         } else System.out.println("Error: invalid number of arguments");
         System.exit(1);
         break;
      case 1:
         if (currentArg + 2 == args.length) {
            File in = new File(args[currentArg]).getAbsoluteFile();
            File inDirectory = in.getParentFile();
            File out = new File(args[currentArg + 1]).getAbsoluteFile();
            if (in.isFile()) {
               if (!out.isDirectory()) {
                  try {
                     LinkedList<File> inList = new LinkedList<File>();
                     BufferedReader inReader = new BufferedReader(new InputStreamReader(new FileInputStream(in), "US-ASCII"));
                     for (;;) {
                        String inFileName = inReader.readLine();
                        if (inFileName != null) {
                           File inFile = new File(inFileName);
                           if (!inFile.isAbsolute()) {
                              inList.add(new File(inDirectory, inFileName));
                           } else {
                              inList.add(inFile);
                           }
                        } else {
                           break;
                        }
                     }
                     inReader.close();
                     // FIXME: This makes no sense with enabled AACS processing, the input list can never be on the disc!
                     ACAPacker packer = initACAPacker(crypto, in);
                     if (packer != null) {
                        try {
                           long written = packer.pack(inList, out);
                           System.out.println("Written: " + written);
                        }
                        catch (ACAException e) {
                           System.out.println("Error: " + e.getMessage());
                           System.exit(2);
                        }
                        System.exit(0);
                     }
                  }
                  catch (IOException e) {
                     System.out.println("Error: " + e.getMessage());
                     e.printStackTrace();
                  }
               } else System.out.println("Error: output is a directory");
            } else System.out.println("Error: input file not found"); 
         } else System.out.println("Error: invalid number of arguments"); 
         System.exit(1);
         break;
      case 2:
         if (currentArg + 2 == args.length) {
            File in = new File(args[currentArg]).getAbsoluteFile();
            File out = new File(args[currentArg + 1]).getAbsoluteFile();
            if (in.isFile()) {
               if (!out.isDirectory()) {
                  try {
                     ACAPacker packer = initACAPacker(crypto, in);
                     if (packer != null) {
                        FileSource inSource = new FileSource(in, FileSource.R_MODE);
                        FileSource outSource = new FileSource(out, FileSource.RW_MODE);
                        try {
                           long written = packer.rePack(inSource, outSource);
                           // TODO: second close gets lost if first close throws an exception
                           outSource.close();
                           inSource.close();
                           System.out.println("Written: " + written);
                        }
                        catch (ACAException e) {
                           System.out.println("Error: " + e.getMessage());
                           // TODO: second close gets lost if first close throws an exception
                           outSource.close();
                           inSource.close();
                           System.exit(2);
                        }
                        System.exit(0);
                     }
                  }
                  catch (IOException e) {
                     System.out.println("Error: " + e.getMessage());
                     e.printStackTrace();
                  }
               } else System.out.println("Error: output is a directory");
            } else System.out.println("Error: input file not found"); 
         } else System.out.println("Error: invalid number of arguments"); 
         System.exit(1);
         break;
      case 3:
         if (currentArg + 1 == args.length) {
            File in = new File(args[currentArg]).getAbsoluteFile();
            if (in.isFile()) {
               ACAPacker packer = new ACAPacker(new PrintStreamPrinter(System.out));
               try {
                  FileSource inSrc = new FileSource(in, FileSource.R_MODE);
                  try {
                     packer.list(inSrc);
                     inSrc.close();
                     System.exit(0);
                  }
                  catch (ACAException e) {
                     System.out.println("Error: " + e.getMessage());
                     inSrc.close();
                     System.exit(2);
                  }
               }
               catch (IOException e) {
                  System.out.println("Error: " + e.getMessage());
                  e.printStackTrace();
               }
               System.exit(0);
            } else System.out.println("Error: input file not found");
         } else System.out.println("Error: invalid number of arguments");
         System.exit(1);
         break;
      }
      System.out.println("Usage: ACAPacker -parameters input [output]");
      System.out.println();
      System.out.println("Parameters:");
      System.out.println(" -d    Depack mode");
      System.out.println("       input  : input ACA file");
      System.out.println("       output : output directory");
      System.out.println("       Depacks the contents of the input file into the output directory");
      System.out.println("       Existing files in the output directory get overwritten");
      System.out.println();
      System.out.println(" -p    Pack mode");
      System.out.println("       input  : input file list file");
      System.out.println("                A text file with a filename in every line");
      System.out.println("                If the filename denotes no / a relative path the path is");
      System.out.println("                resolved against the path of the input file list file");
      System.out.println("       output : output ACA file");
      System.out.println("       Packs the files from the input list into the output ACA file");
      System.out.println("       The output ACA file gets overwritten if it already exists");
      System.out.println();
      System.out.println(" -r    Repack mode");
      System.out.println("       input  : input ACA file");
      System.out.println("       output : output ACA file");
      System.out.println("       Repacks the input ACA file into the output ACA file,");
      System.out.println("       only useful with -c2");
      System.out.println("       The output ACA file gets overwritten if it already exists");
      System.out.println();
      System.out.println(" -l    List mode");
      System.out.println("       input  : input ACA file");
      System.out.println("       Lists the header information and prints out the TOC of the");
      System.out.println("       input ACA file");
      System.out.println();
      System.out.println(" -c[x] Cryptography parameters, used in all modes except list mode");
      System.out.println("       x = 0 : Do not detect AACS");
      System.out.println("       x = 1 : Detect AACS but do not process it");
      System.out.println("       x = 2 : Detect and remove AACS (default)");
      System.out.println("               This mode requires the AACS directory to be present");
      System.out.println("               in the parent directory of the directory of the input file");
   }

   /**
    * Used by the main method to initialize the ACAPacker.
    * 
    * @param crypto Crypto mode to initialize
    * @param srcFile The source file, this must be a file in the ADV_OBJ directory
    * @return The created ACAPAcker or null if an error occurred
    */
   private static ACAPacker initACAPacker(int crypto, File srcFile) {
      ACAPacker returnValue = null;
      if (crypto > 1) {
         KeyDataFile kdf = null;
         AACSDecrypter aacsDec = null;
         try {
            kdf = new KeyDataFile(new File("KEYDB.cfg"));
         }
         catch (IOException e) {
            System.out.println(e.getMessage());
            srcFile = null;
         }
         if (srcFile != null) {
            srcFile = srcFile.getParentFile();
            if (srcFile != null) {
               srcFile = srcFile.getParentFile();
               if (srcFile != null) {
                  if (srcFile.isDirectory()) {
                     DiscSet ws = new DiscSet(DiscSet.HD_ADVANCED_V, srcFile);
                     srcFile = new File(srcFile, "AACS");
                     if (srcFile.isDirectory()) {
                        ws.aacsDir = srcFile;
                        try {
                           aacsDec = new AACSDecrypter(new PrintStreamPrinter(System.out), kdf);
                           aacsDec.identifyDisc(ws);
                           aacsDec.initDisc(ws);
                           aacsDec.init(ws);
                        }
                        catch (AACSException e) {
                           System.out.println(e.getMessage());
                           srcFile = null;
                        }
                        try {
                           kdf.close();
                        }
                        catch (IOException e) {
                           System.out.println("Warning: " + e.getMessage());
                        }
                     } else {
                        System.out.println("AACS directory not found");
                        srcFile = null;
                     }
                  } else {
                     System.out.println("Root is not a directory");
                     srcFile = null;
                  }
               } else {
                  System.out.println("Could not traverse into root directory");
               }
            } else {
               System.out.println("Source has no parent directory");
            }
         }
         if (srcFile == null) {
            System.out.println("Error: AACS not initialized");
            return null;
         } else {
            returnValue = new ACAPacker(new PrintStreamPrinter(System.out), aacsDec);
         }
      } else {
         returnValue = new ACAPacker(new PrintStreamPrinter(System.out));
      }
      if (crypto > 0) {
         returnValue.setContentChecking(true);
      }
      return returnValue;
   }

}
