/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.tv.dvr.ui.playback;

import android.content.Context;
import android.media.PlaybackParams;
import android.media.session.PlaybackState;
import android.media.tv.TvContentRating;
import android.media.tv.TvInputManager;
import android.media.tv.TvTrackInfo;
import android.media.tv.TvView;
import android.text.TextUtils;
import android.util.Log;
import com.android.tv.common.compat.TvViewCompat.TvInputCallbackCompat;
import com.android.tv.dvr.DvrTvView;
import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.ui.AppLayerTvView;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/** Player for recorded programs. */
public class DvrPlayer {
    private static final String TAG = "DvrPlayer";
    private static final boolean DEBUG = false;

    /** The max rewinding speed supported by DVR player. */
    public static final int MAX_REWIND_SPEED = 256;
    /** The max fast-forwarding speed supported by DVR player. */
    public static final int MAX_FAST_FORWARD_SPEED = 256;

    private static final long SEEK_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(2);
    private static final long REWIND_POSITION_MARGIN_MS = 32; // Workaround value. b/29994826
    private static final long FORWARD_POSITION_MARGIN_MS = TimeUnit.SECONDS.toMillis(5);

    private RecordedProgram mProgram;
    private long mInitialSeekPositionMs;
    private final DvrTvView mTvView;
    private DvrPlayerCallback mCallback;
    private OnAspectRatioChangedListener mOnAspectRatioChangedListener;
    private OnContentBlockedListener mOnContentBlockedListener;
    private OnTracksAvailabilityChangedListener mOnTracksAvailabilityChangedListener;
    private OnTrackSelectedListener mOnAudioTrackSelectedListener;
    private OnTrackSelectedListener mOnSubtitleTrackSelectedListener;
    private String mSelectedAudioTrackId;
    private String mSelectedSubtitleTrackId;
    private float mAspectRatio = Float.NaN;
    private int mPlaybackState = PlaybackState.STATE_NONE;
    private long mTimeShiftCurrentPositionMs;
    private boolean mPauseOnPrepared;
    private boolean mHasClosedCaption;
    private boolean mHasMultiAudio;
    private final PlaybackParams mPlaybackParams = new PlaybackParams();
    private final DvrPlayerCallback mEmptyCallback = new DvrPlayerCallback();
    private long mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
    private boolean mTimeShiftPlayAvailable;

    /** Callback of DVR player. */
    public static class DvrPlayerCallback {
        /**
         * Called when the playback position is changed. The normal updating frequency is around 1
         * sec., which is restricted to the implementation of {@link
         * android.media.tv.TvInputService}.
         */
        public void onPlaybackPositionChanged(long positionMs) {}
        /** Called when the playback state or the playback speed is changed. */
        public void onPlaybackStateChanged(int playbackState, int playbackSpeed) {}
        /** Called when the playback toward the end. */
        public void onPlaybackEnded() {}
        /** Called when the playback is resumed to live position. */
        public void onPlaybackResume() {}
    }

    /** Listener for aspect ratio changed events. */
    public interface OnAspectRatioChangedListener {
        /**
         * Called when the Video's aspect ratio is changed.
         *
         * @param videoAspectRatio The aspect ratio of video. 0 stands for unknown ratios. Listeners
         *     should handle it carefully.
         */
        void onAspectRatioChanged(float videoAspectRatio);
    }

    /** Listener for content blocked events. */
    public interface OnContentBlockedListener {
        /** Called when the Video's aspect ratio is changed. */
        void onContentBlocked(TvContentRating rating);
    }

    /** Listener for tracks availability changed events */
    public interface OnTracksAvailabilityChangedListener {
        /** Called when the Video's subtitle or audio tracks are changed. */
        void onTracksAvailabilityChanged(boolean hasClosedCaption, boolean hasMultiAudio);
    }

    /** Listener for track selected events */
    public interface OnTrackSelectedListener {
        /** Called when certain subtitle or audio track is selected. */
        void onTrackSelected(String selectedTrackId);
    }

    /** Constructor of DvrPlayer. */
    public DvrPlayer(AppLayerTvView tvView, Context context) {
        mTvView = new DvrTvView(context, tvView, this);
        mTvView.setCaptionEnabled(true);
        mPlaybackParams.setSpeed(1.0f);
        setTvViewCallbacks();
        setCallback(null);
        mTvView.init();
    }

