/*
 * 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.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;

import bdvm.vm.BDVMException;
import bdvm.vm.BDVMInterface;

import dumphd.aacs.AACSDecrypter;
import dumphd.aacs.AACSException;
import dumphd.bdplus.BDVMLoader;
import dumphd.bdplus.ConversionTable;
import dumphd.bdplus.SubTable;
import dumphd.util.ByteSource;
import dumphd.util.PrintStreamPrinter;
import dumphd.util.FileSource;
import dumphd.util.MessagePrinter;
import dumphd.util.SimpleFilenameFilter;
import dumphd.util.StreamSource;
import dumphd.util.Utils;

import dumphd.gui.Manager;

/**
 * Main class of DumpHD. Parses the given source and processes the files.
 * Acts as interface for the GUI.
 * 
 * @author KenD00
 */
public final class DumpHD {

   /**
    * Name of the program
    */
   public final static String PROGRAM_NAME = "DumpHD";
   /**
    * Program version
    */
   public final static String PROGRAM_VERSION = "0.61";
   /**
    * Program author
    */
   public final static String PROGRAM_AUTHOR = "KenD00";


   /**
    * The MessagePrinter used for textual output 
    */
   private MessagePrinter out = null;
   /**
    * The Key Database
    */
   private KeyDataFile keyDataFile = null;
   /**
    * The AACS module
    */
   private AACSDecrypter aacsDec = null;
   /**
    * The BDVM module for BD+ removal
    */
   private BDVMInterface bdvm = null;
   /**
    * The ACA packer
    */
   private ACAPacker acaPacker = null;
   /**
    * Stores all currently present disc types
    */
   private ArrayList<DiscSet> discSets = new ArrayList<DiscSet>(6);
   /**
    * If true, streaming mode is used, otherwise normal mode
    */
   private boolean streaming = false;


   /**
    * Creates a new DumpHD which writes its textual output to the MessagePrinter received from Utils.
    * 
    * @param streaming If true, streaming mode will be used. In streaming mode only EVO's / M2TS's files will be played back.
    *                  They will be output to stdout, all usual textual output will be output to stderr. Non streamable parts
    *                  of the file (currently only ADV_PCKs in EVOs) will not be decrypted and passed through as is.
    *                  If false, the files will be normally output.  
    */
   public DumpHD(boolean streaming) throws Exception {
      this.out = Utils.getMessagePrinter(); 
      this.streaming = streaming;
      createDumpHD();
   }

   /**
    * Creates a new DumpHD with the given MessagePrinter.
    * 
    * @param mp The MessagePrinter the DumpHD object will use for textual output
    * @param streaming {@link dumphd.core.DumpHD#DumpHD(boolean)}
    */
   public DumpHD(MessagePrinter mp, boolean streaming) throws Exception {
      this.out = mp;
      this.streaming = streaming;
      createDumpHD();
   }

   /**
    * Actually creates the DumpHD object.
    * 
    * FIXME: Throw a more specific exception type
    * 
    * @throws Exception An error occurred while creating a required component
    */
   private void createDumpHD() throws Exception {
      out.println(DumpHD.PROGRAM_NAME + " " + DumpHD.PROGRAM_VERSION + " by " + DumpHD.PROGRAM_AUTHOR);
      out.println();
      out.print("Opening Key Data File... ");
      try {
         keyDataFile = new KeyDataFile(new File("KEYDB.cfg"));
      }
      catch (FileNotFoundException e1) {
         out.println("FAILED");
         throw new Exception("Key Data File not found");
      }
      catch (IOException e2) {
         out.println("FAILED");
         throw new Exception("Could not open Key Data File");
      }
      out.println("OK");
      // Create AACS module
      try {
         aacsDec = new AACSDecrypter(out, keyDataFile);
      }
      catch (AACSException e) {
         throw new Exception(e.getMessage());
      }
      // Create BDVM module
      try {
         out.print("Loading BDVM... ");
         BDVMLoader loader = new BDVMLoader();
         loader.loadClass(new File("bdvmdbg.jar"));
         bdvm = loader.newInstance();
         out.println("OK");
         out.println(bdvm.getVersionString());
      } catch (BDVMException e) {
         out.println("FAILED");
         out.println(e.getMessage());
         out.println("Automatic BD+ removal disabled, specify a Conversion Table manually to remove BD+ if necessary");
      }
      // Create ACA-Packer
      acaPacker = new ACAPacker(out, aacsDec);
      acaPacker.setContentChecking(true);
      out.println();
   }

