/*
 * File:     NativeMediaPlayerWindowsDS.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 com.jniwrapper.DefaultLibraryLoader;
import com.jniwrapper.DoubleFloat;
import com.jniwrapper.ExternalArrayPointer;
import com.jniwrapper.Int32;
import com.jniwrapper.Parameter;
import com.jniwrapper.Pointer;
import com.jniwrapper.PrimitiveArray;
import com.jniwrapper.UInt8;

import com.jniwrapper.win32.automation.types.BStr;

import com.jniwrapper.win32.com.ComException;

import com.jniwrapper.win32.com.types.ClsCtx;
import com.jniwrapper.win32.com.types.LongPtr;

import com.jniwrapper.win32.ole.OleFunctions;

import com.jniwrapper.win32.ui.WindowProc;
import com.jniwrapper.win32.ui.WindowTools;
import com.jniwrapper.win32.ui.Wnd;
import com.jniwrapper.win32.ui.WndClass;

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

import mpi.eudico.client.annotator.export.ImageExporter;

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

import mpi.eudico.client.annotator.nativemedia.quartztypelib.*;
import mpi.eudico.client.annotator.nativemedia.quartztypelib.impl.*;

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

import mpi.eudico.client.util.TimeFormatter;

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

//import mpi.eudico.client.annotator.nativemedia.wmplib.*;
import java.awt.Component;
import java.awt.Image;
import java.awt.Panel;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.image.BufferedImage;

import java.util.Vector;

import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;


/**
 * The direct show based implementation of an elan media player
 */
