/*
 * Copyright (C) 2023 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 androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID;

import static com.android.launcher3.taskbar.KeyboardQuickSwitchController.MAX_TASKS;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Outline;
import android.graphics.Rect;
import android.icu.text.MessageFormat;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.animation.Interpolator;
import android.widget.HorizontalScrollView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;

import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.Interpolators;
import com.android.quickstep.util.GroupTask;

import java.util.HashMap;
import java.util.List;
import java.util.Locale;

/**
 * View that allows quick switching between recent tasks through keyboard alt-tab and alt-shift-tab
 * commands.
 */
public class KeyboardQuickSwitchView extends ConstraintLayout {

    private static final long OUTLINE_ANIMATION_DURATION_MS = 333;
    private static final float OUTLINE_START_HEIGHT_FACTOR = 0.45f;
    private static final float OUTLINE_START_RADIUS_FACTOR = 0.25f;
    private static final Interpolator OPEN_OUTLINE_INTERPOLATOR =
            Interpolators.EMPHASIZED_DECELERATE;
    private static final Interpolator CLOSE_OUTLINE_INTERPOLATOR =
            Interpolators.EMPHASIZED_ACCELERATE;

    private static final long ALPHA_ANIMATION_DURATION_MS = 83;
    private static final long ALPHA_ANIMATION_START_DELAY_MS = 67;

    private static final long CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS = 500;
    private static final long CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS = 333;
    private static final float CONTENT_START_TRANSLATION_X_DP = 32;
    private static final float CONTENT_START_TRANSLATION_Y_DP = 40;
    private static final Interpolator OPEN_TRANSLATION_X_INTERPOLATOR = Interpolators.EMPHASIZED;
    private static final Interpolator OPEN_TRANSLATION_Y_INTERPOLATOR =
            Interpolators.EMPHASIZED_DECELERATE;
    private static final Interpolator CLOSE_TRANSLATION_Y_INTERPOLATOR =
            Interpolators.EMPHASIZED_ACCELERATE;

    private static final long CONTENT_ALPHA_ANIMATION_DURATION_MS = 83;
    private static final long CONTENT_ALPHA_ANIMATION_START_DELAY_MS = 83;

    private final AnimatedFloat mOutlineAnimationProgress = new AnimatedFloat(
            this::invalidateOutline);

    private HorizontalScrollView mScrollView;
    private ConstraintLayout mContent;

    private int mTaskViewHeight;
    private int mSpacing;
    private int mOutlineRadius;
    private boolean mIsRtl;

    @Nullable private AnimatorSet mOpenAnimation;

    @Nullable private KeyboardQuickSwitchViewController.ViewCallbacks mViewCallbacks;

    public KeyboardQuickSwitchView(@NonNull Context context) {
        this(context, null);
    }

