/*
 * Copyright (C) 2021 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.wm.shell.pip;

import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.WindowManager.TRANSIT_PIP;

import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK;
import static com.android.wm.shell.pip.PipAnimationController.isInPipDirection;

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.app.ActivityTaskManager;
import android.app.Flags;
import android.app.PictureInPictureParams;
import android.app.PictureInPictureUiState;
import android.app.TaskInfo;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;

import androidx.annotation.NonNull;

import com.android.internal.protolog.ProtoLog;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
import com.android.wm.shell.common.pip.PipBoundsState;
import com.android.wm.shell.common.pip.PipMenuController;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.transition.DefaultMixedHandler;
import com.android.wm.shell.transition.Transitions;

import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;

/**
 * Responsible supplying PiP Transitions.
 */
public abstract class PipTransitionController implements Transitions.TransitionHandler {

    protected final PipBoundsAlgorithm mPipBoundsAlgorithm;
    protected final PipBoundsState mPipBoundsState;
    protected final ShellTaskOrganizer mShellTaskOrganizer;
    protected final PipMenuController mPipMenuController;
    protected final Transitions mTransitions;
    private final Map<PipTransitionCallback, Executor> mPipTransitionCallbacks = new HashMap<>();
    protected PipTaskOrganizer mPipOrganizer;
    protected DefaultMixedHandler mMixedHandler;

    public static final int ANIM_TYPE_BOUNDS = 0;
    public static final int ANIM_TYPE_ALPHA = 1;

