/*
 * Copyright (C) 2016 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.systemui.pip.phone;

import static android.app.ActivityManager.StackId.PINNED_STACK_ID;

import android.app.ActivityManager.StackInfo;
import android.app.ActivityOptions;
import android.app.IActivityManager;
import android.app.RemoteAction;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ParceledListSlice;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Debug;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Log;
import android.view.IWindowManager;

import com.android.systemui.pip.phone.PipMediaController.ActionListener;
import com.android.systemui.recents.events.EventBus;
import com.android.systemui.recents.events.component.HidePipMenuEvent;
import com.android.systemui.recents.misc.ReferenceCountedTrigger;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

/**
 * Manages the PiP menu activity which can show menu options or a scrim.
 *
 * The current media session provides actions whenever there are no valid actions provided by the
 * current PiP activity. Otherwise, those actions always take precedence.
 */
public class PipMenuActivityController {

    private static final String TAG = "PipMenuActController";
    private static final boolean DEBUG = false;

    public static final String EXTRA_CONTROLLER_MESSENGER = "messenger";
    public static final String EXTRA_ACTIONS = "actions";
    public static final String EXTRA_STACK_BOUNDS = "stack_bounds";
    public static final String EXTRA_MOVEMENT_BOUNDS = "movement_bounds";
    public static final String EXTRA_ALLOW_TIMEOUT = "allow_timeout";
    public static final String EXTRA_DISMISS_FRACTION = "dismiss_fraction";
    public static final String EXTRA_MENU_STATE = "menu_state";

    public static final int MESSAGE_MENU_STATE_CHANGED = 100;
    public static final int MESSAGE_EXPAND_PIP = 101;
    public static final int MESSAGE_MINIMIZE_PIP = 102;
    public static final int MESSAGE_DISMISS_PIP = 103;
    public static final int MESSAGE_UPDATE_ACTIVITY_CALLBACK = 104;
    public static final int MESSAGE_REGISTER_INPUT_CONSUMER = 105;
    public static final int MESSAGE_UNREGISTER_INPUT_CONSUMER = 106;
    public static final int MESSAGE_SHOW_MENU = 107;

    public static final int MENU_STATE_NONE = 0;
    public static final int MENU_STATE_CLOSE = 1;
    public static final int MENU_STATE_FULL = 2;

    // The duration to wait before we consider the start activity as having timed out
    private static final long START_ACTIVITY_REQUEST_TIMEOUT_MS = 300;

    /**
     * A listener interface to receive notification on changes in PIP.
     */
    public interface Listener {
        /**
         * Called when the PIP menu visibility changes.
         *
         * @param menuState the current state of the menu
         * @param resize whether or not to resize the PiP with the state change
         */
        void onPipMenuStateChanged(int menuState, boolean resize);

        /**
         * Called when the PIP requested to be expanded.
         */
        void onPipExpand();

        /**
         * Called when the PIP requested to be minimized.
         */
        void onPipMinimize();

        /**
         * Called when the PIP requested to be dismissed.
         */
        void onPipDismiss();

        /**
         * Called when the PIP requested to show the menu.
         */
        void onPipShowMenu();
    }

    private Context mContext;
    private IActivityManager mActivityManager;
    private PipMediaController mMediaController;
    private InputConsumerController mInputConsumerController;

    private ArrayList<Listener> mListeners = new ArrayList<>();
    private ParceledListSlice mAppActions;
    private ParceledListSlice mMediaActions;
    private int mMenuState;

    // The dismiss fraction update is sent frequently, so use a temporary bundle for the message
    private Bundle mTmpDismissFractionData = new Bundle();

