package com.srbenoit.media.movie;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import javax.media.ConfigureCompleteEvent;
import javax.media.Controller;
import javax.media.ControllerEvent;
import javax.media.ControllerListener;
import javax.media.DataSink;
import javax.media.EndOfMediaEvent;
import javax.media.Format;
import javax.media.Manager;
import javax.media.MediaLocator;
import javax.media.PrefetchCompleteEvent;
import javax.media.Processor;
import javax.media.RealizeCompleteEvent;
import javax.media.ResourceUnavailableEvent;
import javax.media.control.TrackControl;
import javax.media.datasink.DataSinkErrorEvent;
import javax.media.datasink.DataSinkEvent;
import javax.media.datasink.DataSinkListener;
import javax.media.datasink.EndOfStreamEvent;
import javax.media.protocol.ContentDescriptor;
import javax.media.protocol.DataSource;
import javax.media.protocol.FileTypeDescriptor;
import com.srbenoit.log.LoggedObject;

/**
 * Generates a QuickTime movie from a series of JPEG frames.
 */
public class MakeMovie extends LoggedObject implements ControllerListener, DataSinkListener {

    /** object on which to synchronize waiting for the end of a file */
    private final transient Object waitFileSync = new Object();

    /** object on which to synchronize waiting for a state change */
    private final transient Object waitSync = new Object();

    /** flag indicating file is done */
    private transient boolean fileDone = false;

    /** flag indicating file read succeeded */
    private transient boolean fileSuccess = true;

    /** flag indicating state transition succeeded */
    private transient boolean stateTransOK = true;

    /**
     * Builds the movie.
     *
     * @param   width      the movie width
     * @param   height     the movie height
     * @param   frameRate  the movie frame rate
     * @param   inImages   the list of input frame images
     * @param   outML      the media locator for the produced movie
     * @return  <code>true</code> if successful; <code>false</code> otherwise
     * @throws  MovieMakingException  if there was an error
     */
    public boolean doItBuffered(final int width, final int height, final int frameRate,
        final BufferedImage[] inImages, final MediaLocator outML) throws MovieMakingException {

        List<BufferedImage> list;

        list = new ArrayList<BufferedImage>(inImages.length);
        list.addAll(Arrays.asList(inImages));

        return doItBuffered(width, height, frameRate, list, outML);
    }

    /**
     * Builds the movie.
     *
     * @param   width      the movie width
     * @param   height     the movie height
     * @param   frameRate  the movie frame rate
     * @param   inImages   the list of input frame images
     * @param   outML      the media locator for the produced movie
     * @return  <code>true</code> if successful; <code>false</code> otherwise
     * @throws  MovieMakingException  if there was an error
     */
    public boolean doItBuffered(final int width, final int height, final int frameRate,
        final List<BufferedImage> inImages, final MediaLocator outML) throws MovieMakingException {

        BufferedImageDataSource ids;
        Processor proc;
        TrackControl[] tcs;
        Format[] fmt;
        DataSink dsink;

        ids = new BufferedImageDataSource(width, height, frameRate, inImages);

        try {
            proc = Manager.createProcessor(ids);
        } catch (Exception e) {
            throw new MovieMakingException("Cannot create a processor from the data source.", e);
        }

        proc.addControllerListener(this);

        // Put the Processor into configured state so we can set
        // some processing options on the processor.
        proc.configure();

        if (!waitForState(proc, Processor.Configured)) {
            throw new MovieMakingException("Failed to configure the processor.");
        }

        // Set the output content descriptor to QuickTime.
        proc.setContentDescriptor(new ContentDescriptor(FileTypeDescriptor.QUICKTIME));

        // Query for the processor for supported formats.
        // Then set it on the processor.
        tcs = proc.getTrackControls();
        fmt = tcs[0].getSupportedFormats();

        if ((fmt == null) || (fmt.length <= 0)) {
            throw new MovieMakingException("The mux does not support the input format: "
                + tcs[0].getFormat());
        }

        tcs[0].setFormat(fmt[0]);

        // We are done with programming the processor. Let's just
        // realize it.
        proc.realize();

        if (!waitForState(proc, Controller.Realized)) {
            throw new MovieMakingException("Failed to realize the processor.");
        }

        // Now, we'll need to create a DataSink.
        dsink = createDataSink(proc, outML);

        if (dsink == null) {
            throw new MovieMakingException(
                "Failed to create a DataSink for the given output MediaLocator: " + outML);
        }

        dsink.addDataSinkListener(this);
        this.fileDone = false;

        // OK, we can now start the actual transcoding.
        try {
            proc.start();
            dsink.start();
        } catch (IOException e) {
            throw new MovieMakingException("IO error during processing", e);
        }

        // Wait for EndOfStream event.
        waitForFileDone();

        // Cleanup.
        try {
            dsink.close();
        } catch (Exception e) {
            LOG.log(Level.WARNING, "Exception closing data sink: {0}", e.getMessage());
        }

        proc.removeControllerListener(this);

        return true;
    }

