/*
 * File:     JMFMediaPlayer.java
 * Project:  MPI Linguistic Application
 * Date:     03 April 2006
 *
 * Copyright (C) 2001-2006  Max Planck Institute for Psycholinguistics
 *
 * This program 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 2 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package mpi.eudico.client.annotator.player;

import mpi.eudico.client.annotator.ElanLayoutManager;
import mpi.eudico.client.annotator.ElanLocale;

import mpi.eudico.client.annotator.gui.FormattedMessageDlg;

import mpi.eudico.client.mediacontrol.ControllerEvent;
import mpi.eudico.client.mediacontrol.ControllerListener;
import mpi.eudico.client.mediacontrol.ControllerManager;
import mpi.eudico.client.mediacontrol.PeriodicUpdateController;
import mpi.eudico.client.mediacontrol.TimeEvent;

import mpi.eudico.client.util.TimeFormatter;

import mpi.eudico.server.corpora.clomimpl.abstr.MediaDescriptor;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import java.io.IOException;

import java.net.URL;

import javax.media.Control;
import javax.media.EndOfMediaEvent;
import javax.media.GainControl;
import javax.media.Manager;
import javax.media.MediaLocator;
import javax.media.Player;
import javax.media.StopAtTimeEvent;
import javax.media.Time;

//import javax.media.control.*;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;


/**
 * The JMF implementation of an elan media player
 */

