/*
 * Copyright (C) 2018 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.quickstep.util;

import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Matrix.ScaleToFit;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.RemoteException;

import androidx.annotation.Nullable;

import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.views.BaseDragLayer;
import com.android.quickstep.RecentsModel;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskThumbnailView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.ISystemUiProxy;
import com.android.systemui.shared.recents.utilities.RectFEvaluator;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
import com.android.systemui.shared.system.TransactionCompat;
import com.android.systemui.shared.system.WindowManagerWrapper;

import java.util.function.BiFunction;

/**
 * Utility class to handle window clip animation
 */
@TargetApi(Build.VERSION_CODES.P)
public class ClipAnimationHelper {

    // The bounds of the source app in device coordinates
    private final Rect mSourceStackBounds = new Rect();
    // The insets of the source app
    private final Rect mSourceInsets = new Rect();
    // The source app bounds with the source insets applied, in the source app window coordinates
    private final RectF mSourceRect = new RectF();
    // The bounds of the task view in launcher window coordinates
    private final RectF mTargetRect = new RectF();
    // The insets to be used for clipping the app window, which can be larger than mSourceInsets
    // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In
    // app window coordinates.
    private final RectF mSourceWindowClipInsets = new RectF();
    // The insets to be used for clipping the app window. For live tile, we don't transform the clip
    // relative to the target rect.
    private final RectF mSourceWindowClipInsetsForLiveTile = new RectF();

    // The bounds of launcher (not including insets) in device coordinates
    public final Rect mHomeStackBounds = new Rect();

    // The clip rect in source app window coordinates
    private final RectF mClipRectF = new RectF();
    private final RectFEvaluator mRectFEvaluator = new RectFEvaluator();
    private final Matrix mTmpMatrix = new Matrix();
    private final Rect mTmpRect = new Rect();
    private final RectF mTmpRectF = new RectF();
    private final RectF mCurrentRectWithInsets = new RectF();
    // Corner radius of windows, in pixels
    private final float mWindowCornerRadius;
    // Corner radius of windows when they're in overview mode.
    private final float mTaskCornerRadius;
    // If windows can have real time rounded corners.
    private final boolean mSupportsRoundedCornersOnWindows;
    // Whether or not to actually use the rounded cornders on windows
    private boolean mUseRoundedCornersOnWindows;

    // Corner radius currently applied to transformed window.
    private float mCurrentCornerRadius;

    // Whether to boost the opening animation target layers, or the closing
    private int mBoostModeTargetLayers = -1;

    private TargetAlphaProvider mTaskAlphaCallback = (t, a) -> a;
    private TargetAlphaProvider mBaseAlphaCallback = (t, a) -> 1;

    public ClipAnimationHelper(Context context) {
        mWindowCornerRadius = getWindowCornerRadius(context.getResources());
        mSupportsRoundedCornersOnWindows = supportsRoundedCornersOnWindows(context.getResources());
        mTaskCornerRadius = TaskCornerRadius.get(context);
        mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows;
    }

    private void updateSourceStack(RemoteAnimationTargetCompat target) {
        mSourceInsets.set(target.contentInsets);
        mSourceStackBounds.set(target.sourceContainerBounds);

        // TODO: Should sourceContainerBounds already have this offset?
        mSourceStackBounds.offsetTo(target.position.x, target.position.y);

    }