public class NativeMediaPlayerWindowsDS extends ControllerManager
    implements ElanMediaPlayer, ControllerListener, ActionListener {
    /** Holds value of property DOCUMENT ME! */
    private final static int STATE_STOP = 0;

    /** Holds value of property DOCUMENT ME! */
    private final static int STATE_PAUSE = 1;

    /** Holds value of property DOCUMENT ME! */
    private final static int STATE_RUN = 2;

    /** Holds value of property DOCUMENT ME! */
    private final static int DIRECT_SHOW_TRUE = -1;

    /** Holds value of property DOCUMENT ME! */
    private static final String CALLBACK_WINDOW_CLASS_NAME = "CallbackWindowClass";

    /** Holds value of property DOCUMENT ME! */
    private final static String MEDIA_TYPE_WAVE = "{e436eb8b-524f-11ce-9f53-0020af0ba770}";

    /** Holds value of property DOCUMENT ME! */
    private final static String MEDIA_TYPE_MPG1 = "{e436eb84-524f-11ce-9f53-0020af0ba770}";

    /** Holds value of property DOCUMENT ME! */
    private final static String MEDIA_TYPE_MPG2 = "{e06d8022-db46-11cf-b4d1-00805f6cbbea}";
    private MediaDescriptor mediaDescriptor;
    private IMediaControl mediaControl;
    private IMediaPosition mediaPosition;
    private IBasicAudio basicAudio;
    private IBasicVideo2 basicVideo;
    private IVideoWindow videoWindow;

    /** Holds value of property DOCUMENT ME! */
    private final IMediaEventEx mediaEvent;
    private IMediaTypeInfo mediaTypeInfo;
    private VisualComponent visualComponent;
    private Wnd videoWnd;
    private boolean playing;
    private long offset;
    private long duration;
    private long stopTime;
    private boolean adjustedStopTime;
    private float aspectRatio;
    private double millisPerSample;
    private JPopupMenu popup;
    private JMenuItem durationItem;

    /** Holds value of property DOCUMENT ME! */
    protected JMenuItem detachItem;
    private JMenuItem infoItem;
    private JMenuItem saveItem;
    private ElanLayoutManager layoutManager;
    private boolean detached;
    private boolean isWaveMedia;
    private boolean isMpeg1Media;
    private boolean isMpeg2Media;
    private boolean mediaInfoDetected;

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

            // make sure that the jniwrapper library is available
            if (DefaultLibraryLoader.getInstance().findLibrary("jniwrap") == null) {
                // relative path to the cvs MPI directory which is the user.dir for developers
                DefaultLibraryLoader.getInstance().addPath("lib/java/jniwrapper");
                DefaultLibraryLoader.getInstance().loadLibrary("jniwrap");
            }

            String URLString = mediaDescriptor.mediaURL;

            if (URLString.startsWith("file:") &&
                    !URLString.startsWith("file:///")) {
                URLString = URLString.substring(5);
            }

            OleFunctions.oleInitialize();
            mediaControl = FilgraphManager.create(new ClsCtx(
                        ClsCtx.CLSCTX_INPROC_SERVER));

            mediaControl.renderFile(new BStr(URLString));

            final IMediaPositionImpl mp = new IMediaPositionImpl();
            mediaControl.queryInterface(mp.getIID(), mp);
            mediaPosition = mp;

            final IVideoWindowImpl vw = new IVideoWindowImpl();
            mediaControl.queryInterface(vw.getIID(), vw);
            videoWindow = vw;

            final IBasicVideo2Impl bv = new IBasicVideo2Impl();
            mediaControl.queryInterface(bv.getIID(), bv);
            basicVideo = bv;

            final IBasicAudioImpl ba = new IBasicAudioImpl();
            mediaControl.queryInterface(ba.getIID(), ba);
            basicAudio = ba;

            final IMediaEventExImpl me = new IMediaEventExImpl();
            mediaControl.queryInterface(me.getIID(), me);
            mediaEvent = me;

            // analyse the filter structure and force mpeg1 stream splitter for mpeg1 media 
            detectMediaType();
            adjustFiltersIfNeeded();

            // make distinction between audio and video
            // side effect of prepareFilters where media type is determined
            if (isWaveMedia ||
                    mediaDescriptor.mimeType.equals(
                        MediaDescriptor.GENERIC_AUDIO_TYPE)) {
                visualComponent = null;
                millisPerSample = 40; // arbitrary choice, user always has pixel step
            } else {
                visualComponent = new VisualComponent();
                visualComponent.addMouseListener(new MouseHandler());

                mediaControl.pause();

                Int32 plX = new Int32();
                Int32 plY = new Int32();
                basicVideo.getPreferredAspectRatio(plX, plY);
                aspectRatio = ((float) plX.getValue()) / plY.getValue();

                if (aspectRatio == 0.0) {
                    aspectRatio = ((float) basicVideo.getSourceWidth().getValue()) / basicVideo.getSourceHeight()
                                                                                               .getValue();
                }

                //System.out.println("Video w: " + basicVideo.getSourceWidth() + " h: " + basicVideo.getSourceHeight());
                System.out.println("Aspect Ratio: " + aspectRatio);

                double tpf = basicVideo.getAvgTimePerFrame().getValue();
                millisPerSample = 1000 * tpf;
                System.out.println("Millis per Sample: " + millisPerSample);

                if (millisPerSample == 0) {
                    millisPerSample = 40;
                }
            }

            offset = mediaDescriptor.timeOrigin;
            System.out.println("offset: " + offset);
            duration = (long) (1000 * mediaPosition.getDuration().getValue()) -
                offset;
            System.out.println("duration: " + duration);
            stopTime = duration;

            /* DISABLED BLOCK WITH EXPERIMENTAL ACTIVE CALLBACK CODE
               WndClass callbackWndClass = new WndClass(new CallbackWindowProc(), CALLBACK_WINDOW_CLASS_NAME);
               //WndClass callbackWndClass = new WndClass(new CallbackWindowProc(), NativeMediaPlayerWindowsDS.class.getName());
               Int16 result = callbackWndClass.register();
               System.out.println("result " + result.getValue());
               //Wnd listeningWindow = Wnd.createWindow(CALLBACK_WINDOW_CLASS_NAME);
               Module hModule = Module.getCurrent();
               Handle NULL = new Handle();
               Wnd nullParent = new Wnd();
               Wnd listeningWindow = Wnd.createWindow(
                       CALLBACK_WINDOW_CLASS_NAME,
                       "NativeMediaPlayerWindowsDS",
                       Wnd.WS_CHILD,
                       0,
                       0,
                       0,
                       0,
                       //nullParent,
                       nullParent,
                       NULL,
                       hModule,
                       NULL);
               LongPtr windowHandle = new LongPtr();
               windowHandle.setValue(listeningWindow.getValue());
               me.setNotifyWindow(windowHandle, new Int32(12345), null);*/
            JPopupMenu.setDefaultLightWeightPopupEnabled(false);
            popup = new JPopupMenu();
            infoItem = new JMenuItem(ElanLocale.getString("Player.Info"));
            infoItem.addActionListener(this);
            durationItem = new JMenuItem(ElanLocale.getString("Player.duration") +
                    ":  " + TimeFormatter.toString(getMediaDuration()));
            durationItem.setEnabled(false);
            saveItem = new JMenuItem(ElanLocale.getString("Player.SaveFrame"));
            saveItem.addActionListener(this);
            popup.addSeparator();
            popup.add(saveItem);
            popup.add(infoItem);
            popup.add(durationItem);
        } catch (com.jniwrapper.win32.com.ComException e) {
            e.printStackTrace();

            /* ??
               if (isMpeg2Media) {
                   JOptionPane.showMessageDialog(null,
                       (ElanLocale.getString("NativeMediaPlayerWindows.Error2") + "\n" +
                       ElanLocale.getString("NativeMediaPlayerWindows.Error3")),
                       ElanLocale.getString("Message.Error"), JOptionPane.ERROR_MESSAGE);
               }
             */
            throw new NoPlayerException(
                "Problem while creating directshow based media player");
        } catch (Exception e) {
            e.printStackTrace();
            throw new NoPlayerException(
                "Problem while creating directshow based media player");
        }
    }

    /**
     * DOCUMENT ME!
     */
    public void finalize() {
        System.out.println("Finalize media player...");
        OleFunctions.oleUninitialize();
    }

    public void cleanUpOnClose() {
        System.out.println("Clean up media player...");
        OleFunctions.oleUninitialize();
        mediaControl = null;
        mediaPosition = null;
        basicAudio = null;
        basicVideo = null;
        videoWindow = null;
    }

    /**
     * Sets the global booleans isMpeg1Media and isWaveMedia Needs to be called
     * only once;
     */
    private void detectMediaType() {
        if (mediaInfoDetected) {
            return;
        }

        mediaControl.stop();

        IFilterInfoImpl filterInfo = new IFilterInfoImpl();
        IPinInfoImpl pinInfo = new IPinInfoImpl();
        IAMCollectionImpl filterColl = new IAMCollectionImpl();
        IAMCollectionImpl pinColl = new IAMCollectionImpl();
        IMediaTypeInfoImpl mediaInfo = new IMediaTypeInfoImpl();
        mediaControl.stop();
        mediaControl.getFilterCollection().queryInterface(filterColl.getIID(),
            filterColl);

        long nFilters = filterColl.getCount().getValue();

        for (int i = 0; i < nFilters; i++) {
            filterColl.item(new Int32(i)).queryInterface(filterInfo.getIID(),
                filterInfo);

            // only the file source in the filter is needed to see what media we are dealing with
            if (filterInfo.getIsFileSource().getValue() == DIRECT_SHOW_TRUE) {
                filterInfo.getPins().queryInterface(pinColl.getIID(), pinColl);

                long nPins = pinColl.getCount().getValue();

                for (int j = 0; j < nPins; j++) {
                    pinColl.item(new Int32(j)).queryInterface(pinInfo.getIID(),
                        pinInfo);

                    // only one pin but just to be sure
                    if (pinInfo.getName().getValue().equalsIgnoreCase("output")) {
                        pinInfo.getConnectionMediaType().queryInterface(mediaInfo.getIID(),
                            mediaInfo);

                        //System.out.println("MI subtype: " +  mediaInfo.getSubtype().getValue());
                        if (mediaInfo.getSubtype().getValue().equalsIgnoreCase(MEDIA_TYPE_WAVE)) {
                            //System.out.println("Wave media data");
                            isWaveMedia = true;
                        } else if (mediaInfo.getSubtype().getValue()
                                                .equalsIgnoreCase(MEDIA_TYPE_MPG1)) {
                            //System.out.println("MPG1 media data");	
                            isMpeg1Media = true;
                        } else if (mediaInfo.getSubtype().getValue()
                                                .equalsIgnoreCase(MEDIA_TYPE_MPG2)) {
                            isMpeg2Media = true;
                        }
                    }
                }
            }
        }

        mediaInfoDetected = true;
    }

    /**
     * Prints info from the current filters in the filter tree Not needed but
     * can be handy for debugging purposes
     */
    private void printFilterInfo() {
        mediaControl.stop();

        IFilterInfoImpl filterInfo = new IFilterInfoImpl();
        IPinInfoImpl pinInfo = new IPinInfoImpl();
        IAMCollectionImpl filterColl = new IAMCollectionImpl();
        IAMCollectionImpl pinColl = new IAMCollectionImpl();
        mediaControl.stop();
        mediaControl.getFilterCollection().queryInterface(filterColl.getIID(),
            filterColl);

        long nFilters = filterColl.getCount().getValue();

        for (int i = 0; i < nFilters; i++) {
            filterColl.item(new Int32(i)).queryInterface(filterInfo.getIID(),
                filterInfo);
            System.out.println("index: " + i + "  name: " +
                filterInfo.getName().getValue());
            filterInfo.getPins().queryInterface(pinColl.getIID(), pinColl);

            long nPins = pinColl.getCount().getValue();

            for (int j = 0; j < nPins; j++) {
                pinColl.item(new Int32(j)).queryInterface(pinInfo.getIID(),
                    pinInfo);
                System.out.println("	pin: " + j + "  name: " +
                    pinInfo.getName().getValue());
            }
        }
    }

    /**
     * At this moment mpeg2 splitters can mess up mpeg1 rendering behaviour
     * Make sure that mpg1 data gets rendered with the default mpg1 splitter
     * in the filter graph
     *
     * @throws NoPlayerException DOCUMENT ME!
     */
    private void adjustFiltersIfNeeded() throws NoPlayerException {
        mediaControl.stop();
        detectMediaType();

        IFilterInfoImpl filterInfo = new IFilterInfoImpl();
        IFilterInfoImpl currentSplitterFilterInfo = new IFilterInfoImpl();
        IFilterInfoImpl defaultSplitterFilterInfo = new IFilterInfoImpl();
        IAMCollectionImpl splitterPinColl = new IAMCollectionImpl();
        IPinInfoImpl pinInfo = new IPinInfoImpl();
        IPinInfoImpl fileSourceOutputPinInfo = new IPinInfoImpl();
        IPinInfoImpl audioDecoderInputPinInfo = new IPinInfoImpl();
        IPinInfoImpl videoDecoderInputPinInfo = new IPinInfoImpl();
        IMediaTypeInfoImpl mediaInfo = new IMediaTypeInfoImpl();
        IAMCollectionImpl filterColl = new IAMCollectionImpl();
        IAMCollectionImpl pinColl = new IAMCollectionImpl();
        mediaControl.getFilterCollection().queryInterface(filterColl.getIID(),
            filterColl);

        long nFilters = filterColl.getCount().getValue();

        for (int i = 0; i < nFilters; i++) {
            filterColl.item(new Int32(i)).queryInterface(filterInfo.getIID(),
                filterInfo);

            // only the file source in the filter is needed to see what media we are dealing with
            if (filterInfo.getIsFileSource().getValue() == DIRECT_SHOW_TRUE) {
                filterInfo.getPins().queryInterface(pinColl.getIID(), pinColl);

                long nPins = pinColl.getCount().getValue();

                for (int j = 0; j < nPins; j++) {
                    pinColl.item(new Int32(j)).queryInterface(pinInfo.getIID(),
                        pinInfo);

                    // only one pin but just to be sure
                    if (pinInfo.getName().getValue().equalsIgnoreCase("output")) {
                        // we have got the file source output pin
                        pinColl.item(new Int32(j)).queryInterface(fileSourceOutputPinInfo.getIID(),
                            fileSourceOutputPinInfo);

                        // get the pin collection of the current splitter
                        fileSourceOutputPinInfo.getConnectedTo().queryInterface(pinInfo.getIID(),
                            pinInfo);
                        pinInfo.getFilterInfo().queryInterface(currentSplitterFilterInfo.getIID(),
                            currentSplitterFilterInfo);
                        currentSplitterFilterInfo.getPins().queryInterface(splitterPinColl.getIID(),
                            splitterPinColl);
                        System.out.println("Media splitter filter name: " +
                            currentSplitterFilterInfo.getName().getValue());
                    }
                }
            }
        }

        // if it is mpg1 we need to make sure that the mpeg splitter is the default directshow mpeg1 splitter
        // because the cheepo mpeg2 splitters introduce stop accuracy problems
        if (isMpeg1Media) {
            // build a list of known problematic spliiters
            Vector problemSplitters = new Vector();
            problemSplitters.add("Moonlight-Elecard MPEG2 Demultiplexer");
            problemSplitters.add("Elecard MPEG2 Demultiplexer");

            // warn the user for a splitter that can not be removed by ELAN
            String currentSplitter = currentSplitterFilterInfo.getName()
                                                              .getValue();

            //System.out.println("Splitter name: " + currentSplitter);
            if (problemSplitters.contains(currentSplitter)) {
                JOptionPane.showMessageDialog(null,
                    ElanLocale.getString("NativeMediaPlayerWindows.Warning1") +
                    "\"" + currentSplitter + "\"",
                    ElanLocale.getString("Message.Warning"),
                    JOptionPane.WARNING_MESSAGE);

                return;
            }

            // gather the pins the current splitter is connected to
            long nPins = splitterPinColl.getCount().getValue();

            for (int i = 0; i < nPins; i++) {
                splitterPinColl.item(new Int32(i)).queryInterface(pinInfo.getIID(),
                    pinInfo);

                String pinName = pinInfo.getName().getValue().toLowerCase();

                if (pinName.indexOf("input") >= 0) {
                    pinInfo.getConnectedTo().queryInterface(fileSourceOutputPinInfo.getIID(),
                        fileSourceOutputPinInfo);
                } else if (pinName.indexOf("audio") >= 0) {
                    pinInfo.getConnectedTo().queryInterface(audioDecoderInputPinInfo.getIID(),
                        audioDecoderInputPinInfo);
                } else if (pinName.indexOf("video") >= 0) {
                    pinInfo.getConnectedTo().queryInterface(videoDecoderInputPinInfo.getIID(),
                        videoDecoderInputPinInfo);
                }
            }

            // find the default splitter among the registered filters
            boolean defaultSplitterAvailable = false;
            IRegFilterInfoImpl regFilterInfo = new IRegFilterInfoImpl();
            mediaControl.getRegFilterCollection().queryInterface(filterColl.getIID(),
                filterColl);
            nFilters = filterColl.getCount().getValue();

            for (int i = 0; i < nFilters; i++) {
                filterColl.item(new Int32(i)).queryInterface(regFilterInfo.getIID(),
                    regFilterInfo);

                if (regFilterInfo.getName().getValue().equalsIgnoreCase("MPEG-I Stream Splitter")) {
                    regFilterInfo.filter().queryInterface(defaultSplitterFilterInfo.getIID(),
                        defaultSplitterFilterInfo);
                    defaultSplitterAvailable = true;

                    break;
                }
            }

            // only proceed when the default splitter was found
            // there is no check if the current splitter already is the default splitter, who cares
            if (defaultSplitterAvailable) {
                try {
                    //		        	printFilterInfo();
                    // disconnect current splitter. by disconnecting the file source all pins are disconnected
                    fileSourceOutputPinInfo.disconnect();

                    // connect the file source to the default splitter
                    // this must be done to make the audio and video output pins available
                    defaultSplitterFilterInfo.getPins().queryInterface(splitterPinColl.getIID(),
                        splitterPinColl);
                    nPins = splitterPinColl.getCount().getValue();
                    splitterPinColl.item(new Int32(0)).queryInterface(pinInfo.getIID(),
                        pinInfo);
                    fileSourceOutputPinInfo.connect(pinInfo);

                    // let windows build the rest of the filter graph by calling render on the video and audio pins
                    defaultSplitterFilterInfo.getPins().queryInterface(splitterPinColl.getIID(),
                        splitterPinColl);
                    nPins = splitterPinColl.getCount().getValue();

                    for (int i = 0; i < nPins; i++) {
                        splitterPinColl.item(new Int32(i)).queryInterface(pinInfo.getIID(),
                            pinInfo);

                        String pinName = pinInfo.getName().getValue()
                                                .toLowerCase();

                        if (pinName.indexOf("audio") >= 0) {
                            pinInfo.render();
                        } else if (pinName.indexOf("video") >= 0) {
                            pinInfo.render();
                        }
                    }

                    /*
                       // connect the audio and video pins
                       defaultSplitterFilterInfo.getPins().queryInterface(splitterPinColl.getIID(), splitterPinColl);
                       nPins = splitterPinColl.getCount().getValue();
                       for (int i = 0; i < nPins; i++) {
                           splitterPinColl.item(new Int32(i)).queryInterface(pinInfo.getIID(), pinInfo);
                           String pinName = pinInfo.getName().getValue().toLowerCase();
                           if (pinName.indexOf("audio") >= 0) {
                               pinInfo.connect(audioDecoderInputPinInfo);
                           } else if (pinName.indexOf("video") >= 0) {
                               pinInfo.connect(videoDecoderInputPinInfo);
                           }
                       }
                     */

                    //			    	printFilterInfo();
                } catch (com.jniwrapper.win32.com.ComException e) {
                    e.printStackTrace();
                    JOptionPane.showMessageDialog(null,
                        ElanLocale.getString("NativeMediaPlayerWindows.Error1"),
                        ElanLocale.getString("Message.Error"),
                        JOptionPane.ERROR_MESSAGE);
                    throw new NoPlayerException(
                        "Problem while creating directshow based media player");
                }
            }
        }
    }

    /**
     * DOCUMENT ME!
     *
     * @return DOCUMENT ME!
     */
    public MediaDescriptor getMediaDescriptor() {
        return mediaDescriptor;
    }

    /**
     *
     */
    public String getFrameworkDescription() {
        return "Native Windows Media Framework DS01";
    }

    /**
     * Should be removed from interface?
     *
     * @param event DOCUMENT ME!
     */
    public synchronized void controllerUpdate(ControllerEvent event) {
    }

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

    /**
     * Gets the aspectRatio between width and height of the video image
     *
     * @return DOCUMENT ME!
     */
    public float getAspectRatio() {
        return aspectRatio;
    }

    /**
     *
     */
    public synchronized void playInterval(long startTime, long stopTime) {
        // Setting stoptime BEFORE setting media time is important when rendering .wav
        // This sucks and there is no real explanation for it
        setStopTime(stopTime);
        setMediaTime(startTime);
        start();
    }

    /**
     * Sets the time where the player and all its slaves should automaticaly
     * stop playing A correction is made for stop time on a frame boundary. In
     * the pause mode the correct (new) frame is shown when media time is set
     * to the frame boundary but stopping at the frame boundary does not
     * render the correct frame.
     *
     * @param stopTime DOCUMENT ME!
     */
    public void setStopTime(long stopTime) {
        this.stopTime = stopTime;

        // see if the stop time must be increased to ensure correct frame rendering at a frame boundary
        long nFrames = (stopTime + offset) / getMilliSecondsPerSample();
        long extra = 0;

        if ((nFrames * getMilliSecondsPerSample()) == (stopTime + offset)) { // on a frame boundary
            extra = 1;
            this.stopTime += 1;
        }

        mediaPosition.setStopTime(new DoubleFloat(
                ((float) stopTime + offset + extra) / 1000));
        setControllersStopTime(stopTime);
    }

    /**
     * Starts the Player as soon as possible. is not synchronized in JMF
     */
    public synchronized void start() {
        if (playing) {
            return;
        }

        // play at start of media if at end of media
        if ((getMediaDuration() - getMediaTime()) < 40) {
            setMediaTime(0);
        }

        playing = true;
        mediaControl.run();

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

        // temporary polling for player state, will be obsolete when active callback is available
        Thread tw = new Thread(new PlayerStateWatcher());

        //		tw.setPriority(Thread.MIN_PRIORITY);
        tw.start();
    }

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

        mediaControl.pause();
        stopControllers();
        setControllersMediaTime(getMediaTime());
        playing = false;
    }

    /* DISABLED BLOCK WITH EXPERIMENTAL ACTIVE CALLBACK CODE
       private class CallbackWindowProc extends WindowProc {
           //public CallbackWindowProc(Wnd window) {
           //    super(window);
           //}

           public void callback() {
               System.out.println("filter graph message: " + (int)_msg.getValue());
               switch ((int)_msg.getValue()) {
                   case 10:

                       break;
                   default:

                       break;
               }
           }
       }

       private static class ListenerWindow extends Wnd {
           public ListenerWindow(Component component) {
               super(component);
           }

           public static void eventLoop(long event) {
               System.out.println("event: " + event);
           }
       }
     */

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

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

    /**
     * DOCUMENT ME!
     *
     * @param milliSeconds the step size for one frame
     */
    public void setMilliSecondsPerSample(long milliSeconds) {
        // disabled because this player knows itself better
        //millisPerSample = milliSeconds;
    }

    /**
     * Gets the volume as a number between 0 and 1
     *
     * @return DOCUMENT ME!
     */
    public float getVolume() {
        // should do something more logarithmic?
        try {
            Int32 volume = basicAudio.getVolume();

            return ((float) volume.getValue() + 10000) / 10000;
        } catch (Exception e) {
            return 1.0f;

            // do nothing here, The empty try catch might solve problems that occurred
            // on some clients when saving the volume during exit in ELAN 2.3
        }
    }

    /**
     * Gets the volume as a number between 0 and 1
     *
     * @param level DOCUMENT ME!
     */
    public void setVolume(float level) {
        try {
            // should do something more logarithmic?
            basicAudio.setVolume(new Int32((int) ((level * 10000) - 10000)));
        } catch (Exception e) {
            // video without audio gives a problem when not catched
            // e.printStackTrace();
        }
    }

    /**
     * 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;
        duration = (long) (1000 * mediaPosition.getDuration().getValue()) -
            offset;
        stopTime = duration;
    }

    /**
     * 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) {
        // the player must be in state 1(pause) when immediate rendering after set media time is required.
        if (mediaControl.getState(new Int32(0)).getValue() != STATE_PAUSE) {
            mediaControl.pause();
        }

        if (time < 0) {
            time = 0;
        }

        mediaPosition.setCurrentPosition(new DoubleFloat(
                ((float) time + offset) / 1000));
        setControllersMediaTime(time);
    }

    /**
     * No native frame stepping available but use maximum precision NTSC has
     * non integer frame durations so rounding errors should be avoided
     */
    public void nextFrame() {
        if (mediaControl.getState(new Int32(0)).getValue() != STATE_PAUSE) {
            mediaControl.pause();
        }

        double pos = mediaPosition.getCurrentPosition().getValue();
        pos += (millisPerSample / 1000);
        mediaPosition.setCurrentPosition(new DoubleFloat(pos));
        setControllersMediaTime(getMediaTime());
    }

    /**
     * No native frame stepping available but use maximum precision NTSC has
     * non integer frame durations so rounding errors should be avoided
     */
    public void previousFrame() {
        if (mediaControl.getState(new Int32(0)).getValue() != STATE_PAUSE) {
            mediaControl.pause();
        }

        double pos = mediaPosition.getCurrentPosition().getValue();
        pos -= (millisPerSample / 1000);

        if (pos < 0) {
            pos = 0;
        }

        mediaPosition.setCurrentPosition(new DoubleFloat(pos));
        setControllersMediaTime(getMediaTime());
    }

    /**
     * Gets this players current media time in milli seconds.
     *
     * @return DOCUMENT ME!
     */
    public long getMediaTime() {
        double pos = 0.0005 + mediaPosition.getCurrentPosition().getValue(); // 0.0005 to avoid rounding errors

        return (long) (1000 * pos) - offset;
    }

    /**
     * Gets the current temporal scale factor.
     *
     * @return DOCUMENT ME!
     */
    public float getRate() {
        return (float) mediaPosition.getRate().getValue();
    }

    /**
     * Sets the temporal scale factor.
     *
     * @param rate DOCUMENT ME!
     */
    public synchronized void setRate(float rate) {
        mediaPosition.setRate(new DoubleFloat(rate));
        setControllersRate(rate);
    }

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

    /**
     * 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()));
        saveItem.setText(ElanLocale.getString("Player.SaveFrame"));

        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(getVisualComponent());
                detachItem.setText(ElanLocale.getString("Detachable.detach"));
                detached = false;
            } else {
                layoutManager.detach(getVisualComponent());
                detachItem.setText(ElanLocale.getString("Detachable.attach"));
                detached = true;
            }

            getVisualComponent().addNotify();
        } else if (e.getSource() == infoItem) {
            new FormattedMessageDlg(this);
        } else if (e.getSource() == saveItem) {
            ImageExporter export = new ImageExporter();
            export.exportImage((BufferedImage) getCurrentFrameImage());
        }
    }

    /**
     * Grabs the current video frame and converts it to an Image object.
     *
     * @return the current video frame
     */
    public Image getCurrentFrameImage() {
        return getFrameImageForTime(getMediaTime());
    }

    /**
     * Tries to grab the frame for the specified time and convert it to a
     * BufferedImage. The player will be set to the specified media time.
     *
     * @param time the media time to grab the frame for
     *
     * @return the frame image or null
     */
    public Image getFrameImageForTime(long time) {
        if (time != getMediaTime()) {
            setMediaTime(time);
        }

        BufferedImage image = null;

        try {
            mediaControl.pause();

            // calling getCurrentImage with two valid Int32 objects causes
            // an E_OUTOFMEMORY exception
            Int32 bufSize = new Int32();
            Int32 imPointer = null;

            // fill the buffer size
            basicVideo.getCurrentImage(bufSize, imPointer);

            int bufferSize = (int) bufSize.getValue();

            //System.out.println("Buffer Size: " + bufferSize);
            //imPointer = new Int32();
            PrimitiveArray myArray = new PrimitiveArray(UInt8.class, bufferSize);
            ExternalArrayPointer pArray = new ExternalArrayPointer(myArray);
            Parameter[] params = new Parameter[] {
                    new Pointer(new Int32(bufferSize)), pArray
                };
            ((IBasicVideoImpl) basicVideo).getCurrentImage2(params);

            byte[] data = myArray.getBytes();

            image = DIBToImage.DIBDataToBufferedImage(data);
        } catch (ComException ce) {
            ce.printStackTrace();
        }

        return image;
    }

    /**
     * DOCUMENT ME!
     * $Id: NativeMediaPlayerWindowsDS.java,v 1.5 2006/03/30 14:59:42 hasloe Exp $
     * @author $Author: hasloe $
     * @version $Revision: 1.5 $
     */
    private class VisualComponent extends Panel implements ComponentListener,
        HierarchyListener {
        /**
         * Creates a new VisualComponent instance
         */
        public VisualComponent() {
            videoWindow.setWindowStyle(new Int32(Wnd.WS_CHILD));

            //			videoWindow.setAutoShow(new Int32(-1));
            addComponentListener(this);
            addHierarchyListener(this);
        }

        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void componentResized(ComponentEvent e) {
            if (videoWindow.getVisible().getValue() != -1) {
                return;
            }

            Int32 left = new Int32();
            left.setValue(0);

            Int32 top = new Int32();
            top.setValue(0);

            Int32 width = new Int32();
            width.setValue(getWidth());

            Int32 height = new Int32();
            height.setValue(getHeight());
            videoWindow.setWindowPosition(left, top, width, height);
        }

        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void hierarchyChanged(HierarchyEvent e) { //System.out.println("hier");

            if (isDisplayable()) {
                int handle = WindowTools.getWindowHandle(this);
                LongPtr ownerWindow = new LongPtr();
                ownerWindow.setValue(handle);

                if (videoWindow != null) {
                    videoWindow.setOwner(ownerWindow);
                    videoWindow.setMessageDrain(ownerWindow);
                    setVisible(true);
                    videoWindow.setVisible(new Int32(-1));
                }
            }
        }

        /**
         * DOCUMENT ME!
         */
        public void addNotify() { //System.out.println("addNotify");
            super.addNotify();

            int handle = WindowTools.getWindowHandle(this);
            LongPtr ownerWindow = new LongPtr();
            ownerWindow.setValue(handle);

            if (videoWindow != null) {
                videoWindow.setOwner(ownerWindow);
                videoWindow.setMessageDrain(ownerWindow);
            }
        }

        /**
         * DOCUMENT ME!
         */
        public void removeNotify() { //System.out.println("removeNotify");           

            if (videoWindow != null) {
                videoWindow.setVisible(new Int32(0));
                videoWindow.setOwner(new LongPtr());
            }

            super.removeNotify();
        }

        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void componentShown(ComponentEvent e) { //System.out.println("show");
        }

        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void componentHidden(ComponentEvent e) { //System.out.println("hide");
        }

        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void componentMoved(ComponentEvent e) { //System.out.println("move");
        }

        /**
         * DOCUMENT ME!
         *
         * @throws Throwable DOCUMENT ME!
         */
        protected void finalize() throws Throwable {
            System.out.println("finalize visual component");
            super.finalize();
        }
    }

    /**
     * Temporary class to take care of state changes  after the player finished
     * playing an interval  or reached end of media  As soon as active
     * callback can be handled this class will become obsolete
     */
    private class PlayerStateWatcher implements Runnable {
        /**
         * DOCUMENT ME!
         */
        public void run() {
            while (playing && (getMediaTime() < stopTime)) {
                try {
                    Thread.sleep(300);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            // if at stop time (i.e. not stopped by hand) do some extra stuff
            if (playing) {
                mediaControl.stopWhenReady();
                stopControllers();

                // some mpeg2 codecs need an extra set media time to render the correct frame
                // if needed undo stop time correction, see setStopTime for details
                if (getMediaTime() == (stopTime + 1)) {
                    setMediaTime(stopTime); // sometimes needed for mpeg2
                } else {
                    setMediaTime(getMediaTime()); // sometimes needed for mpeg2
                }

                setStopTime(duration);
                mediaControl.pause();

                playing = false;
            }
        }
    }

    /**
     * DOCUMENT ME!
     * $Id: NativeMediaPlayerWindowsDS.java,v 1.5 2006/03/30 14:59:42 hasloe Exp $
     * @author $Author: hasloe $
     * @version $Revision: 1.5 $
     */
    private class MouseHandler extends MouseAdapter {
        /**
         * DOCUMENT ME!
         *
         * @param e DOCUMENT ME!
         */
        public void mouseClicked(java.awt.event.MouseEvent e) {
            if (e.getClickCount() >= 2) {
                if (layoutManager != null) {
                    layoutManager.setFirstPlayer(NativeMediaPlayerWindowsDS.this);
                }

                return;
            }

            JPopupMenu.setDefaultLightWeightPopupEnabled(false);

            if (SwingUtilities.isRightMouseButton(e)) {
                popup.show(getVisualComponent(), e.getPoint().x, e.getPoint().y);
            }
        }
    }
}
