/*
 * Copyright (C) 2014 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 android.media.session;

import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.media.AudioManager;
import android.media.MediaMetadata;
import android.media.MediaMetadataEditor;
import android.media.MediaMetadataRetriever;
import android.media.Rating;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.ArrayMap;
import android.util.Log;
import android.view.KeyEvent;

/**
 * Helper for connecting existing APIs up to the new session APIs. This can be
 * used by RCC, AudioFocus, etc. to create a single session that translates to
 * all those components.
 *
 * @hide
 */
public class MediaSessionLegacyHelper {
    private static final String TAG = "MediaSessionHelper";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private static final Object sLock = new Object();
    private static MediaSessionLegacyHelper sInstance;

    private Context mContext;
    private MediaSessionManager mSessionManager;
    private Handler mHandler = new Handler(Looper.getMainLooper());
    // The legacy APIs use PendingIntents to register/unregister media button
    // receivers and these are associated with RCC.
    private ArrayMap<PendingIntent, SessionHolder> mSessions
            = new ArrayMap<PendingIntent, SessionHolder>();

    private MediaSessionLegacyHelper(Context context) {
        mContext = context;
        mSessionManager = (MediaSessionManager) context
                .getSystemService(Context.MEDIA_SESSION_SERVICE);
    }

    public static MediaSessionLegacyHelper getHelper(Context context) {
        synchronized (sLock) {
            if (sInstance == null) {
                sInstance = new MediaSessionLegacyHelper(context.getApplicationContext());
            }
        }
        return sInstance;
    }

