package com.cpacm.moemusic.music;

import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.PowerManager;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;

import com.cpacm.core.bean.Song;

import java.util.ArrayList;

import static android.media.AudioManager.OnAudioFocusChangeListener;
import static android.media.MediaPlayer.OnCompletionListener;
import static android.media.MediaPlayer.OnErrorListener;
import static android.media.MediaPlayer.OnPreparedListener;
import static android.media.MediaPlayer.OnSeekCompleteListener;
import static android.support.v4.media.session.PlaybackStateCompat.STATE_FAST_FORWARDING;
import static android.support.v4.media.session.PlaybackStateCompat.STATE_PAUSED;
import static android.support.v4.media.session.PlaybackStateCompat.STATE_PLAYING;
import static android.support.v4.media.session.PlaybackStateCompat.STATE_REWINDING;
import static android.support.v4.media.session.PlaybackStateCompat.STATE_STOPPED;


/**
 * @author: cpacm
 * @date: 2016/7/19
 * @desciption: 播放管理器
 */
public class MusicPlayerManager implements OnAudioFocusChangeListener, OnPreparedListener, OnCompletionListener, OnErrorListener, OnSeekCompleteListener {

    private static final String TAG = "MusicPlayer";

    /**
     * set the play mode
     */
    public final static int SINGLETYPE = 0;//单曲循环
    public final static int CYCLETYPE = 1;//列表循环
    public final static int RANDOMTYPE = 2;//随机播放
    /**
     * The volume we set the media player to when GEM loses audio focus, but is allowed to reduce the volume instead of stopping playback.
     */
    public static final float VOLUME_DUCK = 0.2f;
    /**
     * The volume we set the media player when GEM has audio focus.
     */
    public static final float VOLUME_NORMAL = 1.0f;
    /**
     * This is the value you would use for tis situation:
     * The user presses "Previous". if the current position in the song is greater then this value,
     * pressing "Previous" will restart the song, if not it will play the previous song
     */
    public static final int MAX_DURATION_FOR_REPEAT = 3000;

    /**
     * GEM doesn't have audio focus, and can't duck (play at a low volume)
     */
    private static final int AUDIO_NO_FOCUS_NO_DUCK = 0;
    /**
     * GEM doesn't have focus, but can duck (play at a low volume)
     */
    private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1;
    /**
     * GEM has full audio focus
     */
    private static final int AUDIO_FOCUSED = 2;

    private final static MusicPlayerManager instance = new MusicPlayerManager();

    private Context mContext;

    private MusicService musicService;
    private AudioManager audioManager;
    private MediaPlayer mediaPlayer;
    private MusicPlaylist musicPlaylist;
    private MediaControllerCompat mediaController;

    private boolean playFocusGain;
    private int audioFocus = AUDIO_NO_FOCUS_NO_DUCK;
    private long currentMediaId = -1;
    private int currentPlayType = CYCLETYPE;
    private int currentProgress;
    private int currentMaxDuration = MAX_DURATION_FOR_REPEAT;

    private ArrayList<OnSongChangedListener> changedListeners = new ArrayList<>();


    private MusicPlayerManager() {
    }

    public static MusicPlayerManager get() {
        return instance;
    }

    public static MusicPlayerManager from(MusicService service) {
        return MusicPlayerManager.get().setContext(service).setService(service);
    }

    public static void startServiceIfNecessary(Context c) {
        if (get().musicService == null) {
            MediaServiceHelper.get(c).initService();
            get().musicService = MediaServiceHelper.get(c).getService();
        }
    }

    public MusicPlayerManager setContext(Context c) {
        this.mContext = c;
        audioManager = (AudioManager) c.getSystemService(Context.AUDIO_SERVICE);
        return this;
    }

    public MusicPlayerManager setService(MusicService service) {
        this.musicService = service;
        return this;
    }


    /**
     * set a new playlist
     *
     * @param musicPlaylist
     * @param position      first play position
     */
    public void playQueue(MusicPlaylist musicPlaylist, int position) {
        this.musicPlaylist = musicPlaylist;
        musicPlaylist.setCurrentPlay(position);
        play(musicPlaylist.getCurrentPlay());
    }


