/*
 * Copyright (C) 2013 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.example.android.mediarouter.player;

import android.app.Activity;
import android.app.Presentation;
import android.content.Context;
import android.content.DialogInterface;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.support.v7.media.MediaItemStatus;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.util.Log;
import android.view.Display;
import android.view.Gravity;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;

import com.example.android.mediarouter.R;

import java.io.IOException;

/**
 * Handles playback of a single media item using MediaPlayer.
 */
public abstract class LocalPlayer extends Player implements
        MediaPlayer.OnPreparedListener,
        MediaPlayer.OnCompletionListener,
        MediaPlayer.OnErrorListener,
        MediaPlayer.OnSeekCompleteListener {
    private static final String TAG = "LocalPlayer";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private static final int STATE_IDLE = 0;
    private static final int STATE_PLAY_PENDING = 1;
    private static final int STATE_READY = 2;
    private static final int STATE_PLAYING = 3;
    private static final int STATE_PAUSED = 4;

    private final Context mContext;
    private final Handler mHandler = new Handler();
    private MediaPlayer mMediaPlayer;
    private int mState = STATE_IDLE;
    private int mSeekToPos;
    private int mVideoWidth;
    private int mVideoHeight;
    private Surface mSurface;
    private SurfaceHolder mSurfaceHolder;

    public LocalPlayer(Context context) {
        mContext = context;

        // reset media player
        reset();
    }

    @Override
    public boolean isRemotePlayback() {
        return false;
    }

    @Override
    public boolean isQueuingSupported() {
        return false;
    }

    @Override
    public void connect(RouteInfo route) {
        if (DEBUG) {
            Log.d(TAG, "connecting to: " + route);
        }
    }

    @Override
    public void release() {
        if (DEBUG) {
            Log.d(TAG, "releasing");
        }
        // release media player
        if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
    }

    // Player
    @Override
    public void play(final PlaylistItem item) {
        if (DEBUG) {
            Log.d(TAG, "play: item=" + item);
        }
        reset();
        mSeekToPos = (int)item.getPosition();
        try {
            mMediaPlayer.setDataSource(mContext, item.getUri());
            mMediaPlayer.prepareAsync();
        } catch (IllegalStateException e) {
            Log.e(TAG, "MediaPlayer throws IllegalStateException, uri=" + item.getUri());
        } catch (IOException e) {
            Log.e(TAG, "MediaPlayer throws IOException, uri=" + item.getUri());
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "MediaPlayer throws IllegalArgumentException, uri=" + item.getUri());
        } catch (SecurityException e) {
            Log.e(TAG, "MediaPlayer throws SecurityException, uri=" + item.getUri());
        }
        if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
            resume();
        } else {
            pause();
        }
    }

    @Override
    public void seek(final PlaylistItem item) {
        if (DEBUG) {
            Log.d(TAG, "seek: item=" + item);
        }
        int pos = (int)item.getPosition();
        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
            mMediaPlayer.seekTo(pos);
            mSeekToPos = pos;
        } else if (mState == STATE_IDLE || mState == STATE_PLAY_PENDING) {
            // Seek before onPrepared() arrives,
            // need to performed delayed seek in onPrepared()
            mSeekToPos = pos;
        }
    }

    @Override
    public void getStatus(final PlaylistItem item, final boolean update) {
        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
            // use mSeekToPos if we're currently seeking (mSeekToPos is reset
            // when seeking is completed)
            item.setDuration(mMediaPlayer.getDuration());
            item.setPosition(mSeekToPos > 0 ?
                    mSeekToPos : mMediaPlayer.getCurrentPosition());
            item.setTimestamp(SystemClock.elapsedRealtime());
        }
        if (update && mCallback != null) {
            mCallback.onPlaylistReady();
        }
    }

    @Override
    public void pause() {
        if (DEBUG) {
            Log.d(TAG, "pause");
        }
        if (mState == STATE_PLAYING) {
            mMediaPlayer.pause();
            mState = STATE_PAUSED;
        }
    }

    @Override
    public void resume() {
        if (DEBUG) {
            Log.d(TAG, "resume");
        }
        if (mState == STATE_READY || mState == STATE_PAUSED) {
            mMediaPlayer.start();
            mState = STATE_PLAYING;
        } else if (mState == STATE_IDLE){
            mState = STATE_PLAY_PENDING;
        }
    }

    @Override
    public void stop() {
        if (DEBUG) {
            Log.d(TAG, "stop");
        }
        if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
            mMediaPlayer.stop();
            mState = STATE_IDLE;
        }
    }

    @Override
    public void enqueue(final PlaylistItem item) {
        throw new UnsupportedOperationException("LocalPlayer doesn't support enqueue!");
    }

    @Override
    public PlaylistItem remove(String iid) {
        throw new UnsupportedOperationException("LocalPlayer doesn't support remove!");
    }

    //MediaPlayer Listeners
    @Override
    public void onPrepared(MediaPlayer mp) {
        if (DEBUG) {
            Log.d(TAG, "onPrepared");
        }
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                if (mState == STATE_IDLE) {
                    mState = STATE_READY;
                    updateVideoRect();
                } else if (mState == STATE_PLAY_PENDING) {
                    mState = STATE_PLAYING;
                    updateVideoRect();
                    if (mSeekToPos > 0) {
                        if (DEBUG) {
                            Log.d(TAG, "seek to initial pos: " + mSeekToPos);
                        }
                        mMediaPlayer.seekTo(mSeekToPos);
                    }
                    mMediaPlayer.start();
                }
                if (mCallback != null) {
                    mCallback.onPlaylistChanged();
                }
            }
        });
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        if (DEBUG) {
            Log.d(TAG, "onCompletion");
        }
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                if (mCallback != null) {
                    mCallback.onCompletion();
                }
            }
        });
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        if (DEBUG) {
            Log.d(TAG, "onError");
        }
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                if (mCallback != null) {
                    mCallback.onError();
                }
            }
        });
        // return true so that onCompletion is not called
        return true;
    }

    @Override
    public void onSeekComplete(MediaPlayer mp) {
        if (DEBUG) {
            Log.d(TAG, "onSeekComplete");
        }
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mSeekToPos = 0;
                if (mCallback != null) {
                    mCallback.onPlaylistChanged();
                }
            }
        });
    }

    protected Context getContext() { return mContext; }
    protected MediaPlayer getMediaPlayer() { return mMediaPlayer; }
    protected int getVideoWidth() { return mVideoWidth; }
    protected int getVideoHeight() { return mVideoHeight; }
    protected void setSurface(Surface surface) {
        mSurface = surface;
        mSurfaceHolder = null;
        updateSurface();
    }

    protected void setSurface(SurfaceHolder surfaceHolder) {
        mSurface = null;
        mSurfaceHolder = surfaceHolder;
        updateSurface();
    }

    protected void removeSurface(SurfaceHolder surfaceHolder) {
        if (surfaceHolder == mSurfaceHolder) {
            setSurface((SurfaceHolder)null);
        }
    }

    protected void updateSurface() {
        if (mMediaPlayer == null) {
            // just return if media player is already gone
            return;
        }
        if (mSurface != null) {
            // The setSurface API does not exist until V14+.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                ICSMediaPlayer.setSurface(mMediaPlayer, mSurface);
            } else {
                throw new UnsupportedOperationException("MediaPlayer does not support "
                        + "setSurface() on this version of the platform.");
            }
        } else if (mSurfaceHolder != null) {
            mMediaPlayer.setDisplay(mSurfaceHolder);
        } else {
            mMediaPlayer.setDisplay(null);
        }
    }

    protected abstract void updateSize();

    private void reset() {
        if (mMediaPlayer != null) {
            mMediaPlayer.stop();
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
        mMediaPlayer = new MediaPlayer();
        mMediaPlayer.setOnPreparedListener(this);
        mMediaPlayer.setOnCompletionListener(this);
        mMediaPlayer.setOnErrorListener(this);
        mMediaPlayer.setOnSeekCompleteListener(this);
        updateSurface();
        mState = STATE_IDLE;
        mSeekToPos = 0;
    }

    private void updateVideoRect() {
        if (mState != STATE_IDLE && mState != STATE_PLAY_PENDING) {
            int width = mMediaPlayer.getVideoWidth();
            int height = mMediaPlayer.getVideoHeight();
            if (width > 0 && height > 0) {
                mVideoWidth = width;
                mVideoHeight = height;
                updateSize();
            } else {
                Log.e(TAG, "video rect is 0x0!");
                mVideoWidth = mVideoHeight = 0;
            }
        }
    }

    private static final class ICSMediaPlayer {
        public static final void setSurface(MediaPlayer player, Surface surface) {
            player.setSurface(surface);
        }
    }

    /**
     * Handles playback of a single media item using MediaPlayer in SurfaceView
     */
    public static class SurfaceViewPlayer extends LocalPlayer implements
            SurfaceHolder.Callback {
        private static final String TAG = "SurfaceViewPlayer";
        private RouteInfo mRoute;
        private final SurfaceView mSurfaceView;
        private final FrameLayout mLayout;
        private DemoPresentation mPresentation;

        public SurfaceViewPlayer(Context context) {
            super(context);

            mLayout = (FrameLayout)((Activity)context).findViewById(R.id.player);
            mSurfaceView = (SurfaceView)((Activity)context).findViewById(R.id.surface_view);

            // add surface holder callback
            SurfaceHolder holder = mSurfaceView.getHolder();
            holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
            holder.addCallback(this);
        }

        @Override
        public void connect(RouteInfo route) {
            super.connect(route);
            mRoute = route;
        }

        @Override
        public void release() {
            super.release();

            // dismiss presentation display
            if (mPresentation != null) {
                Log.i(TAG, "Dismissing presentation because the activity is no longer visible.");
                mPresentation.dismiss();
                mPresentation = null;
            }

            // remove surface holder callback
            SurfaceHolder holder = mSurfaceView.getHolder();
            holder.removeCallback(this);

            // hide the surface view when SurfaceViewPlayer is destroyed
            mSurfaceView.setVisibility(View.GONE);
            mLayout.setVisibility(View.GONE);
        }

        @Override
        public void updatePresentation() {
            // Get the current route and its presentation display.
            Display presentationDisplay = mRoute != null ? mRoute.getPresentationDisplay() : null;

            // Dismiss the current presentation if the display has changed.
            if (mPresentation != null && mPresentation.getDisplay() != presentationDisplay) {
                Log.i(TAG, "Dismissing presentation because the current route no longer "
                        + "has a presentation display.");
                mPresentation.dismiss();
                mPresentation = null;
            }

            // Show a new presentation if needed.
            if (mPresentation == null && presentationDisplay != null) {
                Log.i(TAG, "Showing presentation on display: " + presentationDisplay);
                mPresentation = new DemoPresentation(getContext(), presentationDisplay);
                mPresentation.setOnDismissListener(mOnDismissListener);
                try {
                    mPresentation.show();
                } catch (WindowManager.InvalidDisplayException ex) {
                    Log.w(TAG, "Couldn't show presentation!  Display was removed in "
                              + "the meantime.", ex);
                    mPresentation = null;
                }
            }

            updateContents();
        }

        // SurfaceHolder.Callback
        @Override
        public void surfaceChanged(SurfaceHolder holder, int format,
                int width, int height) {
            if (DEBUG) {
                Log.d(TAG, "surfaceChanged: " + width + "x" + height);
            }
            setSurface(holder);
        }

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            if (DEBUG) {
                Log.d(TAG, "surfaceCreated");
            }
            setSurface(holder);
            updateSize();
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            if (DEBUG) {
                Log.d(TAG, "surfaceDestroyed");
            }
            removeSurface(holder);
        }

        @Override
        protected void updateSize() {
            int width = getVideoWidth();
            int height = getVideoHeight();
            if (width > 0 && height > 0) {
                if (mPresentation == null) {
                    int surfaceWidth = mLayout.getWidth();
                    int surfaceHeight = mLayout.getHeight();

                    // Calculate the new size of mSurfaceView, so that video is centered
                    // inside the framelayout with proper letterboxing/pillarboxing
                    ViewGroup.LayoutParams lp = mSurfaceView.getLayoutParams();
                    if (surfaceWidth * height < surfaceHeight * width) {
                        // Black bars on top&bottom, mSurfaceView has full layout width,
                        // while height is derived from video's aspect ratio
                        lp.width = surfaceWidth;
                        lp.height = surfaceWidth * height / width;
                    } else {
                        // Black bars on left&right, mSurfaceView has full layout height,
                        // while width is derived from video's aspect ratio
                        lp.width = surfaceHeight * width / height;
                        lp.height = surfaceHeight;
                    }
                    Log.i(TAG, "video rect is " + lp.width + "x" + lp.height);
                    mSurfaceView.setLayoutParams(lp);
                } else {
                    mPresentation.updateSize(width, height);
                }
            }
        }

        private void updateContents() {
            // Show either the content in the main activity or the content in the presentation
            if (mPresentation != null) {
                mLayout.setVisibility(View.GONE);
                mSurfaceView.setVisibility(View.GONE);
            } else {
                mLayout.setVisibility(View.VISIBLE);
                mSurfaceView.setVisibility(View.VISIBLE);
            }
        }

        // Listens for when presentations are dismissed.
        private final DialogInterface.OnDismissListener mOnDismissListener =
                new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                if (dialog == mPresentation) {
                    Log.i(TAG, "Presentation dismissed.");
                    mPresentation = null;
                    updateContents();
                }
            }
        };

        // Presentation
        private final class DemoPresentation extends Presentation {
            private SurfaceView mPresentationSurfaceView;

            public DemoPresentation(Context context, Display display) {
                super(context, display);
            }

            @Override
            protected void onCreate(Bundle savedInstanceState) {
                // Be sure to call the super class.
                super.onCreate(savedInstanceState);

                // Inflate the layout.
                setContentView(R.layout.sample_media_router_presentation);

                // Set up the surface view.
                mPresentationSurfaceView = (SurfaceView)findViewById(R.id.surface_view);
                SurfaceHolder holder = mPresentationSurfaceView.getHolder();
                holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
                holder.addCallback(SurfaceViewPlayer.this);
                Log.i(TAG, "Presentation created");
            }

            public void updateSize(int width, int height) {
                int surfaceHeight = getWindow().getDecorView().getHeight();
                int surfaceWidth = getWindow().getDecorView().getWidth();
                ViewGroup.LayoutParams lp = mPresentationSurfaceView.getLayoutParams();
                if (surfaceWidth * height < surfaceHeight * width) {
                    lp.width = surfaceWidth;
                    lp.height = surfaceWidth * height / width;
                } else {
                    lp.width = surfaceHeight * width / height;
                    lp.height = surfaceHeight;
                }
                Log.i(TAG, "Presentation video rect is " + lp.width + "x" + lp.height);
                mPresentationSurfaceView.setLayoutParams(lp);
            }
        }
    }

    /**
     * Handles playback of a single media item using MediaPlayer in
     * OverlayDisplayWindow.
     */
    public static class OverlayPlayer extends LocalPlayer implements
            OverlayDisplayWindow.OverlayWindowListener {
        private static final String TAG = "OverlayPlayer";
        private final OverlayDisplayWindow mOverlay;

        public OverlayPlayer(Context context) {
            super(context);

            mOverlay = OverlayDisplayWindow.create(getContext(),
                    getContext().getResources().getString(
                            R.string.sample_media_route_provider_remote),
                    1024, 768, Gravity.CENTER);

            mOverlay.setOverlayWindowListener(this);
        }

        @Override
        public void connect(RouteInfo route) {
            super.connect(route);
            mOverlay.show();
        }

        @Override
        public void release() {
            super.release();
            mOverlay.dismiss();
        }

        @Override
        protected void updateSize() {
            int width = getVideoWidth();
            int height = getVideoHeight();
            if (width > 0 && height > 0) {
                mOverlay.updateAspectRatio(width, height);
            }
        }

        // OverlayDisplayWindow.OverlayWindowListener
        @Override
        public void onWindowCreated(Surface surface) {
            setSurface(surface);
        }

        @Override
        public void onWindowCreated(SurfaceHolder surfaceHolder) {
            setSurface(surfaceHolder);
        }

        @Override
        public void onWindowDestroyed() {
            setSurface((SurfaceHolder)null);
        }
    }
}