    /**
     * Prepares playback.
     *
     * @param doPlay indicates DVR player do or do not start playback after media is prepared.
     */
    public void prepare(boolean doPlay) throws IllegalStateException {
        if (DEBUG) Log.d(TAG, "prepare()");
        if (mProgram == null) {
            throw new IllegalStateException("Recorded program not set");
        } else if (mPlaybackState != PlaybackState.STATE_NONE) {
            throw new IllegalStateException("Playback is already prepared");
        }
        mTvView.timeShiftPlay(mProgram.getInputId(), mProgram.getUri());
        mPlaybackState = PlaybackState.STATE_CONNECTING;
        mPauseOnPrepared = !doPlay;
        mCallback.onPlaybackStateChanged(mPlaybackState, 1);
    }

    /** Resumes playback. */
    public void play() throws IllegalStateException {
        if (DEBUG) Log.d(TAG, "play()");
        if (!isPlaybackPrepared()) {
            throw new IllegalStateException("Recorded program not set or video not ready yet");
        }
        switch (mPlaybackState) {
            case PlaybackState.STATE_FAST_FORWARDING:
            case PlaybackState.STATE_REWINDING:
                setPlaybackSpeed(1);
                break;
            default:
                mTvView.timeShiftResume();
        }
        mPlaybackState = PlaybackState.STATE_PLAYING;
        mCallback.onPlaybackStateChanged(mPlaybackState, 1);
    }

    /** Pauses playback. */
    public void pause() throws IllegalStateException {
        if (DEBUG) Log.d(TAG, "pause()");
        if (!isPlaybackPrepared()) {
            throw new IllegalStateException("Recorded program not set or playback not started yet");
        }
        switch (mPlaybackState) {
            case PlaybackState.STATE_FAST_FORWARDING:
            case PlaybackState.STATE_REWINDING:
                setPlaybackSpeed(1);
                // falls through
            case PlaybackState.STATE_PLAYING:
                mTvView.timeShiftPause();
                mPlaybackState = PlaybackState.STATE_PAUSED;
                break;
            default:
                break;
        }
        mCallback.onPlaybackStateChanged(mPlaybackState, 1);
    }