    /**
     * change music in the queue
     *
     * @param position
     */
    public void playQueueItem(int position) {
        musicPlaylist.setCurrentPlay(position);
        play(musicPlaylist.getCurrentPlay());
    }

    /**
     * play music from item
     *
     * @param song
     */
    public void play(Song song) {
        if (song == null) return;
        playFocusGain = true;
        tryToGetAudioFocus();
        boolean mediaHasChanged = !(song.getId() == currentMediaId);
        if (mediaHasChanged) {
            currentProgress = 0;
            currentMediaId = song.getId();
        }
        if (musicService.getState() == STATE_PAUSED && !mediaHasChanged && mediaPlayer != null) {
            configMediaPlayerState();
        } else {
            musicService.setState(STATE_STOPPED);
            relaxResources(false);

            try {
                createMediaPlayerIfNeeded();
                musicService.setState(STATE_PLAYING);

                mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
                mediaPlayer.setDataSource(mContext, song.getUri());
                mediaPlayer.prepareAsync();

                for (OnSongChangedListener l : changedListeners) {
                    l.onSongChanged(song);
                }

                musicService.setAsForeground();
            } catch (Exception e) {
                Log.e(TAG, "playing song:", e);
            }
        }
    }

    public void play() {
        if (musicPlaylist == null || musicPlaylist.getCurrentPlay() == null) return;
        play(musicPlaylist.getCurrentPlay());
    }

    /**
     * next song
     */
    public void playNext() {
        currentProgress = 0;
        play(musicPlaylist.getNextSong());
    }

    /**
     * plays the previous song in the queue
     */
    public void playPrev() {
        currentProgress = 0;
        play(musicPlaylist.getPreSong());
    }

    /**
     * pauses the playback
     */
    public void pause() {
        if (musicService.getState() == STATE_PLAYING) {
            if (mediaPlayer != null && mediaPlayer.isPlaying()) {
                mediaPlayer.pause();
                currentProgress = mediaPlayer.getCurrentPosition();
            }
            relaxResources(false);
            giveUpAudioFocus();
            musicService.removeForeground(false);
        }
        musicService.setState(STATE_PAUSED);
    }

    /**
     * resumes the playback
     */
    public void resume() {
        if (musicService.getState() == STATE_PAUSED && mediaPlayer != null) {
            mediaPlayer.start();
            musicService.setState(STATE_PLAYING);
            tryToGetAudioFocus();
            musicService.setAsForeground();
        } else {
            Log.d(TAG, "Not paused or MediaPlayer is null. Player is null: " + (mediaPlayer == null));
        }
    }

    public void clearPlayer() {
        // stop and release the Media Player, if it's available
        if (getMusicPlaylist() != null) {
            getMusicPlaylist().clear();
        }
        musicService.setState(STATE_STOPPED);
        if (mediaPlayer != null) {
            mediaPlayer.reset();
        }
        giveUpAudioFocus();
        musicService.removeForeground(false);
    }

    /**
     * this will be called to stop the playback
     */
    public void stop() {
        musicService.setState(STATE_STOPPED);
        currentProgress = getCurrentProgressInSong();
        giveUpAudioFocus();
        relaxResources(true);
        musicService.removeForeground(true);
        musicService.stopService();
    }

    public void seekTo(int progress) {
        if (mediaPlayer == null)
            currentProgress = progress;
        else {
            if (getCurrentProgressInSong() > progress) {
                musicService.setState(STATE_REWINDING);
            } else {
                musicService.setState(STATE_FAST_FORWARDING);
            }
            currentProgress = progress;
            mediaPlayer.seekTo(currentProgress);

        }
    }

    public void setVolume(int mediaVolume) {
        audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mediaVolume, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
    }

    /**
     * switch play mode —— cycle -> single -> random -> cycle
     */
    public int switchPlayMode() {
        if (currentPlayType == CYCLETYPE) {
            setPlayMode(SINGLETYPE);
        } else if (currentPlayType == SINGLETYPE) {
            setPlayMode(RANDOMTYPE);
        } else if (currentPlayType == RANDOMTYPE) {
            setPlayMode(CYCLETYPE);
        }
        return currentPlayType;
    }

