/**
 * Created by G-Canvas Open Source Team.
 * Copyright (c) 2017, Alibaba, Inc. All rights reserved.
 *
 * This source code is licensed under the Apache Licence 2.0.
 * For the full copyright and license information, please view
 * the LICENSE file in the root directory of this source tree.
 */
package com.taobao.gcanvas.audio;

import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.media.MediaRecorder;
import android.os.Environment;

import com.taobao.gcanvas.GCanvasResult;
import com.taobao.gcanvas.util.GLog;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;


/**
 * This class implements the audio playback and recording capabilities used by Cordova.
 * It is called by the AudioHandler Cordova class.
 * Only one file can be played or recorded per class instance.
 * <p>
 * Local audio files must reside in one of two places:
 * android_asset:      file name must start with /android_asset/sound.mp3
 * sdcard:             file name is just sound.mp3
 */
public class GAudioPlayer implements OnCompletionListener, OnPreparedListener, OnErrorListener {

    private static final String LOG_TAG = "GAudioPlayer";

    // AudioPlayer message ids
    private static int MEDIA_STATE = 1;
    private static int MEDIA_DURATION = 2;
    private static int MEDIA_POSITION = 3;
    private static int MEDIA_READY = 4;
    private static int MEDIA_ERROR = 9;

    // Media error codes
    private static int MEDIA_ERR_NONE_ACTIVE = 0;
    private static int MEDIA_ERR_ABORTED = 1;
    private static int MEDIA_ERR_NETWORK = 2;
    private static int MEDIA_ERR_DECODE = 3;
    private static int MEDIA_ERR_NONE_SUPPORTED = 4;

    private GAudioHandler handler;          // The AudioHandler object
    private String id;                      // The id of this player (used to identify Media object in JavaScript)
    private MODE mode = MODE.NONE;          // Playback or Recording mode
    private STATE state = STATE.MEDIA_NONE; // State of recording or playback
    private String audioFile = null;        // File name to play or record to
    private float duration = -1;            // Duration of audio
    private MediaRecorder recorder = null;  // Audio recording object
    private String tempFile = null;         // Temporary recording file name
    private MediaPlayer player = null;      // Audio player object
    private boolean prepareOnly = true;     // playback after file prepare flag
    private int seekOnPrepared = 0;     // seek to this location once media is prepared
    private int recorderCount = 0;
    private GCanvasResult resultContext;


