/*
 * Copyright (C) 2017 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.googlecode.android_scripting.facade.bluetooth;

import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.media.MediaMetadata;
import android.media.browse.MediaBrowser;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;

import com.googlecode.android_scripting.Log;
import com.googlecode.android_scripting.facade.EventFacade;
import com.googlecode.android_scripting.facade.FacadeManager;
import com.googlecode.android_scripting.facade.bluetooth.media.BluetoothSL4AAudioSrcMBS;
import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
import com.googlecode.android_scripting.rpc.Rpc;
import com.googlecode.android_scripting.rpc.RpcParameter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * SL4A Facade for running Bluetooth Media related test cases
 * The APIs provided here can be grouped into 3 categories:
 * 1. Those that can run on both an Audio Source and Sink
 * 2. Those that makes sense to run only on a Audio Source like a phone
 * 3. Those that makes sense to run only on a Audio Sink like a Car.
 *
 * This media test framework consists of 3 classes:
 * 1. BluetoothMediaFacade - this class that provides the APIs that a RPC client can interact with
 * 2. BluetoothSL4AMBS - This is a MediaBrowserService that is intended to run on the Audio Source
 * (phone).  This MediaBrowserService that runs as part of the SL4A app is used to intercept
 * Media key events coming in from a AVRCP Controller like Car.  Intercepting these events lets us
 * instrument the Bluetooth media related tests.
 * 3. BluetoothMediaPlayback - The class that the MediaBrowserService uses to play media files.
 * It is a UI-less MediaPlayer that serves the purpose of Bluetooth Media testing.
 *
 * The idea is for the BluetoothMediaFacade to create a BluetoothSL4AMBS MediaSession on the
 * Phone (Bluetooth Audio source/Avrcp Target) and use it intercept the Media commands coming
 * from the CarKitt (Bluetooth Audio Sink / Avrcp Controller).
 * On the Carkitt side, we just create and connect a MediaBrowser to the
 * BluetoothMediaBrowserService that is part of the Carkitt's Bluetooth Audio App.  We use this
 * browser to send media commands to the Phone side and intercept the commands with the
 * BluetoothSL4AMBS.
 * This set up helps to instrument tests that can test various Bluetooth Media usecases.
 */

public class BluetoothMediaFacade extends RpcReceiver {
    private static final String TAG = "BluetoothMediaFacade";
    private static final boolean VDBG = false;
    private final Service mService;
    private final Context mContext;
    private Handler mHandler;
    private MediaSessionManager mSessionManager;
    private MediaController mMediaController = null;
    private MediaController.Callback mMediaCtrlCallback = null;
    private MediaSessionManager.OnActiveSessionsChangedListener mSessionListener;
    private MediaBrowser mBrowser = null;

    private static EventFacade mEventFacade;
    // Events posted
    private static final String EVENT_PLAY_RECEIVED = "playReceived";
    private static final String EVENT_PAUSE_RECEIVED = "pauseReceived";
    private static final String EVENT_SKIP_PREV_RECEIVED = "skipPrevReceived";
    private static final String EVENT_SKIP_NEXT_RECEIVED = "skipNextReceived";

    // Commands received
    private static final String CMD_MEDIA_PLAY = "play";
    private static final String CMD_MEDIA_PAUSE = "pause";
    private static final String CMD_MEDIA_SKIP_NEXT = "skipNext";
    private static final String CMD_MEDIA_SKIP_PREV = "skipPrev";

    private static final String BLUETOOTH_PKG_NAME = "com.android.bluetooth";
    private static final String BROWSER_SERVICE_NAME =
            "com.android.bluetooth.avrcpcontroller.BluetoothMediaBrowserService";
    private static final String BLUETOOTH_MBS_TAG = "BluetoothMediaBrowserService";

    // MediaMetadata keys
    private static final String MEDIA_KEY_TITLE = "keyTitle";
    private static final String MEDIA_KEY_ALBUM = "keyAlbum";
    private static final String MEDIA_KEY_ARTIST = "keyArtist";
    private static final String MEDIA_KEY_DURATION = "keyDuration";
    private static final String MEDIA_KEY_NUM_TRACKS = "keyNumTracks";