    public void setPlayMode(int type) {
        if (type < 0 || type > 2)
            throw new IllegalArgumentException("incorrect type");
        createMediaPlayerIfNeeded();
        currentPlayType = type;
        if (type == SINGLETYPE)
            mediaPlayer.setLooping(true);
        else
            mediaPlayer.setLooping(false);
    }

    public int getPlayMode() {
        return currentPlayType;
    }


    /**
     * Makes sure the media player exists and has been reset. This will create
     * the media player if needed, or reset the existing media player if one
     * already exists.
     */
    private void createMediaPlayerIfNeeded() {
        if (mediaPlayer == null) {
            mediaPlayer = new MediaPlayer();

            // Make sure the media player will acquire a wake-lock while
            // playing. If we don't do that, the CPU might go to sleep while the
            // song is playing, causing playback to stop.
            mediaPlayer.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK);

            // we want the media player to notify us when it's ready preparing,
            // and when it's done playing:
            mediaPlayer.setOnPreparedListener(this);
            mediaPlayer.setOnCompletionListener(this);
            mediaPlayer.setOnErrorListener(this);
            mediaPlayer.setOnSeekCompleteListener(this);
        } else {
            mediaPlayer.reset();
        }
    }

    /**
     * Releases resources used by the service for playback. This includes the
     * "foreground service" status, the wake locks and possibly the MediaPlayer.
     *
     * @param releaseMediaPlayer Indicates whether the Media Player should also
     *                           be released or not
     */
    private void relaxResources(boolean releaseMediaPlayer) {

        musicService.removeForeground(false);

        // stop and release the Media Player, if it's available
        if (releaseMediaPlayer && mediaPlayer != null) {
            mediaPlayer.reset();
            mediaPlayer.release();
            mediaPlayer = null;
        }
    }

    /**
     * Try to get the system audio focus.
     */
    private void tryToGetAudioFocus() {
        Log.d(TAG, "tryToGetAudioFocus");
        if (audioFocus != AUDIO_FOCUSED) {
            int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
                    AudioManager.AUDIOFOCUS_GAIN);
            if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
                audioFocus = AUDIO_FOCUSED;
            }
        }
    }

    /**
     * Give up the audio focus.
     */
    private void giveUpAudioFocus() {
        Log.d(TAG, "giveUpAudioFocus");
        if (audioFocus == AUDIO_FOCUSED) {
            if (audioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
                audioFocus = AUDIO_NO_FOCUS_NO_DUCK;
            }
        }
    }

    /**
     * Reconfigures MediaPlayer according to audio focus settings and
     * starts/restarts it. This method starts/restarts the MediaPlayer
     * respecting the current audio focus state. So if we have focus, it will
     * play normally; if we don't have focus, it will either leave the
     * MediaPlayer paused or set it to a low volume, depending on what is
     * allowed by the current focus settings. This method assumes mPlayer !=
     * null, so if you are calling it, you have to do so from a context where
     * you are sure this is the case.
     */
    private void configMediaPlayerState() {
        Log.d(TAG, "configMediaPlayerState. mAudioFocus=" + audioFocus);
        if (audioFocus == AUDIO_NO_FOCUS_NO_DUCK) {
            // If we don't have audio focus and can't duck, we have to pause,
            if (musicService.getState() == STATE_PLAYING) {
                pause();
            }
        } else {  // we have audio focus:
            if (audioFocus == AUDIO_NO_FOCUS_CAN_DUCK) {
                mediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet
            } else {
                if (mediaPlayer != null) {
                    mediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again
                } // else do something for remote client.
            }
            // If we were playing when we lost focus, we need to resume playing.
            if (playFocusGain) {
                if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
                    Log.d(TAG, "configMediaPlayerState startMediaPlayer. seeking to " + currentProgress);
                    if (currentProgress == mediaPlayer.getCurrentPosition()) {
                        mediaPlayer.start();
                        musicService.setState(STATE_PLAYING);
                    } else {
                        mediaPlayer.seekTo(currentProgress);
                        mediaPlayer.start();
                        musicService.setState(STATE_PLAYING);
                    }
                }
                playFocusGain = false;
            }
        }
    }

    public int getCurrentProgressInSong() {
        return mediaPlayer != null ? mediaPlayer.getCurrentPosition() : currentProgress;
    }

    ///////////////////////////////////////////////////////////////////////////
    // Listeners
    ///////////////////////////////////////////////////////////////////////////
    @Override
    public void onAudioFocusChange(int focusChange) {
        Log.d(TAG, "onAudioFocusChange. focusChange=" + focusChange);
        if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
            // We have gained focus:
            audioFocus = AUDIO_FOCUSED;

        } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS ||
                focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ||
                focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
            // We have lost focus. If we can duck (low playback volume), we can keep playing.
            // Otherwise, we need to pause the playback.
            boolean canDuck = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK;
            audioFocus = canDuck ? AUDIO_NO_FOCUS_CAN_DUCK : AUDIO_NO_FOCUS_NO_DUCK;

            // If we are playing, we need to reset media player by calling configMediaPlayerState
            // with mAudioFocus properly set.
            if (musicService.getState() == STATE_PLAYING && !canDuck) {
                // If we don't have audio focus and can't duck, we save the information that
                // we were playing, so that we can resume playback once we get the focus back.
                //todo setting
                playFocusGain = true;
            }
        } else {
            Log.e(TAG, "onAudioFocusChange: Ignoring unsupported focusChange: " + focusChange);
        }
        configMediaPlayerState();
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        Log.d(TAG, "onCompletion from MediaPlayer");
        if (!mp.isLooping()) {
            // The media player finished playing the current song, so we go ahead and start the next.
            currentProgress = 0;
            playNext();
        }
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        Log.e(TAG, "Media player error: what=" + what + ", extra=" + extra);
        return true;
    }

    @Override
    public void onPrepared(MediaPlayer mp) {
        currentMaxDuration = mediaPlayer.getDuration();
        configMediaPlayerState();
    }

    @Override
    public void onSeekComplete(MediaPlayer mp) {
        Log.d(TAG, "onSeekComplete from MediaPlayer:" + mp.getCurrentPosition());
        currentProgress = mp.getCurrentPosition();
        if (musicService.getState() == STATE_REWINDING || musicService.getState() == STATE_FAST_FORWARDING) {
            mediaPlayer.start();
            musicService.setState(STATE_PLAYING);
        }
    }

    ///////////////////////////////////////////////////////////////////////////
    // getter
    ///////////////////////////////////////////////////////////////////////////

    public MediaSessionCompat.Token getServiceToken() {
        return musicService.getMediaSession().getSessionToken();
    }

    public void registerListener(OnSongChangedListener l) {
        changedListeners.add(l);
    }

    public void unregisterListener(OnSongChangedListener l) {
        changedListeners.remove(l);
    }

    public ArrayList<OnSongChangedListener> getChangedListeners() {
        return changedListeners;
    }

    public MediaPlayer getMediaPlayer() {
        if (mediaPlayer == null) {
            createMediaPlayerIfNeeded();
        }
        return mediaPlayer;
    }

    public int getState() {
        if (musicService != null)
            return musicService.getState();
        return PlaybackStateCompat.STATE_STOPPED;
    }

    public Song getPlayingSong() {
        if (musicPlaylist != null)
            return musicPlaylist.getCurrentPlay();
        else return null;
    }

    public MusicPlaylist getMusicPlaylist() {
        return musicPlaylist;
    }

    public void setMusicPlaylist(MusicPlaylist musicPlaylist) {
        this.musicPlaylist = musicPlaylist;
    }

    public int getCurrentMaxDuration() {
        return currentMaxDuration;
    }

    public int getCurrentPosition() {
        if (mediaPlayer != null)
            return mediaPlayer.getCurrentPosition();
        return 0;
    }
}
