/*
 * 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 android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Message;
import android.support.v4.util.ObjectsCompat;

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

/**
 * Media route providers are used to publish additional media routes for
 * use within an application.  Media route providers may also be declared
 * as a service to publish additional media routes to all applications
 * in the system.
 * <p>
 * The purpose of a media route provider is to discover media routes that satisfy
 * the criteria specified by the current {@link MediaRouteDiscoveryRequest} and publish a
 * {@link MediaRouteProviderDescriptor} with information about each route by calling
 * {@link #setDescriptor} to notify the currently registered {@link Callback}.
 * </p><p>
 * The provider should watch for changes to the discovery request by implementing
 * {@link #onDiscoveryRequestChanged} and updating the set of routes that it is
 * attempting to discover.  It should also handle route control requests such
 * as volume changes or {@link MediaControlIntent media control intents}
 * by implementing {@link #onCreateRouteController} to return a {@link RouteController}
 * for a particular route.
 * </p><p>
 * A media route provider may be used privately within the scope of a single
 * application process by calling {@link MediaRouter#addProvider MediaRouter.addProvider}
 * to add it to the local {@link MediaRouter}.  A media route provider may also be made
 * available globally to all applications by registering a {@link MediaRouteProviderService}
 * in the provider's manifest.  When the media route provider is registered
 * as a service, all applications that use the media router API will be able to
 * discover and used the provider's routes without having to install anything else.
 * </p><p>
 * This object must only be accessed on the main thread.
 * </p>
 */
public abstract class MediaRouteProvider {
    static final int MSG_DELIVER_DESCRIPTOR_CHANGED = 1;
    static final int MSG_DELIVER_DISCOVERY_REQUEST_CHANGED = 2;

    private final Context mContext;
    private final ProviderMetadata mMetadata;
    private final ProviderHandler mHandler = new ProviderHandler();

    private Callback mCallback;

    private MediaRouteDiscoveryRequest mDiscoveryRequest;
    private boolean mPendingDiscoveryRequestChange;

    private MediaRouteProviderDescriptor mDescriptor;
    private boolean mPendingDescriptorChange;

    /**
     * Creates a media route provider.
     *
     * @param context The context.
     */
    public MediaRouteProvider(@NonNull Context context) {
        this(context, null);
    }

    MediaRouteProvider(Context context, ProviderMetadata metadata) {
        if (context == null) {
            throw new IllegalArgumentException("context must not be null");
        }

        mContext = context;
        if (metadata == null) {
            mMetadata = new ProviderMetadata(new ComponentName(context, getClass()));
        } else {
            mMetadata = metadata;
        }
    }

    /**
     * Gets the context of the media route provider.
     */
    public final Context getContext() {
        return mContext;
    }

    /**
     * Gets the provider's handler which is associated with the main thread.
     */
    public final Handler getHandler() {
        return mHandler;
    }

    /**
     * Gets some metadata about the provider's implementation.
     */
    public final ProviderMetadata getMetadata() {
        return mMetadata;
    }

    /**
     * Sets a callback to invoke when the provider's descriptor changes.
     *
     * @param callback The callback to use, or null if none.
     */
    public final void setCallback(@Nullable Callback callback) {
        MediaRouter.checkCallingThread();
        mCallback = callback;
    }

    /**
     * Gets the current discovery request which informs the provider about the
     * kinds of routes to discover and whether to perform active scanning.
     *
     * @return The current discovery request, or null if no discovery is needed at this time.
     *
     * @see #onDiscoveryRequestChanged
     */
    @Nullable
    public final MediaRouteDiscoveryRequest getDiscoveryRequest() {
        return mDiscoveryRequest;
    }

    /**
     * Sets a discovery request to inform the provider about the kinds of
     * routes that its clients would like to discover and whether to perform active scanning.
     *
     * @param request The discovery request, or null if no discovery is needed at this time.
     *
     * @see #onDiscoveryRequestChanged
     */
    public final void setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
        MediaRouter.checkCallingThread();

        if (ObjectsCompat.equals(mDiscoveryRequest, request)) {
            return;
        }

