package org.limewire.ui.swing.player;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.limewire.core.api.file.CategoryManager;
import org.limewire.core.api.library.LocalFileItem;
import org.limewire.player.api.AudioPlayer;
import org.limewire.player.api.AudioPlayerEvent;
import org.limewire.player.api.AudioPlayerListener;
import org.limewire.player.api.AudioSource;
import org.limewire.player.api.PlayerState;
import org.limewire.ui.swing.library.LibraryMediator;
import org.limewire.ui.swing.util.I18n;
import org.limewire.ui.swing.util.NativeLaunchUtils;
import org.limewire.util.FileUtils;

import ca.odell.glazedlists.EventList;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

/**
 * Mediator that controls the interaction between the player view, the current
 * playlist, and the audio player.
 */
@Singleton
class AudioPlayerMediator implements PlayerMediator {

    public static final String AUDIO_LENGTH_BYTES = "audio.length.bytes";
    public static final String AUDIO_TYPE = "audio.type";
    
    private static final String MP3 = "mp3";
    private static final String WAVE = "wave";
    
    private final Provider<AudioPlayer> audioPlayerProvider;
    private final LibraryMediator libraryMediator;
    private final List<PlayerMediatorListener> listenerList;
    private final CategoryManager categoryManager;
    private final Playlist playlist;
    
    /** Audio player component. */
    private AudioPlayer audioPlayer;
    
    /** File item for the last opened song. */
    private LocalFileItem fileItem = null;
    
    /** Map containing properties for the last opened song. */
    private Map audioProperties = null;
    
    /** Progress of current song from 0.0 to 1.0. */
    private float progress;
    
    private long playingWindowStartTime = -1;
    private int playingSwitches = -1;
    
    /**
     * Constructs a PlayerMediator using the specified services.
     */
    @Inject
    public AudioPlayerMediator(Provider<AudioPlayer> audioPlayerProvider,
            LibraryMediator libraryMediator, CategoryManager categoryManager) {
        this.audioPlayerProvider = audioPlayerProvider;
        this.libraryMediator = libraryMediator;
        this.categoryManager = categoryManager;
        
        this.listenerList = new ArrayList<PlayerMediatorListener>();
        this.playlist = new Playlist();
    }
    
