/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.app.mediafilter;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintWriter;

import java.lang.Thread;

import java.nio.file.Files;
import java.nio.file.FileSystems;
import java.nio.file.Path;

import java.util.Arrays;
import java.util.HashMap;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.dspace.content.Bitstream;
import org.dspace.content.DCValue;
import org.dspace.content.Item;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Context;

/** A media filter to provide basic video processing to DSpace. This class will
 *  attempt to convert the input media into a web-optimized format (mp4) and at
 *  the same time will generate a series of keyframe images and attach them to
 *  the items bundle.
 */
public class VideoFilter
extends MediaFilter
{

  // Path to dspace
  String dspace_home;

  // We need the handle uri when doing an itemupdate
  String item_handle;
  // We need the internal id when performing a metadata import
  int item_id;
  // We can extract an aired date from the dc.Title of some media files
  String item_date;

  Path temp_dir_path;

  String streaming_hq_size;
  String streaming_hq_video_bitrate;
  String streaming_hq_audio_bitrate;
  boolean video_deinterlacing_filter;

  /** Constructor - read in configuration options
   */
  public VideoFilter()
  {
    // Look up configuration
    dspace_home = ConfigurationManager.getProperty("dspace.dir");
    //String streaming_format =  ConfigurationManager.getProperty(MediaFilterManager.FILTER_PREFIX + ".VideoFilter.outputFormat");
    streaming_hq_size = "720";
    streaming_hq_video_bitrate = "496";
    streaming_hq_audio_bitrate = "80";
    video_deinterlacing_filter = false;
  }
  /** VideoFilter() **/

  /**
   *  @return name of the bundle this filter will stick its generated
   *          Bitstreams
   */
  public String getBundleName()
  {
    // For now we will just add the converted bitstream to the original
    // bundle in order to make it easy to see. In theory it should probably
    // have its own bundle and black magic associated ala THUMBNAIL...
    // however, I note there is a lot of discussion on the DSpace forums of
    // exactly this point - that it's black magic and not scalable/portable/
    // modular/safe.
    return "ORIGINAL";
  }
  /** getBundleName() **/

  /**
   *  @return string to describe the newly-generated Bitstream's - how it was
   *          produced is a good idea
   */
  public String getDescription()
  {
    // @todo make this description dependent on the configuration options
    return "Converted into streaming format using Handbrake and generated keyframes (if required) using Hive2";
  }
  /** getDescription() **/

  /**
   *  @param source
   *            source input stream
   *
   *  @return InputStream the resulting input stream
   */
  public InputStream getDestinationStream(InputStream source)
    throws Exception
  {
    System.out.println("VideoFilter::getDestinationStream()");
    // 0. Create temporary directory and make a local file copy of the source
    //    video as video conversion tools require a file for random access.
    temp_dir_path = Files.createTempDirectory("dspace-video-filter");
    if (MediaFilterManager.isVerbose)
    {
      System.out.println("[temporary directory: " + temp_dir_path.toString() + "]");
    }
    Path item_path = temp_dir_path.resolve("item_000");
    item_path.toFile().mkdirs(); // bet there is a nice nio way to do this
    Path raw_video_path = temp_dir_path.resolve("gsv.ts");
    if (MediaFilterManager.isVerbose)
    {
      System.out.println(" * saving copy of media to temporary directory");
    }
    Files.copy(source, raw_video_path);

    // 1. Use MediaFilter to extract metadata about this multimedia file
    if (MediaFilterManager.isVerbose)
    {
      System.out.println(" * extracting metadata using MediaInfo");
    }
    String metadata_command[] = {
      "mediainfo",
      "--Output=XML",
      raw_video_path.toString()
    };
    Process metadata_process = Runtime.getRuntime().exec(metadata_command);
    StreamGobbler metadata_process_error_gobbler = new StreamGobbler(metadata_process.getErrorStream());
    metadata_process_error_gobbler.start();
    BufferedReader metadata_br = new BufferedReader(new InputStreamReader(metadata_process.getInputStream()));
    String line;
    String type = "Unknown";
    HashMap metadata = new HashMap();
    Pattern type_pattern = Pattern.compile("<track type=\"([a-zA-Z]+)\">");
    Pattern metadata_pattern = Pattern.compile("<([a-zA-Z_]+)>(.*)</\\1>");
    while ((line = metadata_br.readLine()) != null)
    {
      Matcher type_matcher = type_pattern.matcher(line);
      if (type_matcher.matches())
      {
        type = type_matcher.group(1);
      }
      else
      {
        Matcher metadata_matcher = metadata_pattern.matcher(line);
        if (metadata_matcher.matches())
        {
          String field = metadata_matcher.group(1);
          String value = metadata_matcher.group(2);
          metadata.put(type + ":" + field, value);
        }
      }
    }
    int metadata_status = metadata_process.waitFor();
    // * write a comma separated file containing the new metadata
    Path metadata_path = temp_dir_path.resolve("metadata.csv");
    BufferedWriter metadata_bw = new BufferedWriter(new FileWriter(metadata_path.toString()));
    // - first row contains the (dublin core) headers
    metadata_bw.write("id,dc.type,dc.date,dc.format,dc.format.extent");
    metadata_bw.newLine();
    // - second row contains multimedia-centric metadata ala
    //   Greenstone:SimpleVideoPlugin
    metadata_bw.write(item_id + ",");
    metadata_bw.write("\"multimedia (" + metadata.get("General:Format") + ")\",");
    metadata_bw.write("\"" + item_date + "\",");
    // - dc.format
    metadata_bw.write("\"" + metadata.get("General:Format") + "||video:" + metadata.get("Video:Format_Info") + " (" + metadata.get("Video:Format") + ")||audio:" + metadata.get("Audio:Format_Info") + " (" + metadata.get("Audio:Format") + ")\",");
    // - dc.format.extent
    metadata_bw.write("\"" + metadata.get("General:File_size") + "||" + metadata.get("General:Duration") + "||" + metadata.get("Video:Width") + " x " + metadata.get("Video:Height") + "\"");
    metadata_bw.flush();
    metadata_bw.close();
    // * Call CLI script to attach this metadata
    if (MediaFilterManager.isVerbose)
    {
      System.out.println(" * adding multimedia metadata using DSpace's metadata-import");
      ///ystem.out.println("[DEBUG] command: " + Arrays.toString(metadata_import_command)); 
    }
    String metadata_import_command[] = {
      dspace_home + "/bin/dspace",
      "metadata-import",
      "-s",
      "-f", metadata_path.toString()
    };
    Process metadata_import_process = Runtime.getRuntime().exec(metadata_import_command);
    StreamGobbler metadata_import_process_err_gobbler = new StreamGobbler(metadata_import_process.getErrorStream());
    metadata_import_process_err_gobbler.start();
    StreamGobbler metadata_import_process_out_gobbler = new StreamGobbler(metadata_import_process.getInputStream());
    metadata_import_process_out_gobbler.start();

    int metadata_import_status = metadata_import_process.waitFor();
    if (metadata_import_status != 0)
    {
      throw new Exception("Metadata import command failed (exit status: " + metadata_import_status + ")");
    }

    // 2. Call Handbrake to convert the video into streaming format
    if (MediaFilterManager.isVerbose)
    {
      System.out.println(" * performing conversion using HandBrake");
    }
    Path converted_video_path = temp_dir_path.resolve("gsv.mp4");
    String convert_command[] = {
      "HandBrakeCLI",
      "-i", raw_video_path.toString(),
      "-t", "1",
      "-c", "1",
      "-o", converted_video_path.toString(),
      "-f", "mp4",
      "-O",
      "-w", streaming_hq_size,
      "--loose-anamorphic",
      "-e", "x264",
      "-b", streaming_hq_video_bitrate,
      "-a", "1",
      "-E", "faac",
      "-6", "dpl2",
      "-R", "Auto",
      "-B", streaming_hq_audio_bitrate,
      "-D", "0.0",
      "-x", "ref=2:bframes=2:subq=6:mixed-refs=0:weightb=0:8x8dct=0:trellis=0:threads=1"
    };
    // @todo determine the best way to account for configuration options
    Process convert_process = Runtime.getRuntime().exec(convert_command);
    StreamGobbler convert_process_error_gobbler = new StreamGobbler(convert_process.getErrorStream());
    convert_process_error_gobbler.start();
    StreamGobbler convert_process_out_gobbler = new StreamGobbler(convert_process.getInputStream());
    convert_process_out_gobbler.start();
    int convert_status = convert_process.waitFor();
    if (convert_status != 0 || !converted_video_path.toFile().exists())
    {
      throw new Exception("Convert command failed (exit status: " + convert_status + ")");
    }

    // 3. Generate keyframes from converted video
    if (MediaFilterManager.isVerbose)
    {
      System.out.println(" * generating keyframes using Hive2");
    }
    Path shots_path = temp_dir_path.resolve("shots.xml");
    String keyframe_command[] = {
      "hive2_ffmpegsvn",
      "-o", shots_path.toString(),
      "-k", item_path.toString(),
      "-m", "0.5",
      "-l", "0.05",
      converted_video_path.toString()
    };
    if (MediaFilterManager.isVerbose)
    {
      System.out.println("[DEBUG " + Arrays.toString(keyframe_command) + "]");
    }
    Process keyframe_process = Runtime.getRuntime().exec(keyframe_command);
    //Path keyframe_err_file = temp_dir_path.resolve("hive2-err.txt");
    StreamGobbler keyframe_error_gobbler = new StreamGobbler(keyframe_process.getErrorStream());//, keyframe_err_file.toString());
    keyframe_error_gobbler.start();
    //Path keyframe_out_file = temp_dir_path.resolve("hive2-out.txt");
    StreamGobbler keyframe_out_gobbler = new StreamGobbler(keyframe_process.getInputStream()); //, keyframe_out_file.toString());
    keyframe_out_gobbler.start();
    int keyframe_status = keyframe_process.waitFor();
    if (keyframe_status != 0 || !shots_path.toFile().exists())
    {
      throw new Exception("Keyframe command failed (exit status: " + keyframe_status + ")");
    }

    // 4. Create the necessary files for the itemupdate script in the item
    //    directory
    // * a metadata file (dublin_core.xml) containing the handle (uri) for the
    //   item to update
    if (MediaFilterManager.isVerbose)
    {
      System.out.println(" * adding keyframes to item using DSpace's itemupdate");
    }
    Path dcmeta_path = item_path.resolve("dublin_core.xml");
    BufferedWriter dcmeta_bw = new BufferedWriter(new FileWriter(dcmeta_path.toString()));
    dcmeta_bw.write("<dublin_core>");
    dcmeta_bw.newLine();
    dcmeta_bw.write("\t<dcvalue element=\"identifier\" qualifier=\"uri\">http://hdl.handle.net/" + item_handle + "</dcvalue>");
    dcmeta_bw.newLine();
    dcmeta_bw.write("</dublin_core>");
    dcmeta_bw.newLine();
    dcmeta_bw.flush();
    dcmeta_bw.close();
    // * a file (contents) referring to all of the keyframe images to add (and
    //   the bundle to add them to)
    File item_dir = item_path.toFile();
    File files[] = item_dir.listFiles();
    Arrays.sort(files);
    Path contents_path = item_path.resolve("contents");
    BufferedWriter contents_bw = new BufferedWriter(new FileWriter(contents_path.toString()));
    Pattern image_filename_pattern = Pattern.compile(".*\\.jpg");
    for (int i = 0; i < files.length; i++)
    {
      File image_file = files[i];
      Path image_path = image_file.toPath();
      Path image_filename_path = image_path.getFileName();
      String image_filename = image_filename_path.toString();
      System.out.println(" - considering file: " + image_filename);
      Matcher image_filename_matcher = image_filename_pattern.matcher(image_filename);
      if (image_filename_matcher.matches())
      {
        contents_bw.write(image_filename + "\tbundle:ORIGINAL\tdescription:Hive2 Keyframe");
        contents_bw.newLine();
      }
    }
    contents_bw.flush();
    contents_bw.close();
    // * Call itemupdate
    String itemupdate_command[] = {
      dspace_home + "/bin/dspace",
      "itemupdate",
      "--noundo", // Supress undo
      "-A", // Add bitstreams
      "-e", "videofilter@dspace", // Special eperson
      "-s", temp_dir_path.toString() // Within which lies 'item_000'
    };
    Process itemupdate_process = Runtime.getRuntime().exec(itemupdate_command);
    StreamGobbler itemupdate_process_error_gobbler = new StreamGobbler(itemupdate_process.getErrorStream());
    itemupdate_process_error_gobbler.start();
    StreamGobbler itemupdate_process_out_gobbler = new StreamGobbler(itemupdate_process.getInputStream());
    itemupdate_process_out_gobbler.start();
    int itemupdate_status = itemupdate_process.waitFor();
    if (itemupdate_status != 0)
    {
      throw new Exception("ItemUpdate command failed (exit status: " + itemupdate_status + ")");
    }

    if (MediaFilterManager.isVerbose)
    {
      System.out.println("VideoFilter::getDestinationSteam() - Complete!");
    }

    // Read back in the converted video and return as stream
    InputStream result = Files.newInputStream(converted_video_path);
    return result;
  }
  /** getDestinationStream(InputStream source) **/

  /** Get a filename for a newly created filtered bitstream
   *
   *  @param sourceName
   *            name of source bitstream
   *  @return filename generated by the filter - for example, document.pdf
   *          becomes document.pdf.txt
   */
  public String getFilteredName(String sourceName)
  {
    // @todo make this dependent on the configuration options (flv|mp4)
    return sourceName + ".mp4";
  }
  /** getFilteredName(String sourceName) **/

  /**
   *  @return name of the bitstream format (say "HTML" or "Microsoft Word")
   *         returned by this filter look in the bitstream format registry or
   *         mediafilter.cfg for valid format strings.
   */
  public String getFormatString()
  {
    return "video/mp4";
  }
  /** getFormatString() **/

  /** Perform any pre-processing of the source bitstream *before* the actual
   *  filtering takes place in MediaFilterManager.processBitstream().
   *
   *  Return true if pre-processing is successful (or no pre-processing
   *  is necessary).  Return false if bitstream should be skipped
   *  for any reason.
   *
   *  @param c
   *            context
   *  @param item
   *            item containing bitstream to process
   *  @param source
   *            source bitstream to be processed
   * 
   *  @return true  if bitstream processing should continue,
   *          false  if this bitstream should be skipped
   */
  public boolean preProcessBitstream(Context c, Item item, Bitstream source)
    throws Exception
  {
    item_handle = item.getHandle();
    item_id = item.getID();
    // We can extract the aired date from the title (hopefully)
    item_date = "Unknown";
    DCValue titles[] = item.getMetadata("dc", "title", Item.ANY, "en_US");
    if (titles.length > 0)
    {
      String title = titles[0].value;
      Pattern date_pattern = Pattern.compile(".*(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d).*");
      Matcher date_matcher = date_pattern.matcher(title);
      if (date_matcher.matches())
      {
        item_date = date_matcher.group(1) + date_matcher.group(2) + date_matcher.group(3);
      }
    }
    return true;
  }
  /** preProcessBitstream(Context c, Item item, Bitstream source) **/

  /** Perform any post-processing of the generated bitstream *after* this
   *  filter has already been run.
   *
   *  Return true if pre-processing is successful (or no pre-processing
   *  is necessary).  Return false if bitstream should be skipped
   *  for some reason.
   *
   *  @param c
   *            context
   *  @param item
   *            item containing bitstream to process
   *  @param generatedBitstream
   *            the bitstream which was generated by
   *            this filter.
   *
   *  @return true  if bitstream processing should continue,
   *          false  if this bitstream should be skipped
   */
  public void postProcessBitstream(Context c, Item item, Bitstream generatedBitstream)
    throws Exception
  {
    // Clean up all temporary files
    // @todo@ some sort of recursive delete... although it looks like there is
    //        still nothing built in
  }
  /** postProcessBitstream(Context c, Item item, Bitstream generatedBitstream) **/

}

class StreamGobbler
extends Thread
{
  InputStream is;
  String file_path;
  boolean output_to_file;

  StreamGobbler(InputStream is)
  {
    this.is = is;
    this.output_to_file = false;
  }

  StreamGobbler(InputStream is, String file_path)
  {
    this.is = is;
    this.file_path = file_path;
    this.output_to_file = true;
  }

  public void run()
  {
    try
    {
      InputStreamReader isr = new InputStreamReader(is);
      BufferedReader br = new BufferedReader(isr);
      String line = null;
      if (output_to_file)
      {
        PrintWriter pw = new PrintWriter(new BufferedOutputStream(new FileOutputStream(file_path)));
        while ( (line = br.readLine()) != null)
        {
          pw.println(line);
        }
        pw.flush();
        pw.close();
      }
      else
      {
        while ( (line  = br.readLine()) != null)
        {
          // Do nothing - equivalent to > /dev/null
        }
      }
    }
    catch (IOException ioe)
    {
      ioe.printStackTrace();
    }
  }
}
