/*
 * Copyright (C) 2017 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.server.wm;

import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.INVALID_DISPLAY;

import static com.android.server.wm.ActivityStarter.Request;
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS;
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE;
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_DONE;
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.app.WindowConfiguration.WindowingMode;
import android.content.Context;
import android.content.pm.ActivityInfo.WindowLayout;
import android.graphics.Rect;

import com.android.internal.policy.DesktopModeCompatPolicy;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * {@link LaunchParamsController} calculates the {@link LaunchParams} by coordinating between
 * registered {@link LaunchParamsModifier}s.
 */
class LaunchParamsController {
    private final ActivityTaskManagerService mService;
    private final LaunchParamsPersister mPersister;
    private final List<LaunchParamsModifier> mModifiers = new ArrayList<>();

    // Temporary {@link LaunchParams} for internal calculations. This is kept separate from
    // {@code mTmpCurrent} and {@code mTmpResult} to prevent clobbering values.
    private final LaunchParams mTmpParams = new LaunchParams();

    private final LaunchParams mTmpCurrent = new LaunchParams();
    private final LaunchParams mTmpResult = new LaunchParams();

    LaunchParamsController(ActivityTaskManagerService service, LaunchParamsPersister persister) {
        mService = service;
        mPersister = persister;
    }

    /**
     * Creates a {@link LaunchParamsController} with default registered
     * {@link LaunchParamsModifier}s.
     */
    void registerDefaultModifiers(ActivityTaskSupervisor supervisor) {
        // {@link TaskLaunchParamsModifier} handles window layout preferences.
        final Context context = mService.mContext;
        registerModifier(new TaskLaunchParamsModifier(supervisor, context));
        registerModifier(new DesktopModeLaunchParamsModifier(context, supervisor,
                new DesktopModeCompatPolicy(context)));
    }

    /**
     * Returns the {@link LaunchParams} calculated by the registered modifiers
     * @param task      The {@link Task} currently being positioned.
     * @param layout    The specified {@link WindowLayout}.
     * @param activity  The {@link ActivityRecord} currently being positioned.
     * @param source    The {@link ActivityRecord} from which activity was started from.
     * @param options   The {@link ActivityOptions} specified for the activity.
     * @param request   The optional request from the activity starter.
     * @param phase     The {@link LaunchParamsModifier.Phase} that the resolution should finish.
     * @param result    The resulting params.
     */
    void calculate(Task task, WindowLayout layout, ActivityRecord activity, ActivityRecord source,
            ActivityOptions options, @Nullable Request request,
            @LaunchParamsModifier.Phase int phase, LaunchParams result) {
        result.reset();

        if (task != null || activity != null) {
            mPersister.getLaunchParams(task, activity, result);
        }

        // We start at the last registered {@link LaunchParamsModifier} as this represents
        // The modifier closest to the product level. Moving back through the list moves closer to
        // the platform logic.
        for (int i = mModifiers.size() - 1; i >= 0; --i) {
            mTmpCurrent.set(result);
            mTmpResult.reset();
            final LaunchParamsModifier modifier = mModifiers.get(i);

            switch (modifier.onCalculate(task, layout, activity, source, options, request, phase,
                    mTmpCurrent, mTmpResult)) {
                case RESULT_SKIP:
                    // Do not apply any results when we are told to skip
                    continue;
                case RESULT_DONE:
                    // Set result and return immediately.
                    result.merge(mTmpResult);
                    return;
                case RESULT_CONTINUE:
                    // Set result and continue
                    result.merge(mTmpResult);
                    break;
            }
        }

        if (activity != null && activity.requestedVrComponent != null) {
            // Check if the Activity is a VR activity. If so, it should be launched in main display.
            result.mPreferredTaskDisplayArea = mService.mRootWindowContainer
                    .getDefaultTaskDisplayArea();
        } else if (mService.mVr2dDisplayId != INVALID_DISPLAY) {
            // Get the virtual display ID from ActivityTaskManagerService. If that's set we
            // should always use that.
            result.mPreferredTaskDisplayArea = mService.mRootWindowContainer
                    .getDisplayContent(mService.mVr2dDisplayId).getDefaultTaskDisplayArea();
        }
    }