    /**
     * Following things are initialized here:
     * 1. Setup Listeners to Active Media Session changes
     * 2. Create a new MediaController.callback instance
     */
    public BluetoothMediaFacade(FacadeManager manager) {
        super(manager);
        mService = manager.getService();
        mEventFacade = manager.getReceiver(EventFacade.class);
        mHandler = new Handler(Looper.getMainLooper());
        mContext = mService.getApplicationContext();
        mSessionManager =
                (MediaSessionManager) mContext.getSystemService(mContext.MEDIA_SESSION_SERVICE);
        mSessionListener = new SessionChangeListener();
        // Listen on Active MediaSession changes, so we can get the active session's MediaController
        if (mSessionManager != null) {
            ComponentName compName =
                    new ComponentName(mContext.getPackageName(), this.getClass().getName());
            mSessionManager.addOnActiveSessionsChangedListener(mSessionListener, null,
                    mHandler);
            if (VDBG) {
                List<MediaController> mcl = mSessionManager.getActiveSessions(null);
                Log.d(TAG + " Num Sessions " + mcl.size());
                for (int i = 0; i < mcl.size(); i++) {
                    Log.d(TAG + "Active session : " + i + ((MediaController) (mcl.get(
                            i))).getPackageName() + ((MediaController) (mcl.get(i))).getTag());
                }
            }
        }
        mMediaCtrlCallback = new MediaControllerCallback();
    }

    /**
     * The listener that was setup for listening to changes to Active Media Sessions.
     * This listener is useful in both Car and Phone sides.
     */
    private class SessionChangeListener
            implements MediaSessionManager.OnActiveSessionsChangedListener {
        /**
         * On the Phone side, it listens to the BluetoothSL4AAudioSrcMBS (that the SL4A app runs)
         * becoming active.
         * On the Car side, it listens to the BluetoothMediaBrowserService (associated with the
         * Bluetooth Audio App) becoming active.
         * The idea is to get a handle to the MediaController appropriate for the device, so
         * that we can send and receive Media commands.
         */
        @Override
        public void onActiveSessionsChanged(List<MediaController> controllers) {
            if (VDBG) {
                Log.d(TAG + " onActiveSessionsChanged : " + controllers.size());
                for (int i = 0; i < controllers.size(); i++) {
                    Log.d(TAG + "Active session : " + i + ((MediaController) (controllers.get(
                            i))).getPackageName() + ((MediaController) (controllers.get(
                            i))).getTag());
                }
            }
            // As explained above, looking for the BluetoothSL4AAudioSrcMBS (when running on Phone)
            // or BluetoothMediaBrowserService (when running on Carkitt).
            for (int i = 0; i < controllers.size(); i++) {
                MediaController controller = (MediaController) controllers.get(i);
                if ((controller.getTag().contains(BluetoothSL4AAudioSrcMBS.getTag()))
                        || (controller.getTag().contains(BLUETOOTH_MBS_TAG))) {
                    setCurrentMediaController(controller);
                    return;
                }
            }
        }
    }

    /**
     * When the MediaController for the required MediaSession is obtained, register for its
     * callbacks.
     * Not used yet, but this can be used to verify state changes in both ends.
     */
    private class MediaControllerCallback extends MediaController.Callback {
        @Override
        public void onPlaybackStateChanged(PlaybackState state) {
            Log.d(TAG + " onPlaybackStateChanged: " + state.getState());
        }

        @Override
        public void onMetadataChanged(MediaMetadata metadata) {
            Log.d(TAG + " onMetadataChanged ");
        }
    }