//public class JMFMediaPlayer extends ControllerManager implements ElanMediaPlayer, javax.media.ControllerListener {
public class JMFMediaPlayer extends ControllerManager implements ElanMediaPlayer,
    ControllerListener, javax.media.ControllerListener, ActionListener {
    /** Holds value of property DOCUMENT ME! */
    protected Player player; // the JMF player

    /** Holds value of property DOCUMENT ME! */
    protected float shouldBeRate; // used to remember the player rate since JMF sometimes forgets it

    /** Holds value of property DOCUMENT ME! */
    protected long intervalStopTime;

    /** Holds value of property DOCUMENT ME! */
    protected long frozenMediaTime;

    /** Holds value of property DOCUMENT ME! */
    protected long offset;

    /** Holds value of property DOCUMENT ME! */
    protected long milliSecondsPerSample;

    /** Holds value of property DOCUMENT ME! */
    protected boolean playingInterval;

    /** Holds value of property DOCUMENT ME! */
    protected PeriodicUpdateController periodicController;

    /** Holds value of property DOCUMENT ME! */
    protected float systemVolumeLevel;

    /** Holds value of property DOCUMENT ME! */
    protected JPopupMenu popup;

    /** Holds value of property DOCUMENT ME! */
    protected MediaDescriptor mediaDescriptor;

    /** Holds value of property DOCUMENT ME! */
    protected ElanLayoutManager layoutManager;

    /** Holds value of property DOCUMENT ME! */
    protected boolean detached;

    /** Holds value of property DOCUMENT ME! */
    protected JMenuItem durationItem;

    /** Holds value of property DOCUMENT ME! */
    protected JMenuItem detachItem;
    private JMenuItem infoItem;

    /** Holds value of property DOCUMENT ME! */
    protected NoMouseComponent noMouseComponent;

    //protected FramePositioningControl fpc;
    //	private FramePositioningControl fpc;

    /**
     * Create a JMFMediaPlayer for a media URL
     *
     * @param mediaDescriptor DOCUMENT ME!
     *
     * @throws NoPlayerException DOCUMENT ME!
     */
    public JMFMediaPlayer(MediaDescriptor mediaDescriptor)
        throws NoPlayerException {
        this.mediaDescriptor = mediaDescriptor;

        // WebStart related initialization, see at the end of this file for details
        JMFClassLoader.initJMFJNI();

        // light weight in JMF is unusable because some mpegs can not be rendered in this mode
        //		Manager.setHint(Manager.LIGHTWEIGHT_RENDERER, new Boolean(true));
        //		Manager.setHint(Manager.PLUGIN_PLAYER, new Boolean(true));
        String URLString = mediaDescriptor.mediaURL;

        //URLString = "file:///" + URLString.substring(i);
        //URLString = "file://Sun03/Corpora/media-archive/lac_data/senft/StagedEvents/Media/SE-1.mpg";
        //URLString = "rtsp://nt06.mpi.nl/onno/echo2a_6.mp4";
        //URLString = "rtsp://nt06.mpi.nl/SyncTest_hinted.mov";
        //URLString = "rtsp://nt06.mpi.nl:80/De_Eng.mpg";
        player = null;

        try {
            System.out.println("mediaURL = " + URLString);

            if (URLString.startsWith("rtsp")) {
                // do something for streaming
                System.out.println("stream");

                MediaLocator ml = new MediaLocator(URLString);
                player = Manager.createPlayer(ml);
            } else {
                URL mediaURL = new URL(URLString);
                player = Manager.createPlayer(mediaURL);

                //               player = (Player) Manager.createProcessor(mediaURL);
            }
        } catch (javax.media.NoPlayerException e) {
            System.out.println(
                "javax.media.NoPlayerException while creating JMF player");
            e.printStackTrace();
            throw new NoPlayerException("javax.media.NoPlayerException");
        } catch (IOException e) {
            System.out.println("IO exception while creating JMF player");
            e.printStackTrace();
            throw new NoPlayerException(
                "IO exception, problem to connect to data source");
        } catch (Exception e) {
            System.out.println("Unknown exception while creating JMF player");
            e.printStackTrace();
            throw new NoPlayerException(
                "Exception, problem to connect to data source");
        }

        // initialize local parameters
        offset = mediaDescriptor.timeOrigin;
        shouldBeRate = 1.0f;
        frozenMediaTime = -1;
        milliSecondsPerSample = 40;

        // make sure the player is prefetched so the client code is not bothered with it
        // for .mpg a start stop sequence is needed to make setMediaTime possible before
        // the client code called player.start(). This is needed to be able to look for
        // a certain scene before the movie was started. It looks like JMF does not read
        // all the necessary info to do this when it prefetches a player on mpg data.
        // mute the player during start stop
        long timeout = System.currentTimeMillis() + 5000;
        player.realize();

        while (getState() != javax.media.Controller.Realized) {
            try {
                Thread.sleep(50);

                if (System.currentTimeMillis() > timeout) {
                    player.close();
                    throw new NoPlayerException(
                        "Could not realize the jmf player");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //       printControls(player);

        /*       fpc = (FramePositioningControl)player.getControl("javax.media.control.FramePositioningControl");
               if (fpc == null) {
                   System.err.println("The player does not support FramePositioningControl.");
               } else {
                   System.err.println("OK");
               }
        */
        GainControl gain = player.getGainControl();
        float level = 1.0f;

        if (gain != null) {
            level = gain.getLevel();
            gain.setLevel(0);
        }

        systemVolumeLevel = level;
        start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        stop();
        setMediaTime(0);

        if (gain != null) {
            gain.setLevel(level);
        }

        //		aspectRatio = (float) (getVisualComponent().getPreferredSize().getWidth() /
        //									getVisualComponent().getPreferredSize().getHeight());
        // this takes care of detecting the EndOfMediaEvent
        player.addControllerListener(this);

        if (player.getVisualComponent() != null) {
            player.getVisualComponent().addMouseListener(new MouseHandler());
        }

        //		fpc = (FramePositioningControl) player.getControl("javax.media.control.FramePositioningControl");
        popup = new JPopupMenu();
        durationItem = new JMenuItem(ElanLocale.getString("Player.duration") +
                ":  " + TimeFormatter.toString(getMediaDuration()));
        durationItem.setEnabled(false);
        infoItem = new JMenuItem(ElanLocale.getString("Player.Info"));
        infoItem.addActionListener(this);
        popup.addSeparator();
        popup.add(infoItem);
        popup.add(durationItem);

        //		popup.add("duration : " + ViewerTools.toString(getMediaDuration())).setFont(Constants.SMALLFONT);
    }

    public MediaDescriptor getMediaDescriptor() {
        return mediaDescriptor;
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public String getFrameworkDescription() {
        return "Java Media Framework " + Manager.getVersion();
    }

    /**
     * Restore the system volume level, JMF destroys it
     */
    public void finalize() {
        if (player.getState() >= Player.Realized) {
            GainControl gain = player.getGainControl();

            if (gain != null) {
                gain.setLevel(systemVolumeLevel);
            }
        }
    }

    /**
     * JMF controllerUpdate
     *
     * @param event DOCUMENT ME!
     */
    public void controllerUpdate(javax.media.ControllerEvent event) {
        if (event instanceof EndOfMediaEvent) {
            // stops all the controllers and sets the media time
            stop();
        } else if (event instanceof StopAtTimeEvent) {
            System.out.println("stopped by jmf itself");
            frozenMediaTime = intervalStopTime;
            stop();
        }
    }

    /**
     * Elan controllerUpdate Used to stop at the stop time in cooperation with
     * the playInterval method
     *
     * @param event DOCUMENT ME!
     */
    public synchronized void controllerUpdate(ControllerEvent event) {
        if (event instanceof TimeEvent) {
            if (periodicController != null) {
                if (getMediaTime() >= intervalStopTime) {
                    // freeze mediaTime
                    frozenMediaTime = intervalStopTime;

                    // remember the volume
                    float volume = getVolume();

                    // mute the player, makes the acoustic behaviour more deterministic
                    // because stop() does not stop instantaneously
                    setVolume(0);

                    // stop the player
                    stop();

                    //restore the volume
                    setVolume(volume);
                }
            }
        }
    }

    /**
     * play between two times. This method uses the contollerUpdate method to
     * detect if the stop time is passed. The setStopTime method of JMF can
     * not be used because it gives unstable behaviour
     *
     * @param startTime DOCUMENT ME!
     * @param stopTime DOCUMENT ME!
     */
    public synchronized void playInterval(long startTime, long stopTime) {
        if ((player == null) || playingInterval || (stopTime <= startTime)) {
            return;
        }

        periodicController = new PeriodicUpdateController(25);
        periodicController.addControllerListener(this);
        addController(periodicController);
        intervalStopTime = stopTime + offset;
        setMediaTime(startTime);
        playingInterval = true;

        //player.setStopTime(new Time(1000000 * stopTime));
        start();
    }

    /**
     * Empty implementation for ElanMediaPlayer Interface
     * Only usefull for player that corectly supports setting stop time
     */
    public void setStopTime(long stopTime) {
    }

    /**
     * Disable all code for interval playing
     */
    private void stopPlayingInterval() {
        if (periodicController != null) {
            periodicController.removeControllerListener(this);
            removeController(periodicController);
            periodicController = null;
        }

        playingInterval = false;
    }

    /**
     * Gets the display Component for this Player.
     *
     * @return DOCUMENT ME!
     */
    public java.awt.Component getVisualComponent() {
        if (player == null) {
            return null;
        }

        //		if (noMouseComponent == null) {
        //			noMouseComponent = new NoMouseComponent();
        //			noMouseComponent.add(player.getVisualComponent());
        //		}
        //		return noMouseComponent;
        return player.getVisualComponent();
    }

    /**
     * Gets the control Component for this Player. Necessary for CorexViewer.
     * A.K.
     *
     * @return DOCUMENT ME!
     */
    public java.awt.Component getControlPanelComponent() {
        if (player == null) {
            return null;
        }

        return player.getControlPanelComponent();
    }

    /**
     * Gets the ratio between width and height of the video image
     *
     * @return DOCUMENT ME!
     */
    public float getAspectRatio() {
        if (player == null) {
            return 0;
        }

        float aspectRatio = (float) (getVisualComponent().getPreferredSize()
                                         .getWidth() / getVisualComponent()
                                                           .getPreferredSize()
                                                           .getHeight());

        return aspectRatio;
    }

    /**
     * Starts the Player as soon as possible. is not synchronized in JMF
     */
    public synchronized void start() {
        if ((player == null) || (getState() == javax.media.Controller.Started)) {
            return;
        }

        // do not try to start at the end of the media, the JMF player blocks
        // start playing at the beginning of the media data
        if ((getMediaDuration() - getMediaTime()) < 40) {
            setMediaTime(0);
        }

        // tell JMF to start the player
        player.start();

        // wait for the player to be in the started state
        while (getState() != javax.media.Controller.Started) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // make sure all managed controllers are started
        startControllers();
    }

    /**
     * Stop the media player
     */
    public synchronized void stop() {
        if (player == null) {
            return;
        }

        // only freeze media time if it is not frozen yet
        if (frozenMediaTime < 0) {
            frozenMediaTime = getMediaTime();
        }

        // make sure all managed controllers are stopped
        stopControllers();

        // tell JMF to stop the player
        player.stop();

        // wait for the player to stop
        while (player.getState() == javax.media.Controller.Started) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // make sure that all interval playing is finished
        if (playingInterval) {
            stopPlayingInterval();
        }

        // make sure the player is at the correct time after this stop
        setMediaTime(frozenMediaTime);

        // undefine the freeze time
        frozenMediaTime = -1;

        // the following code is needed because JMF sometimes forgets its player rate
        // First an other rate than the current (shouldBe) rate must be set because
        // the JMF player thinks it is playing at the shouldBeRate and ignores calls
        // to setRate for the rate value it thinks it is playing at.
        player.setRate(0.995f * shouldBeRate);
        player.setRate(shouldBeRate);
    }

    /**
     * Tell if this player is playing
     *
     * @return DOCUMENT ME!
     */
    public boolean isPlaying() {
        if (player == null) {
            return false;
        }

        return getState() == javax.media.Controller.Started;
    }

    /**
     * DOCUMENT ME!
     *
     * @return the step size for one frame
     */
    public long getMilliSecondsPerSample() {
        return milliSecondsPerSample;
    }

    /**
     * DOCUMENT ME!
     *
     * @param milliSeconds the step size for one frame
     */
    public void setMilliSecondsPerSample(long milliSeconds) {
        milliSecondsPerSample = milliSeconds;
    }

    /**
     * Return the players state
     *
     * @return DOCUMENT ME!
     */
    private int getState() {
        if (player == null) {
            return javax.media.Controller.Prefetched;
        }

        return player.getState();
    }

    /**
     * Gets the volume as a number between 0 and 1
     *
     * @return DOCUMENT ME!
     */
    public float getVolume() {
        if (player.getGainControl() == null) {
            return 0;
        }

        return player.getGainControl().getLevel();
    }

    /**
     * Gets the volume as a number between 0 and 1
     *
     * @param level DOCUMENT ME!
     */
    public void setVolume(float level) {
        if (player.getGainControl() == null) {
            return;
        }

        player.getGainControl().setLevel(level);
    }

    /**
     * Set the offset to be used in get and set media time for this player
     *
     * @param offset the offset in milli seconds
     */
    public void setOffset(long offset) {
        this.offset = offset;
        mediaDescriptor.timeOrigin = offset;
    }

    /**
     * DOCUMENT ME!
     *
     * @return the offset used by this player
     */
    public long getOffset() {
        return offset;
    }

    /**
     * Sets the Clock's media time in milli seconds. is not synchronized in JMF
     *
     * @param time DOCUMENT ME!
     */
    public synchronized void setMediaTime(long time) {
        if (player == null) {
            return;
        }

        // do not set media time on a started player
        if (isPlaying()) {
            stop();
        }

        time = time + offset;
        player.setMediaTime(new Time(time * 1000000));

        // set the media time for the connected controllers
        setControllersMediaTime(time - offset);
    }

    public void nextFrame() {
        setMediaTime(getMediaTime() + getMilliSecondsPerSample());
    }

    public void previousFrame() {
        setMediaTime(getMediaTime() - getMilliSecondsPerSample());
    }

    /**
     * Gets this Clock's current media time in milli seconds.
     *
     * @return DOCUMENT ME!
     */
    public long getMediaTime() {
        if (player == null) {
            return 0;
        }

        // return frozen media time if it is set
        if (frozenMediaTime >= 0) {
            return frozenMediaTime;
        }

        return (long) (1000 * player.getMediaTime().getSeconds()) - offset;
    }

    /**
     * Gets the current temporal scale factor.
     *
     * @return DOCUMENT ME!
     */
    public float getRate() {
        if (player == null) {
            return 0;
        }

        return player.getRate();
    }

    /**
     * Sets the temporal scale factor.
     *
     * @param rate DOCUMENT ME!
     */
    public synchronized void setRate(float rate) {
        if (player == null) {
            return;
        }

        // do not set rate on a started player
        if (isPlaying()) {
            stop();
        }

        // set the rate for the connected controllers
        setControllersRate(rate);

        shouldBeRate = rate;
        player.setRate(rate);
    }

    /**
     * Get the duration of the media represented by this object in milli
     * seconds.
     *
     * @return DOCUMENT ME!
     */
    public long getMediaDuration() {
        if (player == null) {
            return 0;
        }

        return (long) (1000 * player.getDuration().getSeconds()) - offset;
    }

    /**
     * DOCUMENT ME!
     *
     * @param layoutManager DOCUMENT ME!
     */
    public void setLayoutManager(ElanLayoutManager layoutManager) {
        if (this.layoutManager == null) {
            detachItem = new JMenuItem(ElanLocale.getString("Detachable.detach"));
            detachItem.addActionListener(this);
            popup.insert(detachItem, 0);
        }

        this.layoutManager = layoutManager;
    }

    /**
     * DOCUMENT ME!
     */
    public void updateLocale() {
        infoItem.setText(ElanLocale.getString("Player.Info"));
        durationItem.setText(ElanLocale.getString("Player.duration") + ":  " +
            TimeFormatter.toString(getMediaDuration()));

        if (detachItem != null) {
            if (detached) {
                detachItem.setText(ElanLocale.getString("Detachable.attach"));
            } else {
                detachItem.setText(ElanLocale.getString("Detachable.detach"));
            }
        }
    }

    /*
     *
     */
    public void actionPerformed(ActionEvent e) {
        if (e.getSource().equals(detachItem) && (layoutManager != null)) {
            if (detached) {
                layoutManager.attach(JMFMediaPlayer.this.getVisualComponent());
                detachItem.setText(ElanLocale.getString("Detachable.detach"));
                detached = false;
            } else {
                layoutManager.detach(JMFMediaPlayer.this.getVisualComponent());
                detachItem.setText(ElanLocale.getString("Detachable.attach"));
                detached = true;
            }
        } else if (e.getSource() == infoItem) {
            new FormattedMessageDlg(this);
        }
    }

    void printControls(Player player) {
        System.out.println("Player info: " + player);

        Control[] controls = player.getControls();

        for (int i = 0; i < controls.length; i++) {
            System.out.print("\t" + i + ": ");
            System.out.println(controls[i].getClass());

            /*
                        if (controls[i] instanceof FormatControl) {
                            FormatControl fc = (FormatControl) controls[i];
                            System.out.println("\tFormatcontrol format: " + fc.getFormat());
                            System.out.println("\tControl Component: " +
                                fc.getControlComponent());
                        }

                        if (controls[i] instanceof FrameGrabbingControl) {
                            System.out.println("\t - is framgrabbing control");
                        }

                        if (controls[i] instanceof FramePositioningControl) {
                            System.out.println("\t - is framepositioning control");
                        }*/
        }
    }

    public void cleanUpOnClose() {
    }

    // Code coppied from Greg's version of JMFVideoPlayer
    // ALex heeft deze code verbeterd in de client/util package, die code gaan gebruiken zodra de nieywe package structuur er is

    /**
     * DOCUMENT ME!
     * $Id: JMFMediaPlayer.java,v 1.5 2006/03/30 14:59:42 hasloe Exp $
     * @author $Author: hasloe $
     * @version $Revision: 1.5 $
     */
    private class NoMouseComponent extends java.awt.Container {
        /**
         * DOCUMENT ME!
         *
         * @param ml DOCUMENT ME!
         */
        public void addMouseListener(java.awt.event.MouseListener ml) {
            // no mouselistener for you!
        }
    }

    /**
     * DOCUMENT ME!
     * $Id: JMFMediaPlayer.java,v 1.5 2006/03/30 14:59:42 hasloe Exp $
     * @author $Author: hasloe $
     * @version $Revision: 1.5 $
     */
    protected class MouseHandler extends MouseAdapter {
        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void mousePressed(MouseEvent e) {
            if (SwingUtilities.isRightMouseButton(e) || e.isPopupTrigger()) {
                popup.show(player.getVisualComponent(), e.getPoint().x,
                    e.getPoint().y);
            }
        }

        /**
         * On a double click on the visual component of this player it will become
         * the first (largest) player.
         * @param e the mouse event
         */
        public void mouseClicked(java.awt.event.MouseEvent e) {
            if (e.getClickCount() >= 2) {
                if (layoutManager != null) {
                    layoutManager.setFirstPlayer(JMFMediaPlayer.this);
                }

                return;
            }
        }
    }
}
