package org.limewire.ui.swing.player;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;

import javax.media.ClockStoppedException;
import javax.media.Control;
import javax.media.Controller;
import javax.media.ControllerEvent;
import javax.media.ControllerListener;
import javax.media.Duration;
import javax.media.EndOfMediaEvent;
import javax.media.GainControl;
import javax.media.IncompatibleSourceException;
import javax.media.IncompatibleTimeBaseException;
import javax.media.Player;
import javax.media.StartEvent;
import javax.media.StopEvent;
import javax.media.Time;
import javax.media.TimeBase;
import javax.media.protocol.DataSource;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.Timer;

import org.limewire.concurrent.ExecutorsHelper;
import org.limewire.concurrent.ThreadPoolListeningExecutor;
import org.limewire.core.api.Category;
import org.limewire.core.api.file.CategoryManager;
import org.limewire.core.api.library.LocalFileItem;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.player.api.PlayerState;
import org.limewire.setting.evt.SettingEvent;
import org.limewire.setting.evt.SettingListener;
import org.limewire.ui.swing.settings.SwingUiSettings;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.NativeLaunchUtils;
import org.limewire.ui.swing.util.SwingUtils;

import ca.odell.glazedlists.EventList;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.lti.utils.OSUtils;

@Singleton
class PlayerMediatorImpl implements PlayerMediator {

    private static final Log LOG = LogFactory.getLog(PlayerMediatorImpl.class);
    
    private final VideoDisplayDirector displayDirector;
    private final List<PlayerMediatorListener> listenerList;
    private final CategoryManager categoryManager;
    private final PlayerInitializer playerInitializer;
    private final ControllerListener controllerListener;
    private final Provider<MediaPlayerFactoryImpl> mediaPlayerFactory;
    private final Playlist playlist;
    
    /** A static Player to avoid null checks when no player currently is in use. */
    private static final Player EMPTY_PLAYER = new EmptyPlayer();
    
    /** The current instance of the Player that is being used. This will change on a file per file basis.
     *  This Player should never be null, if no Player exists currently it should be an instanceOf EMPTY_PLAYER. 
     */
    private Player player;
    
    /** The File that is currently playing. If no File has been set, this is null. */
    private File currentMediaFile;
    
    /** If the Player is active, this will return a valid Timer, otherwise return null. */
    private volatile Timer updateTimer;
    
    /** Returns true if this Player is currently seeking, false otherwise.*/
    private boolean isSeeking;
    
    private long playingWindowStartTime = -1;
    private int playingSwitches = -1;
    
    @SuppressWarnings("unused")
    private volatile boolean videoPlayedInSessionByLW = false;
    
    @SuppressWarnings("unused")
    private volatile boolean audioPlayedInSession = false;
    
    @Inject
    PlayerMediatorImpl(VideoDisplayDirector displayDirector, CategoryManager categoryManager,
            Provider<MediaPlayerFactoryImpl> mediaPlayerFactory) {
        this.displayDirector = displayDirector;
        this.categoryManager = categoryManager;
        this.mediaPlayerFactory = mediaPlayerFactory;
        this.listenerList = new ArrayList<PlayerMediatorListener>();
        this.playerInitializer = new PlayerInitializer();
        this.controllerListener = new VideoControllerListener();
        this.playlist = new Playlist();
        this.player = EMPTY_PLAYER;
        
        //NOTE: used in Handler to catch runtime exceptions and report them, don't delete this
        ThreadPoolListeningExecutor tpe = new ThreadPoolListeningExecutor(1, 1,
                5L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(),
                ExecutorsHelper.daemonThreadFactory("Video ThreadPool"));
    
        ExecutorsHelper.unconfigurableExecutorService(tpe);
    }

    @Inject
    void register(){
        // if playing audio/video files within LW is disabled, stop any file
        // that may be playing currently.
        SwingUiSettings.PLAYER_ENABLED.addSettingListener(new SettingListener(){
            @Override
            public void settingChanged(final SettingEvent evt) {
                SwingUtilities.invokeLater(new Runnable(){
                    @Override
					public void run() {
                        if (!SwingUiSettings.PLAYER_ENABLED.getValue()) {
                            stop();
                        }
                    }
                });
            }
        });
    }

    @Override
    public void addMediatorListener(PlayerMediatorListener listener) {
        listenerList.add(listener);
    }

    @Override
    public void removeMediatorListener(PlayerMediatorListener listener) {
        listenerList.remove(listener);
    }