    /** @return {@code true} if bounds were set on the task. {@code false} otherwise. */
    boolean layoutTask(Task task, WindowLayout layout, ActivityRecord activity,
            ActivityRecord source, ActivityOptions options) {
        calculate(task, layout, activity, source, options, null /* request */, PHASE_BOUNDS,
                mTmpParams);

        // No changes, return.
        if (mTmpParams.isEmpty()) {
            return false;
        }

        mService.deferWindowLayout();
        try {
            if (mTmpParams.mBounds.isEmpty()) {
                if (!mTmpParams.mBoundsSet) {
                    return false;
                }
                // reset the task bounds
                task.setBounds(mTmpParams.mBounds);
                return true;
            }
            if (task.getRootTask().inMultiWindowMode()) {
                if (!mTmpParams.mAppBounds.isEmpty()) {
                    task.getRequestedOverrideConfiguration().windowConfiguration.setAppBounds(
                            mTmpParams.mAppBounds);
                }
                task.setBounds(mTmpParams.mBounds);
                return true;
            }

            // Setting last non-fullscreen bounds to the bounds so next time the task enters
            // freeform windowing mode it can be in this bounds.
            task.setLastNonFullscreenBounds(mTmpParams.mBounds);
            return false;
        } finally {
            mService.continueWindowLayout();
        }
    }

    /**
     * Adds a modifier to participate in future bounds calculation. Note that the last registered
     * {@link LaunchParamsModifier} will be the first to calculate the bounds.
     */
    void registerModifier(LaunchParamsModifier modifier) {
        if (mModifiers.contains(modifier)) {
            return;
        }

        mModifiers.add(modifier);
    }

    /**
     * A container for holding launch related fields.
     */
    static class LaunchParams {
        /** The bounds within the parent container. */
        @NonNull
        final Rect mBounds = new Rect();
        /** Whether the bounds have been set. */
        boolean mBoundsSet = false;
        /** The bounds within the parent container respecting insets. Usually empty. */
        @NonNull
        final Rect mAppBounds = new Rect();

        /** The display area the {@link Task} would prefer to be on. */
        @Nullable
        TaskDisplayArea mPreferredTaskDisplayArea;

        /** The root task the {@link Task} would prefer to be on. */
        @Nullable
        Task mPreferredRootTask;

        /** The windowing mode to be in. */
        @WindowingMode
        int mWindowingMode;

        /** Whether the Activity needs the safe region bounds. A {@code null} value means unset. */
        @Nullable
        Boolean mNeedsSafeRegionBounds = null;

        /** Sets values back to default. {@link #isEmpty} will return {@code true} once called. */
        void reset() {
            mBounds.setEmpty();
            mBoundsSet = false;
            mAppBounds.setEmpty();
            mPreferredTaskDisplayArea = null;
            mPreferredRootTask = null;
            mWindowingMode = WINDOWING_MODE_UNDEFINED;
            mNeedsSafeRegionBounds = null;
        }

        /** Copies the values set on the passed in {@link LaunchParams}. */
        void set(LaunchParams params) {
            mBounds.set(params.mBounds);
            mBoundsSet = params.mBoundsSet;
            mAppBounds.set(params.mAppBounds);
            mPreferredTaskDisplayArea = params.mPreferredTaskDisplayArea;
            mPreferredRootTask = params.mPreferredRootTask;
            mWindowingMode = params.mWindowingMode;
            mNeedsSafeRegionBounds = params.mNeedsSafeRegionBounds;
        }

        /** Merges the values set on the passed in {@link LaunchParams}. */
        void merge(LaunchParams params) {
            mBounds.set(params.mBounds);
            mBoundsSet = params.mBoundsSet;
            mAppBounds.set(params.mAppBounds);
            mPreferredTaskDisplayArea = params.mPreferredTaskDisplayArea;
            mPreferredRootTask = params.mPreferredRootTask;
            mWindowingMode = params.mWindowingMode;
            // Only update mNeedsSafeRegionBounds if a modifier updates it by setting a non null
            // value. Otherwise, carry over from previous modifiers
            if (params.mNeedsSafeRegionBounds != null) {
                mNeedsSafeRegionBounds = params.mNeedsSafeRegionBounds;
            }
        }

        /** Returns {@code true} if no values have been explicitly set. */
        boolean isEmpty() {
            return (mBounds.isEmpty() && !mBoundsSet) && mAppBounds.isEmpty()
                    && mPreferredTaskDisplayArea == null
                    && mPreferredRootTask == null
                    && mWindowingMode == WINDOWING_MODE_UNDEFINED && mNeedsSafeRegionBounds == null;
        }

        boolean hasWindowingMode() {
            return mWindowingMode != WINDOWING_MODE_UNDEFINED;
        }