    public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs,
            int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public KeyboardQuickSwitchView(@NonNull Context context, @Nullable AttributeSet attrs,
            int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mScrollView = findViewById(R.id.scroll_view);
        mContent = findViewById(R.id.content);

        Resources resources = getResources();
        mTaskViewHeight = resources.getDimensionPixelSize(
                R.dimen.keyboard_quick_switch_taskview_height);
        mSpacing = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_spacing);
        mOutlineRadius = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_view_radius);
        mIsRtl = Utilities.isRtl(resources);
    }

    @NonNull
    private KeyboardQuickSwitchTaskView createAndAddTaskView(
            int index,
            int width,
            boolean isFinalView,
            boolean updateTasks,
            @NonNull LayoutInflater layoutInflater,
            @Nullable View previousView,
            @NonNull List<GroupTask> groupTasks) {
        KeyboardQuickSwitchTaskView taskView = (KeyboardQuickSwitchTaskView) layoutInflater.inflate(
                R.layout.keyboard_quick_switch_taskview, mContent, false);
        taskView.setId(View.generateViewId());
        taskView.setOnClickListener(v -> mViewCallbacks.launchTappedTask(index));

        LayoutParams lp = new LayoutParams(width, mTaskViewHeight);
        // Create a right-to-left ordering of views (or left-to-right in RTL locales)
        if (previousView != null) {
            lp.endToStart = previousView.getId();
        } else {
            lp.endToEnd = PARENT_ID;
        }
        lp.topToTop = PARENT_ID;
        lp.bottomToBottom = PARENT_ID;
        // Add spacing between views
        lp.setMarginEnd(mSpacing);
        if (isFinalView) {
            // Add spacing to the start of the final view so that scrolling ends with some padding.
            lp.startToStart = PARENT_ID;
            lp.setMarginStart(mSpacing);
            lp.horizontalBias = 1f;
        }

        GroupTask groupTask = groupTasks.get(index);
        taskView.setThumbnails(
                groupTask.task1,
                groupTask.task2,
                updateTasks ? mViewCallbacks::updateThumbnailInBackground : null,
                updateTasks ? mViewCallbacks::updateTitleInBackground : null);

        mContent.addView(taskView, lp);
        return taskView;
    }

    private void createAndAddOverviewButton(
            int width,
            @NonNull LayoutInflater layoutInflater,
            @Nullable View previousView,
            @NonNull String overflowString) {
        KeyboardQuickSwitchTaskView overviewButton =
                (KeyboardQuickSwitchTaskView) layoutInflater.inflate(
                        R.layout.keyboard_quick_switch_overview, this, false);
        overviewButton.setOnClickListener(v -> mViewCallbacks.launchTappedTask(MAX_TASKS));

        overviewButton.<TextView>findViewById(R.id.text).setText(overflowString);

        ConstraintLayout.LayoutParams lp = new ConstraintLayout.LayoutParams(
                width, mTaskViewHeight);
        lp.startToStart = PARENT_ID;
        lp.endToStart = previousView.getId();
        lp.topToTop = PARENT_ID;
        lp.bottomToBottom = PARENT_ID;
        lp.setMarginEnd(mSpacing);
        lp.setMarginStart(mSpacing);

        mContent.addView(overviewButton, lp);
    }

    protected void applyLoadPlan(
            @NonNull Context context,
            @NonNull List<GroupTask> groupTasks,
            int numHiddenTasks,
            boolean updateTasks,
            int currentFocusIndexOverride,
            @NonNull KeyboardQuickSwitchViewController.ViewCallbacks viewCallbacks) {
        if (groupTasks.isEmpty()) {
            // Do not show the quick switch view.
            return;
        }
        mViewCallbacks = viewCallbacks;
        Resources resources = context.getResources();
        int width = resources.getDimensionPixelSize(R.dimen.keyboard_quick_switch_taskview_width);
        View previousView = null;

        LayoutInflater layoutInflater = LayoutInflater.from(context);
        int tasksToDisplay = Math.min(MAX_TASKS, groupTasks.size());
        for (int i = 0; i < tasksToDisplay; i++) {
            previousView = createAndAddTaskView(
                    i,
                    width,
                    /* isFinalView= */ i == tasksToDisplay - 1 && numHiddenTasks == 0,
                    updateTasks,
                    layoutInflater,
                    previousView,
                    groupTasks);
        }

        if (numHiddenTasks > 0) {
            HashMap<String, Integer> args = new HashMap<>();
            args.put("count", numHiddenTasks);
            createAndAddOverviewButton(
                    width,
                    layoutInflater,
                    previousView,
                    new MessageFormat(
                            resources.getString(R.string.quick_switch_overflow),
                            Locale.getDefault()).format(args));
        }

        getViewTreeObserver().addOnGlobalLayoutListener(
                new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        animateOpen(currentFocusIndexOverride);

                        getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    }
                });
    }

    protected Animator getCloseAnimation() {
        AnimatorSet closeAnimation = new AnimatorSet();

        Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(0f);
        outlineAnimation.setDuration(OUTLINE_ANIMATION_DURATION_MS);
        outlineAnimation.setInterpolator(CLOSE_OUTLINE_INTERPOLATOR);
        closeAnimation.play(outlineAnimation);

        Animator alphaAnimation = ObjectAnimator.ofFloat(this, ALPHA, 1f, 0f);
        alphaAnimation.setStartDelay(ALPHA_ANIMATION_START_DELAY_MS);
        alphaAnimation.setDuration(ALPHA_ANIMATION_DURATION_MS);
        closeAnimation.play(alphaAnimation);

        Animator translationYAnimation = ObjectAnimator.ofFloat(
                mScrollView, TRANSLATION_Y, 0, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP));
        translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS);
        translationYAnimation.setInterpolator(CLOSE_TRANSLATION_Y_INTERPOLATOR);
        closeAnimation.play(translationYAnimation);

        Animator contentAlphaAnimation = ObjectAnimator.ofFloat(mScrollView, ALPHA, 1f, 0f);
        contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS);
        closeAnimation.play(contentAlphaAnimation);

        closeAnimation.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                if (mOpenAnimation != null) {
                    mOpenAnimation.cancel();
                }
            }
        });

        return closeAnimation;
    }

    private void animateOpen(int currentFocusIndexOverride) {
        if (mOpenAnimation != null) {
            // Restart animation since currentFocusIndexOverride can change the initial scroll.
            mOpenAnimation.cancel();
        }
        mOpenAnimation = new AnimatorSet();

        Animator outlineAnimation = mOutlineAnimationProgress.animateToValue(1f);
        outlineAnimation.setDuration(OUTLINE_ANIMATION_DURATION_MS);
        mOpenAnimation.play(outlineAnimation);

        Animator alphaAnimation = ObjectAnimator.ofFloat(this, ALPHA, 0f, 1f);
        alphaAnimation.setDuration(ALPHA_ANIMATION_DURATION_MS);
        mOpenAnimation.play(alphaAnimation);

        Animator translationXAnimation = ObjectAnimator.ofFloat(
                mScrollView, TRANSLATION_X, -Utilities.dpToPx(CONTENT_START_TRANSLATION_X_DP), 0);
        translationXAnimation.setDuration(CONTENT_TRANSLATION_X_ANIMATION_DURATION_MS);
        translationXAnimation.setInterpolator(OPEN_TRANSLATION_X_INTERPOLATOR);
        mOpenAnimation.play(translationXAnimation);

        Animator translationYAnimation = ObjectAnimator.ofFloat(
                mScrollView, TRANSLATION_Y, -Utilities.dpToPx(CONTENT_START_TRANSLATION_Y_DP), 0);
        translationYAnimation.setDuration(CONTENT_TRANSLATION_Y_ANIMATION_DURATION_MS);
        translationYAnimation.setInterpolator(OPEN_TRANSLATION_Y_INTERPOLATOR);
        mOpenAnimation.play(translationYAnimation);

        Animator contentAlphaAnimation = ObjectAnimator.ofFloat(mScrollView, ALPHA, 0f, 1f);
        contentAlphaAnimation.setStartDelay(CONTENT_ALPHA_ANIMATION_START_DELAY_MS);
        contentAlphaAnimation.setDuration(CONTENT_ALPHA_ANIMATION_DURATION_MS);
        mOpenAnimation.play(contentAlphaAnimation);

        ViewOutlineProvider outlineProvider = getOutlineProvider();
        mOpenAnimation.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                setClipToPadding(false);
                setOutlineProvider(new ViewOutlineProvider() {
                    @Override
                    public void getOutline(View view, Outline outline) {
                        outline.setRoundRect(
                                /* rect= */ new Rect(
                                        /* left= */ 0,
                                        /* top= */ 0,
                                        /* right= */ getWidth(),
                                        /* bottom= */
                                        (int) (getHeight() * Utilities.mapBoundToRange(
                                                mOutlineAnimationProgress.value,
                                                /* lowerBound= */ 0f,
                                                /* upperBound= */ 1f,
                                                /* toMin= */ OUTLINE_START_HEIGHT_FACTOR,
                                                /* toMax= */ 1f,
                                                OPEN_OUTLINE_INTERPOLATOR))),
                                /* radius= */ mOutlineRadius * Utilities.mapBoundToRange(
                                        mOutlineAnimationProgress.value,
                                        /* lowerBound= */ 0f,
                                        /* upperBound= */ 1f,
                                        /* toMin= */ OUTLINE_START_RADIUS_FACTOR,
                                        /* toMax= */ 1f,
                                        OPEN_OUTLINE_INTERPOLATOR));
                    }
                });
                if (currentFocusIndexOverride == -1) {
                    initializeScroll(/* index= */ 0, /* shouldTruncateTarget= */ false);
                } else {
                    animateFocusMove(-1, currentFocusIndexOverride);
                }
                mScrollView.setVisibility(VISIBLE);
                setVisibility(VISIBLE);
                requestFocus();
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                setClipToPadding(true);
                setOutlineProvider(outlineProvider);
                invalidateOutline();
                mOpenAnimation = null;
            }
        });

        mOpenAnimation.start();
    }

    protected void animateFocusMove(int fromIndex, int toIndex) {
        KeyboardQuickSwitchTaskView focusedTask = getTaskAt(toIndex);
        if (focusedTask == null) {
            return;
        }
        AnimatorSet focusAnimation = new AnimatorSet();
        focusAnimation.play(focusedTask.getFocusAnimator(true));

        KeyboardQuickSwitchTaskView previouslyFocusedTask = getTaskAt(fromIndex);
        if (previouslyFocusedTask != null) {
            focusAnimation.play(previouslyFocusedTask.getFocusAnimator(false));
        }

        focusAnimation.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
                focusedTask.requestAccessibilityFocus();
                if (fromIndex == -1) {
                    int firstVisibleTaskIndex = toIndex == 0
                            ? toIndex
                            : getTaskAt(toIndex - 1) == null
                                    ? toIndex : toIndex - 1;
                    // Scroll so that the previous task view is truncated as a visual hint that
                    // there are more tasks
                    initializeScroll(
                            firstVisibleTaskIndex,
                            /* shouldTruncateTarget= */ firstVisibleTaskIndex != toIndex);
                } else if (toIndex > fromIndex || toIndex == 0) {
                    // Scrolling to next task view
                    if (mIsRtl) {
                        scrollRightTo(focusedTask);
                    } else {
                        scrollLeftTo(focusedTask);
                    }
                } else {
                    // Scrolling to previous task view
                    if (mIsRtl) {
                        scrollLeftTo(focusedTask);
                    } else {
                        scrollRightTo(focusedTask);
                    }
                }
                if (mViewCallbacks != null) {
                    mViewCallbacks.updateCurrentFocusIndex(toIndex);
                }
            }
        });

        focusAnimation.start();
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        return (mViewCallbacks != null && mViewCallbacks.onKeyUp(keyCode, event, mIsRtl))
                || super.onKeyUp(keyCode, event);
    }

    private void initializeScroll(int index, boolean shouldTruncateTarget) {
        View task = getTaskAt(index);
        if (task == null) {
            return;
        }
        if (mIsRtl) {
            scrollRightTo(
                    task, shouldTruncateTarget, /* smoothScroll= */ false);
        } else {
            scrollLeftTo(
                    task, shouldTruncateTarget, /* smoothScroll= */ false);
        }
    }

    private void scrollRightTo(@NonNull View targetTask) {
        scrollRightTo(targetTask, /* shouldTruncateTarget= */ false, /* smoothScroll= */ true);
    }

    private void scrollRightTo(
            @NonNull View targetTask, boolean shouldTruncateTarget, boolean smoothScroll) {
        if (smoothScroll && !shouldScroll(targetTask, shouldTruncateTarget)) {
            return;
        }
        int scrollTo = targetTask.getLeft() - mSpacing
                + (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0);
        // Scroll so that the focused task is to the left of the list
        if (smoothScroll) {
            mScrollView.smoothScrollTo(scrollTo, 0);
        } else {
            mScrollView.scrollTo(scrollTo, 0);
        }
    }

    private void scrollLeftTo(@NonNull View targetTask) {
        scrollLeftTo(targetTask, /* shouldTruncateTarget= */ false, /* smoothScroll= */ true);
    }

    private void scrollLeftTo(
            @NonNull View targetTask, boolean shouldTruncateTarget, boolean smoothScroll) {
        if (smoothScroll && !shouldScroll(targetTask, shouldTruncateTarget)) {
            return;
        }
        int scrollTo = targetTask.getRight() + mSpacing - mScrollView.getWidth()
                - (shouldTruncateTarget ? targetTask.getWidth() / 2 : 0);
        // Scroll so that the focused task is to the right of the list
        if (smoothScroll) {
            mScrollView.smoothScrollTo(scrollTo, 0);
        } else {
            mScrollView.scrollTo(scrollTo, 0);
        }
    }

    private boolean shouldScroll(@NonNull View targetTask, boolean shouldTruncateTarget) {
        boolean isTargetTruncated =
                targetTask.getRight() + mSpacing > mScrollView.getScrollX() + mScrollView.getWidth()
                        || Math.max(0, targetTask.getLeft() - mSpacing) < mScrollView.getScrollX();

        return isTargetTruncated && !shouldTruncateTarget;
    }

    @Nullable
    protected KeyboardQuickSwitchTaskView getTaskAt(int index) {
        return index < 0 || index >= mContent.getChildCount()
                ? null : (KeyboardQuickSwitchTaskView) mContent.getChildAt(index);
    }
}