    /**
     * Constructor.
     *
     * @param handler       The audio handler object
     * @param id            The id of this audio player
     * @param file          The audio file path
     * @param resultContext
     */
    public GAudioPlayer(GAudioHandler handler, String id, String file, GCanvasResult resultContext) {
        this.handler = handler;
        this.id = id;
        this.audioFile = file;
        this.recorder = new MediaRecorder();
        this.resultContext = resultContext;

        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            this.tempFile = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tmprecording.3gp";
        } else {
            this.tempFile = "/data/data/" + handler.getActivity().getPackageName() + "/cache/tmprecording.3gp";
        }
    }

    /**
     * @param file The audio file path
     */
    public void setSourceAudio(String file) {
        this.audioFile = file;
    }

    private void sendJavascript(String js) {
        if (this.resultContext != null) {
            this.resultContext.calljs(js);
        }
    }

    /**
     * Start recording the specified file.
     *
     * @param file The name of the file
     */
    public void startRecording(String file) {
        switch (this.mode) {
            case PLAY:
                this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
                break;
            case NONE:
                this.audioFile = file;
                this.recorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT); // THREE_GPP);
                this.recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT); // AMR_NB);
                this.recorder.setOutputFile(this.tempFile);
                try {
                    this.recorder.prepare();
                    this.recorder.start();
                    this.setState(STATE.MEDIA_RUNNING);
                    this.recorderCount++;
                    return;
                } catch (IllegalStateException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }

                this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
                break;
            case RECORD:
                this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
        }
    }

    /**
     * Save temporary recorded file to specified name
     *
     * @param file
     */
    public void moveFile(String file) {
        /* this is a hack to save the file as the specified name */
        File f = new File(this.tempFile);

        if (!file.startsWith("/")) {
            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                file = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + file;
            } else {
                file = "/data/data/" + handler.getActivity().getPackageName() + "/cache/" + file;
            }
        }

        if (!f.renameTo(new File(file))){
            String logMsg = "renaming " + this.tempFile + " to " + file;
            GLog.e(LOG_TAG, "FAILED " + logMsg);
        }
    }

    /**
     * Stop recording and save to the file specified when recording started.
     */
    public void stopRecording() {
        if ((this.recorder != null) && (this.recorderCount > 0)) {
            try {
                if (this.state == STATE.MEDIA_RUNNING) {
                    this.recorder.stop();
                    this.setState(STATE.MEDIA_STOPPED);
                }
                this.recorderCount--;
                this.recorder.reset();
                this.moveFile(this.audioFile);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Start or resume playing audio file.
     *
     * @param file The name of the audio file.
     */
    public void startPlaying(String file) {

        if (this.readyPlayer(file) && this.player != null) {
            this.player.start();
            this.setState(STATE.MEDIA_RUNNING);
            this.seekOnPrepared = 0; //insures this is always reset
        } else {
            this.prepareOnly = false;
        }
    }

    /**
     * Seek or jump to a new time in the track.
     */
    public void seekToPlaying(int milliseconds) {
        if (this.readyPlayer(this.audioFile)) {
            this.player.seekTo(milliseconds);
            this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_POSITION + ", " + milliseconds / 1000.0f + ");");
        } else {
            this.seekOnPrepared = milliseconds;
        }
    }

    public void loadingAudio() {
        if (this.readyPlayer(this.audioFile)) {
            this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_READY + ");");
        }
    }

    /**
     * Pause playing.
     */
    public void pausePlaying() {
        // If playing, then pause
        if (this.state == STATE.MEDIA_RUNNING && this.player != null) {
            this.player.pause();
            this.setState(STATE.MEDIA_PAUSED);
        } else {
            this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_NONE_ACTIVE + "});");
        }
    }

    /**
     * Stop playing the audio file.
     */
    public void stopPlaying() {
        if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) {
            this.player.pause();
            this.player.seekTo(0);
            this.setState(STATE.MEDIA_STOPPED);
        } else {
            this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_NONE_ACTIVE + "});");
        }
    }

    /**
     * Callback to be invoked when playback of a media source has completed.
     *
     * @param player The MediaPlayer that reached the end of the file
     */
    public void onCompletion(MediaPlayer player) {
        this.setState(STATE.MEDIA_STOPPED);
    }

    /**
     * Get current position of playback.
     *
     * @return position in msec or -1 if not playing
     */
    public long getCurrentPosition() {
        if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) {
            int curPos = this.player.getCurrentPosition();
            this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_POSITION + ", " + curPos / 1000.0f + ");");
            return curPos;
        } else {
            return -1;
        }
    }

    /**
     * Determine if playback file is streaming or local.
     * It is streaming if file name starts with "http://"
     *
     * @param file The file name
     * @return T=streaming, F=local
     */
    public boolean isStreaming(String file) {
        if (file.contains("http://") || file.contains("https://")) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Get the duration of the audio file.
     *
     * @param file The name of the audio file.
     * @return The duration in msec.
     * -1=can't be determined
     * -2=not allowed
     */
    public float getDuration(String file) {
        // Can't get duration of recording
        if (this.recorder != null) {
            return (-2); // not allowed
        }

        // If audio file already loaded and started, then return duration
        if (this.player != null) {
            return this.duration;
        }

        // If no player yet, then create one
        else {
            this.prepareOnly = true;
            this.startPlaying(file);

            // This will only return value for local, since streaming
            // file hasn't been read yet.
            return this.duration;
        }
    }

    /**
     * Callback to be invoked when the media source is ready for playback.
     *
     * @param player The MediaPlayer that is ready for playback
     */
    public void onPrepared(MediaPlayer player) {
        // Listen for playback completion
        this.player.setOnCompletionListener(this);
        // seek to any location received while not prepared
        this.seekToPlaying(this.seekOnPrepared);

        // If start playing after prepared
        if (!this.prepareOnly) {
            this.player.start();
            this.setState(STATE.MEDIA_RUNNING);
            this.seekOnPrepared = 0; //reset only when played
        } else {
            this.setState(STATE.MEDIA_STARTING);
        }

        // Save off duration
        this.duration = getDurationInSeconds();

        // reset prepare only flag
        this.prepareOnly = true;

        // Send status notification to JavaScript
        this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_DURATION + "," + this.duration + ");");
    }

    /**
     * By default Android returns the length of audio in mills but we want seconds
     *
     * @return length of clip in seconds
     */
    private float getDurationInSeconds() {
        return (this.player.getDuration() / 1000.0f);
    }

    /**
     * Callback to be invoked when there has been an error during an asynchronous operation
     * (other errors will throw exceptions at method call time).
     *
     * @param player the MediaPlayer the error pertains to
     * @param arg1   the type of error that has occurred: (MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_SERVER_DIED)
     * @param arg2   an extra code, specific to the error.
     */
    public boolean onError(MediaPlayer player, int arg1, int arg2) {
        this.player.stop();
        this.player.release();

        // Send error notification to JavaScript
        this.sendJavascript("Media.onStatus('" + this.id + "', { \"code\":" + arg1 + "});");
        return false;
    }

    /**
     * Set the mode
     *
     * @param mode
     */
    private void setMode(MODE mode) {
        this.mode = mode;
    }

    /**
     * Get the audio state.
     *
     * @return int
     */
    public int getState() {
        return this.state.ordinal();
    }

    /**
     * Set the state and send it to JavaScript.
     *
     * @param state
     */
    private void setState(STATE state) {
        if (this.state != state) {
            this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_STATE + ", " + state.ordinal() + ");");
        }
        this.state = state;
    }

    /**
     * Set the volume for audio player
     *
     * @param volume
     */
    public void setVolume(float volume) {
        if (this.player != null) {
            this.player.setVolume(volume, volume);
        }
    }

    /**
     * Destroy player and stop audio playing or recording.
     */
    public void destroy() {
        // Stop any play or record
        if (this.player != null) {
            if ((this.state == STATE.MEDIA_RUNNING) || (this.state == STATE.MEDIA_PAUSED)) {
                this.player.stop();
                this.setState(STATE.MEDIA_STOPPED);
            }
            this.player.release();
            this.player = null;
        }

        if (this.recorder != null) {
            this.stopRecording();
            this.recorder.release();
            this.recorder = null;
        }
    }

    /**
     * attempts to put the player in play mode
     *
     * @return true if in playmode, false otherwise
     */
    private boolean playMode() {
        switch (this.mode) {
            case NONE:
                this.setMode(MODE.PLAY);
                break;
            case PLAY:
                break;
            case RECORD:
                this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
                return false; //player is not ready
        }
        return true;
    }

    /**
     * attempts to initialize the media player for playback
     *
     * @param file the file to play
     * @return false if player not ready, reports if in wrong mode or state
     */
    private boolean readyPlayer(String file) {
        if (playMode()) {
            switch (this.state) {
                case MEDIA_NONE:
                    if (this.player == null) {
                        this.player = new MediaPlayer();
                    }
                    try {
                        this.loadAudioFile(file);
                    } catch (Exception e) {
                        this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
                    }
                    return false;
                case MEDIA_LOADING:
                    //cordova js is not aware of MEDIA_LOADING, so we send MEDIA_STARTING instead
                    this.prepareOnly = false;
                    return false;
                case MEDIA_STARTING:
                case MEDIA_RUNNING:
                case MEDIA_PAUSED:
                    return true;
                case MEDIA_STOPPED:
                    //if we are readying the same file
                    if (this.audioFile.compareTo(file) == 0) {
                        //reset the audio file
                        player.seekTo(0);
                        player.pause();
                        return true;
                    } else {
                        //reset the player
                        this.player.reset();
                        try {
                            this.loadAudioFile(file);
                        } catch (Exception e) {
                            this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
                        }
                        //if we had to prepare= the file, we won't be in the correct state for playback
                        return false;
                    }
                default:
                    this.sendJavascript("Media.onStatus('" + this.id + "', " + MEDIA_ERROR + ", { \"code\":" + MEDIA_ERR_ABORTED + "});");
            }
        }
        return false;
    }

    /**
     * load audio file
     *
     * @throws IOException
     * @throws IllegalStateException
     * @throws SecurityException
     * @throws IllegalArgumentException
     */
    private void loadAudioFile(String file) throws IllegalArgumentException, SecurityException, IllegalStateException, IOException {
        if (this.isStreaming(file)) {
            this.player.setDataSource(file);
            this.player.setAudioStreamType(AudioManager.STREAM_MUSIC);
            //if it's a streaming file, play mode is implied
            this.setMode(MODE.PLAY);
            this.setState(STATE.MEDIA_STARTING);
            this.player.setOnPreparedListener(this);
            this.player.prepareAsync();
        } else {
            if (file.startsWith("/android_asset/")) {
                String f = file.substring(15);
                android.content.res.AssetFileDescriptor fd = this.handler.getActivity().getAssets().openFd(f);
                this.player.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
            } else {
                File fp = new File(file);
                if (fp.exists()) {
                    FileInputStream fileInputStream = new FileInputStream(file);
                    this.player.setDataSource(fileInputStream.getFD());
                    fileInputStream.close();
                } else {
                    //file not exists should throw exception!
                    throw new IOException();
                }
            }

            this.setState(STATE.MEDIA_STARTING);
            this.player.setOnPreparedListener(this);
            this.player.prepare();

            // Get duration
            this.duration = getDurationInSeconds();
        }
    }

    // AudioPlayer modes
    public enum MODE {
        NONE, PLAY, RECORD
    }

    // AudioPlayer states
    public enum STATE {
        MEDIA_NONE,
        MEDIA_STARTING,
        MEDIA_RUNNING,
        MEDIA_PAUSED,
        MEDIA_STOPPED,
        MEDIA_LOADING
    }
}