        boolean hasPreferredTaskDisplayArea() {
            return mPreferredTaskDisplayArea != null;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            LaunchParams that = (LaunchParams) o;

            if (mPreferredTaskDisplayArea != that.mPreferredTaskDisplayArea) return false;
            if (mPreferredRootTask != that.mPreferredRootTask) return false;
            if (mWindowingMode != that.mWindowingMode) return false;
            if (!mAppBounds.equals(that.mAppBounds)) return false;
            if (!Objects.equals(mNeedsSafeRegionBounds, that.mNeedsSafeRegionBounds)) return false;
            if (mBoundsSet != that.mBoundsSet) return false;
            return !mBounds.isEmpty() ? mBounds.equals(that.mBounds) : that.mBounds.isEmpty();
        }

        @Override
        public int hashCode() {
            int result = !mBounds.isEmpty() ? mBounds.hashCode() : 0;
            result = 31 * result + Boolean.hashCode(mBoundsSet);
            result = 31 * result + mAppBounds.hashCode();
            result = 31 * result + (mPreferredTaskDisplayArea != null
                    ? mPreferredTaskDisplayArea.hashCode() : 0);
            result = 31 * result + (mPreferredRootTask != null
                    ? mPreferredRootTask.hashCode() : 0);
            result = 31 * result + mWindowingMode;
            result = 31 * result + (mNeedsSafeRegionBounds != null
                    ? Boolean.hashCode(mNeedsSafeRegionBounds) : 0);
            return result;
        }
    }

    /**
     * An interface implemented by those wanting to participate in bounds calculation.
     */
    interface LaunchParamsModifier {
        @Retention(RetentionPolicy.SOURCE)
        @IntDef({RESULT_SKIP, RESULT_DONE, RESULT_CONTINUE})
        @interface Result {}

        /** Returned when the modifier does not want to influence the bounds calculation */
        int RESULT_SKIP = 0;
        /**
         * Returned when the modifier has changed the bounds and would like its results to be the
         * final bounds applied.
         */
        int RESULT_DONE = 1;
        /**
         * Returned when the modifier has changed the bounds but is okay with other modifiers
         * influencing the bounds.
         */
        int RESULT_CONTINUE = 2;

        @Retention(RetentionPolicy.SOURCE)
        @IntDef({PHASE_DISPLAY, PHASE_WINDOWING_MODE, PHASE_BOUNDS})
        @interface Phase {}

        /**
         * Stops once we are done with preferred display calculation. The result returned from this
         * phase is only guaranteed to have a display area which display is the launch display.
         */
        int PHASE_DISPLAY = 0;

        /**
         * Stops once we are done with windowing mode calculation.
         */
        int PHASE_WINDOWING_MODE = 1;

        /**
         * Stops once we are done with display area calculation.
         */
        int PHASE_DISPLAY_AREA = 2;

        /**
         * Stops once we are done with window bounds calculation.
         */
        int PHASE_BOUNDS = 3;

        /**
         * Returns the launch params that the provided activity launch params should be overridden
         * to. {@link LaunchParamsModifier} can use this for various purposes, including: 1)
         * Providing default bounds if the launch bounds have not been provided. 2) Repositioning
         * the task so it doesn't get placed over an existing task. 3) Resizing the task so that its
         * dimensions match the activity's requested orientation.
         *
         * @param task          Can be: 1) the target task in which the source activity wants to
         *                      launch the target activity; 2) a newly created task that Android
         *                      gives a chance to override its launching bounds; 3) {@code null} if
         *                      this is called to override an activity's launching bounds.
         * @param layout        Desired layout when activity is first launched.
         * @param activity      Activity that is being started. This can be {@code null} on
         *                      re-parenting an activity to a new task (e.g. for
         *                      Picture-In-Picture). Tasks being created because an activity was
         *                      launched should have this be non-null.
         * @param source        the Activity that launched a new task. Could be {@code null}.
         * @param options       {@link ActivityOptions} used to start the activity with.
         * @param request       Optional data to give more context on the launch
         * @param phase         the calculation phase, see {@link Phase}
         * @param currentParams launching params after the process of last {@link
         *                      LaunchParamsModifier}.
         * @param outParams     the result params to be set.
         * @return see {@link LaunchParamsModifier.Result}
         */
        @Result
        int onCalculate(@Nullable Task task, @Nullable WindowLayout layout,
                @Nullable ActivityRecord activity, @Nullable ActivityRecord source,
                @Nullable ActivityOptions options, @Nullable Request request,
                @Phase int phase, LaunchParams currentParams, LaunchParams outParams);
    }
}
