/*
 * Copyright 2018 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.support.mediarouter.media;

import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_DATA_ROUTE_ID;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .CLIENT_DATA_ROUTE_LIBRARY_GROUP;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .CLIENT_DATA_UNSELECT_REASON;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_DATA_VOLUME;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .CLIENT_MSG_CREATE_ROUTE_CONTROLLER;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_REGISTER;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .CLIENT_MSG_RELEASE_ROUTE_CONTROLLER;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .CLIENT_MSG_ROUTE_CONTROL_REQUEST;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_SELECT_ROUTE;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .CLIENT_MSG_SET_DISCOVERY_REQUEST;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .CLIENT_MSG_SET_ROUTE_VOLUME;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_UNREGISTER;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .CLIENT_MSG_UNSELECT_ROUTE;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .CLIENT_MSG_UPDATE_ROUTE_VOLUME;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_VERSION_CURRENT;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_DATA_ERROR;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .SERVICE_MSG_CONTROL_REQUEST_FAILED;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .SERVICE_MSG_DESCRIPTOR_CHANGED;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .SERVICE_MSG_GENERIC_FAILURE;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
        .SERVICE_MSG_GENERIC_SUCCESS;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_REGISTERED;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_VERSION_1;
import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.isValidRemoteMessenger;

import android.annotation.NonNull;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.DeadObjectException;
import android.os.Handler;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
import android.util.SparseArray;

import com.android.support.mediarouter.media.MediaRouter.ControlRequestCallback;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

/**
 * Maintains a connection to a particular media route provider service.
 */