    public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) {
        updateSourceStack(target);
        updateHomeBounds(homeStackBounds);
    }

    public void updateHomeBounds(Rect homeStackBounds) {
        mHomeStackBounds.set(homeStackBounds);
    }

    public void updateTargetRect(Rect targetRect) {
        mSourceRect.set(mSourceInsets.left, mSourceInsets.top,
                mSourceStackBounds.width() - mSourceInsets.right,
                mSourceStackBounds.height() - mSourceInsets.bottom);
        mTargetRect.set(targetRect);
        mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left,
                mHomeStackBounds.top - mSourceStackBounds.top);

        // Calculate the clip based on the target rect (since the content insets and the
        // launcher insets may differ, so the aspect ratio of the target rect can differ
        // from the source rect. The difference between the target rect (scaled to the
        // source rect) is the amount to clip on each edge.
        RectF scaledTargetRect = new RectF(mTargetRect);
        Utilities.scaleRectFAboutCenter(scaledTargetRect,
                mSourceRect.width() / mTargetRect.width());
        scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top);
        mSourceWindowClipInsets.set(
                Math.max(scaledTargetRect.left, 0),
                Math.max(scaledTargetRect.top, 0),
                Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0),
                Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0));
        mSourceWindowClipInsetsForLiveTile.set(mSourceWindowClipInsets);
        mSourceRect.set(scaledTargetRect);
    }

    public void prepareAnimation(DeviceProfile dp, boolean isOpening) {
        mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING;
        mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows && !dp.isMultiWindowMode;
    }

    public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params) {
        return applyTransform(targetSet, params, true /* launcherOnTop */);
    }

    public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params,
            boolean launcherOnTop) {
        float progress = params.progress;
        if (params.currentRect == null) {
            RectF currentRect;
            mTmpRectF.set(mTargetRect);
            Utilities.scaleRectFAboutCenter(mTmpRectF, params.offsetScale);
            currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF);
            currentRect.offset(params.offsetX, 0);

            // Don't clip past progress > 1.
            progress = Math.min(1, progress);
            final RectF sourceWindowClipInsets = params.forLiveTile
                    ? mSourceWindowClipInsetsForLiveTile : mSourceWindowClipInsets;
            mClipRectF.left = sourceWindowClipInsets.left * progress;
            mClipRectF.top = sourceWindowClipInsets.top * progress;
            mClipRectF.right =
                    mSourceStackBounds.width() - (sourceWindowClipInsets.right * progress);
            mClipRectF.bottom =
                    mSourceStackBounds.height() - (sourceWindowClipInsets.bottom * progress);
            params.setCurrentRectAndTargetAlpha(currentRect, 1);
        }

        SurfaceParams[] surfaceParams = new SurfaceParams[targetSet.unfilteredApps.length];
        for (int i = 0; i < targetSet.unfilteredApps.length; i++) {
            RemoteAnimationTargetCompat app = targetSet.unfilteredApps[i];
            mTmpMatrix.setTranslate(app.position.x, app.position.y);
            Rect crop = mTmpRect;
            crop.set(app.sourceContainerBounds);
            crop.offsetTo(0, 0);
            float alpha;
            int layer = RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers);
            float cornerRadius = 0f;
            float scale = Math.max(params.currentRect.width(), mTargetRect.width()) / crop.width();
            if (app.mode == targetSet.targetMode) {
                alpha = mTaskAlphaCallback.getAlpha(app, params.targetAlpha);
                if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
                    mTmpMatrix.setRectToRect(mSourceRect, params.currentRect, ScaleToFit.FILL);
                    mTmpMatrix.postTranslate(app.position.x, app.position.y);
                    mClipRectF.roundOut(crop);
                    if (mSupportsRoundedCornersOnWindows) {
                        if (params.cornerRadius > -1) {
                            cornerRadius = params.cornerRadius;
                            scale = params.currentRect.width() / crop.width();
                        } else {
                            float windowCornerRadius = mUseRoundedCornersOnWindows
                                    ? mWindowCornerRadius : 0;
                            cornerRadius = Utilities.mapRange(progress, windowCornerRadius,
                                    mTaskCornerRadius);
                        }
                        mCurrentCornerRadius = cornerRadius;
                    }
                } else if (targetSet.hasRecents) {
                    // If home has a different target then recents, reverse anim the
                    // home target.
                    alpha = 1 - (progress * params.targetAlpha);
                }
            } else {
                alpha = mBaseAlphaCallback.getAlpha(app, progress);
                if (ENABLE_QUICKSTEP_LIVE_TILE.get() && launcherOnTop) {
                    crop = null;
                    layer = Integer.MAX_VALUE;
                }
            }

            // Since radius is in Surface space, but we draw the rounded corners in screen space, we
            // have to undo the scale.
            surfaceParams[i] = new SurfaceParams(app.leash, alpha, mTmpMatrix, crop, layer,
                    cornerRadius / scale);
        }
        applySurfaceParams(params.syncTransactionApplier, surfaceParams);
        return params.currentRect;
    }

    public RectF getCurrentRectWithInsets() {
        mTmpMatrix.mapRect(mCurrentRectWithInsets, mClipRectF);
        return mCurrentRectWithInsets;
    }

    private void applySurfaceParams(@Nullable SyncRtSurfaceTransactionApplierCompat
            syncTransactionApplier, SurfaceParams[] params) {
        if (syncTransactionApplier != null) {
            syncTransactionApplier.scheduleApply(params);
        } else {
            TransactionCompat t = new TransactionCompat();
            for (SurfaceParams param : params) {
                SyncRtSurfaceTransactionApplierCompat.applyParams(t, param);
            }
            t.setEarlyWakeup();
            t.apply();
        }
    }

    public void setTaskAlphaCallback(TargetAlphaProvider callback) {
        mTaskAlphaCallback = callback;
    }

    public void setBaseAlphaCallback(TargetAlphaProvider callback) {
        mBaseAlphaCallback = callback;
    }

    public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) {
        fromTaskThumbnailView(ttv, rv, null);
    }

    public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv,
            @Nullable RemoteAnimationTargetCompat target) {
        BaseDraggingActivity activity = BaseDraggingActivity.fromContext(ttv.getContext());
        BaseDragLayer dl = activity.getDragLayer();

        int[] pos = new int[2];
        dl.getLocationOnScreen(pos);
        mHomeStackBounds.set(0, 0, dl.getWidth(), dl.getHeight());
        mHomeStackBounds.offset(pos[0], pos[1]);

        if (target != null) {
            updateSourceStack(target);
        } else  if (rv.shouldUseMultiWindowTaskSizeStrategy()) {
            updateStackBoundsToMultiWindowTaskSize(activity);
        } else {
            mSourceStackBounds.set(mHomeStackBounds);
            Rect fallback = dl.getInsets();
            mSourceInsets.set(ttv.getInsets(fallback));
        }

        Rect targetRect = new Rect();
        dl.getDescendantRectRelativeToSelf(ttv, targetRect);
        updateTargetRect(targetRect);

        if (target == null) {
            // Transform the clip relative to the target rect. Only do this in the case where we
            // aren't applying the insets to the app windows (where the clip should be in target app
            // space)
            float scale = mTargetRect.width() / mSourceRect.width();
            mSourceWindowClipInsets.left = mSourceWindowClipInsets.left * scale;
            mSourceWindowClipInsets.top = mSourceWindowClipInsets.top * scale;
            mSourceWindowClipInsets.right = mSourceWindowClipInsets.right * scale;
            mSourceWindowClipInsets.bottom = mSourceWindowClipInsets.bottom * scale;
        }
    }

    /**
     * Compute scale and translation y such that the specified task view fills the screen.
     */
    public ClipAnimationHelper updateForFullscreenOverview(TaskView v) {
        TaskThumbnailView thumbnailView = v.getThumbnail();
        RecentsView recentsView = v.getRecentsView();
        fromTaskThumbnailView(thumbnailView, recentsView);
        Rect taskSize = new Rect();
        recentsView.getTaskSize(taskSize);
        updateTargetRect(taskSize);
        return this;
    }

    /**
     * @return The source rect's scale and translation relative to the target rect.
     */
    public LauncherState.ScaleAndTranslation getScaleAndTranslation() {
        float scale = mSourceRect.width() / mTargetRect.width();
        float translationY = mSourceRect.centerY() - mSourceRect.top - mTargetRect.centerY();
        return new LauncherState.ScaleAndTranslation(scale, 0, translationY);
    }

    private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) {
        ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy();
        if (sysUiProxy != null) {
            try {
                mSourceStackBounds.set(sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds());
                return;
            } catch (RemoteException e) {
                // Use half screen size
            }
        }

        // Assume that the task size is half screen size (minus the insets and the divider size)
        DeviceProfile fullDp = activity.getDeviceProfile().getFullScreenProfile();
        // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to
        // account for system insets
        int taskWidth = fullDp.availableWidthPx;
        int taskHeight = fullDp.availableHeightPx;
        int halfDividerSize = activity.getResources()
                .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2;

        Rect insets = new Rect();
        WindowManagerWrapper.getInstance().getStableInsets(insets);
        if (fullDp.isLandscape) {
            taskWidth = taskWidth / 2 - halfDividerSize;
        } else {
            taskHeight = taskHeight / 2 - halfDividerSize;
        }

        // Align the task to bottom left/right edge (closer to nav bar).
        int left = activity.getDeviceProfile().isSeascape() ? insets.left
                : (insets.left + fullDp.availableWidthPx - taskWidth);
        mSourceStackBounds.set(0, 0, taskWidth, taskHeight);
        mSourceStackBounds.offset(left, insets.top + fullDp.availableHeightPx - taskHeight);
    }

    public RectF getTargetRect() {
        return mTargetRect;
    }

    public float getCurrentCornerRadius() {
        return mCurrentCornerRadius;
    }

    public interface TargetAlphaProvider {
        float getAlpha(RemoteAnimationTargetCompat target, float expectedAlpha);
    }

    public static class TransformParams {
        float progress;
        public float offsetX;
        public float offsetScale;
        @Nullable RectF currentRect;
        float targetAlpha;
        boolean forLiveTile;
        float cornerRadius;

        SyncRtSurfaceTransactionApplierCompat syncTransactionApplier;

        public TransformParams() {
            progress = 0;
            offsetX = 0;
            offsetScale = 1;
            currentRect = null;
            targetAlpha = 0;
            forLiveTile = false;
            cornerRadius = -1;
        }

        public TransformParams setProgress(float progress) {
            this.progress = progress;
            this.currentRect = null;
            return this;
        }

        public TransformParams setCornerRadius(float cornerRadius) {
            this.cornerRadius = cornerRadius;
            return this;
        }

        public TransformParams setCurrentRectAndTargetAlpha(RectF currentRect, float targetAlpha) {
            this.currentRect = currentRect;
            this.targetAlpha = targetAlpha;
            return this;
        }

        public TransformParams setOffsetX(float offsetX) {
            this.offsetX = offsetX;
            return this;
        }

        public TransformParams setOffsetScale(float offsetScale) {
            this.offsetScale = offsetScale;
            return this;
        }

        public TransformParams setForLiveTile(boolean forLiveTile) {
            this.forLiveTile = forLiveTile;
            return this;
        }

        public TransformParams setSyncTransactionApplier(
                SyncRtSurfaceTransactionApplierCompat applier) {
            this.syncTransactionApplier = applier;
            return this;
        }
    }
}