    public static Bundle getOldMetadata(MediaMetadata metadata, int artworkWidth,
            int artworkHeight) {
        boolean includeArtwork = artworkWidth != -1 && artworkHeight != -1;
        Bundle oldMetadata = new Bundle();
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) {
            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_ALBUM),
                    metadata.getString(MediaMetadata.METADATA_KEY_ALBUM));
        }
        if (includeArtwork && metadata.containsKey(MediaMetadata.METADATA_KEY_ART)) {
            Bitmap art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART);
            oldMetadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK),
                    scaleBitmapIfTooBig(art, artworkWidth, artworkHeight));
        } else if (includeArtwork && metadata.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART)) {
            // Fall back to album art if the track art wasn't available
            Bitmap art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
            oldMetadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK),
                    scaleBitmapIfTooBig(art, artworkWidth, artworkHeight));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ARTIST)) {
            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST),
                    metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) {
            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_ARTIST),
                    metadata.getString(MediaMetadata.METADATA_KEY_ARTIST));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_AUTHOR)) {
            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_AUTHOR),
                    metadata.getString(MediaMetadata.METADATA_KEY_AUTHOR));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_COMPILATION)) {
            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_COMPILATION),
                    metadata.getString(MediaMetadata.METADATA_KEY_COMPILATION));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_COMPOSER)) {
            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_COMPOSER),
                    metadata.getString(MediaMetadata.METADATA_KEY_COMPOSER));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_DATE)) {
            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_DATE),
                    metadata.getString(MediaMetadata.METADATA_KEY_DATE));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_DISC_NUMBER)) {
            oldMetadata.putLong(String.valueOf(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER),
                    metadata.getLong(MediaMetadata.METADATA_KEY_DISC_NUMBER));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
            oldMetadata.putLong(String.valueOf(MediaMetadataRetriever.METADATA_KEY_DURATION),
                    metadata.getLong(MediaMetadata.METADATA_KEY_DURATION));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_GENRE)) {
            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_GENRE),
                    metadata.getString(MediaMetadata.METADATA_KEY_GENRE));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS)) {
            oldMetadata.putLong(String.valueOf(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS),
                    metadata.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_RATING)) {
            oldMetadata.putParcelable(String.valueOf(MediaMetadataEditor.RATING_KEY_BY_OTHERS),
                    metadata.getRating(MediaMetadata.METADATA_KEY_RATING));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_USER_RATING)) {
            oldMetadata.putParcelable(String.valueOf(MediaMetadataEditor.RATING_KEY_BY_USER),
                    metadata.getRating(MediaMetadata.METADATA_KEY_USER_RATING));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_TITLE)) {
            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_TITLE),
                    metadata.getString(MediaMetadata.METADATA_KEY_TITLE));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) {
            oldMetadata.putLong(
                    String.valueOf(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER),
                    metadata.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_WRITER)) {
            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_WRITER),
                    metadata.getString(MediaMetadata.METADATA_KEY_WRITER));
        }
        if (metadata.containsKey(MediaMetadata.METADATA_KEY_YEAR)) {
            oldMetadata.putLong(String.valueOf(MediaMetadataRetriever.METADATA_KEY_YEAR),
                    metadata.getLong(MediaMetadata.METADATA_KEY_YEAR));
        }
        return oldMetadata;
    }

    public MediaSession getSession(PendingIntent pi) {
        SessionHolder holder = mSessions.get(pi);
        return holder == null ? null : holder.mSession;
    }

    public void sendMediaButtonEvent(KeyEvent keyEvent, boolean needWakeLock) {
        if (keyEvent == null) {
            Log.w(TAG, "Tried to send a null key event. Ignoring.");
            return;
        }
        mSessionManager.dispatchMediaKeyEvent(keyEvent, needWakeLock);
        if (DEBUG) {
            Log.d(TAG, "dispatched media key " + keyEvent);
        }
    }

    public void sendVolumeKeyEvent(KeyEvent keyEvent, boolean musicOnly) {
        if (keyEvent == null) {
            Log.w(TAG, "Tried to send a null key event. Ignoring.");
            return;
        }
        boolean down = keyEvent.getAction() == KeyEvent.ACTION_DOWN;
        boolean up = keyEvent.getAction() == KeyEvent.ACTION_UP;
        int direction = 0;
        boolean isMute = false;
        switch (keyEvent.getKeyCode()) {
            case KeyEvent.KEYCODE_VOLUME_UP:
                direction = AudioManager.ADJUST_RAISE;
                break;
            case KeyEvent.KEYCODE_VOLUME_DOWN:
                direction = AudioManager.ADJUST_LOWER;
                break;
            case KeyEvent.KEYCODE_VOLUME_MUTE:
                isMute = true;
                break;
        }
        if (down || up) {
            int flags = AudioManager.FLAG_FROM_KEY;
            if (musicOnly) {
                // This flag is used when the screen is off to only affect
                // active media
                flags |= AudioManager.FLAG_ACTIVE_MEDIA_ONLY;
            } else {
                // These flags are consistent with the home screen
                if (up) {
                    flags |= AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE;
                } else {
                    flags |= AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE;
                }
            }
            if (direction != 0) {
                // If this is action up we want to send a beep for non-music events
                if (up) {
                    direction = 0;
                }
                mSessionManager.dispatchAdjustVolume(AudioManager.USE_DEFAULT_STREAM_TYPE,
                        direction, flags);
            } else if (isMute) {
                if (down && keyEvent.getRepeatCount() == 0) {
                    mSessionManager.dispatchAdjustVolume(AudioManager.USE_DEFAULT_STREAM_TYPE,
                            AudioManager.ADJUST_TOGGLE_MUTE, flags);
                }
            }
        }
    }

    public void sendAdjustVolumeBy(int suggestedStream, int delta, int flags) {
        mSessionManager.dispatchAdjustVolume(suggestedStream, delta, flags);
        if (DEBUG) {
            Log.d(TAG, "dispatched volume adjustment");
        }
    }

    public boolean isGlobalPriorityActive() {
        return mSessionManager.isGlobalPriorityActive();
    }

    public void addRccListener(PendingIntent pi, MediaSession.Callback listener) {
        if (pi == null) {
            Log.w(TAG, "Pending intent was null, can't add rcc listener.");
            return;
        }
        SessionHolder holder = getHolder(pi, true);
        if (holder == null) {
            return;
        }
        if (holder.mRccListener != null) {
            if (holder.mRccListener == listener) {
                if (DEBUG) {
                    Log.d(TAG, "addRccListener listener already added.");
                }
                // This is already the registered listener, ignore
                return;
            }
        }
        holder.mRccListener = listener;
        holder.mFlags |= MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS;
        holder.mSession.setFlags(holder.mFlags);
        holder.update();
        if (DEBUG) {
            Log.d(TAG, "Added rcc listener for " + pi + ".");
        }
    }

    public void removeRccListener(PendingIntent pi) {
        if (pi == null) {
            return;
        }
        SessionHolder holder = getHolder(pi, false);
        if (holder != null && holder.mRccListener != null) {
            holder.mRccListener = null;
            holder.mFlags &= ~MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS;
            holder.mSession.setFlags(holder.mFlags);
            holder.update();
            if (DEBUG) {
                Log.d(TAG, "Removed rcc listener for " + pi + ".");
            }
        }
    }

    public void addMediaButtonListener(PendingIntent pi, ComponentName mbrComponent,
            Context context) {
        if (pi == null) {
            Log.w(TAG, "Pending intent was null, can't addMediaButtonListener.");
            return;
        }
        SessionHolder holder = getHolder(pi, true);
        if (holder == null) {
            return;
        }
        if (holder.mMediaButtonListener != null) {
            // Already have this listener registered
            if (DEBUG) {
                Log.d(TAG, "addMediaButtonListener already added " + pi);
            }
        }
        holder.mMediaButtonListener = new MediaButtonListener(pi, context);
        // TODO determine if handling transport performer commands should also
        // set this flag
        holder.mFlags |= MediaSession.FLAG_HANDLES_MEDIA_BUTTONS;
        holder.mSession.setFlags(holder.mFlags);
        holder.mSession.setMediaButtonReceiver(pi);
        holder.update();
        if (DEBUG) {
            Log.d(TAG, "addMediaButtonListener added " + pi);
        }
    }

    public void removeMediaButtonListener(PendingIntent pi) {
        if (pi == null) {
            return;
        }
        SessionHolder holder = getHolder(pi, false);
        if (holder != null && holder.mMediaButtonListener != null) {
            holder.mFlags &= ~MediaSession.FLAG_HANDLES_MEDIA_BUTTONS;
            holder.mSession.setFlags(holder.mFlags);
            holder.mMediaButtonListener = null;

            holder.update();
            if (DEBUG) {
                Log.d(TAG, "removeMediaButtonListener removed " + pi);
            }
        }
    }

    /**
     * Scale a bitmap to fit the smallest dimension by uniformly scaling the
     * incoming bitmap. If the bitmap fits, then do nothing and return the
     * original.
     *
     * @param bitmap
     * @param maxWidth
     * @param maxHeight
     * @return
     */
    private static Bitmap scaleBitmapIfTooBig(Bitmap bitmap, int maxWidth, int maxHeight) {
        if (bitmap != null) {
            final int width = bitmap.getWidth();
            final int height = bitmap.getHeight();
            if (width > maxWidth || height > maxHeight) {
                float scale = Math.min((float) maxWidth / width, (float) maxHeight / height);
                int newWidth = Math.round(scale * width);
                int newHeight = Math.round(scale * height);
                Bitmap.Config newConfig = bitmap.getConfig();
                if (newConfig == null) {
                    newConfig = Bitmap.Config.ARGB_8888;
                }
                Bitmap outBitmap = Bitmap.createBitmap(newWidth, newHeight, newConfig);
                Canvas canvas = new Canvas(outBitmap);
                Paint paint = new Paint();
                paint.setAntiAlias(true);
                paint.setFilterBitmap(true);
                canvas.drawBitmap(bitmap, null,
                        new RectF(0, 0, outBitmap.getWidth(), outBitmap.getHeight()), paint);
                bitmap = outBitmap;
            }
        }
        return bitmap;
    }

    private SessionHolder getHolder(PendingIntent pi, boolean createIfMissing) {
        SessionHolder holder = mSessions.get(pi);
        if (holder == null && createIfMissing) {
            MediaSession session;
            session = new MediaSession(mContext, TAG + "-" + pi.getCreatorPackage());
            session.setActive(true);
            holder = new SessionHolder(session, pi);
            mSessions.put(pi, holder);
        }
        return holder;
    }

    private static void sendKeyEvent(PendingIntent pi, Context context, Intent intent) {
        try {
            pi.send(context, 0, intent);
        } catch (CanceledException e) {
            Log.e(TAG, "Error sending media key down event:", e);
            // Don't bother sending up if down failed
            return;
        }
    }

    private static final class MediaButtonListener extends MediaSession.Callback {
        private final PendingIntent mPendingIntent;
        private final Context mContext;

        public MediaButtonListener(PendingIntent pi, Context context) {
            mPendingIntent = pi;
            mContext = context;
        }

        @Override
        public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
            MediaSessionLegacyHelper.sendKeyEvent(mPendingIntent, mContext, mediaButtonIntent);
            return true;
        }

        @Override
        public void onPlay() {
            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY);
        }

        @Override
        public void onPause() {
            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_PAUSE);
        }

        @Override
        public void onSkipToNext() {
            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT);
        }

        @Override
        public void onSkipToPrevious() {
            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_PREVIOUS);
        }

        @Override
        public void onFastForward() {
            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD);
        }

        @Override
        public void onRewind() {
            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_REWIND);
        }

        @Override
        public void onStop() {
            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_STOP);
        }

        private void sendKeyEvent(int keyCode) {
            KeyEvent ke = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
            Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
            intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);

            intent.putExtra(Intent.EXTRA_KEY_EVENT, ke);
            MediaSessionLegacyHelper.sendKeyEvent(mPendingIntent, mContext, intent);

            ke = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
            intent.putExtra(Intent.EXTRA_KEY_EVENT, ke);
            MediaSessionLegacyHelper.sendKeyEvent(mPendingIntent, mContext, intent);

            if (DEBUG) {
                Log.d(TAG, "Sent " + keyCode + " to pending intent " + mPendingIntent);
            }
        }
    }

    private class SessionHolder {
        public final MediaSession mSession;
        public final PendingIntent mPi;
        public MediaButtonListener mMediaButtonListener;
        public MediaSession.Callback mRccListener;
        public int mFlags;

        public SessionCallback mCb;

        public SessionHolder(MediaSession session, PendingIntent pi) {
            mSession = session;
            mPi = pi;
        }

        public void update() {
            if (mMediaButtonListener == null && mRccListener == null) {
                mSession.setCallback(null);
                mSession.release();
                mCb = null;
                mSessions.remove(mPi);
            } else if (mCb == null) {
                mCb = new SessionCallback();
                Handler handler = new Handler(Looper.getMainLooper());
                mSession.setCallback(mCb, handler);
            }
        }

        private class SessionCallback extends MediaSession.Callback {

            @Override
            public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
                if (mMediaButtonListener != null) {
                    mMediaButtonListener.onMediaButtonEvent(mediaButtonIntent);
                }
                return true;
            }

            @Override
            public void onPlay() {
                if (mMediaButtonListener != null) {
                    mMediaButtonListener.onPlay();
                }
            }

            @Override
            public void onPause() {
                if (mMediaButtonListener != null) {
                    mMediaButtonListener.onPause();
                }
            }

            @Override
            public void onSkipToNext() {
                if (mMediaButtonListener != null) {
                    mMediaButtonListener.onSkipToNext();
                }
            }

            @Override
            public void onSkipToPrevious() {
                if (mMediaButtonListener != null) {
                    mMediaButtonListener.onSkipToPrevious();
                }
            }

            @Override
            public void onFastForward() {
                if (mMediaButtonListener != null) {
                    mMediaButtonListener.onFastForward();
                }
            }

            @Override
            public void onRewind() {
                if (mMediaButtonListener != null) {
                    mMediaButtonListener.onRewind();
                }
            }

            @Override
            public void onStop() {
                if (mMediaButtonListener != null) {
                    mMediaButtonListener.onStop();
                }
            }

            @Override
            public void onSeekTo(long pos) {
                if (mRccListener != null) {
                    mRccListener.onSeekTo(pos);
                }
            }

            @Override
            public void onSetRating(Rating rating) {
                if (mRccListener != null) {
                    mRccListener.onSetRating(rating);
                }
            }
        }
    }
}