   /**
    * Parses the given source and detects the disc type(s) it contains.
    * Stores all found disc types in this DumpHD object.
    * 
    * @param src The source to parse
    * @param inFiles If not null, only the files listed in these ArrayList will be processed, otherwise the whole disc
    *                The path of the files needs to be relative to the disc source directory.
    * @param convTable If not null this Conversion Table binary file will be used to remove BD+ (if present) 
    * @throws IOException An I/O error occurred
    */
   public void initSource(File src, ArrayList<String> inFiles, String convTable) throws IOException {
      boolean HD_STANDARD = false;
      discSets.clear();
      out.println("Initializing source... ");
      if (!src.exists()) {
         throw new IOException(src.toString() + " does not exist");
      }
      if (!src.isDirectory()) {
         throw new IOException(src.toString() + " is not a directory");
      }
      if (inFiles != null) {
         if (inFiles.isEmpty()) {
            inFiles = null;
         }
      }
      if (convTable != null) {
         if (convTable.isEmpty()) {
            convTable = null;
         }
      }
      // ### Identify content types ###
      // FIXME: Don't know how to identify HD-DVD Audio content
      // Check for HD-DVD Standard Content
      File idFile = new File(src, "HVDVD_TS" + File.separator + "HV000I01.IFO");
      if (idFile.isFile()) {
         out.println("Disc type found: " + DiscSet.DISCTYPE_NAMES[DiscSet.HD_STANDARD_V]);
         DiscSet ds = new DiscSet(DiscSet.HD_STANDARD_V, src);
         File aacsDir = new File(src, "AACS");
         if (aacsDir.isDirectory()) {
            ds.aacsDir = aacsDir;
         }
         discSets.add(ds);
         HD_STANDARD = true;
      }
      // Check for HD-DVD Advanced Content
      idFile = new File(src, "ADV_OBJ" + File.separator + "DISCID.DAT");
      if (idFile.isFile()) {
         out.println("Disc type found: " + DiscSet.DISCTYPE_NAMES[DiscSet.HD_ADVANCED_V]);
         DiscSet ds = new DiscSet(DiscSet.HD_ADVANCED_V, src);
         File aacsDir = new File(src, "AACS");
         if (aacsDir.isDirectory()) {
            ds.aacsDir = aacsDir;
         }
         discSets.add(ds);
         // FIXME: Temporary, remove when full parsing is integrated!
         if (HD_STANDARD) {
            discSets.remove(0);
            HD_STANDARD = false;
         }
      }
      // Check for BD MV content
      idFile = new File(src, "BDMV" + File.separator + "index.bdmv");
      if (idFile.isFile()) {
         // TODO: BD Recordable has the same file, currently i don't know any other "high level" way to distinguish both types
         //       than checking for the specific AACS component. Check only for the directory or also for the Unit Key file?
         File aacsDir = new File(src, "AACS_mv");
         if (aacsDir.isDirectory()) {
            // AACS for BD Recordable MV present, assume BDR_MV
            out.println("Disc type found: " + DiscSet.DISCTYPE_NAMES[DiscSet.BDR_MV]);
            DiscSet ds = new DiscSet(DiscSet.BDR_MV, src);
            ds.aacsDir = aacsDir;
            discSets.add(ds);
         } else {
            // No AACS for BD Recordable MV present, assume BD_MV
            out.println("Disc type found: " + DiscSet.DISCTYPE_NAMES[DiscSet.BD_MV]);
            DiscSet ds = new DiscSet(DiscSet.BD_MV, src);
            aacsDir = new File(src, "AACS");
            if (aacsDir.isDirectory()) {
               ds.aacsDir = aacsDir;
            }
            // Check for BD+
            if (new File(src, "BDSVM").isDirectory()) {
               ds.bdplus = true;
            }
            discSets.add(ds);
         }
      }
      // Check for BD AV content
      idFile = new File(src, "BDAV" + File.separator + "info.bdav");
      if (idFile.isFile()) {
         out.println("Disc type found: " + DiscSet.DISCTYPE_NAMES[DiscSet.BDR_AV]);
         DiscSet ds = new DiscSet(DiscSet.BDR_AV, src);
         File aacsDir = new File(src, "AACS" + File.separator + "AACS_av");
         if (aacsDir.isDirectory()) {
            ds.aacsDir = aacsDir;
         }
         discSets.add(ds);
      }

      // Check if anything was found, return if not
      if (discSets.size() > 0) {
         if (inFiles == null) {
            out.println("Collecting input files...");
         } else {
            out.println("Processing input files...");
         }
      } else {
         out.println("No disc type found");
         return;
      }

      // Process input files depending on mode
      if (inFiles == null) {
         // *****************
         // *** Disc mode ***
         // *****************
         ArrayList<String> streamSet = null;
         Iterator<DiscSet> it = discSets.iterator();
         while (it.hasNext()) {
            DiscSet ds = it.next();
            // FIXME: Don't know where to look for HD-DVD Audio files
            switch (ds.contentType) {
            case DiscSet.HD_STANDARD_V:
               streamSet = new ArrayList<String>(8192);
               if (streaming) {
                  Utils.scanForFilenames(new File(ds.srcDir, "HVDVD_TS"), "HVDVD_TS", streamSet, true, new SimpleFilenameFilter(".*\\.EVO", true, false), true);
               } else {
                  Utils.scanForFilenames(new File(ds.srcDir, "HVDVD_TS"), "HVDVD_TS", streamSet, true, null, true);
               }
               Collections.sort(streamSet);
               ds.streamSets.put(0, streamSet);
               break;
            case DiscSet.HD_ADVANCED_V:
               streamSet = new ArrayList<String>(8192);
               if (streaming) {
                  Utils.scanForFilenames(new File(ds.srcDir, "HVDVD_TS"), "HVDVD_TS", streamSet, true, new SimpleFilenameFilter(".*\\.EVO", true, false), true);
               } else {
                  Utils.scanForFilenames(new File(ds.srcDir, "HVDVD_TS"), "HVDVD_TS", streamSet, true, null, true);
                  Utils.scanForFilenames(new File(ds.srcDir, "ADV_OBJ"), "ADV_OBJ", ds.fileSet, true, null, true);
               }
               Collections.sort(streamSet);
               ds.streamSets.put(0, streamSet);
               Collections.sort(ds.fileSet);
               break;
            case DiscSet.BD_MV:
            case DiscSet.BDR_MV:
               streamSet = new ArrayList<String>(8192);
               if (streaming) {
                  Utils.scanForFilenames(new File(ds.srcDir, "BDMV"), "BDMV", streamSet, true, new SimpleFilenameFilter(".*\\.M2TS", true, false), true);
               } else {
                  Utils.scanForFilenames(new File(ds.srcDir, "BDMV"), "BDMV", streamSet, true, null, true);
                  // Do recordables also have this? It doesn't hurt if they don't 
                  Utils.scanForFilenames(new File(ds.srcDir, "CERTIFICATE"), "CERTIFICATE", ds.fileSet, true, null, true);
               }
               Collections.sort(streamSet);
               ds.streamSets.put(0, streamSet);
               Collections.sort(ds.fileSet);
               break;
            case DiscSet.BDR_AV:
               int currentStreamSet = 0;
               String checkString = "BDAV";
               File checkDir = new File(ds.srcDir, checkString);
               while (checkDir.isDirectory()) {
                  streamSet = new ArrayList<String>(8192);
                  if (streaming) {
                     Utils.scanForFilenames(checkDir, checkString, streamSet, true, new SimpleFilenameFilter(".*\\.M2TS", true, false), true);
                  } else {
                     Utils.scanForFilenames(checkDir, checkString, streamSet, true, null, true);
                  }
                  Collections.sort(streamSet);
                  ds.streamSets.put(currentStreamSet, streamSet);
                  currentStreamSet++;
                  checkString = String.format("BDAV%1$d", currentStreamSet);
                  checkDir = new File(ds.srcDir, checkString);
               }
               break;
            }
         }
      } else {
         // *****************
         // *** File mode ***
         // *****************
         if (discSets.size() > 0) {
            DiscSet currentDs = discSets.get(0);
            Iterator<String> fileIt = inFiles.iterator();
            // The currently used fileSet, MUST be set to null after each change of currentDs!!
            ArrayList<String> fileSet = null;
            // The number of the currently used fileSet, only used by BDAV
            int fileSetNumber = 0;
            while (fileIt.hasNext()) {
               // TODO: Check if inFile exists?
               String inFile = fileIt.next();
               String upperInFile = inFile.toUpperCase();
               // FIXME: Don't know where to look for HD-DVD Audio files
               if (upperInFile.startsWith("HVDVD_TS")) {
                  // HD-DVD file
                  if (!streaming || upperInFile.endsWith(".EVO")) {
                     if (currentDs.contentType != DiscSet.HD_STANDARD_V && currentDs.contentType != DiscSet.HD_ADVANCED_V) {
                        DiscSet tmp = getFirstDiscSet(DiscSet.HD_STANDARD_V | DiscSet.HD_ADVANCED_V);
                        if (tmp != null) {
                           currentDs = tmp;
                           fileSet = null;
                        } else {
                           out.println("Disc does not contain the required content type, ignoring: " + inFile);
                           continue;
                        }
                     }
                     if (fileSet == null) {
                        fileSet = getStreamSet(currentDs, 0);
                     }
                     fileSet.add(inFile);
                  } else {
                     out.println("Ignored because of streaming: " + inFile);
                  }
               } else if (upperInFile.startsWith("ADV_OBJ")) {
                  // HD-DVD Advanced Content file
                  if (!streaming) {
                     if (currentDs.contentType != DiscSet.HD_ADVANCED_V) {
                        DiscSet tmp = getFirstDiscSet(DiscSet.HD_ADVANCED_V);
                        if (tmp != null) {
                           currentDs = tmp;
                           fileSet = null;
                        } else {
                           out.println("Disc does not contain the required content type, ignoring: " + inFile);
                           continue;
                        }
                     }
                     currentDs.fileSet.add(inFile);
                  } else {
                     out.println("Ignored because of streaming: " + inFile);
                  }
               } else if (upperInFile.startsWith("BDMV")) {
                  // Blu-Ray BDMV file
                  if (!streaming || upperInFile.endsWith(".M2TS")) {
                     if (currentDs.contentType != DiscSet.BD_MV && currentDs.contentType != DiscSet.BDR_MV) {
                        DiscSet tmp = getFirstDiscSet(DiscSet.BD_MV | DiscSet.BDR_MV);
                        if (tmp != null) {
                           currentDs = tmp;
                           fileSet = null;
                        } else {
                           out.println("Disc does not contain the required content type, ignoring: " + inFile);
                           continue;
                        }
                     }
                     if (fileSet == null) {
                        fileSet = getStreamSet(currentDs, 0);
                     }
                     fileSet.add(inFile);
                  } else {
                     out.println("Ignored because of streaming: " + inFile);
                  }
               } else if (upperInFile.startsWith("BDAV")) {
                  // Blu-Ray BDAV file
                  if (!streaming || upperInFile.endsWith(".M2TS")) {
                     if (currentDs.contentType != DiscSet.BDR_AV) {
                        DiscSet tmp = getFirstDiscSet(DiscSet.BDR_AV);
                        if (tmp != null) {
                           currentDs = tmp;
                           fileSet = null;
                        } else {
                           out.println("Disc does not contain the required content type, ignoring: " + inFile);
                           continue;
                        }
                     }
                     // Determine fileSetNumber first
                     if (upperInFile.length() > 4) {
                        try {
                           int tmp = Integer.parseInt(inFile.substring(4, 5));
                           if (tmp > 0 && tmp < 5) {
                              fileSetNumber = tmp;
                           } else {
                              out.println("Invalid BDAV Aux Directory number, ignoring: " + inFile);
                              continue;
                           }
                        }
                        catch (NumberFormatException e) {
                           // Ignore it, this can happen with the Basic BDAV directory and is not an error
                        }
                     } else if (fileSetNumber != 0) {
                        fileSetNumber = 0;
                     }
                     if (fileSet == null) {
                        fileSet = getStreamSet(currentDs, fileSetNumber);
                     }
                     fileSet.add(inFile);
                  } else {
                     out.println("Ignored because of streaming: " + inFile);
                  }
               } else if (upperInFile.startsWith("CERTIFICATE")) {
                  if (!streaming) {
                     if (currentDs.contentType != DiscSet.BD_MV && currentDs.contentType != DiscSet.BDR_MV) {
                        DiscSet tmp = getFirstDiscSet(DiscSet.BD_MV | DiscSet.BDR_MV);
                        if (tmp != null) {
                           currentDs = tmp;
                           fileSet = null;
                        } else {
                           out.println("Disc does not contain the required content type, ignoring: " + inFile);
                           continue;
                        }
                     }
                     currentDs.fileSet.add(inFile);
                  } else {
                     out.println("Ignored because of streaming: " + inFile);
                  }
               } else {
                  // Unknown file
                  out.println("Unknown file type, ignoring: " + inFile);
               }
            }
         }
      }

      // Process a given Conversion Table
      if (convTable != null) {
         File convTableFile = new File(convTable).getAbsoluteFile();
         DiscSet ds = getFirstDiscSet(DiscSet.BD_MV);
         if (ds != null) {
            // Check if DiscSet is BD+ protected
            if (ds.bdplus) {
               // This may throw an exception but it is OK to abort if something is wrong with the conversion table
               // TODO: Maybe this is not OK? All code "ignores" errors currently.
               FileSource convTableSrc = new FileSource(convTableFile, ByteSource.R_MODE);
               ds.convTable = new ConversionTable(convTableSrc);
            } else {
               out.println("Disc is not BD+ protected, ignoring Conversion Table: " + convTableFile);
            }
         } else {
            out.println("Disc does not contain the required content type, ignoring Conversion Table: " + convTableFile);
         }
      }

      out.println("Source initialized");
   }