final class RegisteredMediaRouteProvider extends MediaRouteProvider
        implements ServiceConnection {
    static final String TAG = "MediaRouteProviderProxy";  // max. 23 chars
    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private final ComponentName mComponentName;
    final PrivateHandler mPrivateHandler;
    private final ArrayList<Controller> mControllers = new ArrayList<Controller>();

    private boolean mStarted;
    private boolean mBound;
    private Connection mActiveConnection;
    private boolean mConnectionReady;

    public RegisteredMediaRouteProvider(Context context, ComponentName componentName) {
        super(context, new ProviderMetadata(componentName));

        mComponentName = componentName;
        mPrivateHandler = new PrivateHandler();
    }

    @Override
    public RouteController onCreateRouteController(@NonNull String routeId) {
        if (routeId == null) {
            throw new IllegalArgumentException("routeId cannot be null");
        }
        return createRouteController(routeId, null);
    }

    @Override
    public RouteController onCreateRouteController(
            @NonNull String routeId, @NonNull String routeGroupId) {
        if (routeId == null) {
            throw new IllegalArgumentException("routeId cannot be null");
        }
        if (routeGroupId == null) {
            throw new IllegalArgumentException("routeGroupId cannot be null");
        }
        return createRouteController(routeId, routeGroupId);
    }

    @Override
    public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
        if (mConnectionReady) {
            mActiveConnection.setDiscoveryRequest(request);
        }
        updateBinding();
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        if (DEBUG) {
            Log.d(TAG, this + ": Connected");
        }

        if (mBound) {
            disconnect();

            Messenger messenger = (service != null ? new Messenger(service) : null);
            if (isValidRemoteMessenger(messenger)) {
                Connection connection = new Connection(messenger);
                if (connection.register()) {
                    mActiveConnection = connection;
                } else {
                    if (DEBUG) {
                        Log.d(TAG, this + ": Registration failed");
                    }
                }
            } else {
                Log.e(TAG, this + ": Service returned invalid messenger binder");
            }
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        if (DEBUG) {
            Log.d(TAG, this + ": Service disconnected");
        }
        disconnect();
    }

    @Override
    public String toString() {
        return "Service connection " + mComponentName.flattenToShortString();
    }

    public boolean hasComponentName(String packageName, String className) {
        return mComponentName.getPackageName().equals(packageName)
                && mComponentName.getClassName().equals(className);
    }

    public void start() {
        if (!mStarted) {
            if (DEBUG) {
                Log.d(TAG, this + ": Starting");
            }

            mStarted = true;
            updateBinding();
        }
    }

    public void stop() {
        if (mStarted) {
            if (DEBUG) {
                Log.d(TAG, this + ": Stopping");
            }

            mStarted = false;
            updateBinding();
        }
    }

    public void rebindIfDisconnected() {
        if (mActiveConnection == null && shouldBind()) {
            unbind();
            bind();
        }
    }

    private void updateBinding() {
        if (shouldBind()) {
            bind();
        } else {
            unbind();
        }
    }

    private boolean shouldBind() {
        if (mStarted) {
            // Bind whenever there is a discovery request.
            if (getDiscoveryRequest() != null) {
                return true;
            }

            // Bind whenever the application has an active route controller.
            // This means that one of this provider's routes is selected.
            if (!mControllers.isEmpty()) {
                return true;
            }
        }
        return false;
    }

    private void bind() {
        if (!mBound) {
            if (DEBUG) {
                Log.d(TAG, this + ": Binding");
            }

            Intent service = new Intent(MediaRouteProviderProtocol.SERVICE_INTERFACE);
            service.setComponent(mComponentName);
            try {
                mBound = getContext().bindService(service, this, Context.BIND_AUTO_CREATE);
                if (!mBound && DEBUG) {
                    Log.d(TAG, this + ": Bind failed");
                }
            } catch (SecurityException ex) {
                if (DEBUG) {
                    Log.d(TAG, this + ": Bind failed", ex);
                }
            }
        }
    }

    private void unbind() {
        if (mBound) {
            if (DEBUG) {
                Log.d(TAG, this + ": Unbinding");
            }

            mBound = false;
            disconnect();
            getContext().unbindService(this);
        }
    }

    private RouteController createRouteController(String routeId, String routeGroupId) {
        MediaRouteProviderDescriptor descriptor = getDescriptor();
        if (descriptor != null) {
            List<MediaRouteDescriptor> routes = descriptor.getRoutes();
            final int count = routes.size();
            for (int i = 0; i < count; i++) {
                final MediaRouteDescriptor route = routes.get(i);
                if (route.getId().equals(routeId)) {
                    Controller controller = new Controller(routeId, routeGroupId);
                    mControllers.add(controller);
                    if (mConnectionReady) {
                        controller.attachConnection(mActiveConnection);
                    }
                    updateBinding();
                    return controller;
                }
            }
        }
        return null;
    }

    void onConnectionReady(Connection connection) {
        if (mActiveConnection == connection) {
            mConnectionReady = true;
            attachControllersToConnection();

            MediaRouteDiscoveryRequest request = getDiscoveryRequest();
            if (request != null) {
                mActiveConnection.setDiscoveryRequest(request);
            }
        }
    }

    void onConnectionDied(Connection connection) {
        if (mActiveConnection == connection) {
            if (DEBUG) {
                Log.d(TAG, this + ": Service connection died");
            }
            disconnect();
        }
    }

    void onConnectionError(Connection connection, String error) {
        if (mActiveConnection == connection) {
            if (DEBUG) {
                Log.d(TAG, this + ": Service connection error - " + error);
            }
            unbind();
        }
    }

    void onConnectionDescriptorChanged(Connection connection,
            MediaRouteProviderDescriptor descriptor) {
        if (mActiveConnection == connection) {
            if (DEBUG) {
                Log.d(TAG, this + ": Descriptor changed, descriptor=" + descriptor);
            }
            setDescriptor(descriptor);
        }
    }

    private void disconnect() {
        if (mActiveConnection != null) {
            setDescriptor(null);
            mConnectionReady = false;
            detachControllersFromConnection();
            mActiveConnection.dispose();
            mActiveConnection = null;
        }
    }

    void onControllerReleased(Controller controller) {
        mControllers.remove(controller);
        controller.detachConnection();
        updateBinding();
    }

    private void attachControllersToConnection() {
        int count = mControllers.size();
        for (int i = 0; i < count; i++) {
            mControllers.get(i).attachConnection(mActiveConnection);
        }
    }

    private void detachControllersFromConnection() {
        int count = mControllers.size();
        for (int i = 0; i < count; i++) {
            mControllers.get(i).detachConnection();
        }
    }

    private final class Controller extends RouteController {
        private final String mRouteId;
        private final String mRouteGroupId;

        private boolean mSelected;
        private int mPendingSetVolume = -1;
        private int mPendingUpdateVolumeDelta;

        private Connection mConnection;
        private int mControllerId;

        public Controller(String routeId, String routeGroupId) {
            mRouteId = routeId;
            mRouteGroupId = routeGroupId;
        }

        public void attachConnection(Connection connection) {
            mConnection = connection;
            mControllerId = connection.createRouteController(mRouteId, mRouteGroupId);
            if (mSelected) {
                connection.selectRoute(mControllerId);
                if (mPendingSetVolume >= 0) {
                    connection.setVolume(mControllerId, mPendingSetVolume);
                    mPendingSetVolume = -1;
                }
                if (mPendingUpdateVolumeDelta != 0) {
                    connection.updateVolume(mControllerId, mPendingUpdateVolumeDelta);
                    mPendingUpdateVolumeDelta = 0;
                }
            }
        }

        public void detachConnection() {
            if (mConnection != null) {
                mConnection.releaseRouteController(mControllerId);
                mConnection = null;
                mControllerId = 0;
            }
        }

        @Override
        public void onRelease() {
            onControllerReleased(this);
        }

        @Override
        public void onSelect() {
            mSelected = true;
            if (mConnection != null) {
                mConnection.selectRoute(mControllerId);
            }
        }

        @Override
        public void onUnselect() {
            onUnselect(MediaRouter.UNSELECT_REASON_UNKNOWN);
        }

        @Override
        public void onUnselect(int reason) {
            mSelected = false;
            if (mConnection != null) {
                mConnection.unselectRoute(mControllerId, reason);
            }
        }

        @Override
        public void onSetVolume(int volume) {
            if (mConnection != null) {
                mConnection.setVolume(mControllerId, volume);
            } else {
                mPendingSetVolume = volume;
                mPendingUpdateVolumeDelta = 0;
            }
        }

        @Override
        public void onUpdateVolume(int delta) {
            if (mConnection != null) {
                mConnection.updateVolume(mControllerId, delta);
            } else {
                mPendingUpdateVolumeDelta += delta;
            }
        }

        @Override
        public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
            if (mConnection != null) {
                return mConnection.sendControlRequest(mControllerId, intent, callback);
            }
            return false;
        }
    }

    private final class Connection implements DeathRecipient {
        private final Messenger mServiceMessenger;
        private final ReceiveHandler mReceiveHandler;
        private final Messenger mReceiveMessenger;

        private int mNextRequestId = 1;
        private int mNextControllerId = 1;
        private int mServiceVersion; // non-zero when registration complete

        private int mPendingRegisterRequestId;
        private final SparseArray<ControlRequestCallback> mPendingCallbacks =
                new SparseArray<ControlRequestCallback>();

        public Connection(Messenger serviceMessenger) {
            mServiceMessenger = serviceMessenger;
            mReceiveHandler = new ReceiveHandler(this);
            mReceiveMessenger = new Messenger(mReceiveHandler);
        }

        public boolean register() {
            mPendingRegisterRequestId = mNextRequestId++;
            if (!sendRequest(CLIENT_MSG_REGISTER,
                    mPendingRegisterRequestId,
                    CLIENT_VERSION_CURRENT, null, null)) {
                return false;
            }

            try {
                mServiceMessenger.getBinder().linkToDeath(this, 0);
                return true;
            } catch (RemoteException ex) {
                binderDied();
            }
            return false;
        }

        public void dispose() {
            sendRequest(CLIENT_MSG_UNREGISTER, 0, 0, null, null);
            mReceiveHandler.dispose();
            mServiceMessenger.getBinder().unlinkToDeath(this, 0);

            mPrivateHandler.post(new Runnable() {
                @Override
                public void run() {
                    failPendingCallbacks();
                }
            });
        }

        void failPendingCallbacks() {
            int count = 0;
            for (int i = 0; i < mPendingCallbacks.size(); i++) {
                mPendingCallbacks.valueAt(i).onError(null, null);
            }
            mPendingCallbacks.clear();
        }

        public boolean onGenericFailure(int requestId) {
            if (requestId == mPendingRegisterRequestId) {
                mPendingRegisterRequestId = 0;
                onConnectionError(this, "Registration failed");
            }
            ControlRequestCallback callback = mPendingCallbacks.get(requestId);
            if (callback != null) {
                mPendingCallbacks.remove(requestId);
                callback.onError(null, null);
            }
            return true;
        }

        public boolean onGenericSuccess(int requestId) {
            return true;
        }

        public boolean onRegistered(int requestId, int serviceVersion,
                Bundle descriptorBundle) {
            if (mServiceVersion == 0
                    && requestId == mPendingRegisterRequestId
                    && serviceVersion >= SERVICE_VERSION_1) {
                mPendingRegisterRequestId = 0;
                mServiceVersion = serviceVersion;
                onConnectionDescriptorChanged(this,
                        MediaRouteProviderDescriptor.fromBundle(descriptorBundle));
                onConnectionReady(this);
                return true;
            }
            return false;
        }

        public boolean onDescriptorChanged(Bundle descriptorBundle) {
            if (mServiceVersion != 0) {
                onConnectionDescriptorChanged(this,
                        MediaRouteProviderDescriptor.fromBundle(descriptorBundle));
                return true;
            }
            return false;
        }

        public boolean onControlRequestSucceeded(int requestId, Bundle data) {
            ControlRequestCallback callback = mPendingCallbacks.get(requestId);
            if (callback != null) {
                mPendingCallbacks.remove(requestId);
                callback.onResult(data);
                return true;
            }
            return false;
        }

        public boolean onControlRequestFailed(int requestId, String error, Bundle data) {
            ControlRequestCallback callback = mPendingCallbacks.get(requestId);
            if (callback != null) {
                mPendingCallbacks.remove(requestId);
                callback.onError(error, data);
                return true;
            }
            return false;
        }

        @Override
        public void binderDied() {
            mPrivateHandler.post(new Runnable() {
                @Override
                public void run() {
                    onConnectionDied(Connection.this);
                }
            });
        }

        public int createRouteController(String routeId, String routeGroupId) {
            int controllerId = mNextControllerId++;
            Bundle data = new Bundle();
            data.putString(CLIENT_DATA_ROUTE_ID, routeId);
            data.putString(CLIENT_DATA_ROUTE_LIBRARY_GROUP, routeGroupId);
            sendRequest(CLIENT_MSG_CREATE_ROUTE_CONTROLLER,
                    mNextRequestId++, controllerId, null, data);
            return controllerId;
        }

        public void releaseRouteController(int controllerId) {
            sendRequest(CLIENT_MSG_RELEASE_ROUTE_CONTROLLER,
                    mNextRequestId++, controllerId, null, null);
        }

        public void selectRoute(int controllerId) {
            sendRequest(CLIENT_MSG_SELECT_ROUTE,
                    mNextRequestId++, controllerId, null, null);
        }

        public void unselectRoute(int controllerId, int reason) {
            Bundle extras = new Bundle();
            extras.putInt(CLIENT_DATA_UNSELECT_REASON, reason);
            sendRequest(CLIENT_MSG_UNSELECT_ROUTE,
                    mNextRequestId++, controllerId, null, extras);
        }

        public void setVolume(int controllerId, int volume) {
            Bundle data = new Bundle();
            data.putInt(CLIENT_DATA_VOLUME, volume);
            sendRequest(CLIENT_MSG_SET_ROUTE_VOLUME,
                    mNextRequestId++, controllerId, null, data);
        }

        public void updateVolume(int controllerId, int delta) {
            Bundle data = new Bundle();
            data.putInt(CLIENT_DATA_VOLUME, delta);
            sendRequest(CLIENT_MSG_UPDATE_ROUTE_VOLUME,
                    mNextRequestId++, controllerId, null, data);
        }

        public boolean sendControlRequest(int controllerId, Intent intent,
                ControlRequestCallback callback) {
            int requestId = mNextRequestId++;
            if (sendRequest(CLIENT_MSG_ROUTE_CONTROL_REQUEST,
                    requestId, controllerId, intent, null)) {
                if (callback != null) {
                    mPendingCallbacks.put(requestId, callback);
                }
                return true;
            }
            return false;
        }

        public void setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
            sendRequest(CLIENT_MSG_SET_DISCOVERY_REQUEST,
                    mNextRequestId++, 0, request != null ? request.asBundle() : null, null);
        }

        private boolean sendRequest(int what, int requestId, int arg, Object obj, Bundle data) {
            Message msg = Message.obtain();
            msg.what = what;
            msg.arg1 = requestId;
            msg.arg2 = arg;
            msg.obj = obj;
            msg.setData(data);
            msg.replyTo = mReceiveMessenger;
            try {
                mServiceMessenger.send(msg);
                return true;
            } catch (DeadObjectException ex) {
                // The service died.
            } catch (RemoteException ex) {
                if (what != CLIENT_MSG_UNREGISTER) {
                    Log.e(TAG, "Could not send message to service.", ex);
                }
            }
            return false;
        }
    }

    private static final class PrivateHandler extends Handler {
        PrivateHandler() {
        }
    }

    /**
     * Handler that receives messages from the server.
     * <p>
     * This inner class is static and only retains a weak reference to the connection
     * to prevent the client from being leaked in case the service is holding an
     * active reference to the client's messenger.
     * </p><p>
     * This handler should not be used to handle any messages other than those
     * that come from the service.
     * </p>
     */
    private static final class ReceiveHandler extends Handler {
        private final WeakReference<Connection> mConnectionRef;

        public ReceiveHandler(Connection connection) {
            mConnectionRef = new WeakReference<Connection>(connection);
        }

        public void dispose() {
            mConnectionRef.clear();
        }

        @Override
        public void handleMessage(Message msg) {
            Connection connection = mConnectionRef.get();
            if (connection != null) {
                final int what = msg.what;
                final int requestId = msg.arg1;
                final int arg = msg.arg2;
                final Object obj = msg.obj;
                final Bundle data = msg.peekData();
                if (!processMessage(connection, what, requestId, arg, obj, data)) {
                    if (DEBUG) {
                        Log.d(TAG, "Unhandled message from server: " + msg);
                    }
                }
            }
        }

        private boolean processMessage(Connection connection,
                int what, int requestId, int arg, Object obj, Bundle data) {
            switch (what) {
                case SERVICE_MSG_GENERIC_FAILURE:
                    connection.onGenericFailure(requestId);
                    return true;

                case SERVICE_MSG_GENERIC_SUCCESS:
                    connection.onGenericSuccess(requestId);
                    return true;

                case SERVICE_MSG_REGISTERED:
                    if (obj == null || obj instanceof Bundle) {
                        return connection.onRegistered(requestId, arg, (Bundle)obj);
                    }
                    break;

                case SERVICE_MSG_DESCRIPTOR_CHANGED:
                    if (obj == null || obj instanceof Bundle) {
                        return connection.onDescriptorChanged((Bundle)obj);
                    }
                    break;

                case SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED:
                    if (obj == null || obj instanceof Bundle) {
                        return connection.onControlRequestSucceeded(
                                requestId, (Bundle)obj);
                    }
                    break;

                case SERVICE_MSG_CONTROL_REQUEST_FAILED:
                    if (obj == null || obj instanceof Bundle) {
                        String error = (data == null ? null :
                                data.getString(SERVICE_DATA_ERROR));
                        return connection.onControlRequestFailed(
                                requestId, error, (Bundle)obj);
                    }
                    break;
            }
            return false;
        }
    }
}
