/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
  Part of the Processing project - http://processing.org

  Copyright (c) 2004-07 Ben Fry and Casey Reas
  The previous version of this code was developed by Hernando Barragan

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library 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
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General
  Public License along with this library; if not, write to the
  Free Software Foundation, Inc., 59 Temple Place, Suite 330,
  Boston, MA  02111-1307  USA
*/

package processing.video;

import processing.core.*;

import java.io.*;
import java.lang.reflect.*;

import quicktime.*;
import quicktime.io.QTFile;
import quicktime.qd.*;
import quicktime.std.*;
import quicktime.std.movies.media.DataRef;
import quicktime.util.QTHandle;
import quicktime.util.RawEncodedImage;


@SuppressWarnings("deprecation")
public class Movie extends PImage implements PConstants, Runnable {
  Method movieEventMethod;
  String filename;
  Thread runner;

  PImage borderImage;
  boolean removeBorders = true;

  boolean play;
  boolean repeat;
  boolean available;
  int fps;

  /**
   * The QuickTime for Java "Movie" object, made public
   * in case anyone wants to play with it.
   */
  public quicktime.std.movies.Movie movie;

  QDRect movieRect;
  QDGraphics movieGraphics;
  boolean firstFrame = true;
  RawEncodedImage raw;


    /*
  static {
    try {
      //System.out.println("jlp = " + System.getProperty("java.library.path"));
      QTSession.open();
    } catch (QTException e) {
      e.printStackTrace();
    }
      // shutting off for 0116, hoping for better exception handling
    QTRuntimeException.registerHandler(new QTRuntimeHandler() {
        public void exceptionOccurred(QTRuntimeException e,
                                      Object obj, String s, boolean flag) {
          System.err.println("Problem inside Movie");
          e.printStackTrace();
        }
      });
  }
    */


  public Movie(PApplet parent, String filename) {
    this(parent, filename, 30);
  }


  public Movie(final PApplet parent, final String filename, final int ifps) {
    // this creates a fake image so that the first time this
    // attempts to draw, something happens that's not an exception
    super(1, 1, RGB);

    // http://dev.processing.org/bugs/show_bug.cgi?id=882
    //SwingUtilities.invokeLater(new Runnable() {
    //public void run() {
    init(parent, filename, ifps);
    //}
    //});
  }