    private ReferenceCountedTrigger mOnAttachDecrementTrigger;
    private boolean mStartActivityRequested;
    private long mStartActivityRequestedTime;
    private Messenger mToActivityMessenger;
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_MENU_STATE_CHANGED: {
                    int menuState = msg.arg1;
                    onMenuStateChanged(menuState, true /* resize */);
                    break;
                }
                case MESSAGE_EXPAND_PIP: {
                    mListeners.forEach(l -> l.onPipExpand());
                    break;
                }
                case MESSAGE_MINIMIZE_PIP: {
                    mListeners.forEach(l -> l.onPipMinimize());
                    break;
                }
                case MESSAGE_DISMISS_PIP: {
                    mListeners.forEach(l -> l.onPipDismiss());
                    break;
                }
                case MESSAGE_SHOW_MENU: {
                    mListeners.forEach(l -> l.onPipShowMenu());
                    break;
                }
                case MESSAGE_REGISTER_INPUT_CONSUMER: {
                    mInputConsumerController.registerInputConsumer();
                    break;
                }
                case MESSAGE_UNREGISTER_INPUT_CONSUMER: {
                    mInputConsumerController.unregisterInputConsumer();
                    break;
                }
                case MESSAGE_UPDATE_ACTIVITY_CALLBACK: {
                    mToActivityMessenger = msg.replyTo;
                    setStartActivityRequested(false);
                    if (mOnAttachDecrementTrigger != null) {
                        mOnAttachDecrementTrigger.decrement();
                        mOnAttachDecrementTrigger = null;
                    }
                    // Mark the menu as invisible once the activity finishes as well
                    if (mToActivityMessenger == null) {
                        onMenuStateChanged(MENU_STATE_NONE, true /* resize */);
                    }
                    break;
                }
            }
        }
    };
    private Messenger mMessenger = new Messenger(mHandler);

    private Runnable mStartActivityRequestedTimeoutRunnable = () -> {
        setStartActivityRequested(false);
        if (mOnAttachDecrementTrigger != null) {
            mOnAttachDecrementTrigger.decrement();
            mOnAttachDecrementTrigger = null;
        }
        Log.e(TAG, "Expected start menu activity request timed out");
    };

    private ActionListener mMediaActionListener = new ActionListener() {
        @Override
        public void onMediaActionsChanged(List<RemoteAction> mediaActions) {
            mMediaActions = new ParceledListSlice<>(mediaActions);
            updateMenuActions();
        }
    };

    public PipMenuActivityController(Context context, IActivityManager activityManager,
            PipMediaController mediaController, InputConsumerController inputConsumerController) {
        mContext = context;
        mActivityManager = activityManager;
        mMediaController = mediaController;
        mInputConsumerController = inputConsumerController;

        EventBus.getDefault().register(this);
    }

    public void onActivityPinned() {
        if (mMenuState == MENU_STATE_NONE) {
            // If the menu is not visible, then re-register the input consumer if it is not already
            // registered
            mInputConsumerController.registerInputConsumer();
        }
    }

    public void onActivityUnpinned(ComponentName topPipActivity) {
        hideMenu();
        setStartActivityRequested(false);
    }

    public void onPinnedStackAnimationEnded() {
        // Note: Only active menu activities care about this event
        if (mToActivityMessenger != null) {
            Message m = Message.obtain();
            m.what = PipMenuActivity.MESSAGE_ANIMATION_ENDED;
            try {
                mToActivityMessenger.send(m);
            } catch (RemoteException e) {
                Log.e(TAG, "Could not notify menu pinned animation ended", e);
            }
        }
    }

    /**
     * Adds a new menu activity listener.
     */
    public void addListener(Listener listener) {
        if (!mListeners.contains(listener)) {
            mListeners.add(listener);
        }
    }

    /**
     * Updates the appearance of the menu and scrim on top of the PiP while dismissing.
     */
    public void setDismissFraction(float fraction) {
        if (DEBUG) {
            Log.d(TAG, "setDismissFraction() hasActivity=" + (mToActivityMessenger != null)
                    + " fraction=" + fraction);
        }
        if (mToActivityMessenger != null) {
            mTmpDismissFractionData.clear();
            mTmpDismissFractionData.putFloat(EXTRA_DISMISS_FRACTION, fraction);
            Message m = Message.obtain();
            m.what = PipMenuActivity.MESSAGE_UPDATE_DISMISS_FRACTION;
            m.obj = mTmpDismissFractionData;
            try {
                mToActivityMessenger.send(m);
            } catch (RemoteException e) {
                Log.e(TAG, "Could not notify menu to update dismiss fraction", e);
            }
        } else if (!mStartActivityRequested || isStartActivityRequestedElapsed()) {
            // If we haven't requested the start activity, or if it previously took too long to
            // start, then start it
            startMenuActivity(MENU_STATE_NONE, null /* stackBounds */,
                    null /* movementBounds */, false /* allowMenuTimeout */);
        }
    }

    /**
     * Shows the menu activity.
     */
    public void showMenu(int menuState, Rect stackBounds, Rect movementBounds,
            boolean allowMenuTimeout) {
        if (DEBUG) {
            Log.d(TAG, "showMenu() state=" + menuState
                    + " hasActivity=" + (mToActivityMessenger != null)
                    + " callers=\n" + Debug.getCallers(5, "    "));
        }
        if (mToActivityMessenger != null) {
            Bundle data = new Bundle();
            data.putInt(EXTRA_MENU_STATE, menuState);
            data.putParcelable(EXTRA_STACK_BOUNDS, stackBounds);
            data.putParcelable(EXTRA_MOVEMENT_BOUNDS, movementBounds);
            data.putBoolean(EXTRA_ALLOW_TIMEOUT, allowMenuTimeout);
            Message m = Message.obtain();
            m.what = PipMenuActivity.MESSAGE_SHOW_MENU;
            m.obj = data;
            try {
                mToActivityMessenger.send(m);
            } catch (RemoteException e) {
                Log.e(TAG, "Could not notify menu to show", e);
            }
        } else if (!mStartActivityRequested || isStartActivityRequestedElapsed()) {
            // If we haven't requested the start activity, or if it previously took too long to
            // start, then start it
            startMenuActivity(menuState, stackBounds, movementBounds, allowMenuTimeout);
        }
    }

    /**
     * Pokes the menu, indicating that the user is interacting with it.
     */
    public void pokeMenu() {
        if (DEBUG) {
            Log.d(TAG, "pokeMenu() hasActivity=" + (mToActivityMessenger != null));
        }
        if (mToActivityMessenger != null) {
            Message m = Message.obtain();
            m.what = PipMenuActivity.MESSAGE_POKE_MENU;
            try {
                mToActivityMessenger.send(m);
            } catch (RemoteException e) {
                Log.e(TAG, "Could not notify poke menu", e);
            }
        }
    }

    /**
     * Hides the menu activity.
     */
    public void hideMenu() {
        if (DEBUG) {
            Log.d(TAG, "hideMenu() state=" + mMenuState
                    + " hasActivity=" + (mToActivityMessenger != null)
                    + " callers=\n" + Debug.getCallers(5, "    "));
        }
        if (mToActivityMessenger != null) {
            Message m = Message.obtain();
            m.what = PipMenuActivity.MESSAGE_HIDE_MENU;
            try {
                mToActivityMessenger.send(m);
            } catch (RemoteException e) {
                Log.e(TAG, "Could not notify menu to hide", e);
            }
        }
    }

    /**
     * Preemptively mark the menu as invisible, used when we are directly manipulating the pinned
     * stack and don't want to trigger a resize which can animate the stack in a conflicting way
     * (ie. when manually expanding or dismissing).
     */
    public void hideMenuWithoutResize() {
        onMenuStateChanged(MENU_STATE_NONE, false /* resize */);
    }

    /**
     * Sets the menu actions to the actions provided by the current PiP activity.
     */
    public void setAppActions(ParceledListSlice appActions) {
        mAppActions = appActions;
        updateMenuActions();
    }

    /**
     * @return the best set of actions to show in the PiP menu.
     */
    private ParceledListSlice resolveMenuActions() {
        if (isValidActions(mAppActions)) {
            return mAppActions;
        }
        return mMediaActions;
    }

    /**
     * Starts the menu activity on the top task of the pinned stack.
     */
    private void startMenuActivity(int menuState, Rect stackBounds, Rect movementBounds,
            boolean allowMenuTimeout) {
        try {
            StackInfo pinnedStackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
            if (pinnedStackInfo != null && pinnedStackInfo.taskIds != null &&
                    pinnedStackInfo.taskIds.length > 0) {
                Intent intent = new Intent(mContext, PipMenuActivity.class);
                intent.putExtra(EXTRA_CONTROLLER_MESSENGER, mMessenger);
                intent.putExtra(EXTRA_ACTIONS, resolveMenuActions());
                if (stackBounds != null) {
                    intent.putExtra(EXTRA_STACK_BOUNDS, stackBounds);
                }
                if (movementBounds != null) {
                    intent.putExtra(EXTRA_MOVEMENT_BOUNDS, movementBounds);
                }
                intent.putExtra(EXTRA_MENU_STATE, menuState);
                intent.putExtra(EXTRA_ALLOW_TIMEOUT, allowMenuTimeout);
                ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, 0, 0);
                options.setLaunchTaskId(
                        pinnedStackInfo.taskIds[pinnedStackInfo.taskIds.length - 1]);
                options.setTaskOverlay(true, true /* canResume */);
                mContext.startActivityAsUser(intent, options.toBundle(), UserHandle.CURRENT);
                setStartActivityRequested(true);
            } else {
                Log.e(TAG, "No PIP tasks found");
            }
        } catch (RemoteException e) {
            setStartActivityRequested(false);
            Log.e(TAG, "Error showing PIP menu activity", e);
        }
    }

    /**
     * Updates the PiP menu activity with the best set of actions provided.
     */
    private void updateMenuActions() {
        if (mToActivityMessenger != null) {
            // Fetch the pinned stack bounds
            Rect stackBounds = null;
            try {
                StackInfo pinnedStackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
                if (pinnedStackInfo != null) {
                    stackBounds = pinnedStackInfo.bounds;
                }
            } catch (RemoteException e) {
                Log.e(TAG, "Error showing PIP menu activity", e);
            }

            Bundle data = new Bundle();
            data.putParcelable(EXTRA_STACK_BOUNDS, stackBounds);
            data.putParcelable(EXTRA_ACTIONS, resolveMenuActions());
            Message m = Message.obtain();
            m.what = PipMenuActivity.MESSAGE_UPDATE_ACTIONS;
            m.obj = data;
            try {
                mToActivityMessenger.send(m);
            } catch (RemoteException e) {
                Log.e(TAG, "Could not notify menu activity to update actions", e);
            }
        }
    }

    /**
     * Returns whether the set of actions are valid.
     */
    private boolean isValidActions(ParceledListSlice actions) {
        return actions != null && actions.getList().size() > 0;
    }

    /**
     * @return whether the time of the activity request has exceeded the timeout.
     */
    private boolean isStartActivityRequestedElapsed() {
        return (SystemClock.uptimeMillis() - mStartActivityRequestedTime)
                >= START_ACTIVITY_REQUEST_TIMEOUT_MS;
    }

    /**
     * Handles changes in menu visibility.
     */
    private void onMenuStateChanged(int menuState, boolean resize) {
        if (DEBUG) {
            Log.d(TAG, "onMenuStateChanged() mMenuState=" + mMenuState
                    + " menuState=" + menuState + " resize=" + resize);
        }
        if (menuState == MENU_STATE_NONE) {
            mInputConsumerController.registerInputConsumer();
        } else {
            mInputConsumerController.unregisterInputConsumer();
        }
        if (menuState != mMenuState) {
            mListeners.forEach(l -> l.onPipMenuStateChanged(menuState, resize));
            if (menuState == MENU_STATE_FULL) {
                // Once visible, start listening for media action changes. This call will trigger
                // the menu actions to be updated again.
                mMediaController.addListener(mMediaActionListener);
            } else {
                // Once hidden, stop listening for media action changes. This call will trigger
                // the menu actions to be updated again.
                mMediaController.removeListener(mMediaActionListener);
            }
        }
        mMenuState = menuState;
    }

    private void setStartActivityRequested(boolean requested) {
        mHandler.removeCallbacks(mStartActivityRequestedTimeoutRunnable);
        mStartActivityRequested = requested;
        mStartActivityRequestedTime = requested ? SystemClock.uptimeMillis() : 0;
    }

    public final void onBusEvent(HidePipMenuEvent event) {
        if (mStartActivityRequested) {
            // If the menu has been start-requested, but not actually started, then we defer the
            // trigger callback until the menu has started and called back to the controller.
            mOnAttachDecrementTrigger = event.getAnimationTrigger();
            mOnAttachDecrementTrigger.increment();

            // Fallback for b/63752800, we have started the PipMenuActivity but it has not made any
            // callbacks. Don't continue to wait for the menu to show past some timeout.
            mHandler.removeCallbacks(mStartActivityRequestedTimeoutRunnable);
            mHandler.postDelayed(mStartActivityRequestedTimeoutRunnable,
                    START_ACTIVITY_REQUEST_TIMEOUT_MS);
        }
    }

    public void dump(PrintWriter pw, String prefix) {
        final String innerPrefix = prefix + "  ";
        pw.println(prefix + TAG);
        pw.println(innerPrefix + "mMenuState=" + mMenuState);
        pw.println(innerPrefix + "mToActivityMessenger=" + mToActivityMessenger);
        pw.println(innerPrefix + "mListeners=" + mListeners.size());
        pw.println(innerPrefix + "mStartActivityRequested=" + mStartActivityRequested);
        pw.println(innerPrefix + "mStartActivityRequestedTime=" + mStartActivityRequestedTime);
    }
}
