/*
 * Copyright (C) 2019 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.settingslib.volume;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.media.IRemoteVolumeController;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaController.PlaybackInfo;
import android.media.session.MediaSession.QueueItem;
import android.media.session.MediaSession.Token;
import android.media.session.MediaSessionManager;
import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.util.Log;

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * Convenience client for all media session updates.  Provides a callback interface for events
 * related to remote media sessions.
 */
public class MediaSessions {
    private static final String TAG = Util.logTag(MediaSessions.class);

    private static final boolean USE_SERVICE_LABEL = false;

    private final Context mContext;
    private final H mHandler;
    private final MediaSessionManager mMgr;
    private final Map<Token, MediaControllerRecord> mRecords = new HashMap<>();
    private final Callbacks mCallbacks;

    private boolean mInit;

    public MediaSessions(Context context, Looper looper, Callbacks callbacks) {
        mContext = context;
        mHandler = new H(looper);
        mMgr = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
        mCallbacks = callbacks;
    }

    /**
     * Dump to {@code writer}
     */
    public void dump(PrintWriter writer) {
        writer.println(getClass().getSimpleName() + " state:");
        writer.print("  mInit: ");
        writer.println(mInit);
        writer.print("  mRecords.size: ");
        writer.println(mRecords.size());
        int i = 0;
        for (MediaControllerRecord r : mRecords.values()) {
            dump(++i, writer, r.controller);
        }
    }

    /**
     * init MediaSessions
     */
    public void init() {
        if (D.BUG) Log.d(TAG, "init");
        // will throw if no permission
        mMgr.addOnActiveSessionsChangedListener(mSessionsListener, null, mHandler);
        mInit = true;
        postUpdateSessions();
        mMgr.registerRemoteVolumeController(mRvc);
    }

    protected void postUpdateSessions() {
        if (!mInit) return;
        mHandler.sendEmptyMessage(H.UPDATE_SESSIONS);
    }

    /**
     * Destroy MediaSessions
     */
    public void destroy() {
        if (D.BUG) Log.d(TAG, "destroy");
        mInit = false;
        mMgr.removeOnActiveSessionsChangedListener(mSessionsListener);
        mMgr.unregisterRemoteVolumeController(mRvc);
    }

    /**
     * Set volume {@code level} to remote media {@code token}
     */
    public void setVolume(Token token, int level) {
        final MediaControllerRecord r = mRecords.get(token);
        if (r == null) {
            Log.w(TAG, "setVolume: No record found for token " + token);
            return;
        }
        if (D.BUG) Log.d(TAG, "Setting level to " + level);
        r.controller.setVolumeTo(level, 0);
    }

    private void onRemoteVolumeChangedH(Token sessionToken, int flags) {
        final MediaController controller = new MediaController(mContext, sessionToken);
        if (D.BUG) {
            Log.d(TAG, "remoteVolumeChangedH " + controller.getPackageName() + " "
                    + Util.audioManagerFlagsToString(flags));
        }
        final Token token = controller.getSessionToken();
        mCallbacks.onRemoteVolumeChanged(token, flags);
    }

    private void onUpdateRemoteControllerH(Token sessionToken) {
        final MediaController controller =
                sessionToken != null ? new MediaController(mContext, sessionToken) : null;
        final String pkg = controller != null ? controller.getPackageName() : null;
        if (D.BUG) Log.d(TAG, "updateRemoteControllerH " + pkg);
        // this may be our only indication that a remote session is changed, refresh
        postUpdateSessions();
    }

