/*
 * 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.launcher3.taskbar;

import static android.view.Display.DEFAULT_DISPLAY;

import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Outline;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewOutlineProvider;

import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.RevealOutlineAnimation;
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.quickstep.NavHandle;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
import com.android.wm.shell.shared.handles.RegionSamplingHelper;

import java.io.PrintWriter;

/**
 * Handles properties/data collection, then passes the results to our stashed handle View to render.
 */
public class StashedHandleViewController implements TaskbarControllers.LoggableTaskbarController,
        NavHandle {

    public static final int ALPHA_INDEX_STASHED = 0;
    public static final int ALPHA_INDEX_HOME_DISABLED = 1;
    public static final int ALPHA_INDEX_ASSISTANT_INVOKED = 2;
    public static final int ALPHA_INDEX_HIDDEN_WHILE_DREAMING = 3;
    private static final int NUM_ALPHA_CHANNELS = 4;

    // Values for long press animations, picked to most closely match navbar spec.
    private static final float SCALE_TOUCH_ANIMATION_SHRINK = 0.85f;
    private static final float SCALE_TOUCH_ANIMATION_EXPAND = 1.18f;

    /**
     * The SharedPreferences key for whether the stashed handle region is dark.
     */
    private static final String SHARED_PREFS_STASHED_HANDLE_REGION_DARK_KEY =
            "stashed_handle_region_is_dark";

    private final TaskbarActivityContext mActivity;
    private final SharedPreferences mPrefs;
    private final StashedHandleView mStashedHandleView;
    private int mStashedHandleWidth;
    private final int mStashedHandleHeight;
    private RegionSamplingHelper mRegionSamplingHelper;
    private final MultiValueAlpha mTaskbarStashedHandleAlpha;
    private final AnimatedFloat mTaskbarStashedHandleHintScale = new AnimatedFloat(
            this::updateStashedHandleHintScale);

    // Initialized in init.
    private TaskbarControllers mControllers;
    private int mTaskbarSize;

    // The bounds we want to clip to in the settled state when showing the stashed handle.
    private final Rect mStashedHandleBounds = new Rect();
    private float mStashedHandleRadius;

    // When the reveal animation is cancelled, we can assume it's about to create a new animation,
    // which should start off at the same point the cancelled one left off.
    private float mStartProgressForNextRevealAnim;
    private boolean mWasLastRevealAnimReversed;

    // States that affect whether region sampling is enabled or not
    private boolean mIsStashed;
    private boolean mIsLumaSamplingEnabled;
    private boolean mIsAppTransitionPending;
    private boolean mTaskbarHidden;

    private float mTranslationYForSwipe;
    private float mTranslationYForStash;

    public StashedHandleViewController(TaskbarActivityContext activity,
            StashedHandleView stashedHandleView) {
        mActivity = activity;
        mPrefs = LauncherPrefs.getPrefs(mActivity);
        mStashedHandleView = stashedHandleView;
        mTaskbarStashedHandleAlpha = new MultiValueAlpha(mStashedHandleView, NUM_ALPHA_CHANNELS);
        mTaskbarStashedHandleAlpha.setUpdateVisibility(true);
        mStashedHandleView.updateHandleColor(
                mPrefs.getBoolean(SHARED_PREFS_STASHED_HANDLE_REGION_DARK_KEY, false),
                false /* animate */);
        final Resources resources = mActivity.getResources();
        mStashedHandleHeight = resources.getDimensionPixelSize(
                R.dimen.taskbar_stashed_handle_height);
    }

    public void init(TaskbarControllers controllers) {
        mControllers = controllers;
        DeviceProfile deviceProfile = mActivity.getDeviceProfile();
        Resources resources = mActivity.getResources();
        if (mActivity.isPhoneGestureNavMode() || mActivity.isTinyTaskbar()
                || mActivity.isBubbleBarOnPhone()) {
            mTaskbarSize = resources.getDimensionPixelSize(R.dimen.taskbar_phone_size);
            mStashedHandleWidth =
                    resources.getDimensionPixelSize(R.dimen.taskbar_stashed_small_screen);
        } else {
            mTaskbarSize = deviceProfile.taskbarHeight;
            mStashedHandleWidth = resources
                    .getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width);
        }
        int taskbarBottomMargin = deviceProfile.taskbarBottomMargin;
        mStashedHandleView.getLayoutParams().height = mTaskbarSize + taskbarBottomMargin;

        mTaskbarStashedHandleAlpha.get(ALPHA_INDEX_STASHED).setValue(
                mActivity.isPhoneGestureNavMode() ? 1 : 0);
        mTaskbarStashedHandleHintScale.updateValue(1f);

        final int stashedTaskbarHeight = mControllers.taskbarStashController.getStashedHeight();
        mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() {
            @Override
            public void getOutline(View view, Outline outline) {
                final int stashedCenterX = view.getWidth() / 2;
                final int stashedCenterY = view.getHeight() - stashedTaskbarHeight / 2;
                mStashedHandleBounds.set(
                        stashedCenterX - mStashedHandleWidth / 2,
                        stashedCenterY - mStashedHandleHeight / 2,
                        stashedCenterX + mStashedHandleWidth / 2,
                        stashedCenterY + mStashedHandleHeight / 2);
                mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
                mStashedHandleRadius = view.getHeight() / 2f;
                outline.setRoundRect(mStashedHandleBounds, mStashedHandleRadius);
            }
        });

        mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) -> {
            final int stashedCenterX = view.getWidth() / 2;
            final int stashedCenterY = view.getHeight() - stashedTaskbarHeight / 2;

            view.setPivotX(stashedCenterX);
            view.setPivotY(stashedCenterY);
        });
        initRegionSampler();
        if (mActivity.isPhoneGestureNavMode()) {
            onIsStashedChanged(true);
        }
    }

    /**
     * Returns the stashed handle bounds.
     * @param out The destination rect.
     */
    public void getStashedHandleBounds(Rect out) {
        out.set(mStashedHandleBounds);
    }

    private void initRegionSampler() {
        mRegionSamplingHelper = new RegionSamplingHelper(mStashedHandleView,
                new RegionSamplingHelper.SamplingCallback() {
                    @Override
                    public void onRegionDarknessChanged(boolean isRegionDark) {
                        mStashedHandleView.updateHandleColor(isRegionDark, true /* animate */);
                        mPrefs.edit().putBoolean(SHARED_PREFS_STASHED_HANDLE_REGION_DARK_KEY,
                                isRegionDark).apply();
                    }

                    @Override
                    public Rect getSampledRegion(View sampledView) {
                        return mStashedHandleView.getSampledRegion();
                    }
                }, Executors.UI_HELPER_EXECUTOR);
    }


    public void onDestroy() {
        if (mRegionSamplingHelper != null) {
            mRegionSamplingHelper.stopAndDestroy();
        }
        mRegionSamplingHelper = null;
    }

    public MultiPropertyFactory<View> getStashedHandleAlpha() {
        return mTaskbarStashedHandleAlpha;
    }

    public AnimatedFloat getStashedHandleHintScale() {
        return mTaskbarStashedHandleHintScale;
    }

    /**
     * Creates and returns a {@link RevealOutlineAnimation} Animator that updates the stashed handle
     * shape and size. When stashed, the shape is a thin rounded pill. When unstashed, the shape
     * morphs into the size of where the taskbar icons will be.
     */
    public Animator createRevealAnimToIsStashed(boolean isStashed) {
        Rect visualBounds = mControllers.taskbarViewController
                .getTransientTaskbarIconLayoutBounds();
        float startRadius = mStashedHandleRadius;

        if (mActivity.isTransientTaskbar()) {
            // Account for the full visual height of the transient taskbar.
            int heightDiff = (mTaskbarSize - visualBounds.height()) / 2;
            visualBounds.top -= heightDiff;
            visualBounds.bottom += heightDiff;

            startRadius = visualBounds.height() / 2f;
        }

        final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider(
                startRadius, mStashedHandleRadius, visualBounds, mStashedHandleBounds);

        boolean isReversed = !isStashed;
        boolean changingDirection = mWasLastRevealAnimReversed != isReversed;
        mWasLastRevealAnimReversed = isReversed;
        if (changingDirection) {
            mStartProgressForNextRevealAnim = 1f - mStartProgressForNextRevealAnim;
        }

        ValueAnimator revealAnim = handleRevealProvider.createRevealAnimator(mStashedHandleView,
                isReversed, mStartProgressForNextRevealAnim);
        revealAnim.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                mStartProgressForNextRevealAnim = ((ValueAnimator) animation).getAnimatedFraction();
            }
        });
        return revealAnim;
    }

    /** Called when taskbar is stashed or unstashed. */
    public void onIsStashedChanged(boolean isStashed) {
        mIsStashed = isStashed;
        updateSamplingState();
    }

    public void onNavigationBarLumaSamplingEnabled(int displayId, boolean enable) {
        if (DEFAULT_DISPLAY != displayId) {
            return;
        }

        mIsLumaSamplingEnabled = enable;
        updateSamplingState();
    }

    public void setIsAppTransitionPending(boolean pending) {
        mIsAppTransitionPending = pending;
        updateSamplingState();
    }

    private void updateSamplingState() {
        updateRegionSamplingWindowVisibility();
        if (shouldSample()) {
            mStashedHandleView.updateSampledRegion(mStashedHandleBounds);
            mRegionSamplingHelper.start(mStashedHandleView.getSampledRegion());
        } else {
            mRegionSamplingHelper.stop();
        }
    }

    private boolean shouldSample() {
        return mIsStashed && mIsLumaSamplingEnabled && !mIsAppTransitionPending;
    }

    protected void updateStashedHandleHintScale() {
        mStashedHandleView.setScaleX(mTaskbarStashedHandleHintScale.value);
        mStashedHandleView.setScaleY(mTaskbarStashedHandleHintScale.value);
    }

    /**
     * Sets the translation of the stashed handle during the swipe up gesture.
     */
    protected void setTranslationYForSwipe(float transY) {
        mTranslationYForSwipe = transY;
        updateTranslationY();
    }

    /**
     * Sets the translation of the stashed handle during the spring on stash animation.
     */
    protected void setTranslationYForStash(float transY) {
        mTranslationYForStash = transY;
        updateTranslationY();
    }

    private void updateTranslationY() {
        mStashedHandleView.setTranslationY(mTranslationYForSwipe + mTranslationYForStash);
    }

    /**
     * Should be called when the home button is disabled, so we can hide this handle as well.
     */
    public void setIsHomeButtonDisabled(boolean homeDisabled) {
        mTaskbarStashedHandleAlpha.get(ALPHA_INDEX_HOME_DISABLED).setValue(
                homeDisabled ? 0 : 1);
    }

    public void updateStateForSysuiFlags(@SystemUiStateFlags long systemUiStateFlags) {
        mTaskbarHidden = (systemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) != 0;
        updateRegionSamplingWindowVisibility();
    }

    private void updateRegionSamplingWindowVisibility() {
        mRegionSamplingHelper.setWindowVisible(shouldSample() && !mTaskbarHidden);
    }

    public boolean isStashedHandleVisible() {
        return mStashedHandleView.getVisibility() == View.VISIBLE;
    }

    @Override
    public void dumpLogs(String prefix, PrintWriter pw) {
        pw.println(prefix + "StashedHandleViewController:");

        pw.println(prefix + "\tisStashedHandleVisible=" + isStashedHandleVisible());
        pw.println(prefix + "\tmStashedHandleWidth=" + mStashedHandleWidth);
        pw.println(prefix + "\tmStashedHandleHeight=" + mStashedHandleHeight);
        mRegionSamplingHelper.dump(prefix, pw);
    }

    @Override
    public void animateNavBarLongPress(boolean isTouchDown, boolean shrink, long durationMs) {
        float targetScale;
        if (isTouchDown) {
            targetScale = shrink ? SCALE_TOUCH_ANIMATION_SHRINK : SCALE_TOUCH_ANIMATION_EXPAND;
        } else {
            targetScale = 1f;
        }
        mStashedHandleView.animateScale(targetScale, durationMs);
    }

    @Override
    public boolean isNavHandleStashedTaskbar() {
        return true;
    }

    @Override
    public boolean canNavHandleBeLongPressed() {
        return isStashedHandleVisible();
    }

    @Override
    public int getNavHandleWidth(Context context) {
        return mStashedHandleWidth;
    }
}
