/*
 * 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.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.media.MediaItemStatus;
import android.support.v7.media.MediaRouter.ControlRequestCallback;
import android.support.v7.media.MediaRouter.RouteInfo;
import android.support.v7.media.MediaSessionStatus;
import android.support.v7.media.RemotePlaybackClient;
import android.support.v7.media.RemotePlaybackClient.ItemActionCallback;
import android.support.v7.media.RemotePlaybackClient.SessionActionCallback;
import android.support.v7.media.RemotePlaybackClient.StatusCallback;
import android.util.Log;

import com.example.android.mediarouter.player.Player;
import com.example.android.mediarouter.player.PlaylistItem;
import com.example.android.mediarouter.provider.SampleMediaRouteProvider;

import java.util.ArrayList;
import java.util.List;

/**
 * Handles playback of media items using a remote route.
 *
 * This class is used as a backend by PlaybackManager to feed media items to
 * the remote route. When the remote route doesn't support queuing, media items
 * are fed one-at-a-time; otherwise media items are enqueued to the remote side.
 */
public class RemotePlayer extends Player {
    private static final String TAG = "RemotePlayer";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    private Context mContext;
    private RouteInfo mRoute;
    private boolean mEnqueuePending;
    private String mStatsInfo = "";
    private List<PlaylistItem> mTempQueue = new ArrayList<PlaylistItem>();

    private RemotePlaybackClient mClient;
    private StatusCallback mStatusCallback = new StatusCallback() {
        @Override
        public void onItemStatusChanged(Bundle data,
                String sessionId, MediaSessionStatus sessionStatus,
                String itemId, MediaItemStatus itemStatus) {
            logStatus("onItemStatusChanged", sessionId, sessionStatus, itemId, itemStatus);
            if (mCallback != null) {
                if (itemStatus.getPlaybackState() ==
                        MediaItemStatus.PLAYBACK_STATE_FINISHED) {
                    mCallback.onCompletion();
                } else if (itemStatus.getPlaybackState() ==
                        MediaItemStatus.PLAYBACK_STATE_ERROR) {
                    mCallback.onError();
                }
            }
        }

        @Override
        public void onSessionStatusChanged(Bundle data,
                String sessionId, MediaSessionStatus sessionStatus) {
            logStatus("onSessionStatusChanged", sessionId, sessionStatus, null, null);
            if (mCallback != null) {
                mCallback.onPlaylistChanged();
            }
        }

        @Override
        public void onSessionChanged(String sessionId) {
            if (DEBUG) {
                Log.d(TAG, "onSessionChanged: sessionId=" + sessionId);
            }
        }
    };

    public RemotePlayer(Context context) {
        mContext = context;
    }

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

    @Override
    public boolean isQueuingSupported() {
        return mClient.isQueuingSupported();
    }

    @Override
    public void connect(RouteInfo route) {
        mRoute = route;
        mClient = new RemotePlaybackClient(mContext, route);
        mClient.setStatusCallback(mStatusCallback);

        if (DEBUG) {
            Log.d(TAG, "connected to: " + route
                    + ", isRemotePlaybackSupported: " + mClient.isRemotePlaybackSupported()
                    + ", isQueuingSupported: "+ mClient.isQueuingSupported());
        }
    }

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