  public void init(PApplet parent, String filename, int fps) {
    this.parent = parent;
    this.fps = fps;

    try {
      QTSession.open();
    } catch (QTException e) {
      e.printStackTrace();
      return;
    }

    // first check to see if this can be read locally from a file.
    // otherwise, will have to load the file into memory, which is
    // gonna make people unhappy who are trying to play back 50 MB
    // quicktime movies with a locally installed piece exported
    // as an application.
    try {
      try {
        // first try a local file using the dataPath. usually this will
        // work ok, but sometimes the dataPath is inside a jar file,
        // which is less fun, so this will crap out.
        File file = new File(parent.dataPath(filename));
        if (file.exists()) {
          movie = fromDataRef(new DataRef(new QTFile(file)));
          //init(parent, movie, ifps);
          //return;
        }
      } catch (Exception e) { }  // ignored

      // read from a folder local to the current working dir
      // called "data". presumably this might be the data folder,
      // though that should be caught above, if such a folder exists.
      /*
      if (movie == null) {
        try {
          File file = new File("data", filename);
          if (file.exists()) {
            movie = fromDataRef(new DataRef(new QTFile(file)));
            init(parent, movie, ifps);
            return;
          }
        } catch (QTException e2) { }
      }
      */

      // read from a file just hanging out in the local folder.
      // this might happen when the video library is used with some
      // other application, or the person enters a full path name
      if (movie == null) {
        try {
          File file = new File(filename);
          if (file.exists()) {
            movie = fromDataRef(new DataRef(new QTFile(file)));
            //init(parent, movie, ifps);
            //return;
          }
        } catch (QTException e1) { }
      }

    } catch (SecurityException se) {
      // online, whups. catch the security exception out here rather than
      // doing it three times (or whatever) for each of the cases above.
    }

    // if the movie can't be read from a local file, it has to be read
    // into a byte array and passed to qtjava. it's annoying that apple
    // doesn't have something in the api to read a movie from a friggin
    // InputStream, but oh well. it's their api.
    if (movie == null) {
      byte data[] = parent.loadBytes(filename);
      //int dot = filename.lastIndexOf(".");
      // grab the extension from the file, use mov if there is none
      //String extension = (dot == -1) ? "mov" :
      //  filename.substring(dot + 1).toLowerCase();
      try {
        movie = fromDataRef(new DataRef(new QTHandle(data)));
      } catch (QTException e) {
        e.printStackTrace();
      }
    }

    /*
    URL url = null;
    this.filename = filename; // for error messages

    if (filename.startsWith("http://")) {
      try {
        url = new URL(filename);
        DataRef urlRef = new DataRef(url.toExternalForm());
        movie = fromDataRef(urlRef);
        init(parent, movie, ifps);
        return;

      } catch (QTException qte) {
        qte.printStackTrace();
        return;

      } catch (MalformedURLException e) {
        e.printStackTrace();
        return;
      }
    }

    // updated for new loading style of 0096
    ClassLoader cl = parent.getClass().getClassLoader();
    url = cl.getResource("data/" + filename);
    if (url != null) {
      init(parent, url, ifps);
      return;
    }

    try {
      try {
        File file = new File(parent.dataPath(filename));
        if (file.exists()) {
          movie = fromDataRef(new DataRef(new QTFile(file)));
          init(parent, movie, ifps);
          return;
        }
      } catch (Exception e) { }  // ignored

      try {
        File file = new File("data", filename);
        if (file.exists()) {
          movie = fromDataRef(new DataRef(new QTFile(file)));
          init(parent, movie, ifps);
          return;
        }
      } catch (QTException e2) { }

      try {
        File file = new File(filename);
        if (file.exists()) {
          movie = fromDataRef(new DataRef(new QTFile(file)));
          init(parent, movie, ifps);
          return;
        }
      } catch (QTException e1) { }

    } catch (SecurityException se) { }  // online, whups
    */

    if (movie == null) {
      parent.die("Could not find movie file " + filename, null);
    }

    // we've got a valid movie! let's rock.
    try {
      // this is probably causing the 2 seconds of audio
      // disabled pre-preroll on 0126 because of security problems
      //movie.prePreroll(0, 1.0f);
      movie.preroll(0, 1.0f);

      // this has a possibility of running forever..
      // should probably happen on the thread as well.
      while (movie.maxLoadedTimeInMovie() == 0) {
        movie.task(100);

        // 0106: tried adding sleep time so this doesn't spin out of control
        // works fine but doesn't really help anything
        //try {
        //Thread.sleep(5);
        //} catch (InterruptedException e) { }
      }
      movie.setRate(1);
      //fps = ifps;

      // register methods
      parent.registerDispose(this);

      try {
        movieEventMethod =
          parent.getClass().getMethod("movieEvent",
                                      new Class[] { Movie.class });
      } catch (Exception e) {
        // no such method, or an error.. which is fine, just ignore
      }

      // and now, make the magic happen
      runner = new Thread(this);
      runner.start();

    } catch (QTException qte) {
      qte.printStackTrace();

    } catch (Exception e) {
      e.printStackTrace();
    }
  }


  /*
  public Movie(PApplet parent, URL url) {
    init(parent, url, 30);
  }


  public Movie(PApplet parent, URL url, int ifps) {
    init(parent, url, ifps);
  }


  public void init(PApplet parent, URL url, int ifps) {

    String externalized = url.toExternalForm();
    System.out.println("externalized is " + externalized);

    // qtjava likes file: urls to read file:/// not file:/
    // so this changes them when appropriate
    if (externalized.startsWith("file:/") &&
        !externalized.startsWith("file:///")) {
      externalized = "file:///" + url.getPath();
    }

    // the url version is the only available that can take
    // an InputStream (indirectly) since it uses url syntax
    //DataRef urlRef = new DataRef(requestFile);
    try {
      System.out.println(url);
      System.out.println(externalized);
      DataRef urlRef = new DataRef(externalized);
      System.out.println(urlRef);

      movie = fromDataRef(urlRef);
      init(parent, movie, ifps);

    } catch (QTException e) {
      e.printStackTrace();
    }
  }
  */


  /**
   * Why does this function have to be so bizarre? i love the huge
   * constants! i think they're neato. i feel like i'm coding for
   * think pascal on my mac plus! those were happier times.
   */
  private quicktime.std.movies.Movie fromDataRef(DataRef ref)
    throws QTException {

    return
      quicktime.std.movies.Movie.fromDataRef(ref,
                                             StdQTConstants4.newMovieAsyncOK |
                                             StdQTConstants.newMovieActive);
  }