   /**
    * Identifies the current present disc types.
    * If the source has not been initialized or no disc types are present, this method does nothing. 
    * 
    * @return An unmodifyable collection of all present disc types
    */
   public Collection<DiscSet> identifySource() {
      Iterator<DiscSet> it = discSets.iterator();
      while (it.hasNext()) {
         DiscSet ds = it.next();
         try {
            aacsDec.identifyDisc(ds);
         }
         catch (AACSException e) {
            out.println(e.getMessage());
         }
      }
      return Collections.unmodifiableList(discSets);        
   }

   /**
    * Dumps the present disc types.
    * The source must have been identified or it will not be dumped.
    * A destination directory must have been set to all present disc types.
    * If a disc type is not marked as selected, it will be skipped.
    */
   public void dumpSource() {
      ByteSource inSource = null;
      ByteSource outSource = null;
      HashSet<File> knownDirectories = new HashSet<File>(64);

      if (streaming) {
         outSource = new StreamSource(System.out, StreamSource.W_MODE, false);
      }
      Iterator<DiscSet> discIt = discSets.iterator();
      while (discIt.hasNext()) {
         DiscSet ds = discIt.next();
         if (!ds.selected) {
            out.println("Skipping disc set:");
            out.println(ds.toString());
            continue;
         }
         out.println("Processing disc set:");
         out.println(ds.toString());
         if (!streaming) {
            if (ds.dstDir == null) {
               out.println("No destination set, skipping disc set");
               continue;
            } else if (ds.dstDir.exists() && !ds.dstDir.isDirectory()) {
               out.println("Destination is not a directory, skipping disc set");
               continue;
            } else if (ds.srcDir.equals(ds.dstDir)) {
               out.println("Source and destination are the same, skipping disc set");
               continue;
            }
         }
         // Initialize AACS Decrypter
         boolean aacsInitialized = false;
         try {
            aacsDec.initDisc(ds);
            aacsDec.init(ds);
            aacsInitialized = true;
         }
         catch (AACSException e) {
            out.println("Error initializing AACS, skipping disc set");
            out.println(e.getMessage());
         }
         if (!aacsInitialized) {
            continue;
         }
         // Initialize BD+ Decrypter
         if (ds.bdplus) {
            if (ds.convTable == null && bdvm != null) {
               try {
                  out.print("Initializing the BDVM... ");
                  bdvm.initVM(ds.srcDir, ds.keyData.getVid());
                  out.println("OK");
                  out.print("Executing the BDVM... ");
                  bdvm.run();
                  out.println("OK");
                  byte[] rawTable = bdvm.getRAWConversionTable();
                  if (rawTable != null) {
                     try {
                        out.print("Parsing the Conversion Table... ");
                        ConversionTable convTable = new ConversionTable();
                        convTable.parse(rawTable, 0);
                        out.println("OK");
                        ds.convTable = convTable;
                     } catch (IndexOutOfBoundsException e) {
                        out.println("FAILED");
                        out.println("Error! Parsing the Conversion Table failed, dumping may fail!");
                     }
                  } else {
                     out.println("Error! The BDVM has not produced a Conversion Table, dumping may fail!");
                  }
               } catch (BDVMException e) {
                  out.println("ERROR");
                  out.println(e.getMessage());
                  out.println("Error! Failed to execute the BDVM, dumping may fail!");
               }
            } else {
               out.println("Error! BD+ detected and no Conversion Table present, dumping may fail!");
            }
         }

         File inFile = null;
         File outFile = null;
         String filename = null;
         // First process all streamSets
         Iterator<Map.Entry<Integer, ArrayList<String>>> setIt = ds.streamSets.entrySet().iterator();
         while (setIt.hasNext()) {
            Map.Entry<Integer, ArrayList<String>> currentEntry = setIt.next();
            int currentSet = currentEntry.getKey();

            Iterator<String> fileIt = currentEntry.getValue().iterator();
            while (fileIt.hasNext()) {
               // Current filename, this may include a directory part!!
               filename = fileIt.next();
               out.println("Processing: " + filename);
               inFile = new File(ds.srcDir, filename);
               try {
                  if (!streaming) {
                     outFile = new File(ds.dstDir, filename);
                     File outDir = outFile.getParentFile();
                     if (knownDirectories.add(outDir)) {
                        // No need to check if it is a directory, the list only contains files, so this is true
                        if (!outDir.exists()) {
                           try {
                              if (!outDir.mkdirs()) {
                                 throw new IOException("Failed creating destination directory: " + outDir);
                              }
                           }
                           catch (SecurityException e) {
                              throw new IOException("Insufficient rights to create destination directory: " + outDir);
                           }
                        }
                     }
                     if (outFile.exists()) {
                        out.println("Destination file already exists, skipping file");
                        continue;
                     }
                  }
                  inSource = new FileSource(inFile, FileSource.R_MODE);
                  if (!streaming) {
                     outSource = new FileSource(outFile, FileSource.RW_MODE);
                  }
                  switch (ds.contentType) {
                  case DiscSet.HD_STANDARD_V:
                  case DiscSet.HD_STANDARD_A:
                  case DiscSet.HD_ADVANCED_V:
                  case DiscSet.HD_ADVANCED_A:
                     if (filename.toUpperCase().endsWith(".EVO")) {
                        Collection<MultiplexedArf> arfs = aacsDec.decryptEvob(inSource, outSource);
                        if (!streaming && arfs.size() > 0) {
                           out.println("Processing multiplexed ARFs...");
                           Iterator<MultiplexedArf> arfIt = arfs.iterator();
                           while (arfIt.hasNext()) {
                              MultiplexedArf arf = arfIt.next();
                              ByteSource inArf = arf.getReader();
                              ByteSource outArf = arf.getWriter();
                              out.println(inArf.getFile().getName());
                              if (inArf.getFile().getName().toUpperCase().endsWith(".ACA")) {
                                 try {
                                    acaPacker.rePack(inArf, outArf);
                                 }
                                 catch (ACAException acae) {
                                    out.println("ACA-Exception: " + acae.getMessage());
                                    out.println("Skipping ARF");
                                 }
                              } else {
                                 try {
                                    aacsDec.decryptArf(inArf, outArf);
                                 }
                                 catch (AACSException aacse) {
                                    out.println("AACS-Exception: " + aacse.getMessage());
                                    out.println("Skipping ARF");
                                 }
                              }
                              arf.close();
                           }
                           out.println("Multiplexed ARFs processed");
                        }
                     } else {
                        // No need to check for ARFs, streamSets don't contain ARFs
                        // To be on the safe side, check for streaming
                        if (!streaming) {
                           Utils.copyBs(inSource, outSource, inSource.size());
                        }
                     }
                     break;
                  case DiscSet.BD_MV:
                     if (filename.toUpperCase().endsWith(".M2TS")) {
                        int clipNr = Utils.getClipNumber(inFile.getName());
                        if (clipNr < 0) {
                           out.println("Error parsing filename, assuming clip number 0");
                           clipNr = 0;
                        }
                        SubTable subTable = null;
                        if (ds.bdplus) {
                           // No need to emit warning about missing Conversion Table, this was already be done
                           if (ds.convTable != null) {
                              subTable = ds.convTable.getSubTable(clipNr);
                           }
                           if (subTable == null) {
                              out.println("Error! BD+ SubTable not found, dumping may fail");
                           }
                        }
                        // TODO: Not nice that the AACS Decrypter has to handle BD+ too, but currently there is no other way to do it
                        // TODO: Uses brute force approach to determine the CPS Unit Key
                        aacsDec.decryptTs(inSource, outSource, null, subTable);
                     } else {
                        // To be on the safe side, check for streaming
                        if (!streaming) {
                           Utils.copyBs(inSource, outSource, inSource.size());
                        }
                     }
                     break;
                  case DiscSet.BDR_MV:
                  case DiscSet.BDR_AV: {
                     // This is the plain filename without directory
                     String plainFilename = inFile.getName();
                     // Index of the CPS Unit Key to use
                     int keyIndex = 0;
                     if (filename.toUpperCase().endsWith(".M2TS")) {
                        // The number of the clip
                        int clipNr = Utils.getClipNumber(plainFilename);
                        if (clipNr < 0) {
                           out.println("Error parsing filename, assuming clip number 0");
                           clipNr = 0;
                        }
                        keyIndex = ds.keyData.getTcUnit(currentSet, clipNr);
                        if (keyIndex != 0) {
                           // Index of the CPS Unit Key found, decrypt the file
                           aacsDec.decryptTs(inSource, outSource, ds.keyData.getTuk(keyIndex), null);
                        } else {
                           // No CPS Unit Key index found, that means the file should not be encrypted, run it
                           // through the decrypter to see if its really unencrypted (emits warnings if not)
                           aacsDec.decryptTs(inSource, outSource, null, null);
                        }
                     } else {
                        // To be on the safe side, check for streaming
                        if (!streaming) {
                           if (ds.contentType == DiscSet.BDR_AV) {
                              // For BDAV, check if we are dealing with a Thumbnail Data file
                              boolean isTdf = false;
                              if (plainFilename.equals("menu.tdt1")) {
                                 isTdf = true;
                                 keyIndex = ds.keyData.getHeadUnit(currentSet, 0);
                              } else if (plainFilename.equals("mark.tdt1")) {
                                 isTdf = true;
                                 keyIndex = ds.keyData.getHeadUnit(currentSet, 1);
                              }
                              if (isTdf) {
                                 if (keyIndex != 0) {
                                    aacsDec.decryptTdf(inSource, outSource, ds.keyData.getTuk(keyIndex));
                                 } else {
                                    out.println("Error! CPS Unit Key index for Thumbnail Data file not found, copying file");
                                    Utils.copyBs(inSource, outSource, inSource.size());
                                 }
                              } else {
                                 Utils.copyBs(inSource, outSource, inSource.size());
                              }
                           } else {
                              Utils.copyBs(inSource, outSource, inSource.size());
                           }
                        }
                     }
                     break;
                  }
                  default:
                     // Unknown disc type, it's broken code if this section is reached!!
                     out.println("Error! Unknown disc type: " + ds.contentType + ", skipping file");
                  }
               }
               catch (IOException ei) {
                  out.println("I/O-Exception: " + ei.getMessage());
                  out.println("Skipping file");
               }
               catch (AACSException ea) {
                  out.println("AACS-Exception: " + ea.getMessage());
                  out.println("Skipping file");
               }
               finally {
                  try {
                     if (inSource != null) {
                        inSource.close();
                     }
                  }
                  catch (IOException e) {
                     out.println("Error closing input file: " + e.getMessage());
                  }
                  inSource = null;
                  if (!streaming) {
                     try {
                        if (outSource != null) {
                           outSource.close();
                        }
                     }
                     catch (IOException e) {
                        out.println("Error closing output file: " + e.getMessage());
                     }
                     outSource = null;
                  }
               }
            }
         }

         // Now process the fileSet
         if (streaming) {
            // In streaming mode the fileSet isn't processed
            continue;
         }
         // TODO: This is partially duplicate code, can this be avoided?
         Iterator<String> fileIt = ds.fileSet.iterator();
         while (fileIt.hasNext()) {
            // Current filename, this may include a directory part!!
            filename = fileIt.next();
            out.println("Processing: " + filename);
            inFile = new File(ds.srcDir, filename);
            try {
               outFile = new File(ds.dstDir, filename);
               File outDir = outFile.getParentFile();
               if (knownDirectories.add(outDir)) {
                  // No need to check if it is a directory, the list only contains files, so this is true
                  if (!outDir.exists()) {
                     try {
                        if (!outDir.mkdirs()) {
                           throw new IOException("Failed creating destination directory: " + outDir);
                        }
                     }
                     catch (SecurityException e) {
                        throw new IOException("Insufficient rights to create destination directory: " + outDir);
                     }
                  }
               }
               if (outFile.exists()) {
                  out.println("Destination file already exists, skipping file");
                  continue;
               }

               inSource = new FileSource(inFile, FileSource.R_MODE);
               outSource = new FileSource(outFile, FileSource.RW_MODE);
               switch (ds.contentType) {
               case DiscSet.HD_ADVANCED_A:
               case DiscSet.HD_ADVANCED_V:
                  if (filename.toUpperCase().endsWith(".ACA")) {
                     acaPacker.rePack(inSource, outSource);
                  }
                  else {
                     // This copies the file if its not encrypted
                     aacsDec.decryptArf(inSource, outSource);
                  }
                  break;
               default:
                  // For every other content type just copy the files
                  Utils.copyBs(inSource, outSource, inSource.size());
               }
            }
            catch (IOException ei) {
               out.println("I/O-Exception: " + ei.getMessage());
               out.println("Skipping file");
            }
            catch (ACAException ea) {
               out.println("ACA-Exception: " + ea.getMessage());
               out.println("Skipping file");
            }
            catch (AACSException eaa) {
               out.println("AACS-Exception: " + eaa.getMessage());
               out.println("Skipping file");
            }
            finally {
               try {
                  if (inSource != null) {
                     inSource.close();
                  }
               }
               catch (IOException e) {
                  out.println("Error closing input file: " + e.getMessage());
               }
               inSource = null;
               try {
                  if (outSource != null) {
                     outSource.close();
                  }
               }
               catch (IOException e) {
                  out.println("Error closing output file: " + e.getMessage());
               }
               outSource = null;
            }
         }
         out.println("Disc set processed");
      }
   }