    /**
     * Callback on <code>MediaBrowser.connect()</code>
     * This is relevant only on the Carkitt side, since the intent is to connect a MediaBrowser
     * to the BluetoothMediaBrowserService that is run by the Car's Bluetooth Audio App.
     * On successful connection, we obtain the handle to the corresponding MediaController,
     * so we can imitate sending media commands via the Bluetooth Audio App.
     */
    MediaBrowser.ConnectionCallback mBrowserConnectionCallback =
            new MediaBrowser.ConnectionCallback() {
                private static final String classTag = TAG + " BrowserConnectionCallback";

                @Override
                public void onConnected() {
                    Log.d(classTag + " onConnected: session token " + mBrowser.getSessionToken());
                    MediaController mediaController = new MediaController(mContext,
                            mBrowser.getSessionToken());
                    // Update the MediaController
                    setCurrentMediaController(mediaController);
                }

                @Override
                public void onConnectionFailed() {
                    Log.d(classTag + " onConnectionFailed");
                }
            };

    /**
     * Update the Current MediaController.
     * As has been commented above, we need the MediaController handles to the
     * BluetoothSL4AAudioSrcMBS on Phone and BluetoothMediaBrowserService on Car to send and receive
     * media commands.
     *
     * @param controller - Controller to update with
     */
    private void setCurrentMediaController(MediaController controller) {
        Handler mainHandler = new Handler(mContext.getMainLooper());
        if (mMediaController == null && controller != null) {
            Log.d(TAG + " Setting MediaController " + controller.getTag());
            mMediaController = controller;
            mMediaController.registerCallback(mMediaCtrlCallback);
        } else if (mMediaController != null && controller != null) {
            // We have a new MediaController that we have to update to.
            if (controller.getSessionToken().equals(mMediaController.getSessionToken())
                    == false) {
                Log.d(TAG + " Changing MediaController " + controller.getTag());
                mMediaController.unregisterCallback(mMediaCtrlCallback);
                mMediaController = controller;
                mMediaController.registerCallback(mMediaCtrlCallback, mainHandler);
            }
        } else if (mMediaController != null && controller == null) {
            // Clearing the current MediaController
            Log.d(TAG + " Clearing MediaController " + mMediaController.getTag());
            mMediaController.unregisterCallback(mMediaCtrlCallback);
            mMediaController = controller;
        }
    }

    /**
     * Class method called from {@link BluetoothSL4AAudioSrcMBS} to post an Event through
     * EventFacade back to the RPC client.
     * This is dispatched from the Phone to the host (RPC Client) to acknowledge that it
     * received a playback command.
     *
     * @param playbackState PlaybackState change that is posted as an Event to the client.
     */
    public static void dispatchPlaybackStateChanged(int playbackState) {
        Bundle news = new Bundle();
        switch (playbackState) {
            case PlaybackState.STATE_PLAYING:
                mEventFacade.postEvent(EVENT_PLAY_RECEIVED, news);
                break;
            case PlaybackState.STATE_PAUSED:
                mEventFacade.postEvent(EVENT_PAUSE_RECEIVED, news);
                break;
            case PlaybackState.STATE_SKIPPING_TO_NEXT:
                mEventFacade.postEvent(EVENT_SKIP_NEXT_RECEIVED, news);
                break;
            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
                mEventFacade.postEvent(EVENT_SKIP_PREV_RECEIVED, news);
                break;
            default:
                break;
        }
    }

    /******************************RPC APIS************************************************/

