/*
 * Copyright (C) 2007-2009 KenD00
 * 
 * This file is part of DumpHD.
 * 
 * DumpHD is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package dumphd.util;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;


/**
 * Scans EVO files, identifies pack types and counts them.
 * 
 * @author KenD00 
 */
public class PackScanner {

   public final static int SCAN_MODE = 0;
   public final static int EDIT_MODE = 1;

   private MessagePrinter out = null;

   private int mode = SCAN_MODE;
   private boolean quickScan = false;
   private boolean blanking = false;
   private boolean reverting = false;
   private boolean navFix = false;
   private boolean zeroCPI = false;
   private boolean logging = false;

   private PrintWriter logger = null;


   /**
    * Creates a new PackScanner in SCAN_MODE with all options disabled using the given MessagePrinter for textual output.
    */
   public PackScanner(MessagePrinter mp) {
      this.out = mp;
   }

   /**
    * Scans the given file and patches / reverts it, depending of current mode.
    * 
    * @param inFileName Input filename
    * @return True, if scanning was successful (or unexpected EOF, programming mistake, who cares?), false if an error occurred
    */
   public boolean scan(String inFileName) {
      File inFileAbsolute = new File(inFileName).getAbsoluteFile();
      FileSource inFile = null;
      out.println("File   : " + inFileAbsolute.getPath());
      try {
         if (mode == EDIT_MODE) {
            out.println("Mode   : EDIT_MODE");
            inFile = new FileSource(inFileAbsolute, FileSource.RW_MODE);
         } else {
            out.println("Mode   : SCAN_MODE");
            inFile = new FileSource(inFileAbsolute, FileSource.R_MODE);
         }
      }
      catch (FileNotFoundException e) {
         out.println("Could not open input file: " + e.getMessage());
         //e.printStackTrace();
         return false;
      }
      boolean noOptions = true;
      out.println("Options:");
      if (quickScan) {
         noOptions = false;
         out.println("  QuickScan");
      }
      if (blanking) {
         noOptions = false;
         out.println("  Blanking");
      }
      if (reverting) {
         noOptions = false;
         out.println("  Reverting");
      }
      if (navFix) {
         noOptions = false;
         out.println("  Nav chain bugfix");
      }
      if (zeroCPI) {
         noOptions = false;
         out.println("  Zero CPI field");
      }
      if (logging) {
         noOptions = false;
         out.println("  Logging");
         try {
            logger = new PrintWriter(new BufferedWriter(new FileWriter(new File(inFileName).getName() + ".log")));
         }
         catch (IOException e) {
            out.println("Could not open log file: " + e.getMessage());
            //e.printStackTrace();
            try {
               inFile.close();
            }
            catch (IOException e2) {
               out.println("Error closing input file: " + e2.getMessage());
               //e2.printStackTrace();
            }
            return false;
         }
      }
      if (noOptions) {
         out.println("  NONE");
      }
      out.println();


      long offset = 0;
      byte[] packData = new byte[PESPack.PACK_LENGTH];
      PESPack pack = new PESPack();

      boolean returnResult = true;
      String msg = null;

      boolean writeBackPack = false;

      TreeMap<String, Counter> packTable = new TreeMap<String, Counter>();

      out.println("Starting scan: " + new Date());
      try {
         while (inFile.read(packData, 0, packData.length) != -1) {
            try {
               //out.println("Pack retrieved");
               pack.parse(packData, 0, quickScan);
               // Check for NAV_PCK and apply operations if necessary
               if (pack.isNavPck()) {
                  if (navFix) {
                     //out.println("NavFixxing");
                     packData[0x516] = 127;
                     writeBackPack = true;
                  }
                  if (zeroCPI) {
                     //out.println("ZeroCPIing");
                     for (int i = 0x3C; i < 0x4C; i++) {
                        packData[i] = 0;
                     }
                     writeBackPack = true;
                  }
               } else {
                  // Apply blanking or reverting if necessary
                  PESPack.PESPacket firstPacket = pack.getPacket(0);
                  if (blanking) {
                     // TODO: change for parameterized blanking?
                     // Blanks the first DD+ Audio Stream for Sub and the VC-1 Stream for Sub
                     if (firstPacket.getStreamId() == 0xBD && firstPacket.getSubstreamId() == 0xC8) {
                        firstPacket.setStreamId(0x00);
                        writeBackPack = true;
                     } else {
                        if (firstPacket.getStreamId() == 0xFD && firstPacket.getSubstreamId() == 0x56) {
                           firstPacket.setStreamId(0xFF);
                           writeBackPack = true;
                        }
                     }
                  }
                  if (reverting) {
                     // TODO: change for parameterized reverting?
                     if (firstPacket.getStreamId() == 0x00) {
                        firstPacket.setStreamId(0xBD);
                        writeBackPack = true;
                     } else {
                        if (firstPacket.getStreamId() == 0xFF) {
                           firstPacket.setStreamId(0xFD);
                           writeBackPack = true;
                        }
                     }
                  }
               }
               String idString = pack.toString();
               msg = String.format("0x%1$016X: %2$s" , offset, idString);
               logMessage(msg);
               Counter counter = packTable.get(idString);
               if (counter != null) {
                  counter.inc();
               } else {
                  packTable.put(idString, new Counter(1));
               }
               if (writeBackPack) {
                  inFile.setPosition(inFile.getPosition() - packData.length);
                  inFile.write(packData, 0, packData.length);
                  writeBackPack = false;
               }
               // TODO: Does this work when called from a GUI?
                     // Available is 0 until enter has been pressed, so let enter be the abort constraint
               if (System.in.available() != 0) {
                  break;
               }
            }
            catch (PESParserException e) {
               msg = String.format("0x%1$016X: Invalid pack found: %2$s" , offset, e.getMessage());
               logMessage(msg);
               out.println(msg);
            }
            offset += packData.length;
         }
      }
      catch (IOException e) {
         returnResult = false;
         out.println("Error reading file: " + e.getMessage());
         //e.printStackTrace();
         try {
            inFile.close();
         }
         catch (IOException e2) {
            out.println("Error closing input file: " + e2.getMessage());
            //e2.printStackTrace();
         }
      }
      out.println("Scan finished: " + new Date());

      if (logging) {
         logger.close();
      }
      try {
         inFile.close();
      }
      catch (IOException e) {
         returnResult = false;
         out.println("Error closing input file: " + e.getMessage());
         //e.printStackTrace();
      }
      Iterator<Map.Entry<String, Counter>> it = packTable.entrySet().iterator();
      out.println();
      out.println("Pack statistics");
      out.println("---------------");
      while (it.hasNext()) {
         Map.Entry<String, Counter> entry = it.next();
         out.println(entry.getKey() + " :: " + entry.getValue().getCount());
      }
      return returnResult;
   }