    @Override
    public File getCurrentMediaFile() {
        return currentMediaFile;
    }

    @Override
    public PlayerState getStatus() {
        return convertControllerState(player.getState());
    }

    private PlayerState convertControllerState(int controllerState) {
        if(isSeeking){
            return PlayerState.SEEKING;
        }
        
        switch (controllerState) {
        case Controller.Started:
            return PlayerState.PLAYING;
        case Controller.Realized:
            return PlayerState.PAUSED;
        default:
            return PlayerState.UNKNOWN;
        }
    }
    
    @Override
    public void setActivePlaylist(EventList<LocalFileItem> fileList) {
        playlist.setActivePlaylist(fileList);
    }

    @Override
    public boolean isPaused(File file) {
        return player.getState() != Controller.Started && file.equals(currentMediaFile);
    }

    @Override
    public boolean isPlaying(File file) {
        return player.getState() == Controller.Started && file.equals(currentMediaFile);
    }

    @Override
    public boolean isSeekable() {
        return isDurationMeasurable();
    }

    @Override
    public boolean isShuffle() {
        return playlist.isShuffle();
    }

    @Override
    public void pause() {
        if(player instanceof JavaSoundPlayer)
            ((JavaSoundPlayer)player).pause();
        else
            player.stop();
    }

    @Override
    public void play(LocalFileItem localFileItem) {
        playlist.setCurrentItem(localFileItem);
        play(localFileItem.getFile(), true);
    }
    
    @Override
    public void playOrLaunchNatively(File file) {
        playlist.setCurrentItem(null);
        playlist.setActivePlaylist(null);
        play(file, true);
    }
    
    /**
     * Handles the actual launching of the media file. 
     * @param isLaunchOnFailure if true will launch the file natively if it fails to
     * open within LW. If false and an audio file, will attempt to play the next
     * file within a playlist if one exists.
     */
    private void play(File file, boolean isLaunchOnFailure) {
        Category category = categoryManager.getCategoryForFile(file);
        if (SwingUiSettings.PLAYER_ENABLED.getValue() && (OSUtils.isWindows() || OSUtils.isMacOSX()) && 
                (category == Category.AUDIO || category == Category.VIDEO)) {
            currentMediaFile = file;
            initializePlayerOrNativeLaunch(file, null, false, true, isLaunchOnFailure);
        } else {
            currentMediaFile = null;
            NativeLaunchUtils.safeLaunchFile(file, categoryManager);
        }
    }

    /**
     * Initializes an FMJ player for the video if possible, launches natively if
     * not.
     * 
     * @param file the video file to be played
     * @param time the starting time of the video. null to start at the
     *        beginning.
     * @param autoPlay whether or not to start playback
     * @return true if the player is successfully initialized, false if it is
     *         not initialized and the file is natively launched
     */
    private void initializePlayerOrNativeLaunch(final File file, Time time, boolean isFullScreen, boolean autoStart, boolean isLaunchOnFailure) {
        GuiUtils.getMainFrame().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
     
        if(playerInitializer.isInitializing()) {
            playerInitializer.cancel();
        }
        killPlayer();
        playerInitializer.initialize(file, time, isFullScreen, autoStart, isLaunchOnFailure);
    }
    
    @Override
    public void nextSong() {
        stop();

        LocalFileItem fileItem = playlist.getNextFileItem();

        if (fileItem != null) {
            play(fileItem.getFile(), false);
        }
    }

    @Override
    public void prevSong() {
        stop();

        LocalFileItem fileItem = playlist.getPrevFileItem();

        if (fileItem != null) {
            play(fileItem.getFile(), false);
        }
    }

    @Override
    public void resume() {
        if (player.getState() != Controller.Started) {
            player.start();
        }
    }

    @Override
    public void setShuffle(boolean shuffle) {
        playlist.setShuffle(shuffle);
    }

    @Override
    public void setVolume(double value) {
        if ( player.getState() != Controller.Unrealized && player.getState() != Controller.Realizing && 
                player.getGainControl() != null) {
            player.getGainControl().setLevel((float) value);
        }
    }
    
    private boolean isAudioFile(File file) {
        return categoryManager.getCategoryForFile(file) == Category.AUDIO;
    }

    @Override
    public boolean hasVolumeControl() {
        // On Mac OS X we're using true full screen mode, and you can't use popups in true full screen mode.
        // So, since the volume popup won't work, let's disable it.
        if (OSUtils.isMacOSX() && isFullScreen())
            return false;

        int state = player.getState();
        if(state == Controller.Unrealized || state == Controller.Realizing)
            return false;
        return player.getGainControl() != null;
    }

