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

import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP;
import static com.android.window.flags.Flags.predictiveBackThreeButtonNav;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.WindowInsets;
import android.view.animation.Interpolator;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;

import com.android.launcher3.BaseActivity;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
import com.android.launcher3.PendingAddItemInfo;
import com.android.launcher3.Utilities;
import com.android.launcher3.R;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.window.WindowManagerProxy;
import com.android.launcher3.views.AbstractSlideInView;
import com.android.launcher3.widget.picker.model.WidgetPickerDataProvider.WidgetPickerDataChangeListener;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Base class for various widgets popup
 */
public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity>
        implements OnClickListener, OnLongClickListener,
        WidgetPickerDataChangeListener, Insettable, OnDeviceProfileChangeListener {
    /** The default number of cells that can fit horizontally in a widget sheet. */
    public static final int DEFAULT_MAX_HORIZONTAL_SPANS = 4;

    protected final Rect mInsets = new Rect();

    @Px
    protected int mContentHorizontalMargin;
    @Px
    protected int mWidgetCellHorizontalPadding;

    protected int mNavBarScrimHeight;
    private final Paint mNavBarScrimPaint;

    private boolean mDisableNavBarScrim = false;

    @Nullable private WidgetCell mWidgetCellWithAddButton = null;
    @Nullable private WidgetItem mLastSelectedWidgetItem = null;

    public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContentHorizontalMargin = getWidgetListHorizontalMargin();
        mWidgetCellHorizontalPadding = getResources().getDimensionPixelSize(
                R.dimen.widget_cell_horizontal_padding);
        mNavBarScrimPaint = new Paint();
        mNavBarScrimPaint.setColor(Themes.getNavBarScrimColor(mActivityContext));
    }

    /**
     * Returns the margins to be applied to the left and right of the widget apps list.
     */
    protected int getWidgetListHorizontalMargin() {
        return getResources().getDimensionPixelSize(
                R.dimen.widget_list_horizontal_margin);
    }

    protected int getScrimColor(Context context) {
        return context.getResources().getColor(R.color.widgets_picker_scrim);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        WindowInsets windowInsets = WindowManagerProxy.INSTANCE.get(getContext())
                .normalizeWindowInsets(getContext(), getRootWindowInsets(), new Rect());
        mNavBarScrimHeight = getNavBarScrimHeight(windowInsets);
        mActivityContext.getWidgetPickerDataProvider().setChangeListener(this);
        mActivityContext.addOnDeviceProfileChangeListener(this);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mActivityContext.getWidgetPickerDataProvider().setChangeListener(null);
        mActivityContext.removeOnDeviceProfileChangeListener(this);
    }

    @Override
    public void onDeviceProfileChanged(DeviceProfile dp) {
        int navBarScrimColor = Themes.getNavBarScrimColor(mActivityContext);
        if (mNavBarScrimPaint.getColor() != navBarScrimColor) {
            mNavBarScrimPaint.setColor(navBarScrimColor);
            invalidate();
        }
        setupNavBarColor();
    }

    @Override
    public void setScaleY(float scaleY) {
        super.setScaleY(scaleY);
        try {
            if (predictiveBackThreeButtonNav() && mNavBarScrimHeight > 0) {
                // Call invalidate to prevent navbar scrim from scaling. The navbar scrim is drawn
                // directly onto the canvas. To prevent it from being scaled with the canvas, there's a
                // counter scale applied in dispatchDraw.
                invalidate();
            }
        } catch (Throwable t) {
            // LC-Ignored
        }
    }

    @Override
    public final void onClick(View v) {
        WidgetCell wc;
        if (v instanceof WidgetCell view) {
            wc = view;
        }  else if (v.getParent() instanceof WidgetCell parent) {
            wc = parent;
        } else {
            return;
        }

        scrollToWidgetCell(wc);

        if (mWidgetCellWithAddButton != null) {
            if (mWidgetCellWithAddButton.isShowingAddButton()) {
                // If there is a add button currently showing, hide it.
                mWidgetCellWithAddButton.hideAddButton(/* animate= */ true);
            } else {
                // The last recorded widget cell to show an add button is no longer showing it,
                // likely because the widget cell has been recycled or lost focus. If this is
                // the cell that has been clicked, we will show it below.
                mWidgetCellWithAddButton = null;
            }
        }

        if (mWidgetCellWithAddButton != wc) {
            // If click is on a cell not showing an add button, show it now.
            final PendingAddItemInfo info = (PendingAddItemInfo) wc.getTag();
            if (mActivityContext instanceof Launcher) {
                wc.showAddButton((view) -> addWidget(info));
            } else {
                wc.showAddButton((view) -> mActivityContext.getItemOnClickListener()
                        .onClick(wc));
            }
        }

        mWidgetCellWithAddButton = mWidgetCellWithAddButton != wc ? wc : null;
        if (mWidgetCellWithAddButton != null) {
            mLastSelectedWidgetItem = mWidgetCellWithAddButton.getWidgetItem();
        } else {
            mLastSelectedWidgetItem = null;
        }
    }

    @Override
    protected float getShiftRange() {
        // We add the extra height added during predictive back / swipe up to the shift range, so
        // that the idle interpolator knows to animate the view off fully.
        return mContent.getHeight() + getBottomOffsetPx();
    }

    /**
     * Click handler for tap to add button. This handler assumes we are in the Launcher activity and
     * should not be used when the widget sheet is displayed elsewhere.
     */
    private void addWidget(@NonNull PendingAddItemInfo info) {
        // Using a boolean flag here to make sure the callback is only run once. This should never
        // happen because we close the sheet and it will be reconstructed the next time it is
        // needed.
        final AtomicBoolean hasRun = new AtomicBoolean(false);
        addOnCloseListener(() -> {
            if (hasRun.get()) return;
            hasRun.set(true);

            // Going to NORMAL state will also dismiss the All Apps view if it is showing.
            Launcher launcher = Launcher.getLauncher(mActivityContext);
            launcher.getStateManager().goToState(NORMAL, forSuccessCallback(() -> {
                launcher.getAccessibilityDelegate().addToWorkspace(info,
                        /*accessibility=*/ false,
                        /*finishCallback=*/ (success) -> {
                            mActivityContext.getStatsLogManager()
                                    .logger()
                                    .withItemInfo(info)
                                    .log(LAUNCHER_WIDGET_ADD_BUTTON_TAP);
                        });
            }));
        });
        close(/* animate= */ true);
    }

    /**
     * Scroll to show the widget cell. If both the bottom and top of the cell are clipped, this will
     * prioritize showing the bottom of the cell (where the add button is).
     */
    private void scrollToWidgetCell(@NonNull WidgetCell wc) {
        final int headerTopClip = getHeaderTopClip(wc);
        final Rect visibleRect = new Rect();
        final boolean isPartiallyVisible = wc.getLocalVisibleRect(visibleRect);
        int scrollByY = 0;
        if (isPartiallyVisible) {
            final int scrollPadding = getResources()
                    .getDimensionPixelSize(R.dimen.widget_cell_add_button_scroll_padding);
            final int topClip = visibleRect.top + headerTopClip;
            final int bottomClip = wc.getHeight() - visibleRect.bottom;
            if (bottomClip != 0) {
                scrollByY = bottomClip + scrollPadding;
            } else if (topClip != 0) {
                scrollByY = -topClip - scrollPadding;
            }
        }

        if (isPartiallyVisible && scrollByY == 0) {
            // Widget is fully visible.
            return;
        } else if (!isPartiallyVisible) {
            Log.e("BaseWidgetSheet", "click on invisible WidgetCell should not be possible");
            return;
        }

        scrollCellContainerByY(wc, scrollByY);
    }

    /**
     * Find the nearest scrollable container of the given WidgetCell, and scroll by the given
     * amount.
     */
    protected abstract void scrollCellContainerByY(WidgetCell wc, int scrollByY);


    /**
     * Return the top clip of any sticky headers over the given cell.
     */
    protected int getHeaderTopClip(@NonNull WidgetCell cell) {
        return 0;
    }

    /**
     * Returns the component of the widget that is currently showing an add button, if any.
     */
    @Nullable
    protected WidgetItem getLastSelectedWidgetItem() {
        return mLastSelectedWidgetItem;
    }

    @Override
    public boolean onLongClick(View v) {
        TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Widgets.onLongClick");
        v.cancelLongPress();

        boolean result;
        if (v instanceof WidgetCell) {
            result = mActivityContext.getAllAppsItemLongClickListener().onLongClick(v);
        } else if (v.getParent() instanceof WidgetCell wc) {
            result = mActivityContext.getAllAppsItemLongClickListener().onLongClick(wc);
        } else {
            return true;
        }
        if (result) {
            close(true);
        }
        return result;
    }

    @Override
    public void setInsets(Rect insets) {
        mInsets.set(insets);
        @Px int contentHorizontalMargin = getWidgetListHorizontalMargin();
        if (contentHorizontalMargin != mContentHorizontalMargin) {
            onContentHorizontalMarginChanged(contentHorizontalMargin);
            mContentHorizontalMargin = contentHorizontalMargin;
        }
    }

    /** Enables or disables the sheet's nav bar scrim. */
    public void disableNavBarScrim(boolean disable) {
        mDisableNavBarScrim = disable;
    }

    private int getNavBarScrimHeight(WindowInsets insets) {
        if (mDisableNavBarScrim) {
            return 0;
        } else {
            return Utilities.ATLEAST_Q ? insets.getTappableElementInsets().bottom : insets.getStableInsetBottom();
        }
    }

    @Override
    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
        mNavBarScrimHeight = getNavBarScrimHeight(insets);
        return super.onApplyWindowInsets(insets);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);

        if (mNavBarScrimHeight > 0) {
            float left = (getWidth() - getWidth() / getScaleX()) / 2;
            float top = getHeight() / 2f + (getHeight() / 2f - mNavBarScrimHeight) / getScaleY();
            canvas.drawRect(left, top, getWidth() / getScaleX(),
                    top + mNavBarScrimHeight / getScaleY(), mNavBarScrimPaint);
        }
    }

    /** Called when the horizontal margin of the content view has changed. */
    protected abstract void onContentHorizontalMarginChanged(int contentHorizontalMarginInPx);

    /**
     * Measures the dimension of this view and its children by taking system insets, navigation bar,
     * status bar, into account.
     */
    protected void doMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthUsed = getInsetsWidth();

        DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
        measureChildWithMargins(mContent, widthMeasureSpec,
                widthUsed, heightMeasureSpec, deviceProfile.bottomSheetTopPadding);
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
                MeasureSpec.getSize(heightMeasureSpec));
    }

    /**
     * Returns the width used on left and right by the insets / padding.
     */
    protected int getInsetsWidth() {
        int widthUsed;
        DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
        if (deviceProfile.isTablet) {
            widthUsed = Math.max(2 * getTabletHorizontalMargin(deviceProfile),
                    2 * (mInsets.left + mInsets.right));
        } else if (mInsets.bottom > 0) {
            widthUsed = mInsets.left + mInsets.right;
        } else {
            Rect padding = deviceProfile.workspacePadding;
            widthUsed = Math.max(padding.left + padding.right,
                    2 * (mInsets.left + mInsets.right));
        }
        return widthUsed;
    }

    /** Returns the horizontal margins to be applied to the widget sheet. **/
    protected int getTabletHorizontalMargin(DeviceProfile deviceProfile) {
        return deviceProfile.allAppsLeftRightMargin;
    }

    @Override
    protected Interpolator getIdleInterpolator() {
        return mActivityContext.getDeviceProfile().isTablet
                ? EMPHASIZED : super.getIdleInterpolator();
    }

    protected void onCloseComplete() {
        super.onCloseComplete();
        clearNavBarColor();
    }

    protected void clearNavBarColor() {
        getSystemUiController().updateUiState(
                SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET, 0);
    }

    protected void setupNavBarColor() {
        boolean isNavBarDark = Themes.getAttrBoolean(getContext(), R.attr.isMainColorDark);

        // In light mode, landscape reverses navbar background color.
        boolean isPhoneLandscape =
                !mActivityContext.getDeviceProfile().isTablet && mInsets.bottom == 0;
        if (!isNavBarDark && isPhoneLandscape) {
            isNavBarDark = true;
        }

        getSystemUiController().updateUiState(SystemUiController.UI_STATE_WIDGET_BOTTOM_SHEET,
                isNavBarDark ? SystemUiController.FLAG_DARK_NAV
                        : SystemUiController.FLAG_LIGHT_NAV);
    }

    protected SystemUiController getSystemUiController() {
        return mActivityContext.getSystemUiController();
    }

    @Override
    protected void setTranslationShift(float translationShift) {
        super.setTranslationShift(translationShift);
        if (mActivityContext instanceof Launcher ls) {
            ls.onWidgetsTransition(1 - translationShift);
        }
    }
}