    /**
     * Relevance - Phone and Car.
     * Sends the passthrough command through the currently active MediaController.
     * If there isn't one, look for the currently active sessions and just pick the first one,
     * just a fallback.
     * This function is generic enough to be used in either a Phone or the Car side, since
     * all this does is to pick the currently active Media Controller and sends a passthrough
     * command.  In the test setup, this is used to mimic sending a passthrough command from
     * Car.
     */
    @Rpc(description = "Simulate a passthrough command")
    public void bluetoothMediaPassthrough(
            @RpcParameter(name = "passthruCmd", description = "play/pause/skipFwd/skipBack")
                    String passthruCmd) {
        Log.d(TAG + "Passthrough Cmd " + passthruCmd);
        if (mMediaController == null) {
            Log.i(TAG + " Media Controller not ready - Grabbing existing one");
            ComponentName name =
                    new ComponentName(mContext.getPackageName(),
                            mSessionListener.getClass().getName());
            List<MediaController> listMC = mSessionManager.getActiveSessions(null);
            if (listMC.size() > 0) {
                if (VDBG) {
                    Log.d(TAG + " Num Sessions " + listMC.size());
                    for (int i = 0; i < listMC.size(); i++) {
                        Log.d(TAG + "Active session : " + i + ((MediaController) (listMC.get(
                                i))).getPackageName() + ((MediaController) (listMC.get(
                                i))).getTag());
                    }
                }
                mMediaController = (MediaController) listMC.get(0);
            } else {
                Log.d(TAG + " No Active Media Session to grab");
                return;
            }
        }

        switch (passthruCmd) {
            case CMD_MEDIA_PLAY:
                mMediaController.getTransportControls().play();
                break;
            case CMD_MEDIA_PAUSE:
                mMediaController.getTransportControls().pause();
                break;
            case CMD_MEDIA_SKIP_NEXT:
                mMediaController.getTransportControls().skipToNext();
                break;
            case CMD_MEDIA_SKIP_PREV:
                mMediaController.getTransportControls().skipToPrevious();
                break;
            default:
                Log.d(TAG + " Unsupported Passthrough Cmd");
                break;
        }
    }

