/*
 * Copyright (C) 2010 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.internal.view.menu;

import android.annotation.AttrRes;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StyleRes;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.PopupWindow.OnDismissListener;

import com.android.internal.view.menu.MenuPresenter.Callback;

/**
 * Presents a menu as a small, simple popup anchored to another view.
 */
public class MenuPopupHelper implements MenuHelper {
    private static final int TOUCH_EPICENTER_SIZE_DP = 48;

    private final Context mContext;

    // Immutable cached popup menu properties.
    private final MenuBuilder mMenu;
    private final boolean mOverflowOnly;
    private final int mPopupStyleAttr;
    private final int mPopupStyleRes;

    // Mutable cached popup menu properties.
    private View mAnchorView;
    private int mDropDownGravity = Gravity.START;
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private boolean mForceShowIcon;
    private Callback mPresenterCallback;

    private MenuPopup mPopup;
    private OnDismissListener mOnDismissListener;

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu) {
        this(context, menu, null, false, com.android.internal.R.attr.popupMenuStyle, 0);
    }

    @UnsupportedAppUsage
    public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
            @NonNull View anchorView) {
        this(context, menu, anchorView, false, com.android.internal.R.attr.popupMenuStyle, 0);
    }

    public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
            @NonNull View anchorView,
            boolean overflowOnly, @AttrRes int popupStyleAttr) {
        this(context, menu, anchorView, overflowOnly, popupStyleAttr, 0);
    }

    public MenuPopupHelper(@NonNull Context context, @NonNull MenuBuilder menu,
            @NonNull View anchorView, boolean overflowOnly, @AttrRes int popupStyleAttr,
            @StyleRes int popupStyleRes) {
        mContext = context;
        mMenu = menu;
        mAnchorView = anchorView;
        mOverflowOnly = overflowOnly;
        mPopupStyleAttr = popupStyleAttr;
        mPopupStyleRes = popupStyleRes;
    }

    public void setOnDismissListener(@Nullable OnDismissListener listener) {
        mOnDismissListener = listener;
    }

    /**
      * Sets the view to which the popup window is anchored.
      * <p>
      * Changes take effect on the next call to show().
      *
      * @param anchor the view to which the popup window should be anchored
      */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public void setAnchorView(@NonNull View anchor) {
        mAnchorView = anchor;
    }

    /**
     * Sets whether the popup menu's adapter is forced to show icons in the
     * menu item views.
     * <p>
     * Changes take effect on the next call to show().
     *
     * This method should not be accessed directly outside the framework, please use
     * {@link android.widget.PopupMenu#setForceShowIcon(boolean)} instead.
     *
     * @param forceShowIcon {@code true} to force icons to be shown, or
     *                  {@code false} for icons to be optionally shown
     */
    @UnsupportedAppUsage
    public void setForceShowIcon(boolean forceShowIcon) {
        mForceShowIcon = forceShowIcon;
        if (mPopup != null) {
            mPopup.setForceShowIcon(forceShowIcon);
        }
    }

    /**
      * Sets the alignment of the popup window relative to the anchor view.
      * <p>
      * Changes take effect on the next call to show().
      *
      * @param gravity alignment of the popup relative to the anchor
      */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public void setGravity(int gravity) {
        mDropDownGravity = gravity;
    }

    /**
     * @return alignment of the popup relative to the anchor
     */
    public int getGravity() {
        return mDropDownGravity;
    }

    @UnsupportedAppUsage
    public void show() {
        if (!tryShow()) {
            throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
        }
    }

    public void show(int x, int y) {
        if (!tryShow(x, y)) {
            throw new IllegalStateException("MenuPopupHelper cannot be used without an anchor");
        }
    }

    @NonNull
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public MenuPopup getPopup() {
        if (mPopup == null) {
            mPopup = createPopup();
        }
        return mPopup;
    }

    /**
     * Attempts to show the popup anchored to the view specified by {@link #setAnchorView(View)}.
     *
     * @return {@code true} if the popup was shown or was already showing prior to calling this
     *         method, {@code false} otherwise
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    public boolean tryShow() {
        if (isShowing()) {
            return true;
        }

        if (mAnchorView == null) {
            return false;
        }

        showPopup(0, 0, false, false);
        return true;
    }

    /**
     * Shows the popup menu and makes a best-effort to anchor it to the
     * specified (x,y) coordinate relative to the anchor view.
     * <p>
     * Additionally, the popup's transition epicenter (see
     * {@link android.widget.PopupWindow#setEpicenterBounds(Rect)} will be
     * centered on the specified coordinate, rather than using the bounds of
     * the anchor view.
     * <p>
     * If the popup's resolved gravity is {@link Gravity#LEFT}, this will
     * display the popup with its top-left corner at (x,y) relative to the
     * anchor view. If the resolved gravity is {@link Gravity#RIGHT}, the
     * popup's top-right corner will be at (x,y).
     * <p>
     * If the popup cannot be displayed fully on-screen, this method will
     * attempt to scroll the anchor view's ancestors and/or offset the popup
     * such that it may be displayed fully on-screen.
     *
     * @param x x coordinate relative to the anchor view
     * @param y y coordinate relative to the anchor view
     * @return {@code true} if the popup was shown or was already showing prior
     *         to calling this method, {@code false} otherwise
     */
    public boolean tryShow(int x, int y) {
        if (isShowing()) {
            return true;
        }

        if (mAnchorView == null) {
            return false;
        }

        showPopup(x, y, true, true);
        return true;
    }

    /**
     * Creates the popup and assigns cached properties.
     *
     * @return an initialized popup
     */
    @NonNull
    private MenuPopup createPopup() {
        final WindowManager windowManager = mContext.getSystemService(WindowManager.class);
        final Rect maxWindowBounds = windowManager.getMaximumWindowMetrics().getBounds();

        final int smallestWidth = Math.min(maxWindowBounds.width(), maxWindowBounds.height());
        final int minSmallestWidthCascading = mContext.getResources().getDimensionPixelSize(
            com.android.internal.R.dimen.cascading_menus_min_smallest_width);
        final boolean enableCascadingSubmenus = smallestWidth >= minSmallestWidthCascading;

        final MenuPopup popup;
        if (enableCascadingSubmenus) {
            popup = new CascadingMenuPopup(mContext, mAnchorView, mPopupStyleAttr,
                    mPopupStyleRes, mOverflowOnly);
        } else {
            popup = new StandardMenuPopup(mContext, mMenu, mAnchorView, mPopupStyleAttr,
                    mPopupStyleRes, mOverflowOnly);
        }

        // Assign immutable properties.
        popup.addMenu(mMenu);
        popup.setOnDismissListener(mInternalOnDismissListener);

        // Assign mutable properties. These may be reassigned later.
        popup.setAnchorView(mAnchorView);
        popup.setCallback(mPresenterCallback);
        popup.setForceShowIcon(mForceShowIcon);
        popup.setGravity(mDropDownGravity);

        return popup;
    }

    private void showPopup(int xOffset, int yOffset, boolean useOffsets, boolean showTitle) {
        final MenuPopup popup = getPopup();
        popup.setShowTitle(showTitle);

        if (useOffsets) {
            // If the resolved drop-down gravity is RIGHT, the popup's right
            // edge will be aligned with the anchor view. Adjust by the anchor
            // width such that the top-right corner is at the X offset.
            final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity,
                    mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
            if (hgrav == Gravity.RIGHT) {
                xOffset -= mAnchorView.getWidth();
            }

            popup.setHorizontalOffset(xOffset);
            popup.setVerticalOffset(yOffset);

            // Set the transition epicenter to be roughly finger (or mouse
            // cursor) sized and centered around the offset position. This
            // will give the appearance that the window is emerging from
            // the touch point.
            final float density = mContext.getResources().getDisplayMetrics().density;
            final int halfSize = (int) (TOUCH_EPICENTER_SIZE_DP * density / 2);
            final Rect epicenter = new Rect(xOffset - halfSize, yOffset - halfSize,
                    xOffset + halfSize, yOffset + halfSize);
            popup.setEpicenterBounds(epicenter);
        }

        popup.show();
    }

    /**
     * Dismisses the popup, if showing.
     */
    @Override
    @UnsupportedAppUsage
    public void dismiss() {
        if (isShowing()) {
            mPopup.dismiss();
        }
    }

    /**
     * Called after the popup has been dismissed.
     * <p>
     * <strong>Note:</strong> Subclasses should call the super implementation
     * last to ensure that any necessary tear down has occurred before the
     * listener specified by {@link #setOnDismissListener(OnDismissListener)}
     * is called.
     */
    protected void onDismiss() {
        mPopup = null;

        if (mOnDismissListener != null) {
            mOnDismissListener.onDismiss();
        }
    }

    public boolean isShowing() {
        return mPopup != null && mPopup.isShowing();
    }

    @Override
    public void setPresenterCallback(@Nullable MenuPresenter.Callback cb) {
        mPresenterCallback = cb;
        if (mPopup != null) {
            mPopup.setCallback(cb);
        }
    }

    /**
     * Listener used to proxy dismiss callbacks to the helper's owner.
     */
    private final OnDismissListener mInternalOnDismissListener = new OnDismissListener() {
        @Override
        public void onDismiss() {
            MenuPopupHelper.this.onDismiss();
        }
    };
}