   /**
    * Dumps the given source to the destination.
    * 
    * If source is a file, then only that file gets dumped. It is required that the directory AACS is in the parent directory of that file.
    * If source is a directory, then the complete HD-DVD / BluRay gets dumped. Source must be the root directory of the HD-DVD / BluRay.
    * 
    * @param srcStr Source to dump
    * @param dstStr Destination directory to dump into, this is null in streaming mode
    * @param inFiles If not null and not empty, only these files will be processed. They must be given relative to srcStr.
    * @param convTable If not null this Conversion Table binary file will be used to remove BD+ (if present)
    * @return If true, the dump was successful, if false, it failed
    */
   public boolean dump(String srcStr, String dstStr, ArrayList<String> inFiles, String convTable) {
      File src = new File(srcStr).getAbsoluteFile();
      File dst = null;
      if (!streaming) {
         dst = new File(dstStr).getAbsoluteFile();
      }
      out.println("Start time: " + new Date());
      out.println();
      // ***********************
      // *** Source checking ***
      // ***********************
      out.println("Checking source...");
      out.println("Source path: " + src.getPath());
      try {
         initSource(src, inFiles, convTable);
         if (discSets.size() == 0) {
            endMsg("Aborting");
            return false;
         }
      }
      catch (IOException e) {
         endMsg(e.getMessage());
         return false;
      }
      // *****************************
      // *** Source identification ***
      // *****************************
      out.println("Identifying source...");
      identifySource();
      out.println("Finished identifying source");
      // *************************
      // *** Destination check ***
      // *************************
      if (!streaming) {
         out.println("Checking destination...");
         out.println("Destination path: " + dst.getPath());
         if (!dst.exists() || dst.isDirectory()) {
            Iterator<DiscSet> setIt = discSets.iterator();
            while (setIt.hasNext()) {
               DiscSet ds = setIt.next();
               ds.dstDir = dst;
            }
         }
         else {
            endMsg("Destination is not a directory, aborting");
            return false;
         }
      }
      // ***************
      // *** Dumping ***
      // ***************
      out.println("Dumping source...");
      dumpSource();
      endMsg("Dump complete");
      return true;
   }