    /**
     * Returns the audio player component.  When first called, this method
     * creates the component and registers this mediator as a listener.
     */
    private AudioPlayer getPlayer() {
        if (audioPlayer == null) {
            audioPlayer = audioPlayerProvider.get();
            audioPlayer.addAudioPlayerListener(new PlayerListener());
        }
        return audioPlayer;
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#addMediatorListener(org.limewire.ui.swing.player.PlayerMediatorListener)
     */
    @Override
	public void addMediatorListener(PlayerMediatorListener listener) {
        listenerList.add(listener);
    }

    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#removeMediatorListener(org.limewire.ui.swing.player.PlayerMediatorListener)
     */
    @Override
	public void removeMediatorListener(PlayerMediatorListener listener) {
        listenerList.remove(listener);
    }
    
    /**
     * Notifies registered listeners that the progress is updated to the 
     * specified value.
     */
    private void fireProgressUpdated(float progress) {
        for (int i = 0, size = listenerList.size(); i < size; i++) {
            listenerList.get(i).progressUpdated(progress);
        }
    }
    
    /**
     * Notifies registered listeners that the song is changed to the specified
     * song name.
     */
    private void fireSongChanged(String name) {
        for (int i = 0, size = listenerList.size(); i < size; i++) {
            listenerList.get(i).mediaChanged(name);
        }
    }
    
    /**
     * Notifies registered listeners that the player state is changed to the
     * specified state.
     */
    private void fireStateChanged(PlayerState state) {
        for (int i = 0, size = listenerList.size(); i < size; i++) {
            listenerList.get(i).stateChanged(state);
        }
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#getStatus()
     */
    @Override
    public PlayerState getStatus() {
        return getPlayer().getStatus();
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#isShuffle()
     */
    @Override
    public boolean isShuffle() {
        return playlist.isShuffle();
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#setShuffle(boolean)
     */
    @Override
    public void setShuffle(boolean shuffle) {
        playlist.setShuffle(shuffle);
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#setVolume(double)
     */
    @Override
    public void setVolume(double value) {
        getPlayer().setVolume((float)value);
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#pause()
     */
    @Override
    public void pause() {
        getPlayer().pause();
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#resume()
     */
    @Override
    public void resume() {
        PlayerState status = getPlayer().getStatus();
        if ((status == PlayerState.STOPPED  || status == PlayerState.UNKNOWN) && fileItem != null) {
            //resume the file that was stopped
            play(fileItem);
        } else if ((status == PlayerState.STOPPED) || (status == PlayerState.UNKNOWN)) {
            // Get first selected item.
            List<LocalFileItem> selectedItems = libraryMediator.getSelectedItems();
            if (selectedItems.size() > 0) {
                LocalFileItem selectedItem = selectedItems.get(0);
                if (isPlayable(selectedItem.getFile())) {
                    // Set active playlist and play file item.
                    setActivePlaylist(libraryMediator.getPlayableList());
                    play(selectedItem);
                }
            }
            
        } else {
            getPlayer().unpause();
        }
    }

    @Override
    public void setActivePlaylist(EventList<LocalFileItem> fileList) {
        playlist.setActivePlaylist(fileList);
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#play(java.io.File)
     */
    public void play(File file) {
        // Stop current song.
        stop();
        
        // Play new song.
        this.fileItem = null;
        playlist.setActivePlaylist(null);
        loadAndPlay(file);
    }
    

    @Override
    public void playOrLaunchNatively(File file) {
        if(isPlayable(file)) {
            play(file);
        } else {
            NativeLaunchUtils.safeLaunchFile(file, categoryManager);
        }
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#play(org.limewire.core.api.library.LocalFileItem)
     */
    @Override
    public void play(LocalFileItem localFileItem) {
        // Stop current song.
        stop();
        
        // Play new song.
        this.fileItem = localFileItem;
        playlist.setCurrentItem(localFileItem);
        loadAndPlay(localFileItem.getFile());
    }
    
    private void loadAndPlay(File fileToPlay) {
        AudioPlayer player = getPlayer();
        player.loadSong(fileToPlay);
        player.playSong();
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#skip(double)
     */
    @Override
    public void seek(double percent) {
        // need to know something about the audio type to be able to skip
        if (audioProperties != null && audioProperties.containsKey(AUDIO_TYPE)) {
            String songType = (String) audioProperties.get(AUDIO_TYPE);
            
            // currently, only mp3 and wav files can be seeked upon
            if (isSeekable(songType) && audioProperties.containsKey(AUDIO_LENGTH_BYTES)) {
                final long skipBytes = Math.round((Integer) audioProperties.get(AUDIO_LENGTH_BYTES)* percent);
                getPlayer().seekLocation(skipBytes);
            }
        }
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#stop()
     */
    @Override
    public void stop() {
        getPlayer().stop();
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#nextSong()
     */
    @Override
    public void nextSong() {
        // Stop current song.
        stop();

        // Get next file item.
        fileItem = playlist.getNextFileItem();

        // Play song.
        if (fileItem != null) {
            loadAndPlay(fileItem.getFile());
        }
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#prevSong()
     */
    @Override
    public void prevSong() {
        // Stop current song.
        stop();

        fileItem = playlist.getPrevFileItem();

        // Play song.
        if (fileItem != null) {
            loadAndPlay(fileItem.getFile());
        }
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#getCurrentSongFile()
     */
    @Override
    public File getCurrentMediaFile() {
        AudioSource source = getPlayer().getCurrentSong();
        return (source != null) ? source.getFile() : null;
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#isPlaying(java.io.File)
     */
    @Override
    public boolean isPlaying(File file) {
        return getPlayer().isPlaying(file);
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#isPaused(java.io.File)
     */
    @Override
    public boolean isPaused(File file) {
        return getPlayer().isPaused(file);
    }
    
    /* (non-Javadoc)
     * @see org.limewire.ui.swing.player.IPlayerMediator#isSeekable()
     */
    @Override
    public boolean isSeekable() {
        if (audioProperties != null) {
            return isSeekable((String) audioProperties.get(AUDIO_TYPE));
        }
        return false;
    }   
    
    @Override
    public boolean isPlayable(File file) {
        String name = file.getName().toLowerCase(Locale.US);
        return name.endsWith(".mp3") || name.endsWith(".ogg") || name.endsWith(".wav");
    }

    @Override
    public boolean hasVolumeControl() {
        return true;
    }
    
    /**
     * Returns true if the specified song type is seekable, which means that 
     * the progress position can be set.  At present, only MP3 and Wave files 
     * are seekable.
     */
    private boolean isSeekable(String songType) {
        if (songType == null) {
            return false;
        }
        return songType.equalsIgnoreCase(MP3) || songType.equalsIgnoreCase(WAVE);
    }
    
    /**
     * Returns the name of the current song.
     */
    private String getSongName() {
        // Use audio properties if available.
        if (audioProperties != null) {
            Object author = audioProperties.get("author");
            Object title = audioProperties.get("title");
            if ((author != null) && (title != null)) {
                return author + " - " + title;
            }
        }
        
        // Use file item if available.
        if (fileItem != null) {
            return fileItem.getFile().getName();
        } else if(getCurrentMediaFile() != null) {
            return FileUtils.getFilenameNoExtension(getCurrentMediaFile().getName());
        } else {
            return I18n.tr("Unknown");
        }
    }
    
    /**
     * Listener to handle audio player events.
     */
    private class PlayerListener implements AudioPlayerListener {
        @Override
        public void progressChange(int bytesread) {
            // If we know the length of the song, update progress value.
            if ((audioProperties != null) && audioProperties.containsKey(AUDIO_LENGTH_BYTES)) {
                float byteslength = ((Integer) audioProperties.get(AUDIO_LENGTH_BYTES)).floatValue();
                progress = bytesread / byteslength;
                
                // Notify UI about progress.
                fireProgressUpdated(progress);
            }
        }

        @Override
        public void songOpened(Map<String, Object> properties) {
            // Save properties.
            audioProperties = properties;
            
            // Notify UI about new song.
            fireSongChanged(getSongName());
        }

        @Override
        public void stateChange(AudioPlayerEvent event) {
            // Go to next song when finished.
            if (event.getState() == PlayerState.EOM) {
                
                // 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();
                }
            }            
            // Notify UI about state change.
            fireStateChanged(event.getState());
        }
    }
}