    /**
     * Builds the movie.
     *
     * @param   width      the movie width
     * @param   height     the movie height
     * @param   frameRate  the movie frame rate
     * @param   inFiles    the list of input files
     * @param   outML      the media locator for the produced movie
     * @return  <code>true</code> if successful; <code>false</code> otherwise
     * @throws  MovieMakingException  if there was an error
     */
    public boolean doItFiles(final int width, final int height, final int frameRate,
        final List<String> inFiles, final MediaLocator outML) throws MovieMakingException {

        ImageDataSource ids;
        Processor proc;
        TrackControl[] tcs;
        Format[] fmt;
        DataSink dsink;

        ids = new ImageDataSource(width, height, frameRate, inFiles);

        try {
            proc = Manager.createProcessor(ids);
        } catch (Exception e) {
            throw new MovieMakingException("Cannot create a processor from the data source.", e);
        }

        proc.addControllerListener(this);

        // Put the Processor into configured state so we can set
        // some processing options on the processor.
        proc.configure();

        if (!waitForState(proc, Processor.Configured)) {
            throw new MovieMakingException("Failed to configure the processor.");
        }

        // Set the output content descriptor to QuickTime.
        proc.setContentDescriptor(new ContentDescriptor(FileTypeDescriptor.QUICKTIME));

        // Query for the processor for supported formats.
        // Then set it on the processor.
        tcs = proc.getTrackControls();
        fmt = tcs[0].getSupportedFormats();

        if ((fmt == null) || (fmt.length <= 0)) {
            throw new MovieMakingException("The mux does not support the input format: "
                + tcs[0].getFormat());
        }

        tcs[0].setFormat(fmt[0]);

        // We are done with programming the processor. Let's just
        // realize it.
        proc.realize();

        if (!waitForState(proc, Controller.Realized)) {
            throw new MovieMakingException("Failed to realize the processor.");
        }

        // Now, we'll need to create a DataSink.
        dsink = createDataSink(proc, outML);

        if (dsink == null) {
            throw new MovieMakingException(
                "Failed to create a DataSink for the given output MediaLocator: " + outML);
        }

        dsink.addDataSinkListener(this);
        this.fileDone = false;

        // OK, we can now start the actual transcoding.
        try {
            proc.start();
            dsink.start();
        } catch (IOException e) {
            throw new MovieMakingException("IO error during processing", e);
        }

        // Wait for EndOfStream event.
        waitForFileDone();

        // Cleanup.
        try {
            dsink.close();
        } catch (Exception e) {
            LOG.log(Level.WARNING, "Exception closing data sink: {0}", e.getMessage());
        }

        proc.removeControllerListener(this);

        return true;
    }