    @IntDef(prefix = { "ANIM_TYPE_" }, value = {
            ANIM_TYPE_BOUNDS,
            ANIM_TYPE_ALPHA
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface AnimationType {}


    protected final PipAnimationController.PipAnimationCallback mPipAnimationCallback =
            new PipAnimationController.PipAnimationCallback() {
                @Override
                public void onPipAnimationStart(TaskInfo taskInfo,
                        PipAnimationController.PipTransitionAnimator animator) {
                    final int direction = animator.getTransitionDirection();
                    sendOnPipTransitionStarted(direction);
                }

                @Override
                public void onPipAnimationEnd(TaskInfo taskInfo, SurfaceControl.Transaction tx,
                        PipAnimationController.PipTransitionAnimator animator) {
                    final int direction = animator.getTransitionDirection();
                    mPipBoundsState.setBounds(animator.getDestinationBounds());
                    if (direction == TRANSITION_DIRECTION_REMOVE_STACK) {
                        return;
                    }
                    if (isInPipDirection(direction) && mPipOrganizer.mPipOverlay != null) {
                        mPipOrganizer.fadeOutAndRemoveOverlay(mPipOrganizer.mPipOverlay,
                                null /* callback */, true /* withStartDelay*/);
                    }
                    onFinishResize(taskInfo, animator.getDestinationBounds(),
                            animator.getLeashOffset(), direction, tx);
                    sendOnPipTransitionFinished(direction);
                }

                @Override
                public void onPipAnimationCancel(TaskInfo taskInfo,
                        PipAnimationController.PipTransitionAnimator animator) {
                    final int direction = animator.getTransitionDirection();
                    if (isInPipDirection(direction) && mPipOrganizer.mPipOverlay != null) {
                        mPipOrganizer.fadeOutAndRemoveOverlay(mPipOrganizer.mPipOverlay,
                                null /* callback */, true /* withStartDelay */);
                    }
                    sendOnPipTransitionCancelled(animator.getTransitionDirection());
                }
            };

    /**
     * Called when transition is about to finish. This is usually for performing tasks such as
     * applying WindowContainerTransaction to finalize the PiP bounds and send to the framework.
     */
    public void onFinishResize(@NonNull TaskInfo taskInfo, @NonNull Rect destinationBounds,
            @NonNull Point leashOffset, @PipAnimationController.TransitionDirection int direction,
            @NonNull SurfaceControl.Transaction tx) {
    }

    /**
     * Called when the Shell wants to start an exit Pip transition/animation.
     */
    public void startExitTransition(int type, WindowContainerTransaction out,
            @Nullable Rect destinationBounds) {
        // Default implementation does nothing.
    }

    /**
     * Called when the Shell wants to start an exit-via-expand from Pip transition/animation.
     */
    public void startExpandTransition(
            WindowContainerTransaction wct, boolean toSplit, boolean hasFirstHandler) {
        // Default implementation does nothing.
    }

    /**
     * Called when the Shell wants to start a remove Pip transition/animation.
     */
    public void startRemoveTransition(WindowContainerTransaction wct, boolean withFadeout) {
        // Default implementation does nothing.
    }

    /**
     * Called when the Shell wants to start changing the Pip bounds transition/animation.
     *
     * @param duration the suggested duration for bounds change animation.
     */
    public void startPipBoundsChangeTransition(WindowContainerTransaction wct, int duration) {
        // Default implementation does nothing.
    }

    /**
     * Called when the transition animation can't continue (eg. task is removed during
     * animation)
     */
    public void forceFinishTransition() {
    }

    /** Called when the fixed rotation started. */
    public void onFixedRotationStarted() {
    }

    /** Called when the fixed rotation finished. */
    public void onFixedRotationFinished() {
    }

    public PipTransitionController(
            @NonNull ShellInit shellInit,
            @NonNull ShellTaskOrganizer shellTaskOrganizer,
            @NonNull Transitions transitions,
            PipBoundsState pipBoundsState,
            PipMenuController pipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm) {
        mPipBoundsState = pipBoundsState;
        mPipMenuController = pipMenuController;
        mShellTaskOrganizer = shellTaskOrganizer;
        mPipBoundsAlgorithm = pipBoundsAlgorithm;
        mTransitions = transitions;
        shellInit.addInitCallback(this::onInit, this);
    }

    protected void onInit() {
        mTransitions.addHandler(this);
    }

    void setPipOrganizer(PipTaskOrganizer pto) {
        mPipOrganizer = pto;
    }

    public void setMixedHandler(DefaultMixedHandler mixedHandler) {
        mMixedHandler = mixedHandler;
    }

    public void applyTransaction(WindowContainerTransaction wct) {
        mShellTaskOrganizer.applyTransaction(wct);
    }

    /**
     * Registers {@link PipTransitionCallback} to receive transition callbacks.
     */
    public void registerPipTransitionCallback(
            @NonNull PipTransitionCallback callback, @NonNull Executor executor) {
        mPipTransitionCallbacks.put(callback, executor);
    }

    protected void onStartSwipePipToHome() {
        if (Flags.enablePipUiStateCallbackOnEntering()) {
            try {
                ActivityTaskManager.getService().onPictureInPictureUiStateChanged(
                        new PictureInPictureUiState.Builder()
                                .setTransitioningToPip(true)
                                .build());
            } catch (RemoteException | IllegalStateException e) {
                ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                        "Failed to set alert PiP state change.");
            }
        }
    }

    /**
     * Used in {@link #sendOnPipTransitionStarted(int)} to decide whether we should send the
     * PictureInPictureUiState change callback on transition start.
     * For instance, in auto-enter-pip case, {@link #onStartSwipePipToHome()} should have signaled
     * the app, and we can return {@code true} here to avoid double callback.
     *
     * @return {@code true} if there is a ongoing swipe pip to home transition.
     */
    protected boolean isInSwipePipToHomeTransition() {
        return false;
    }

    protected void sendOnPipTransitionStarted(
            @PipAnimationController.TransitionDirection int direction) {
        final Rect pipBounds = mPipBoundsState.getBounds();
        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                "sendOnPipTransitionStarted direction=%d, bounds=%s", direction, pipBounds);
        for (Map.Entry<PipTransitionCallback, Executor> entry
                : mPipTransitionCallbacks.entrySet()) {
            entry.getValue().execute(
                    () -> entry.getKey().onPipTransitionStarted(direction, pipBounds));
        }
        if (isInPipDirection(direction) && Flags.enablePipUiStateCallbackOnEntering()
                && !isInSwipePipToHomeTransition()) {
            try {
                ActivityTaskManager.getService().onPictureInPictureUiStateChanged(
                        new PictureInPictureUiState.Builder()
                                .setTransitioningToPip(true)
                                .build());
            } catch (RemoteException | IllegalStateException e) {
                ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                        "Failed to set alert PiP state change.");
            }
        }
    }

    protected void sendOnPipTransitionFinished(
            @PipAnimationController.TransitionDirection int direction) {
        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                "sendOnPipTransitionFinished direction=%d", direction);
        for (Map.Entry<PipTransitionCallback, Executor> entry
                : mPipTransitionCallbacks.entrySet()) {
            entry.getValue().execute(
                    () -> entry.getKey().onPipTransitionFinished(direction));
        }
        if (isInPipDirection(direction) && Flags.enablePipUiStateCallbackOnEntering()) {
            try {
                ActivityTaskManager.getService().onPictureInPictureUiStateChanged(
                        new PictureInPictureUiState.Builder()
                                .setTransitioningToPip(false)
                                .build());
            } catch (RemoteException | IllegalStateException e) {
                ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                        "Failed to set alert PiP state change.");
            }
        }
    }

    protected void sendOnPipTransitionCancelled(
            @PipAnimationController.TransitionDirection int direction) {
        ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
                "sendOnPipTransitionCancelled direction=%d", direction);
        for (Map.Entry<PipTransitionCallback, Executor> entry
                : mPipTransitionCallbacks.entrySet()) {
            entry.getValue().execute(
                    () -> entry.getKey().onPipTransitionCanceled(direction));
        }
    }

    /**
     * The windowing mode to restore to when resizing out of PIP direction. Defaults to undefined
     * and can be overridden to restore to an alternate windowing mode.
     */
    public int getOutPipWindowingMode() {
        // By default, simply reset the windowing mode to undefined.
        return WINDOWING_MODE_UNDEFINED;
    }

    protected void setBoundsStateForEntry(ComponentName componentName,
            PictureInPictureParams params,
            ActivityInfo activityInfo) {
        mPipBoundsState.setBoundsStateForEntry(componentName, activityInfo, params,
                mPipBoundsAlgorithm);
    }

    /**
     * Called when the display is going to rotate.
     *
     * @return {@code true} if it was handled, otherwise the existing pip logic
     *                      will deal with rotation.
     */
    public boolean handleRotateDisplay(int startRotation, int endRotation,
            WindowContainerTransaction wct) {
        return false;
    }

    /** @return whether the transition-request represents a pip-entry. */
    public boolean requestHasPipEnter(@NonNull TransitionRequestInfo request) {
        return request.getType() == TRANSIT_PIP;
    }

    /** Whether a particular change is a window that is entering pip. */
    public boolean isEnteringPip(@NonNull TransitionInfo.Change change,
            @WindowManager.TransitionType int transitType) {
        return false;
    }

    /** Whether a particular package is same as current pip package. */
    public boolean isPackageActiveInPip(@Nullable String packageName) {
        // No-op, to be handled differently in PIP1 and PIP2
        return false;
    }

    /** Whether a particular task id the current pip task id. */
    public boolean isTaskActiveInPip(int taskId) {
        // No-op, to be handled differently in PIP1 and PIP2
        return false;
    }

    /** Add PiP-related changes to `outWCT` for the given request. */
    public void augmentRequest(@NonNull IBinder transition,
            @NonNull TransitionRequestInfo request, @NonNull WindowContainerTransaction outWCT) {
        throw new IllegalStateException("Request isn't entering PiP");
    }

    /** Sets the type of animation when a PiP task appears. */
    public void setEnterAnimationType(@AnimationType int type) {
    }

    /** Play a transition animation for entering PiP on a specific PiP change. */
    public void startEnterAnimation(@NonNull final TransitionInfo.Change pipChange,
            @NonNull final SurfaceControl.Transaction startTransaction,
            @NonNull final SurfaceControl.Transaction finishTransaction,
            @NonNull final Transitions.TransitionFinishCallback finishCallback) {
    }

    /**
     * Applies the proper surface states (rounded corners/shadows) to pip surfaces in `info`.
     * This is intended to be used when PiP is part of another animation but isn't, itself,
     * animating (eg. unlocking).
     * @return `true` if there was a pip in `info`.
     */
    public boolean syncPipSurfaceState(@NonNull TransitionInfo info,
            @NonNull SurfaceControl.Transaction startTransaction,
            @NonNull SurfaceControl.Transaction finishTransaction) {
        return false;
    }

    /**
     * Gets a change amongst the transition targets that is in a different final orientation than
     * the display, signalling a potential fixed rotation transition.
     */
    @Nullable
    public TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) {
        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
            final TransitionInfo.Change change = info.getChanges().get(i);
            if (change.getEndFixedRotation() != ROTATION_UNDEFINED) {
                return change;
            }
        }
        return null;
    }

    /** End the currently-playing PiP animation. */
    public void end() {
    }

    /**
     * Finish the current transition if possible.
     */
    public void finishTransition() {
    }

    /**
     * Callback when the transition is aborted.
     */
    public void onTransitionAborted() {
    }

    /**
     * End the currently-playing PiP animation.
     *
     * @param onTransitionEnd callback to run upon finishing the playing transition.
     */
    public void end(@Nullable Runnable onTransitionEnd) {
    }

    /**
     * Clean up stored PIP state.
     * <p>
     * Should only be called after a task has exited due to external reasons. Does not modify the
     * task itself or move it out of PIP.
     */
    public void cleanUpState() {}

    /** Starts the {@link android.window.SystemPerformanceHinter.HighPerfSession}. */
    public void startHighPerfSession() {}

    /** Closes the {@link android.window.SystemPerformanceHinter.HighPerfSession}. */
    public void closeHighPerfSession() {}

    /**
     * Callback interface for PiP transitions (both from and to PiP mode)
     */
    public interface PipTransitionCallback {
        /**
         * Callback when the pip transition is started.
         */
        void onPipTransitionStarted(int direction, Rect pipBounds);

        /**
         * Callback when the pip transition is finished.
         */
        void onPipTransitionFinished(int direction);

        /**
         * Callback when the pip transition is cancelled.
         */
        void onPipTransitionCanceled(int direction);
    }

    /**
     * Dumps internal states.
     */
    public void dump(PrintWriter pw, String prefix) {}
}