    @Override
    public void seek(double percent) {
        isSeeking = true;
        player.setMediaTime(new Time(percent * player.getDuration().getSeconds()));
        isSeeking = false;
    }

    @Override
    public void stop() {
        if(playerInitializer.isInitializing()){
            playerInitializer.cancel();
        }
      
        displayDirector.close();
        
        killTimer();
        killPlayer();
    
        currentMediaFile = null;  
    }

    /**
     * Notifies the listeners of the change in state of the player.
     */
    private void firePlayerStateChanged(PlayerState state) {
        for (PlayerMediatorListener listener : listenerList) {
            listener.stateChanged(state);
        }
    }
    
    /**
     * Notifies the listeners of the current progress on the currently playing
     * media file.
     */
    private void fireProgressUpdated() {
        fireProgressUpdated((float) (player.getMediaTime().getSeconds() / player.getDuration().getSeconds()));
    }

    /**
     * Notifies the listeners of the current progress on the currently playing
     * media file.
     * 
     * @param progress - the current progress [0.0f,100.0f]
     */
    private void fireProgressUpdated(float progress) {
        for (PlayerMediatorListener listener : listenerList) {
            listener.progressUpdated(progress);
        }
    }

    /**
     * Notifies the listeners when the media file that is currently playing changes.
     */
    private void fireMediaChanged(String name) {
        for (PlayerMediatorListener listener : listenerList) {
            listener.mediaChanged(name);
        }
    }

    private void killTimer() {
        if (updateTimer != null) {
            if (updateTimer.isRunning()) {
                updateTimer.stop();
            }
            updateTimer = null;
        }
    }

    /**
     * Stops the Player and cleans up any resources used by the Player.
     * Player is reset to EMPTY_PLAYER.
     */
    private void killPlayer() {
        player.stop();
        player.removeControllerListener(controllerListener);
        player.close();
        player.deallocate();
        player = EMPTY_PLAYER;
    }

    public void setFullScreen(boolean isFullScreen) {
        if(player == EMPTY_PLAYER) {
            return;
        }
        
        if(isAudioFile(currentMediaFile))
            return;
        
        if (displayDirector.isFullScreen() == isFullScreen) {
            return;
        }
        
        //task is already running.  user probably hit crtl-f twice quickly.
        if (playerInitializer.isInitializing()){
            return;
        }
        
        boolean isPlaying = player.getState() == Controller.Started;

        Time time = isDurationMeasurable() ? player.getMediaTime() : null;

        killTimer();
        killPlayer();

        initializePlayerOrNativeLaunch(currentMediaFile, time, isFullScreen, isPlaying, false);
    }
    
    
    @Override
    public boolean isPlayable(File file) {
        if(!SwingUiSettings.PLAYER_ENABLED.getValue())
            return false;
        Category category = categoryManager.getCategoryForFile(file);
        return category == Category.AUDIO || category == Category.VIDEO;
    }

    public boolean isFullScreen() {
        return displayDirector.isFullScreen();
    }

    private boolean isDurationMeasurable() {
        if(player == EMPTY_PLAYER) {
            return false;
        }
        long time = player.getDuration().getNanoseconds();        
        return Duration.DURATION_UNBOUNDED.getNanoseconds() != time && Duration.DURATION_UNKNOWN.getNanoseconds() != time && Time.TIME_UNKNOWN.getNanoseconds() != time;
    }

    private class VideoControllerListener implements ControllerListener {
        @Override
        public void controllerUpdate(final ControllerEvent controllerEvent) {
            SwingUtils.invokeNowOrLater(new Runnable() {
                @Override
                public void run() {
                    if (controllerEvent instanceof EndOfMediaEvent) {
                        setEndOfMedia();
                    } else if (controllerEvent instanceof StartEvent || controllerEvent.getSourceController().getState() == Controller.Started) {
                        firePlayerStateChanged(PlayerState.OPENED);
                        firePlayerStateChanged(PlayerState.PLAYING);
                        if (updateTimer == null) {
                            updateTimer = new Timer(100, new TimerAction());
                        }

                        if (!updateTimer.isRunning()) {
                            updateTimer.start();
                        }

                    } else if (controllerEvent instanceof StopEvent) {
                        firePlayerStateChanged(PlayerState.STOPPED);
                        if (updateTimer != null) {
                            updateTimer.stop();
                        }
                    }
                }
            });
        }
    }