   public void close() {
      try {
         keyDataFile.close();
      }
      catch (IOException e) {
         out.println("Error closing Key Data File: " + e.getMessage());
      }
      keyDataFile = null;
   }

   public KeyDataFile getKeyDataFile() {
      return keyDataFile;
   }

   /**
    * Display the given message followed by the end time string
    * 
    * @param reason Message to display
    */
   private void endMsg(String reason) {
      out.println(reason);
      out.println();
      out.println("End time: " + new Date());
      out.println();
   }

   private DiscSet getFirstDiscSet(int type) {
      Iterator<DiscSet> it = discSets.iterator();
      while (it.hasNext()) {
         DiscSet ds = it.next();
         if ((ds.contentType & type) != 0) {
            return ds;
         }
      }
      return null;
   }

   private ArrayList<String> getStreamSet(DiscSet ds, int number) {
      // TODO: I could use ds.streamSets.get(int) alone because there should be no "null" key inside
      if (!ds.streamSets.containsKey(number)) {
         ArrayList<String> streamSet = new ArrayList<String>(64);
         ds.streamSets.put(number, streamSet);
         return streamSet;
      } else {
         return ds.streamSets.get(number);
      }
   }

   /**
    * Simple cmd parser to start the program
    * 
    * @param args First argument is the source, second is the destination directory
    */
   public static void main(String[] args) {
      // Check if parameters are given
      if (args.length == 0) {
         // Start the GUI
         new Manager();
      } else {
         //for (int i = 0; i < args.length; i++) {
         //	System.out.println(args[i]);
         //}
         // Simple command line parser
         boolean streaming = false;
         ArrayList<String> inFiles = new ArrayList<String>(64);
         String convTable = null;
         String input = null;;
         String output = null;
         int currentArg = 0;
         String option = null;
         // Parse options
         for(; currentArg < args.length; currentArg++) {
            if (args[currentArg].startsWith("--")) {
               // Long option
               option = args[currentArg].substring(2);
               if (option.startsWith("infile:")) {
                  // Input file
                  String tmp = option.substring(7);
                  if (!tmp.isEmpty()) {
                     inFiles.add(tmp);
                  } else {
                     System.out.println("No file given for infile");
                     System.exit(1);
                  }
               } else if (option.startsWith("convtable:")) {
                  // Conversion Table
                  convTable = option.substring(10);
                  if (convTable.isEmpty()) {
                     System.out.println("No file given for convtable");
                     System.exit(1);
                  }
               } else {
                  // Unknown long option
                  System.out.println(String.format("Unknown long option: \"%1$s\"", option));
                  System.exit(1);
               }
            } else if (args[currentArg].startsWith("-")) {
               // Short option
               option = args[currentArg].substring(1);
               // Help
               if (option.equals("h")) {
                  System.out.println(DumpHD.PROGRAM_NAME + " " + DumpHD.PROGRAM_VERSION + " by " + DumpHD.PROGRAM_AUTHOR);
                  System.out.println();
                  System.out.println("Usage : DumpHD [options] source [destination]");
                  System.out.println();
                  System.out.println("Options:");
                  System.out.println("-h                 : This help screen");
                  System.out.println("--convtable:<file> : Use this Conversion Table to remove BD+");
                  System.out.println("--infile:<file>    : Only process the specified file. It must be given");
                  System.out.println("                     relative to source. This parameter can be specified");
                  System.out.println("                     multiple times, the files will be processed in the");
                  System.out.println("                     given order.");
                  System.out.println();
                  System.out.println("source             : Source to dump, must be the root directory of the disc");
                  System.out.println("destination        : directory where the dump is written");
                  System.out.println("                     If destination is omitted streaming mode is used.");
                  System.out.println("                     In streaming mode only EVO / M2TS files are processed,");
                  System.out.println("                     they are decrypted to stdout (EVOs without decrypting");
                  System.out.println("                     ADV_PCKs) in alphabetical order.");
                  System.out.println("                     Pipe the output to another program which can read from");
                  System.out.println("                     stdin.");
                  System.out.println("                     Textual output is written to stderr.");
                  System.out.println();
                  System.out.println("If started without arguments, the GUI is loaded");
                  System.exit(0);
               } else {
                  System.out.println(String.format("Unknown option: \"%1$s\"", option));
                  System.exit(1);
               }
            } else {
               // Not an option
               break;
            }
         }
         // Parse input
         if (currentArg < args.length) {
            input = args[currentArg++];
         } else {
            System.out.println("No source specified");
            System.exit(1);
         }
         // Parse output
         if (currentArg < args.length) {
            output = args[currentArg++];
         } else {
            streaming = true;
         }
         // Check for invalid following parameters
         if (currentArg < args.length) {
            System.out.println(String.format("Invalid parameter: \"%1$s\"", args[currentArg]));
            System.exit(1);
         }

         // Execute DumpHD
         try {
            DumpHD dumpHD = null;
            if (!streaming) {
               dumpHD = new DumpHD(false);
            } else {
               PrintStreamPrinter errPrinter = new PrintStreamPrinter(System.err);
               Utils.setMessagePrinter(errPrinter);
               dumpHD = new DumpHD(errPrinter, true);
            }
            dumpHD.dump(input, output, inFiles, convTable);
            dumpHD.close();
         }
         catch (Exception e) {
            Utils.getMessagePrinter().println(e.getMessage());
            System.exit(2);
         }
      }
   }

}