   public int getMode() {
      return mode;
   }

   //public void setMode(int mode) {
   //    this.mode = mode;
   //}

   public boolean isQuickScan() {
      return quickScan;
   }

   public void setQuickScan(boolean quickScan) {
      this.quickScan = quickScan;
   }

   public boolean isBlanking() {
      return blanking;
   }

   public void setBlanking(boolean blanking) {
      this.blanking = blanking;
      if (blanking) {
         this.mode = EDIT_MODE;
         reverting = false;
      }
   }

   public boolean isReverting() {
      return reverting;
   }

   public void setReverting(boolean reverting) {
      this.reverting = reverting;
      if (reverting) {
         this.mode = EDIT_MODE;
         blanking = false;
      }
   }

   public boolean isNavFix() {
      return navFix;
   }

   public void setNavFix(boolean navFix) {
      this.navFix = navFix;
      if (navFix) {
         this.mode = EDIT_MODE;
      }
   }

   public boolean isZeroCPI() {
      return zeroCPI;
   }

   public void setZeroCPI(boolean zeroCPI) {
      this.zeroCPI = zeroCPI;
      if (zeroCPI) {
         this.mode = EDIT_MODE;
      }
   }


   public boolean isLogging() {
      return logging;
   }

   public void setLogging(boolean logging) {
      this.logging = logging;
   }

   private void logMessage(String msg) {
      if (logging) {
         logger.println(msg);
      }
   }


   /**
    * Simple cmdline scanner to start the PackScanner.
    * 
    * @param args If one arg, filename only. If two args, first is parameters, second is filename
    */
   public static void main(String[] args) {
      System.out.println("PackScanner 0.82 by KenD00");
      System.out.println();
      if (args.length == 2) {
         if (args[0].startsWith("-")) {
            PackScanner packScanner = new PackScanner(new PrintStreamPrinter(System.out));
            for (int i = 0; i < args[0].length(); i++) {
               switch (args[0].charAt(i)) {
               case 'q':
               case 'Q':
                  packScanner.setQuickScan(true);
                  break;
               case 'b':
               case 'B':
                  packScanner.setBlanking(true);
                  break;
               case 'r':
               case 'R':
                  packScanner.setReverting(true);
                  break;
               case 'n':
               case 'N':
                  packScanner.setNavFix(true);
                  break;
               case 'z':
               case 'Z':
                  packScanner.setZeroCPI(true);
                  break;
               case 'l':
               case 'L':
                  packScanner.setLogging(true);
                  break;
               }
            }
            if (packScanner.scan(args[1])) {
               System.exit(0);
            }
            else {
               System.exit(2);
            }
         }
      } else {
         if (args.length == 1) {
            if (new PackScanner(new PrintStreamPrinter(System.out)).scan(args[0])) {
               System.exit(0);
            } else {
               System.exit(2);
            }
         }
      }
      System.out.println("Usage: PackScanner [-options] input");
      System.out.println();
      System.out.println("Options:");
      System.out.println(" -q quickscan, does not fully parse the PES header");
      System.out.println(" -b blanks the VC-1 Stream for Sub and the first DD+ Stream for Sub");
      System.out.println(" -r reverts the blanked streams");
      System.out.println(" -n enables Nav chain bugfix");
      System.out.println(" -z zeros the complete CPI field");
      System.out.println(" -l logging enabled, writes a log file with offsets to every pack");
      System.out.println();
      System.out.println("Example: scan C:\\MY_MOVIE.EVO, write log file and blank");
      System.out.println("PackScanner -lb C:\\MY_MOVIE.EVO");
      System.out.println();
      System.out.println("Pressing ENTER during the scan aborts it");
      System.exit(1);
   }


   private final class Counter {

      private long value = 0;

      public Counter(long initValue) {
         value = initValue;
      }

      public void reset() {
         value = 0;
      }

      public void inc() {
         value += 1;
      }

      public void dec() {
         value -= 1;
      }

      public long getCount() {
         return value;
      }
   }

}