    /**
     * Fast-forwards playback with the given speed. If the given speed is larger than {@value
     * #MAX_FAST_FORWARD_SPEED}, uses {@value #MAX_FAST_FORWARD_SPEED}.
     */
    public void fastForward(int speed) throws IllegalStateException {
        if (DEBUG) Log.d(TAG, "fastForward()");
        if (!isPlaybackPrepared()) {
            throw new IllegalStateException("Recorded program not set or playback not started yet");
        }
        if (speed <= 0) {
            throw new IllegalArgumentException("Speed cannot be negative or 0");
        }
        if (mTimeShiftCurrentPositionMs >= mProgram.getDurationMillis() - SEEK_POSITION_MARGIN_MS) {
            return;
        }
        speed = Math.min(speed, MAX_FAST_FORWARD_SPEED);
        if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed);
        setPlaybackSpeed(speed);
        mPlaybackState = PlaybackState.STATE_FAST_FORWARDING;
        mCallback.onPlaybackStateChanged(mPlaybackState, speed);
    }

    /**
     * Rewinds playback with the given speed. If the given speed is larger than {@value
     * #MAX_REWIND_SPEED}, uses {@value #MAX_REWIND_SPEED}.
     */
    public void rewind(int speed) throws IllegalStateException {
        if (DEBUG) Log.d(TAG, "rewind()");
        if (!isPlaybackPrepared()) {
            throw new IllegalStateException("Recorded program not set or playback not started yet");
        }
        if (speed <= 0) {
            throw new IllegalArgumentException("Speed cannot be negative or 0");
        }
        if (mTimeShiftCurrentPositionMs <= REWIND_POSITION_MARGIN_MS) {
            return;
        }
        speed = Math.min(speed, MAX_REWIND_SPEED);
        if (DEBUG) Log.d(TAG, "Let's play with speed: " + speed);
        setPlaybackSpeed(-speed);
        mPlaybackState = PlaybackState.STATE_REWINDING;
        mCallback.onPlaybackStateChanged(mPlaybackState, speed);
    }

    /** Seeks playback to the specified position. */
    public void seekTo(long positionMs) throws IllegalStateException {
        if (DEBUG) Log.d(TAG, "seekTo()");
        if (!isPlaybackPrepared()) {
            throw new IllegalStateException("Recorded program not set or playback not started yet");
        }
        if (mProgram == null || mPlaybackState == PlaybackState.STATE_NONE) {
            return;
        }
        positionMs = getRealSeekPosition(positionMs, SEEK_POSITION_MARGIN_MS);
        if (DEBUG) Log.d(TAG, "Now: " + getPlaybackPosition() + ", shift to: " + positionMs);
        mTvView.timeShiftSeekTo(positionMs + mStartPositionMs);
        if (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING
                || mPlaybackState == PlaybackState.STATE_REWINDING) {
            mPlaybackState = PlaybackState.STATE_PLAYING;
            mTvView.timeShiftResume();
            mCallback.onPlaybackStateChanged(mPlaybackState, 1);
        }
    }

    /** Resets playback. */
    public void reset() {
        if (DEBUG) Log.d(TAG, "reset()");
        mCallback.onPlaybackStateChanged(PlaybackState.STATE_NONE, 1);
        mPlaybackState = PlaybackState.STATE_NONE;
        mTvView.reset();
        mTimeShiftPlayAvailable = false;
        mStartPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
        mTimeShiftCurrentPositionMs = 0;
        mPlaybackParams.setSpeed(1.0f);
        mProgram = null;
        mSelectedAudioTrackId = null;
        mSelectedSubtitleTrackId = null;
    }

    /** Sets callbacks for playback. */
    public void setCallback(DvrPlayerCallback callback) {
        if (callback != null) {
            mCallback = callback;
        } else {
            mCallback = mEmptyCallback;
        }
    }

    /** Sets the listener to aspect ratio changing. */
    public void setOnAspectRatioChangedListener(OnAspectRatioChangedListener listener) {
        mOnAspectRatioChangedListener = listener;
    }

    /** Sets the listener to content blocking. */
    public void setOnContentBlockedListener(OnContentBlockedListener listener) {
        mOnContentBlockedListener = listener;
    }

    /** Sets the listener to tracks changing. */
    public void setOnTracksAvailabilityChangedListener(
            OnTracksAvailabilityChangedListener listener) {
        mOnTracksAvailabilityChangedListener = listener;
    }

    /**
     * Sets the listener to tracks of the given type being selected.
     *
     * @param trackType should be either {@link TvTrackInfo#TYPE_AUDIO} or {@link
     *     TvTrackInfo#TYPE_SUBTITLE}.
     */
    public void setOnTrackSelectedListener(int trackType, OnTrackSelectedListener listener) {
        if (trackType == TvTrackInfo.TYPE_AUDIO) {
            mOnAudioTrackSelectedListener = listener;
        } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
            mOnSubtitleTrackSelectedListener = listener;
        }
    }

    /** Gets the listener to tracks of the given type being selected. */
    public OnTrackSelectedListener getOnTrackSelectedListener(int trackType) {
        if (trackType == TvTrackInfo.TYPE_AUDIO) {
            return mOnAudioTrackSelectedListener;
        } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
            return mOnSubtitleTrackSelectedListener;
        }
        return null;
    }

    /** Sets recorded programs for playback. If the player is playing another program, stops it. */
    public void setProgram(RecordedProgram program, long initialSeekPositionMs) {
        if (mProgram != null && mProgram.equals(program)) {
            return;
        }
        if (mPlaybackState != PlaybackState.STATE_NONE) {
            reset();
        }
        mInitialSeekPositionMs = initialSeekPositionMs;
        mProgram = program;
    }

    /** Returns the recorded program now playing. */
    public RecordedProgram getProgram() {
        return mProgram;
    }

    /** Returns the DVR tv view. */
    public DvrTvView getView() {
        return mTvView;
    }

    /** Returns the currrent playback posistion in msecs. */
    public long getPlaybackPosition() {
        return mTimeShiftCurrentPositionMs;
    }

    /** Returns the playback speed currently used. */
    public int getPlaybackSpeed() {
        return (int) mPlaybackParams.getSpeed();
    }

    /** Returns the playback state defined in {@link android.media.session.PlaybackState}. */
    public int getPlaybackState() {
        return mPlaybackState;
    }

    /** Returns the subtitle tracks of the current playback. */
    public ArrayList<TvTrackInfo> getSubtitleTracks() {
        return new ArrayList<>(mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE));
    }

    /** Returns the audio tracks of the current playback. */
    public ArrayList<TvTrackInfo> getAudioTracks() {
        List<TvTrackInfo> tracks = mTvView.getTracks(TvTrackInfo.TYPE_AUDIO);
        return tracks == null ? new ArrayList<>() : new ArrayList<>(tracks);
    }

    /** Returns the ID of the selected track of the given type. */
    public String getSelectedTrackId(int trackType) {
        if (trackType == TvTrackInfo.TYPE_AUDIO) {
            return mSelectedAudioTrackId;
        } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
            return mSelectedSubtitleTrackId;
        }
        return null;
    }

    /** Returns if playback of the recorded program is started. */
    public boolean isPlaybackPrepared() {
        return mPlaybackState != PlaybackState.STATE_NONE
                && mPlaybackState != PlaybackState.STATE_CONNECTING;
    }

    public void release() {
        mTvView.release();
    }

    /**
     * Selects the given track.
     *
     * @return ID of the selected track.
     */
    String selectTrack(int trackType, TvTrackInfo selectedTrack) {
        String oldSelectedTrackId = getSelectedTrackId(trackType);
        String newSelectedTrackId = selectedTrack == null ? null : selectedTrack.getId();
        if (!TextUtils.equals(oldSelectedTrackId, newSelectedTrackId)) {
            if (selectedTrack == null) {
                mTvView.selectTrack(trackType, null);
                return null;
            } else {
                List<TvTrackInfo> tracks = mTvView.getTracks(trackType);
                if (tracks != null && tracks.contains(selectedTrack)) {
                    mTvView.selectTrack(trackType, newSelectedTrackId);
                    return newSelectedTrackId;
                } else if (trackType == TvTrackInfo.TYPE_SUBTITLE && oldSelectedTrackId != null) {
                    // Track not found, disabled closed caption.
                    mTvView.selectTrack(trackType, null);
                    return null;
                }
            }
        }
        return oldSelectedTrackId;
    }

    private void setSelectedTrackId(int trackType, String trackId) {
        if (trackType == TvTrackInfo.TYPE_AUDIO) {
            mSelectedAudioTrackId = trackId;
        } else if (trackType == TvTrackInfo.TYPE_SUBTITLE) {
            mSelectedSubtitleTrackId = trackId;
        }
    }

    private void setPlaybackSpeed(int speed) {
        mPlaybackParams.setSpeed(speed);
        mTvView.timeShiftSetPlaybackParams(mPlaybackParams);
    }

    private long getRealSeekPosition(long seekPositionMs, long endMarginMs) {
        return Math.max(0, Math.min(seekPositionMs, mProgram.getDurationMillis() - endMarginMs));
    }

    private void setTvViewCallbacks() {
        mTvView.setTimeShiftPositionCallback(
                new TvView.TimeShiftPositionCallback() {
                    @Override
                    public void onTimeShiftStartPositionChanged(String inputId, long timeMs) {
                        if (DEBUG) Log.d(TAG, "onTimeShiftStartPositionChanged:" + timeMs);
                        mStartPositionMs = timeMs;
                        if (mTimeShiftPlayAvailable) {
                            resumeToWatchedPositionIfNeeded();
                        }
                    }

                    @Override
                    public void onTimeShiftCurrentPositionChanged(String inputId, long timeMs) {
                        if (DEBUG) Log.d(TAG, "onTimeShiftCurrentPositionChanged: " + timeMs);
                        if (!mTimeShiftPlayAvailable) {
                            // Workaround of b/31436263
                            return;
                        }
                        // Workaround of b/32211561, TIF won't report start position when TIS report
                        // its start position as 0. In that case, we have to do the prework of
                        // playback
                        // on the first time we get current position, and the start position should
                        // be 0
                        // at that time.
                        if (mStartPositionMs == TvInputManager.TIME_SHIFT_INVALID_TIME) {
                            mStartPositionMs = 0;
                            resumeToWatchedPositionIfNeeded();
                        }
                        timeMs -= mStartPositionMs;
                        long bufferedTimeMs =
                                System.currentTimeMillis()
                                        - mProgram.getStartTimeUtcMillis()
                                        - FORWARD_POSITION_MARGIN_MS;
                        if ((mPlaybackState == PlaybackState.STATE_REWINDING
                                        && timeMs <= REWIND_POSITION_MARGIN_MS)
                                || (mPlaybackState == PlaybackState.STATE_FAST_FORWARDING
                                        && timeMs > bufferedTimeMs)) {
                            play();
                            mCallback.onPlaybackResume();
                        } else {
                            mTimeShiftCurrentPositionMs = getRealSeekPosition(timeMs, 0);
                            mCallback.onPlaybackPositionChanged(mTimeShiftCurrentPositionMs);
                            if (timeMs >= mProgram.getDurationMillis()) {
                                pause();
                                mCallback.onPlaybackEnded();
                            }
                        }
                    }
                });
        mTvView.setCallback(
                new TvInputCallbackCompat() {
                    @Override
                    public void onTimeShiftStatusChanged(String inputId, int status) {
                        if (DEBUG) Log.d(TAG, "onTimeShiftStatusChanged:" + status);
                        if (status == TvInputManager.TIME_SHIFT_STATUS_AVAILABLE
                                && mPlaybackState == PlaybackState.STATE_CONNECTING) {
                            mTimeShiftPlayAvailable = true;
                            if (mStartPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
                                // onTimeShiftStatusChanged is sometimes called after
                                // onTimeShiftStartPositionChanged is called. In this case,
                                // resumeToWatchedPositionIfNeeded needs to be called here.
                                resumeToWatchedPositionIfNeeded();
                            }
                        }
                    }

                    @Override
                    public void onTracksChanged(String inputId, List<TvTrackInfo> tracks) {
                        boolean hasClosedCaption =
                                !mTvView.getTracks(TvTrackInfo.TYPE_SUBTITLE).isEmpty();
                        boolean hasMultiAudio =
                                mTvView.getTracks(TvTrackInfo.TYPE_AUDIO).size() > 1;
                        if ((hasClosedCaption != mHasClosedCaption
                                        || hasMultiAudio != mHasMultiAudio)
                                && mOnTracksAvailabilityChangedListener != null) {
                            mOnTracksAvailabilityChangedListener.onTracksAvailabilityChanged(
                                    hasClosedCaption, hasMultiAudio);
                        }
                        mHasClosedCaption = hasClosedCaption;
                        mHasMultiAudio = hasMultiAudio;
                    }

                    @Override
                    public void onTrackSelected(String inputId, int type, String trackId) {
                        if (type == TvTrackInfo.TYPE_AUDIO || type == TvTrackInfo.TYPE_SUBTITLE) {
                            setSelectedTrackId(type, trackId);
                            OnTrackSelectedListener listener = getOnTrackSelectedListener(type);
                            if (listener != null) {
                                listener.onTrackSelected(trackId);
                            }
                        } else if (type == TvTrackInfo.TYPE_VIDEO
                                && trackId != null
                                && mOnAspectRatioChangedListener != null) {
                            List<TvTrackInfo> trackInfos =
                                    mTvView.getTracks(TvTrackInfo.TYPE_VIDEO);
                            if (trackInfos != null) {
                                for (TvTrackInfo trackInfo : trackInfos) {
                                    if (trackInfo.getId().equals(trackId)) {
                                        float videoAspectRatio;
                                        float videoPixelAspectRatio =
                                                trackInfo.getVideoPixelAspectRatio();
                                        int videoWidth = trackInfo.getVideoWidth();
                                        int videoHeight = trackInfo.getVideoHeight();
                                        if (videoWidth > 0 && videoHeight > 0) {
                                            videoAspectRatio =
                                                    (float) trackInfo.getVideoWidth()
                                                            / trackInfo.getVideoHeight();
                                            videoAspectRatio *=
                                                    videoPixelAspectRatio > 0 ?
                                                            videoPixelAspectRatio : 1;
                                        } else {
                                            // Aspect ratio is unknown. Pass the message to
                                            // listeners.
                                            videoAspectRatio = 0;
                                        }
                                        if (DEBUG) Log.d(TAG, "Aspect Ratio: " + videoAspectRatio);
                                        if (mAspectRatio != videoAspectRatio
                                                || videoAspectRatio == 0) {
                                            mOnAspectRatioChangedListener.onAspectRatioChanged(
                                                    videoAspectRatio);
                                            mAspectRatio = videoAspectRatio;
                                            return;
                                        }
                                    }
                                }
                            }
                        }
                    }

                    @Override
                    public void onContentBlocked(String inputId, TvContentRating rating) {
                        if (mOnContentBlockedListener != null) {
                            mOnContentBlockedListener.onContentBlocked(rating);
                        }
                    }
                });
    }

    private void resumeToWatchedPositionIfNeeded() {
        if (mInitialSeekPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
            mTvView.timeShiftSeekTo(
                    getRealSeekPosition(mInitialSeekPositionMs, SEEK_POSITION_MARGIN_MS)
                            + mStartPositionMs);
            mInitialSeekPositionMs = TvInputManager.TIME_SHIFT_INVALID_TIME;
        }
        if (mPauseOnPrepared) {
            mTvView.timeShiftPause();
            mPlaybackState = PlaybackState.STATE_PAUSED;
            mPauseOnPrepared = false;
        } else {
            mTvView.timeShiftResume();
            mPlaybackState = PlaybackState.STATE_PLAYING;
        }
        mCallback.onPlaybackStateChanged(mPlaybackState, 1);
    }
}