    /**
     * Creates the DataSink.
     *
     * @param   proc   the processor
     * @param   outML  the locator for the output data
     * @return  the data sink
     */
    public DataSink createDataSink(final Processor proc, final MediaLocator outML) {

        DataSource src;
        DataSink temp;
        DataSink sink;

        src = proc.getDataOutput();

        if (src == null) {
            LOG.warning("The processor does not have an output DataSource");
            sink = null;
        } else {

            try {
                temp = Manager.createDataSink(src, outML);
                temp.open();
                sink = temp;
            } catch (Exception e) {
                LOG.log(Level.WARNING, "Cannot create the DataSink", e);
                sink = null;
            }
        }

        return sink;
    }

    /**
     * Blocks until the processor has transitioned to the given state. Returns false if the
     * transition failed.
     *
     * @param   proc   the processor
     * @param   state  the state
     * @return  <code>true</code> if successful; <code>false</code> otherwise
     */
    public boolean waitForState(final Processor proc, final int state) {

        synchronized (this.waitSync) {

            try {

                while ((proc.getState() < state) && this.stateTransOK) {
                    this.waitSync.wait();
                }
            } catch (Exception e) {
                LOG.log(Level.WARNING, "Exception while waiting for state: {0}", e.getMessage());
            }
        }

        return this.stateTransOK;
    }

    /**
     * Controller listener.
     *
     * @param  evt  the controller event
     */
    public void controllerUpdate(final ControllerEvent evt) {

        if ((evt instanceof ConfigureCompleteEvent) || (evt instanceof RealizeCompleteEvent)
                || (evt instanceof PrefetchCompleteEvent)) {

            synchronized (this.waitSync) {
                this.stateTransOK = true;
                this.waitSync.notifyAll();
            }
        } else if (evt instanceof ResourceUnavailableEvent) {

            synchronized (this.waitSync) {
                this.stateTransOK = false;
                this.waitSync.notifyAll();
            }
        } else if (evt instanceof EndOfMediaEvent) {
            evt.getSourceController().stop();
            evt.getSourceController().close();
        }
    }

    /**
     * Blocks until file writing is done.
     *
     * @return  <code>true</code> if success; <code>false</code> otherwise
     */
    public boolean waitForFileDone() {

        synchronized (this.waitFileSync) {

            try {

                while (!this.fileDone) {
                    this.waitFileSync.wait();
                }
            } catch (Exception e) {
                LOG.log(Level.WARNING, "Exception while waiting for done: {0}", e.getMessage());
            }
        }

        return this.fileSuccess;
    }

    /**
     * Event handler for the file writer.
     *
     * @param  evt  the event
     */
    public void dataSinkUpdate(final DataSinkEvent evt) {

        if (evt instanceof EndOfStreamEvent) {

            synchronized (this.waitFileSync) {
                this.fileDone = true;
                this.waitFileSync.notifyAll();
            }
        } else if (evt instanceof DataSinkErrorEvent) {

            synchronized (this.waitFileSync) {
                this.fileDone = true;
                this.fileSuccess = false;
                this.waitFileSync.notifyAll();
            }
        }
    }

    /**
     * Creates a media locator from the given URL.
     *
     * @param   url  the URL (if the URL contains ':', it is assumed to be a complete URL; if it
     *               begins with a file path separator, it is assumed to be an absolute path;
     *               otherwise, it is a relative path beneath the user's home directory)
     * @return  the media locator
     */
    public static MediaLocator createMediaLocator(final String url) {

        String file;
        MediaLocator loc;

        if ((url.startsWith("C:\\")) || (url.startsWith("C:/")) || (url.startsWith("D:\\"))
                || (url.startsWith("D:/"))) {
            loc = new MediaLocator("file://" + url);
        } else if (url.indexOf(':') > 0) {
            loc = new MediaLocator(url);
        } else if (url.startsWith(File.separator)) {
            loc = new MediaLocator("file:" + url);
        } else {
            file = "file:" + System.getProperty("user.dir") + File.separator + url;
            loc = new MediaLocator(file);
        }

        return loc;
    }
}