    protected void onActiveSessionsUpdatedH(List<MediaController> controllers) {
        if (D.BUG) Log.d(TAG, "onActiveSessionsUpdatedH n=" + controllers.size());
        final Set<Token> toRemove = new HashSet<Token>(mRecords.keySet());
        for (MediaController controller : controllers) {
            final Token token = controller.getSessionToken();
            final PlaybackInfo pi = controller.getPlaybackInfo();
            toRemove.remove(token);
            if (!mRecords.containsKey(token)) {
                final MediaControllerRecord r = new MediaControllerRecord(controller);
                r.name = getControllerName(controller);
                mRecords.put(token, r);
                controller.registerCallback(r, mHandler);
            }
            final MediaControllerRecord r = mRecords.get(token);
            final boolean remote = isRemote(pi);
            if (remote) {
                updateRemoteH(token, r.name, pi);
                r.sentRemote = true;
            }
        }
        for (Token t : toRemove) {
            final MediaControllerRecord r = mRecords.get(t);
            r.controller.unregisterCallback(r);
            mRecords.remove(t);
            if (D.BUG) Log.d(TAG, "Removing " + r.name + " sentRemote=" + r.sentRemote);
            if (r.sentRemote) {
                mCallbacks.onRemoteRemoved(t);
                r.sentRemote = false;
            }
        }
    }

    private static boolean isRemote(PlaybackInfo pi) {
        return pi != null && pi.getPlaybackType() == PlaybackInfo.PLAYBACK_TYPE_REMOTE;
    }

    protected String getControllerName(MediaController controller) {
        final PackageManager pm = mContext.getPackageManager();
        final String pkg = controller.getPackageName();
        try {
            if (USE_SERVICE_LABEL) {
                final List<ResolveInfo> ris = pm.queryIntentServices(
                        new Intent("android.media.MediaRouteProviderService").setPackage(pkg), 0);
                if (ris != null) {
                    for (ResolveInfo ri : ris) {
                        if (ri.serviceInfo == null) continue;
                        if (pkg.equals(ri.serviceInfo.packageName)) {
                            final String serviceLabel =
                                    Objects.toString(ri.serviceInfo.loadLabel(pm), "").trim();
                            if (serviceLabel.length() > 0) {
                                return serviceLabel;
                            }
                        }
                    }
                }
            }
            final ApplicationInfo ai = pm.getApplicationInfo(pkg, 0);
            final String appLabel = Objects.toString(ai.loadLabel(pm), "").trim();
            if (appLabel.length() > 0) {
                return appLabel;
            }
        } catch (NameNotFoundException e) {
        }
        return pkg;
    }

    private void updateRemoteH(Token token, String name, PlaybackInfo pi) {
        if (mCallbacks != null) {
            mCallbacks.onRemoteUpdate(token, name, pi);
        }
    }

    private static void dump(int n, PrintWriter writer, MediaController c) {
        writer.println("  Controller " + n + ": " + c.getPackageName());
        final Bundle extras = c.getExtras();
        final long flags = c.getFlags();
        final MediaMetadata mm = c.getMetadata();
        final PlaybackInfo pi = c.getPlaybackInfo();
        final PlaybackState playbackState = c.getPlaybackState();
        final List<QueueItem> queue = c.getQueue();
        final CharSequence queueTitle = c.getQueueTitle();
        final int ratingType = c.getRatingType();
        final PendingIntent sessionActivity = c.getSessionActivity();

        writer.println("    PlaybackState: " + Util.playbackStateToString(playbackState));
        writer.println("    PlaybackInfo: " + Util.playbackInfoToString(pi));
        if (mm != null) {
            writer.println("  MediaMetadata.desc=" + mm.getDescription());
        }
        writer.println("    RatingType: " + ratingType);
        writer.println("    Flags: " + flags);
        if (extras != null) {
            writer.println("    Extras:");
            for (String key : extras.keySet()) {
                writer.println("      " + key + "=" + extras.get(key));
            }
        }
        if (queueTitle != null) {
            writer.println("    QueueTitle: " + queueTitle);
        }
        if (queue != null && !queue.isEmpty()) {
            writer.println("    Queue:");
            for (QueueItem qi : queue) {
                writer.println("      " + qi);
            }
        }
        if (pi != null) {
            writer.println("    sessionActivity: " + sessionActivity);
        }
    }

    private final class MediaControllerRecord extends MediaController.Callback {
        public final MediaController controller;