        mDiscoveryRequest = request;
        if (!mPendingDiscoveryRequestChange) {
            mPendingDiscoveryRequestChange = true;
            mHandler.sendEmptyMessage(MSG_DELIVER_DISCOVERY_REQUEST_CHANGED);
        }
    }

    void deliverDiscoveryRequestChanged() {
        mPendingDiscoveryRequestChange = false;
        onDiscoveryRequestChanged(mDiscoveryRequest);
    }

    /**
     * Called by the media router when the {@link MediaRouteDiscoveryRequest discovery request}
     * has changed.
     * <p>
     * Whenever an applications calls {@link MediaRouter#addCallback} to register
     * a callback, it also provides a selector to specify the kinds of routes that
     * it is interested in.  The media router combines all of these selectors together
     * to generate a {@link MediaRouteDiscoveryRequest} and notifies each provider when a change
     * occurs by calling {@link #setDiscoveryRequest} which posts a message to invoke
     * this method asynchronously.
     * </p><p>
     * The provider should examine the {@link MediaControlIntent media control categories}
     * in the discovery request's {@link MediaRouteSelector selector} to determine what
     * kinds of routes it should try to discover and whether it should perform active
     * or passive scans.  In many cases, the provider may be able to save power by
     * determining that the selector does not contain any categories that it supports
     * and it can therefore avoid performing any scans at all.
     * </p>
     *
     * @param request The new discovery request, or null if no discovery is needed at this time.
     *
     * @see MediaRouter#addCallback
     */
    public void onDiscoveryRequestChanged(@Nullable MediaRouteDiscoveryRequest request) {
    }

    /**
     * Gets the provider's descriptor.
     * <p>
     * The descriptor describes the state of the media route provider and
     * the routes that it publishes.  Watch for changes to the descriptor
     * by registering a {@link Callback callback} with {@link #setCallback}.
     * </p>
     *
     * @return The media route provider descriptor, or null if none.
     *
     * @see Callback#onDescriptorChanged
     */
    @Nullable
    public final MediaRouteProviderDescriptor getDescriptor() {
        return mDescriptor;
    }

    /**
     * Sets the provider's descriptor.
     * <p>
     * The provider must call this method to notify the currently registered
     * {@link Callback callback} about the change to the provider's descriptor.
     * </p>
     *
     * @param descriptor The updated route provider descriptor, or null if none.
     *
     * @see Callback#onDescriptorChanged
     */
    public final void setDescriptor(@Nullable MediaRouteProviderDescriptor descriptor) {
        MediaRouter.checkCallingThread();

        if (mDescriptor != descriptor) {
            mDescriptor = descriptor;
            if (!mPendingDescriptorChange) {
                mPendingDescriptorChange = true;
                mHandler.sendEmptyMessage(MSG_DELIVER_DESCRIPTOR_CHANGED);
            }
        }
    }

    void deliverDescriptorChanged() {
        mPendingDescriptorChange = false;

        if (mCallback != null) {
            mCallback.onDescriptorChanged(this, mDescriptor);
        }
    }

    /**
     * Called by the media router to obtain a route controller for a particular route.
     * <p>
     * The media router will invoke the {@link RouteController#onRelease} method of the route
     * controller when it is no longer needed to allow it to free its resources.
     * </p>
     *
     * @param routeId The unique id of the route.
     * @return The route controller.  Returns null if there is no such route or if the route
     * cannot be controlled using the route controller interface.
     */
    @Nullable
    public RouteController onCreateRouteController(@NonNull String routeId) {
        if (routeId == null) {
            throw new IllegalArgumentException("routeId cannot be null");
        }
        return null;
    }

    /**
     * Called by the media router to obtain a route controller for a particular route which is a
     * member of {@link MediaRouter.RouteGroup}.
     * <p>
     * The media router will invoke the {@link RouteController#onRelease} method of the route
     * controller when it is no longer needed to allow it to free its resources.
     * </p>
     *
     * @param routeId The unique id of the member route.
     * @param routeGroupId The unique id of the route group.
     * @return The route controller.  Returns null if there is no such route or if the route
     * cannot be controlled using the route controller interface.
     * @hide
     */
    // @RestrictTo(LIBRARY_GROUP)
    @Nullable
    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 onCreateRouteController(routeId);
    }

    /**
     * Describes properties of the route provider's implementation.
     * <p>
     * This object is immutable once created.
     * </p>
     */
    public static final class ProviderMetadata {
        private final ComponentName mComponentName;

        ProviderMetadata(ComponentName componentName) {
            if (componentName == null) {
                throw new IllegalArgumentException("componentName must not be null");
            }
            mComponentName = componentName;
        }

        /**
         * Gets the provider's package name.
         */
        public String getPackageName() {
            return mComponentName.getPackageName();
        }

        /**
         * Gets the provider's component name.
         */
        public ComponentName getComponentName() {
            return mComponentName;
        }

        @Override
        public String toString() {
            return "ProviderMetadata{ componentName="
                    + mComponentName.flattenToShortString() + " }";
        }
    }

    /**
     * Provides control over a particular route.
     * <p>
     * The media router obtains a route controller for a route whenever it needs
     * to control a route.  When a route is selected, the media router invokes
     * the {@link #onSelect} method of its route controller.  While selected,
     * the media router may call other methods of the route controller to
     * request that it perform certain actions to the route.  When a route is
     * unselected, the media router invokes the {@link #onUnselect} method of its
     * route controller.  When the media route no longer needs the route controller
     * it will invoke the {@link #onRelease} method to allow the route controller
     * to free its resources.
     * </p><p>
     * There may be multiple route controllers simultaneously active for the
     * same route.  Each route controller will be released separately.
     * </p><p>
     * All operations on the route controller are asynchronous and
     * results are communicated via callbacks.
     * </p>
     */
    public static abstract class RouteController {
        /**
         * Releases the route controller, allowing it to free its resources.
         */
        public void onRelease() {
        }

        /**
         * Selects the route.
         */
        public void onSelect() {
        }

        /**
         * Unselects the route.
         */
        public void onUnselect() {
        }

        /**
         * Unselects the route and provides a reason. The default implementation
         * calls {@link #onUnselect()}.
         * <p>
         * The reason provided will be one of the following:
         * <ul>
         * <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li>
         * <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li>
         * <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li>
         * <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li>
         * </ul>
         *
         * @param reason The reason for unselecting the route.
         */
        public void onUnselect(int reason) {
            onUnselect();
        }

        /**
         * Requests to set the volume of the route.
         *
         * @param volume The new volume value between 0 and {@link MediaRouteDescriptor#getVolumeMax}.
         */
        public void onSetVolume(int volume) {
        }

        /**
         * Requests an incremental volume update for the route.
         *
         * @param delta The delta to add to the current volume.
         */
        public void onUpdateVolume(int delta) {
        }

        /**
         * Performs a {@link MediaControlIntent media control} request
         * asynchronously on behalf of the route.
         *
         * @param intent A {@link MediaControlIntent media control intent}.
         * @param callback A {@link ControlRequestCallback} to invoke with the result
         * of the request, or null if no result is required.
         * @return True if the controller intends to handle the request and will
         * invoke the callback when finished.  False if the controller will not
         * handle the request and will not invoke the callback.
         *
         * @see MediaControlIntent
         */
        public boolean onControlRequest(Intent intent, @Nullable ControlRequestCallback callback) {
            return false;
        }
    }

    /**
     * Callback which is invoked when route information becomes available or changes.
     */
    public static abstract class Callback {
        /**
         * Called when information about a route provider and its routes changes.
         *
         * @param provider The media route provider that changed, never null.
         * @param descriptor The new media route provider descriptor, or null if none.
         */
        public void onDescriptorChanged(@NonNull MediaRouteProvider provider,
                @Nullable MediaRouteProviderDescriptor descriptor) {
        }
    }

    private final class ProviderHandler extends Handler {
        ProviderHandler() {
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_DELIVER_DESCRIPTOR_CHANGED:
                    deliverDescriptorChanged();
                    break;
                case MSG_DELIVER_DISCOVERY_REQUEST_CHANGED:
                    deliverDiscoveryRequestChanged();
                    break;
            }
        }
    }
}