        if (DEBUG) {
            Log.d(TAG, "released.");
        }
    }

    // basic playback operations that are always supported
    @Override
    public void play(final PlaylistItem item) {
        if (DEBUG) {
            Log.d(TAG, "play: item=" + item);
        }
        mClient.play(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() {
            @Override
            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
                    String itemId, MediaItemStatus itemStatus) {
                logStatus("play: succeeded", sessionId, sessionStatus, itemId, itemStatus);
                item.setRemoteItemId(itemId);
                if (item.getPosition() > 0) {
                    seekInternal(item);
                }
                if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
                    pause();
                }
                if (mCallback != null) {
                    mCallback.onPlaylistChanged();
                }
            }

            @Override
            public void onError(String error, int code, Bundle data) {
                logError("play: failed", error, code);
            }
        });
    }

    @Override
    public void seek(final PlaylistItem item) {
        seekInternal(item);
    }

    @Override
    public void getStatus(final PlaylistItem item, final boolean update) {
        if (!mClient.hasSession() || item.getRemoteItemId() == null) {
            // if session is not valid or item id not assigend yet.
            // just return, it's not fatal
            return;
        }

        if (DEBUG) {
            Log.d(TAG, "getStatus: item=" + item + ", update=" + update);
        }
        mClient.getStatus(item.getRemoteItemId(), null, new ItemActionCallback() {
            @Override
            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
                    String itemId, MediaItemStatus itemStatus) {
                logStatus("getStatus: succeeded", sessionId, sessionStatus, itemId, itemStatus);
                int state = itemStatus.getPlaybackState();
                if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING
                        || state == MediaItemStatus.PLAYBACK_STATE_PAUSED
                        || state == MediaItemStatus.PLAYBACK_STATE_PENDING) {
                    item.setState(state);
                    item.setPosition(itemStatus.getContentPosition());
                    item.setDuration(itemStatus.getContentDuration());
                    item.setTimestamp(itemStatus.getTimestamp());
                }
                if (update && mCallback != null) {
                    mCallback.onPlaylistReady();
                }
            }

            @Override
            public void onError(String error, int code, Bundle data) {
                logError("getStatus: failed", error, code);
                if (update && mCallback != null) {
                    mCallback.onPlaylistReady();
                }
            }
        });
    }

    @Override
    public void pause() {
        if (!mClient.hasSession()) {
            // ignore if no session
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "pause");
        }
        mClient.pause(null, new SessionActionCallback() {
            @Override
            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
                logStatus("pause: succeeded", sessionId, sessionStatus, null, null);
                if (mCallback != null) {
                    mCallback.onPlaylistChanged();
                }
            }

            @Override
            public void onError(String error, int code, Bundle data) {
                logError("pause: failed", error, code);
            }
        });
    }

    @Override
    public void resume() {
        if (!mClient.hasSession()) {
            // ignore if no session
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "resume");
        }
        mClient.resume(null, new SessionActionCallback() {
            @Override
            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
                logStatus("resume: succeeded", sessionId, sessionStatus, null, null);
                if (mCallback != null) {
                    mCallback.onPlaylistChanged();
                }
            }

            @Override
            public void onError(String error, int code, Bundle data) {
                logError("resume: failed", error, code);
            }
        });
    }

    @Override
    public void stop() {
        if (!mClient.hasSession()) {
            // ignore if no session
            return;
        }
        if (DEBUG) {
            Log.d(TAG, "stop");
        }
        mClient.stop(null, new SessionActionCallback() {
            @Override
            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
                logStatus("stop: succeeded", sessionId, sessionStatus, null, null);
                if (mClient.isSessionManagementSupported()) {
                    endSession();
                }
                if (mCallback != null) {
                    mCallback.onPlaylistChanged();
                }
            }

            @Override
            public void onError(String error, int code, Bundle data) {
                logError("stop: failed", error, code);
            }
        });
    }

    // enqueue & remove are only supported if isQueuingSupported() returns true
    @Override
    public void enqueue(final PlaylistItem item) {
        throwIfQueuingUnsupported();

        if (!mClient.hasSession() && !mEnqueuePending) {
            mEnqueuePending = true;
            if (mClient.isSessionManagementSupported()) {
                startSession(item);
            } else {
                enqueueInternal(item);
            }
        } else if (mEnqueuePending){
            mTempQueue.add(item);
        } else {
            enqueueInternal(item);
        }
    }

    @Override
    public PlaylistItem remove(String itemId) {
        throwIfNoSession();
        throwIfQueuingUnsupported();

        if (DEBUG) {
            Log.d(TAG, "remove: itemId=" + itemId);
        }
        mClient.remove(itemId, null, new ItemActionCallback() {
            @Override
            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
                    String itemId, MediaItemStatus itemStatus) {
                logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus);
            }

            @Override
            public void onError(String error, int code, Bundle data) {
                logError("remove: failed", error, code);
            }
        });

        return null;
    }

    @Override
    public void updateStatistics() {
        // clear stats info first
        mStatsInfo = "";

        Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_STATISTICS);
        intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE);

        if (mRoute != null && mRoute.supportsControlRequest(intent)) {
            ControlRequestCallback callback = new ControlRequestCallback() {
                @Override
                public void onResult(Bundle data) {
                    if (DEBUG) {
                        Log.d(TAG, "getStatistics: succeeded: data=" + data);
                    }
                    if (data != null) {
                        int playbackCount = data.getInt(
                                SampleMediaRouteProvider.DATA_PLAYBACK_COUNT, -1);
                        mStatsInfo = "Total playback count: " + playbackCount;
                    }
                }

                @Override
                public void onError(String error, Bundle data) {
                    Log.d(TAG, "getStatistics: failed: error=" + error + ", data=" + data);
                }
            };

            mRoute.sendControlRequest(intent, callback);
        }
    }

    @Override
    public String getStatistics() {
        return mStatsInfo;
    }

    private void enqueueInternal(final PlaylistItem item) {
        throwIfQueuingUnsupported();

        if (DEBUG) {
            Log.d(TAG, "enqueue: item=" + item);
        }
        mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() {
            @Override
            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
                    String itemId, MediaItemStatus itemStatus) {
                logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus);
                item.setRemoteItemId(itemId);
                if (item.getPosition() > 0) {
                    seekInternal(item);
                }
                if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
                    pause();
                }
                if (mEnqueuePending) {
                    mEnqueuePending = false;
                    for (PlaylistItem item : mTempQueue) {
                        enqueueInternal(item);
                    }
                    mTempQueue.clear();
                }
                if (mCallback != null) {
                    mCallback.onPlaylistChanged();
                }
            }

            @Override
            public void onError(String error, int code, Bundle data) {
                logError("enqueue: failed", error, code);
                if (mCallback != null) {
                    mCallback.onPlaylistChanged();
                }
            }
        });
    }

    private void seekInternal(final PlaylistItem item) {
        throwIfNoSession();

        if (DEBUG) {
            Log.d(TAG, "seek: item=" + item);
        }
        mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() {
           @Override
           public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
                   String itemId, MediaItemStatus itemStatus) {
               logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus);
               if (mCallback != null) {
                   mCallback.onPlaylistChanged();
               }
           }

           @Override
           public void onError(String error, int code, Bundle data) {
               logError("seek: failed", error, code);
           }
        });
    }

    private void startSession(final PlaylistItem item) {
        mClient.startSession(null, new SessionActionCallback() {
            @Override
            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
                logStatus("startSession: succeeded", sessionId, sessionStatus, null, null);
                enqueueInternal(item);
            }

            @Override
            public void onError(String error, int code, Bundle data) {
                logError("startSession: failed", error, code);
            }
        });
    }

    private void endSession() {
        mClient.endSession(null, new SessionActionCallback() {
            @Override
            public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
                logStatus("endSession: succeeded", sessionId, sessionStatus, null, null);
            }

            @Override
            public void onError(String error, int code, Bundle data) {
                logError("endSession: failed", error, code);
            }
        });
    }

    private void logStatus(String message,
            String sessionId, MediaSessionStatus sessionStatus,
            String itemId, MediaItemStatus itemStatus) {
        if (DEBUG) {
            String result = "";
            if (sessionId != null && sessionStatus != null) {
                result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus;
            }
            if (itemId != null & itemStatus != null) {
                result += (result.isEmpty() ? "" : ", ")
                        + "itemId=" + itemId + ", itemStatus=" + itemStatus;
            }
            Log.d(TAG, message + ": " + result);
        }
    }

    private void logError(String message, String error, int code) {
        Log.d(TAG, message + ": error=" + error + ", code=" + code);
    }

    private void throwIfNoSession() {
        if (!mClient.hasSession()) {
            throw new IllegalStateException("Session is invalid");
        }
    }

    private void throwIfQueuingUnsupported() {
        if (!isQueuingSupported()) {
            throw new UnsupportedOperationException("Queuing is unsupported");
        }
    }
}