  /*
  public void init(PApplet parent,
                   quicktime.std.movies.Movie movie, int ifps) {
    this.parent = parent;

    try {
      // this is probably causing the 2 seconds of audio
      movie.prePreroll(0, 1.0f);
      movie.preroll(0, 1.0f);

      // this has a possibility of running forever..
      // should probably happen on the thread as well.
      while (movie.maxLoadedTimeInMovie() == 0) {
        movie.task(100);

        // 0106: tried adding sleep time so this doesn't spin out of control
        // works fine but doesn't really help anything
        //try {
        //Thread.sleep(5);
        //} catch (InterruptedException e) { }
      }
      movie.setRate(1);
      fps = ifps;

      runner = new Thread(this);
      runner.start();


      // register methods

      parent.registerDispose(this);

      try {
        movieEventMethod =
          parent.getClass().getMethod("movieEvent",
                                      new Class[] { Movie.class });
      } catch (Exception e) {
        // no such method, or an error.. which is fine, just ignore
      }

    } catch (QTException qte) {
      qte.printStackTrace();

    } catch (Exception e) {
      e.printStackTrace();
    }
  }
  */


  public boolean available() {
    return available;
  }


  public void read() {
    try {
      if (firstFrame) {
        movieRect = movie.getBox();
        //movieGraphics = new QDGraphics(movieRect);
        if (quicktime.util.EndianOrder.isNativeLittleEndian()) {
          movieGraphics =
            new QDGraphics(QDConstants.k32BGRAPixelFormat, movieRect);
        } else {
          movieGraphics =
            new QDGraphics(QDGraphics.kDefaultPixelFormat, movieRect);
        }
      }

      Pict pict = movie.getPict(movie.getTime());  // returns an int
      pict.draw(movieGraphics, movieRect);
      PixMap pixmap = movieGraphics.getPixMap();
      raw = pixmap.getPixelData();

      // It needs to get at least a small part
      // of the video to get the parameters
      if (firstFrame) {
        //int intsPerRow = pixmap.getRowBytes() / 4;
        int movieWidth = movieRect.getWidth();
        int movieHeight = movieRect.getHeight();
        int j = raw.getRowBytes() - movieWidth*4;
        // this doesn't round up.. does it need to?
        int k = j / 4;
        int dataWidth = movieWidth + k;

        if (dataWidth != movieWidth) {
          if (removeBorders) {
            borderImage = new PImage(dataWidth, movieHeight, RGB);
          } else {
            movieWidth = dataWidth;
          }
        }
        //int vpixels[] = new int[movieWidth * movieHeight];
        //image = new PImage(vpixels, movieWidth, movieHeight, RGB);
        super.init(movieWidth, movieHeight, RGB);
        //parent.video = image;
        firstFrame = false;
      }
      // this happens later (found by hernando)
      //raw.copyToArray(0, image.pixels, 0, image.width * image.height);

      loadPixels();
      // this is identical to a chunk of code inside PCamera
      // this might be a candidate to move up to PVideo or something
      if (borderImage != null) {  // need to remove borders
        raw.copyToArray(0, borderImage.pixels,
                        0, borderImage.width * borderImage.height);
        int borderIndex = 0;
        int targetIndex = 0;
        for (int i = 0; i < height; i++) {
          System.arraycopy(borderImage.pixels, borderIndex,
                           pixels, targetIndex, width);
          borderIndex += borderImage.width;
          targetIndex += width;
        }
      } else {  // just copy directly
        raw.copyToArray(0, pixels, 0, width * height);
      }

      // ready to rock
      //System.out.println("updating pixels");
      //updatePixels();  // mark as modified
      updatePixels();

    } catch (QTException qte) {
      qte.printStackTrace();
      //QTSession.close();  // let dispose() handle it
    }
  }


  /**
   * Begin playing the movie, with no repeat.
   */
  public void play() {
//    if (runner != null) {
//      stop();
//    }
    play = true;
//    runner = new Thread(this);
//    runner.start();
  }


  /**
   * Begin playing the movie, with repeat.
   */
  public void loop() {
    play();
    repeat = true;
  }


  /**
   * Shut off the repeating loop.
   */
  public void noLoop() {
    repeat = false;
  }


  /**
   * Pause the movie at its current time.
   */
  public void pause() {
    play = false;
    //System.out.println("pause");
  }