        public boolean sentRemote;
        public String name;

        private MediaControllerRecord(MediaController controller) {
            this.controller = controller;
        }

        private String cb(String method) {
            return method + " " + controller.getPackageName() + " ";
        }

        @Override
        public void onAudioInfoChanged(PlaybackInfo info) {
            if (D.BUG) {
                Log.d(TAG, cb("onAudioInfoChanged") + Util.playbackInfoToString(info)
                        + " sentRemote=" + sentRemote);
            }
            final boolean remote = isRemote(info);
            if (!remote && sentRemote) {
                mCallbacks.onRemoteRemoved(controller.getSessionToken());
                sentRemote = false;
            } else if (remote) {
                updateRemoteH(controller.getSessionToken(), name, info);
                sentRemote = true;
            }
        }

        @Override
        public void onExtrasChanged(Bundle extras) {
            if (D.BUG) Log.d(TAG, cb("onExtrasChanged") + extras);
        }

        @Override
        public void onMetadataChanged(MediaMetadata metadata) {
            if (D.BUG) Log.d(TAG, cb("onMetadataChanged") + Util.mediaMetadataToString(metadata));
        }

        @Override
        public void onPlaybackStateChanged(PlaybackState state) {
            if (D.BUG) Log.d(TAG, cb("onPlaybackStateChanged") + Util.playbackStateToString(state));
        }

        @Override
        public void onQueueChanged(List<QueueItem> queue) {
            if (D.BUG) Log.d(TAG, cb("onQueueChanged") + queue);
        }

        @Override
        public void onQueueTitleChanged(CharSequence title) {
            if (D.BUG) Log.d(TAG, cb("onQueueTitleChanged") + title);
        }

        @Override
        public void onSessionDestroyed() {
            if (D.BUG) Log.d(TAG, cb("onSessionDestroyed"));
        }

        @Override
        public void onSessionEvent(String event, Bundle extras) {
            if (D.BUG) Log.d(TAG, cb("onSessionEvent") + "event=" + event + " extras=" + extras);
        }
    }

    private final OnActiveSessionsChangedListener mSessionsListener =
            new OnActiveSessionsChangedListener() {
                @Override
                public void onActiveSessionsChanged(List<MediaController> controllers) {
                    onActiveSessionsUpdatedH(controllers);
                }
            };

    private final IRemoteVolumeController mRvc = new IRemoteVolumeController.Stub() {
        @Override
        public void remoteVolumeChanged(Token sessionToken, int flags)
                throws RemoteException {
            mHandler.obtainMessage(H.REMOTE_VOLUME_CHANGED, flags, 0,
                    sessionToken).sendToTarget();
        }

        @Override
        public void updateRemoteController(final Token sessionToken)
                throws RemoteException {
            mHandler.obtainMessage(H.UPDATE_REMOTE_CONTROLLER, sessionToken).sendToTarget();
        }
    };

    private final class H extends Handler {
        private static final int UPDATE_SESSIONS = 1;
        private static final int REMOTE_VOLUME_CHANGED = 2;
        private static final int UPDATE_REMOTE_CONTROLLER = 3;

        private H(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case UPDATE_SESSIONS:
                    onActiveSessionsUpdatedH(mMgr.getActiveSessions(null));
                    break;
                case REMOTE_VOLUME_CHANGED:
                    onRemoteVolumeChangedH((Token) msg.obj, msg.arg1);
                    break;
                case UPDATE_REMOTE_CONTROLLER:
                    onUpdateRemoteControllerH((Token) msg.obj);
                    break;
            }
        }
    }

    /**
     * Callback for remote media sessions
     */
    public interface Callbacks {
        /**
         * Invoked when remote media session is updated
         */
        void onRemoteUpdate(Token token, String name, PlaybackInfo pi);

        /**
         * Invoked when remote media session is removed
         */
        void onRemoteRemoved(Token t);

        /**
         * Invoked when remote volume is changed
         */
        void onRemoteVolumeChanged(Token token, int flags);
    }

}