    private class TimerAction implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            if (!isDurationMeasurable()) {
                if(updateTimer != null)
                    updateTimer.stop();
                return;
            }

            if (player.getMediaTime().getSeconds() >= player.getDuration().getSeconds()) {
                // some FMJ players don't seem to fire EndOfMediaEvents so we need to do
                // this manually
                setEndOfMedia();
            } else {
                fireProgressUpdated();
            }
        }
    }    
    
    private void setEndOfMedia() {
        player.stop();
        player.setMediaTime(new Time(0));
        if(updateTimer != null)
            updateTimer.stop();
        firePlayerStateChanged(PlayerState.EOM);
        fireProgressUpdated(100f);

        // Sanity check before switching to the next song,
        //  the last 10 songs that switched must have taken over
        //  5 seconds to play.
        playingSwitches = (playingSwitches+1);
        if (playingSwitches % 10 == 0) {
            if(playingSwitches == 0) {
                playingWindowStartTime = System.currentTimeMillis();
                nextSong();
            } else {
                long currentTime = System.currentTimeMillis();
                if(currentTime - playingWindowStartTime < 5000) {
                    playingSwitches = -1;
                    playingWindowStartTime = -1;   
                } else {
                    playingSwitches = -1;
                    playingWindowStartTime = currentTime;
                    nextSong();
                }
            }
        } else {
            nextSong();
        }
    }
    
    interface PlayerCompleteCallback {
        public void complete(Player newPlayer, Time time, boolean autoStart);
    }
    
    /**
     * Asynchronously initializes new media players.
     */
    private class PlayerInitializer {

        private PlayerInitalizationWorker initializationWorker;
        
        public PlayerInitializer(){}
        
        public boolean isInitializing() {
            return initializationWorker != null;
        }

        public void initialize(final File file, Time time, boolean isFullScreen, boolean autoStart, boolean isLaunchOnFailure) {
            if (isInitializing()) {
                cancel();
            }
            if(LOG.isDebugEnabled())
                LOG.debug("initializing player for: " + file);

            initializationWorker = new PlayerInitalizationWorker(file, time, isFullScreen, autoStart, isLaunchOnFailure, new PlayerCompleteCallback() {
                @Override
                public void complete(Player newPlayer, Time time, boolean autoStart) {
                    assert(SwingUtilities.isEventDispatchThread());

                    if(newPlayer == null) {
                        //New player creation failed.  The video was launched natively.
                        displayDirector.close();
                        return;
                    }

                    if (time != null) {
                        newPlayer.setMediaTime(time);
                    }

                    newPlayer.addControllerListener(controllerListener);           

                    player = newPlayer;
               
                    if(!isAudioFile(file))
                        displayDirector.show();   
                    
                    fireMediaChanged(file.getName()); 
                      
                    startMediaPlayer(autoStart); 
                    initializationWorker = null;
                    if(LOG.isDebugEnabled())
                        LOG.debug("player initialized");
                }
            });
            initializationWorker.execute();
        }

        public void cancel() {
            initializationWorker.cancelInitialization();
            initializationWorker = null;
        }
        
        private void startMediaPlayer(final boolean autoStart) {
            ActionListener listener = new ActionListener() {                
                @Override
                public void actionPerformed(ActionEvent e) {
                    if(player != EMPTY_PLAYER) {
                        // start the player regardless of startVideo so that
                        // control panel is correctly updated

                        player.start();
                        // force fire these immediately to make sure
                        // everything updates
                        firePlayerStateChanged(PlayerState.PLAYING);
                        if (isDurationMeasurable()) {
                            fireProgressUpdated();
                        }

                        if (!autoStart) {
                            player.stop();
                        }
                    }
                }
            };
            
            // Prevents the flash of native video window by delaying 
            // video playback so that the video can be fully embedded first.
            Timer timer = new Timer(100, listener);
            timer.setRepeats(false);
            timer.start();
        }

        /**
         * SwingWorker that initializes the new player off of the EDT.
         */
        private class PlayerInitalizationWorker extends SwingWorker<Player, Void> {
            private final File mediaFile;
            private final Time time;
            private final boolean autoStart;
            private final boolean isLaunchOnFailure;
            private final PlayerCompleteCallback callback;
            private final Container renderPanel;
            
            private volatile boolean canceled = false;   

            public PlayerInitalizationWorker(File mediaFile, Time time, boolean isFullScreen,
                    boolean autoStart, boolean isLaunchOnFailure, PlayerCompleteCallback callback) {
                this.mediaFile = mediaFile;
                this.time = time;
                this.autoStart = autoStart;
                this.isLaunchOnFailure = isLaunchOnFailure;
                this.callback = callback;
                this.renderPanel = new JPanel(new BorderLayout());
                
                if(!isAudioFile(mediaFile))
                    displayDirector.initialize(renderPanel, isFullScreen);                
            }

            /**
             * Cancels the player initialization. We don't want to interrupt the
             * thread by using cancel(boolean) because we need to properly
             * dispose of the player.
             */
            public void cancelInitialization() {
                canceled = true;
            }
            
            @Override
            protected Player doInBackground() throws Exception {
                try {
                    Player player = mediaPlayerFactory.get().createMediaPlayer(mediaFile, renderPanel);
                    if(categoryManager.getCategoryForFile(mediaFile) == Category.AUDIO)
                        audioPlayedInSession = true;
                    else
                        videoPlayedInSessionByLW = true;
                    return player;
                } catch (IncompatibleSourceException e) {
                    if(LOG.isDebugEnabled())
                        LOG.debug("failed to obtain a player " + e);
                    if(isLaunchOnFailure && !canceled) {
                        NativeLaunchUtils.safeLaunchFile(mediaFile, categoryManager);
                    } else if(categoryManager.getCategoryForFile(mediaFile) == Category.AUDIO) {
                        SwingUtilities.invokeLater(new Runnable(){
                            @Override
							public void run() {
                                if(!canceled)
                                    nextSong();                                
                            }
                        });
                    } else {
                        if(!canceled)
                            NativeLaunchUtils.safeLaunchFile(mediaFile, categoryManager);
                    }
                    return null;
                }
            }            

            @Override
            protected void done() {
                GuiUtils.getMainFrame().setCursor(Cursor.getDefaultCursor());

                Player player = null;
                try {
                    player = get();
                } catch (InterruptedException e) {
                    // we're already finished so this can't happen
                } catch (ExecutionException e) {
                    throw new RuntimeException(e);
                }

                if (canceled || player == null) {
                    if (player != null) {
                        player.close();
                        player.deallocate();
                    }
                    return;
                }

                // if no renderer has been added yet or its OSX and a video file, add the renderer
                // these are weird work arounds for the DS player on Windows 7 and audio playback on OSX
                if(renderPanel.getComponentCount() == 0 &&
                    (!OSUtils.isMacOSX() || OSUtils.isMacOSX() && categoryManager.getCategoryForFile(mediaFile) == Category.VIDEO))
                        renderPanel.add(player.getVisualComponent());
                callback.complete(player, time, autoStart);
            }
        }
    }
    
    /**
     * Wrapper for Player to avoid null checks when the Player has not been initialized.
     */
    static class EmptyPlayer implements Player {

        @Override public void addController(Controller newController) throws IncompatibleTimeBaseException {}
        @Override public Component getControlPanelComponent() {return null;}
        @Override public GainControl getGainControl() {return null;}
        @Override public Component getVisualComponent() {return null;}
        @Override public void removeController(Controller oldController) {}
        @Override public void start() {}
        @Override public void setSource(DataSource source) throws IOException, IncompatibleSourceException {}
        @Override public void addControllerListener(ControllerListener listener) {}
        @Override public void close() {}
        @Override public void deallocate() {}
        @Override public Control getControl(String forName) {return null;}
        @Override public Control[] getControls() {return new Control[0];}
        @Override public Time getStartLatency() {return new Time(0);}
        @Override public int getState() {return Controller.Unrealized;}
        @Override public int getTargetState() {return 0;}
        @Override public void prefetch() {}
        @Override public void realize() {}
        @Override public void removeControllerListener(ControllerListener listener) {}
        @Override public long getMediaNanoseconds() {return 0;}
        @Override public Time getMediaTime() {return new Time(0);}
        @Override public float getRate() {return 0;}
        @Override public Time getStopTime() {return null;}
        @Override public Time getSyncTime() {return null;}
        @Override public TimeBase getTimeBase() {return null;}
        @Override public Time mapToTimeBase(Time t) throws ClockStoppedException {return null;}
        @Override public void setMediaTime(Time now) {}
        @Override public float setRate(float factor) {return 0;}
        @Override public void setStopTime(Time stopTime) {}
        @Override public void setTimeBase(TimeBase master) throws IncompatibleTimeBaseException {}
        @Override public void stop() {}
        @Override public void syncStart(Time at) {}
        @Override public Time getDuration() {return new Time(1);}
    }    
}