  /**
   * Stop the movie, and rewind.
   */
  public void stop() {
    play = false;
//    runner = null;

    try {
      movie.setTimeValue(0);

    } catch (StdQTException e) {
      errorMessage("stop", e);
    }
  }


  /**
   * Set how often new frames are to be read from the movie.
   * Does not actually set the speed of the movie playback,
   * that's handled by the speed() method.
   */
  public void frameRate(int ifps) {
    if (ifps <= 0) {
      System.err.println("Movie: ignoring bad frame rate of " +
                         ifps + " fps.");
    } else {
      fps = ifps;
    }
  }


  /**
   * Set a multiplier for how fast/slow the movie should be run.
   * The default is 1.0.
   * <UL>
   * <LI>speed(2) will play the movie at double speed (2x).
   * <LI>speed(0.5) will play at half speed.
   * <LI>speed(-1) will play backwards at regular speed.
   * </UL>
   */
  public void speed(float rate) {
    //rate = irate;
    try {
      movie.setRate(rate);

    } catch (StdQTException e) {
      errorMessage("speed", e);
    }
  }


  /**
   * Return the current time in seconds.
   * The number is a float so fractions of seconds can be used.
   */
  public float time() {
    try {
      return (float)movie.getTime() / (float)movie.getTimeScale();

    } catch (StdQTException e) {
      errorMessage("time", e);
    }
    return -1;
  }


  /**
   * Jump to a specific location (in seconds).
   * The number is a float so fractions of seconds can be used.
   */
  public void jump(float where) {
    try {
      //movie.setTime(new TimeRecord(rate, where));  // scale, value
      //movie.setTime(new TimeRecord(1, where));  // scale, value
      int scaledTime = (int) (where * movie.getTimeScale());
      movie.setTimeValue(scaledTime);

    } catch (StdQTException e) {
      errorMessage("jump", e);
    }
  }


  /**
   * Get the full length of this movie (in seconds).
   */
  public float duration() {
    try {
      return (float)movie.getDuration() / (float)movie.getTimeScale();

    } catch (StdQTException e) {
      errorMessage("length", e);
    }
    return -1;
  }


  /*
  public void play() {
    if(!play) {
      play = true;
    }
    start();
    while( image == null) {
      try {
        Thread.sleep(5);
      } catch (InterruptedException e) { }
    }
    pixels = image.pixels;
    width = image.width;
    height = image.height;
  }


  public void repeat() {
    loop = true;
    if(!play) {
      play = true;
    }
    start();
    while( image == null) {
      try {
        Thread.sleep(5);
      } catch (InterruptedException e) { }
    }
    pixels = image.pixels;
    width = image.width;
    height = image.height;
  }


  public void pause() {
    play = false;
  }
  */


  public void run() {
    //System.out.println("entering thread");
    while (Thread.currentThread() == runner) {
      //System.out.print("<");
      try {
        //Thread.sleep(5);
        Thread.sleep(1000 / fps);
      } catch (InterruptedException e) { }
      //System.out.print(">");

      // this could be a lie, but..
      if (play) {
        //read();
        //System.out.println("play");
        available = true;

        if (movieEventMethod == null) {
          // If no special handling, then automatically read from the movie.
          read();

        } else {
          try {
            movieEventMethod.invoke(parent, new Object[] { this });
          } catch (Exception e) {
            System.err.println("error, disabling movieEvent() for " +
                               filename);
            e.printStackTrace();
            movieEventMethod = null;
          }
        }

        try {
          if (movie.isDone() && repeat) {
            movie.goToBeginning();
          }
        } catch (StdQTException e) {
          play = false;
          errorMessage("rewinding", e);
        }
        //} else {
        //System.out.println("no play");
      }

      //try {
      //read();
      //if (movie.isDone() && loop) movie.goToBeginning();

      //} catch (QTException e) {
      //System.err.println("Movie exception");
      //e.printStackTrace();
      //QTSession.close(); ??
      //}
    }
  }


  /**
   * Call this to halt the movie from running, and stop its thread.
   */
  public void dispose() {
    stop();
    runner = null;
    QTSession.close();
  }


  /**
   * General error reporting, all corraled here just in case
   * I think of something slightly more intelligent to do.
   */
  protected void errorMessage(String where, Exception e) {
    parent.die("Error inside Movie." + where + "()", e);
  }
}