    /**
     * Relevance - Phone and Car.
     * Returns the currently playing media's metadata.
     * Can be queried on the car and the phone in the middle of a streaming session to
     * verify they are in sync.
     *
     * @return Currently playing Media's metadata
     */
    @Rpc(description = "Gets the Metadata of currently playing Media")
    public Map<String, String> bluetoothMediaGetCurrentMediaMetaData() {
        Map<String, String> track = null;
        if (mMediaController == null) {
            Log.d(TAG + "MediaController Not set");
            return track;
        }
        MediaMetadata metadata = mMediaController.getMetadata();
        if (metadata == null) {
            Log.e("No Metadata available.");
            return track;
        }
        track = new HashMap<>();
        track.put(MEDIA_KEY_TITLE, metadata.getString(MediaMetadata.METADATA_KEY_TITLE));
        track.put(MEDIA_KEY_ALBUM, metadata.getString(MediaMetadata.METADATA_KEY_ALBUM));
        track.put(MEDIA_KEY_ARTIST, metadata.getString(MediaMetadata.METADATA_KEY_ARTIST));
        track.put(MEDIA_KEY_DURATION,
                String.valueOf(metadata.getLong(MediaMetadata.METADATA_KEY_DURATION)));
        track.put(MEDIA_KEY_NUM_TRACKS,
                String.valueOf(metadata.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS)));
        return track;
    }

    /**
     * Relevance - Phone and Car.
     * Returns the currently playing media's playback state.
     * Can be queried on the car and the phone in the middle of a streaming session to
     * verify they are in sync.
     *
     * @return Currently playing Media's playback state
     */
    @Rpc(description = "Gets the state of current playback")
    public PlaybackState bluetoothMediaGetCurrentPlaybackState() throws Exception {
        if (mMediaController == null) {
            Log.e(TAG + "MediaController not set");
            throw new Exception("MediaController not set");
        }
        PlaybackState playbackState = mMediaController.getPlaybackState();
        if (playbackState == null) {
            Log.d("No playback state available.");
            return null;
        }
        return playbackState;
    }

    /**
     * Relevance - Phone and Car
     * Returns the current active media sessions for the device. This is useful to see if a
     * Media Session we are interested in is currently active.
     * In the Bluetooth Media tests, this is indirectly used to determine if audio is being
     * played via BT.  For ex., when the Car and Phone are connected via BT and audio is being
     * streamed, BluetoothMediaBrowserService will be active on the Car side.  If the connection is
     * terminated in the middle, BluetoothMediaBrowserService will no longer be active on the
     * Carkitt, whereas BluetoothSL4AAudioSrcMBS will still be active.
     *
     * @return A list of names of the active media sessions
     */
    @Rpc(description = "Get the current active Media Sessions")
    public List<String> bluetoothMediaGetActiveMediaSessions() {
        List<MediaController> controllers = mSessionManager.getActiveSessions(null);
        List<String> sessions = new ArrayList<String>();
        for (MediaController mc : controllers) {
            sessions.add(mc.getTag());
        }
        return sessions;
    }

    /**
     * Relevance - Car Only
     * Called from the Carkitt to connect a MediaBrowser to the Bluetooth Audio App's
     * BluetoothMediaBrowserService.  The callback on successful connection gives the handle to
     * the MediaController through which we can send media commands.
     */
    @Rpc(description = "Connect a MediaBrowser to the BluetoothMediaBrowserService in the Carkitt")
    public void bluetoothMediaConnectToCarMBS() {
        ComponentName compName;
        // Create a MediaBrowser to connect to the BluetoothMediaBrowserService
        if (mBrowser == null) {
            compName = new ComponentName(BLUETOOTH_PKG_NAME, BROWSER_SERVICE_NAME);
            // Note - MediaBrowser connect needs to be done on the Main Thread's handler,
            // otherwise we never get the ServiceConnected callback.
            Runnable createAndConnectMediaBrowser = new Runnable() {
                @Override
                public void run() {
                    mBrowser = new MediaBrowser(mContext, compName, mBrowserConnectionCallback,
                            null);
                    if (mBrowser != null) {
                        Log.d(TAG + " Connecting to MBS");
                        mBrowser.connect();
                    } else {
                        Log.d(TAG + " Failed to create a MediaBrowser");
                    }
                }
            };

            Handler mainHandler = new Handler(mContext.getMainLooper());
            mainHandler.post(createAndConnectMediaBrowser);
        } //mBrowser
    }

    /**
     * Relevance - Phone Only
     * Start the BluetoothSL4AAudioSrcMBS on the Phone so the media commands coming in
     * via Bluetooth AVRCP can be intercepted by the SL4A test
     */
    @Rpc(description = "Start the BluetoothSL4AAudioSrcMBS on Phone.")
    public void bluetoothMediaPhoneSL4AMBSStart() {
        Log.d(TAG + "Starting BluetoothSL4AAudioSrcMBS");
        // Start the Avrcp Media Browser service.  Starting it sets it to active.
        Intent startIntent = new Intent(mContext, BluetoothSL4AAudioSrcMBS.class);
        mContext.startService(startIntent);
    }

    /**
     * Relevance - Phone Only
     * Stop the BluetoothSL4AAudioSrcMBS
     */
    @Rpc(description = "Stop the BluetoothSL4AAudioSrcMBS running on Phone.")
    public void bluetoothMediaPhoneSL4AMBSStop() {
        Log.d(TAG + "Stopping BluetoothSL4AAudioSrcMBS");
        // Stop the Avrcp Media Browser service.
        Intent stopIntent = new Intent(mContext, BluetoothSL4AAudioSrcMBS.class);
        mContext.stopService(stopIntent);
    }

    /**
     * Relevance - Phone only
     * This is used to simulate play/pause/skip media commands on the Phone directly, as against
     * receiving these commands via AVRCP from the Carkitt.
     * This function talks to the BluetoothSL4AAudioSrcMBS to simulate the media command.
     * An example test where this would be useful - Play music on Phone that is not connected
     * on bluetooth and connect in the middle to verify if music is steamed to the other end.
     *
     * @param command - Media command to simulate on the Phone
     */
    @Rpc(description = "Media Commands on the Phone's BluetoothAvrcpMBS.")
    public void bluetoothMediaHandleMediaCommandOnPhone(String command) {
        BluetoothSL4AAudioSrcMBS mbs =
                BluetoothSL4AAudioSrcMBS.getAvrcpMediaBrowserService();
        if (mbs != null) {
            mbs.handleMediaCommand(command);
        } else {
            Log.e(TAG + " No BluetoothSL4AAudioSrcMBS running on the device");
        }
    }


    @Override
    public void shutdown() {
        setCurrentMediaController(null);
    }
}
