/*
 * Copyright (C) 2020 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.wallpaper.widget;

import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED;
import static com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED;

import android.app.Activity;
import android.content.Context;
import android.content.res.ColorStateList;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;

import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.widget.ImageViewCompat;

import com.android.internal.util.ArrayUtils;
import com.android.wallpaper.R;
import com.android.wallpaper.util.ResourceUtils;
import com.android.wallpaper.util.SizeCalculator;

import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback;

import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/** A {@code ViewGroup} which provides the specific actions for the user to interact with. */
public class BottomActionBar extends FrameLayout {

    /**
     * Interface to be implemented by an Activity hosting a {@link BottomActionBar}
     */
    public interface BottomActionBarHost {
        /** Gets {@link BottomActionBar}. */
        BottomActionBar getBottomActionBar();
    }

    /**
     * The listener for {@link BottomActionBar} visibility change notification.
     */
    public interface VisibilityChangeListener {
        /**
         * Called when {@link BottomActionBar} visibility changes.
         *
         * @param isVisible {@code true} if it's visible; {@code false} otherwise.
         */
        void onVisibilityChange(boolean isVisible);
    }

    /** This listens to changes to an action view's selected state. */
    public interface OnActionSelectedListener {

        /**
         * This is called when an action view's selected state changes.
         * @param selected whether the action view is selected.
         */
        void onActionSelected(boolean selected);
    }

    /**
     *  A Callback to notify the registrant to change it's accessibility param when
     *  {@link BottomActionBar} state changes.
     */
    public interface AccessibilityCallback {
        /**
         * Called when {@link BottomActionBar} collapsed.
         */
        void onBottomSheetCollapsed();

        /**
         * Called when {@link BottomActionBar} expanded.
         */
        void onBottomSheetExpanded();
    }

    /**
     * Object to host content view for bottom sheet to display.
     *
     * <p> The view would be created in the constructor.
     */
    public static abstract class BottomSheetContent<T extends View> {

        private T mContentView;
        private boolean mIsVisible;

        public BottomSheetContent(Context context) {
            mContentView = createView(context);
            setVisibility(false);
        }

        /** Gets the view id to inflate. */
        @LayoutRes
        public abstract int getViewId();

        /** Gets called when the content view is created. */
        public abstract void onViewCreated(T view);

        /** Gets called when the current content view is going to recreate. */
        public void onRecreateView(T oldView) {}

        private void recreateView(Context context) {
            // Inform that the view is going to recreate.
            onRecreateView(mContentView);
            // Create a new view with the given context.
            mContentView = createView(context);
            setVisibility(mIsVisible);
        }

        private T createView(Context context) {
            T contentView = (T) LayoutInflater.from(context).inflate(getViewId(), null);
            onViewCreated(contentView);
            contentView.setFocusable(true);
            return contentView;
        }

        protected void setVisibility(boolean isVisible) {
            mIsVisible = isVisible;
            mContentView.setVisibility(mIsVisible ? VISIBLE : GONE);
        }
    }

    // TODO(b/154299462): Separate downloadable related actions from WallpaperPicker.
    /** The action items in the bottom action bar. */
    public enum BottomAction {
        ROTATION,
        DELETE,
        INFORMATION(R.string.accessibility_info_shown, R.string.accessibility_info_hidden),
        EDIT,
        CUSTOMIZE(R.string.accessibility_customize_shown, R.string.accessibility_customize_hidden),
        EFFECTS,
        DOWNLOAD,
        PROGRESS,
        APPLY,
        APPLY_TEXT;

        private final int mShownAccessibilityResId;
        private final int mHiddenAccessibilityResId;

        BottomAction() {
            this(/* shownAccessibilityLabelResId= */ 0, /* shownAccessibilityLabelResId= */ 0);
        }

        BottomAction(int shownAccessibilityLabelResId, int hiddenAccessibilityLabelResId) {
            mShownAccessibilityResId = shownAccessibilityLabelResId;
            mHiddenAccessibilityResId = hiddenAccessibilityLabelResId;
        }

        /**
         * Returns the string resource id of the currently bottom action for its shown or hidden
         * state.
         */
        public int getAccessibilityStringRes(boolean isShown) {
            return isShown ? mShownAccessibilityResId : mHiddenAccessibilityResId;
        }
    }

    private final Map<BottomAction, View> mActionMap = new EnumMap<>(BottomAction.class);
    private final Map<BottomAction, BottomSheetContent<?>> mContentViewMap =
            new EnumMap<>(BottomAction.class);
    private final Map<BottomAction, OnActionSelectedListener> mActionSelectedListeners =
            new EnumMap<>(BottomAction.class);

    private final ViewGroup mBottomSheetView;
    private final QueueStateBottomSheetBehavior<ViewGroup> mBottomSheetBehavior;
    private final Set<VisibilityChangeListener> mVisibilityChangeListeners = new HashSet<>();

    // The current selected action in the BottomActionBar, can be null when no action is selected.
    @Nullable private BottomAction mSelectedAction;
    // The last selected action in the BottomActionBar.
    @Nullable private BottomAction mLastSelectedAction;
    @Nullable private AccessibilityCallback mAccessibilityCallback;

    public BottomActionBar(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater.from(context).inflate(R.layout.bottom_actions_layout, this, true);

        mActionMap.put(BottomAction.ROTATION, findViewById(R.id.action_rotation));
        mActionMap.put(BottomAction.DELETE, findViewById(R.id.action_delete));
        mActionMap.put(BottomAction.INFORMATION, findViewById(R.id.action_information));
        mActionMap.put(BottomAction.EDIT, findViewById(R.id.action_edit));
        mActionMap.put(BottomAction.CUSTOMIZE, findViewById(R.id.action_customize));
        mActionMap.put(BottomAction.EFFECTS, findViewById(R.id.action_effects));
        mActionMap.put(BottomAction.DOWNLOAD, findViewById(R.id.action_download));
        mActionMap.put(BottomAction.PROGRESS, findViewById(R.id.action_progress));
        mActionMap.put(BottomAction.APPLY, findViewById(R.id.action_apply));
        mActionMap.put(BottomAction.APPLY_TEXT, findViewById(R.id.action_apply_text_button));

        mBottomSheetView = findViewById(R.id.action_bottom_sheet);
        SizeCalculator.adjustBackgroundCornerRadius(mBottomSheetView);
        setColor(context);

        mBottomSheetBehavior = (QueueStateBottomSheetBehavior<ViewGroup>) BottomSheetBehavior.from(
                mBottomSheetView);
        mBottomSheetBehavior.setState(STATE_COLLAPSED);
        mBottomSheetBehavior.setBottomSheetCallback(new BottomSheetCallback() {
            @Override
            public void onStateChanged(@NonNull View bottomSheet, int newState) {
                if (mBottomSheetBehavior.isQueueProcessing()) {
                    // Avoid button and bottom sheet mismatching from quick tapping buttons when
                    // bottom sheet is changing state.
                    disableActions();
                    // If bottom sheet is going with expanded-collapsed-expanded, the new content
                    // will be updated in collapsed state. The first state change from expanded to
                    // collapsed should still show the previous content view.
                    if (mSelectedAction != null && newState == STATE_COLLAPSED) {
                        updateContentViewFor(mSelectedAction);
                    }
                    return;
                }

                notifyAccessibilityCallback(newState);

                // Enable all buttons when queue is not processing.
                enableActions();
                if (!isExpandable(mSelectedAction)) {
                    return;
                }
                // Ensure the button state is the same as bottom sheet state to catch up the state
                // change from dragging or some unexpected bottom sheet state changes.
                if (newState == STATE_COLLAPSED) {
                    updateSelectedState(mSelectedAction, /* selected= */ false);
                } else if (newState == STATE_EXPANDED) {
                    updateSelectedState(mSelectedAction, /* selected= */ true);
                }
            }
            @Override
            public void onSlide(@NonNull View bottomSheet, float slideOffset) { }
        });

        setOnApplyWindowInsetsListener((v, windowInsets) -> {
            v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(),
                    windowInsets.getSystemWindowInsetBottom());
            return windowInsets;
        });

        // Skip "info selected" and "customize selected" Talkback while double tapping on info and
        // customize action.
        skipAccessibilityEvent(new BottomAction[]{BottomAction.INFORMATION, BottomAction.CUSTOMIZE},
                new int[]{AccessibilityEvent.TYPE_VIEW_CLICKED,
                        AccessibilityEvent.TYPE_VIEW_SELECTED});
    }

    @Override
    public void onVisibilityAggregated(boolean isVisible) {
        super.onVisibilityAggregated(isVisible);
        mVisibilityChangeListeners.forEach(listener -> listener.onVisibilityChange(isVisible));
    }

    /**
     * Binds the {@code bottomSheetContent} with the {@code action}, the {@code action} button
     * would be able to expand/collapse the bottom sheet to show the content.
     *
     * @param bottomSheetContent the content object with view being added to the bottom sheet
     * @param action the action to be bound to expand / collapse the bottom sheet
     */
    public void bindBottomSheetContentWithAction(BottomSheetContent<?> bottomSheetContent,
            BottomAction action) {
        mContentViewMap.put(action, bottomSheetContent);
        mBottomSheetView.addView(bottomSheetContent.mContentView);
        setActionClickListener(action, actionView -> {
            if (mBottomSheetBehavior.getState() == STATE_COLLAPSED) {
                updateContentViewFor(action);
            }
            mBottomSheetView.setAccessibilityTraversalAfter(actionView.getId());
        });
    }

    /** Collapses the bottom sheet. */
    public void collapseBottomSheetIfExpanded() {
        hideBottomSheetAndDeselectButtonIfExpanded();
    }

    /** Enables or disables action buttons that show the bottom sheet. */
    public void enableActionButtonsWithBottomSheet(boolean enabled) {
        if (enabled) {
            enableActions(mContentViewMap.keySet().toArray(new BottomAction[0]));
        } else {
            disableActions(mContentViewMap.keySet().toArray(new BottomAction[0]));
        }
    }

    /**
     * Sets a click listener to a specific action.
     *
     * @param bottomAction the specific action
     * @param actionClickListener the click listener for the action
     */
    public void setActionClickListener(
            BottomAction bottomAction, OnClickListener actionClickListener) {
        View buttonView = mActionMap.get(bottomAction);
        if (buttonView.hasOnClickListeners()) {
            throw new IllegalStateException(
                    "Had already set a click listener to button: " + bottomAction);
        }
        buttonView.setOnClickListener(view -> {
            if (mSelectedAction != null && isActionSelected(mSelectedAction)) {
                updateSelectedState(mSelectedAction, /* selected= */ false);
                if (isExpandable(mSelectedAction)) {
                    mBottomSheetBehavior.enqueue(STATE_COLLAPSED);
                }
            } else {
                // Error handling, set to null if the action is not selected.
                mSelectedAction = null;
            }

            if (bottomAction == mSelectedAction) {
                // Deselect the selected action.
                mSelectedAction = null;
            } else {
                // Select a different action from the current selected action.
                // Also keep the same action for unselected case for a11y.
                mLastSelectedAction = mSelectedAction = bottomAction;
                updateSelectedState(mSelectedAction, /* selected= */ true);
                if (isExpandable(mSelectedAction)) {
                    mBottomSheetBehavior.enqueue(STATE_EXPANDED);
                }
            }
            actionClickListener.onClick(view);
            mBottomSheetBehavior.processQueueForStateChange();
        });
    }

    /**
     * Sets a selected listener to a specific action. This is triggered each time the bottom
     * action's selected state changes.
     *
     * @param bottomAction the specific action
     * @param actionSelectedListener the selected listener for the action
     */
    public void setActionSelectedListener(
            BottomAction bottomAction, OnActionSelectedListener actionSelectedListener) {
        if (mActionSelectedListeners.containsKey(bottomAction)) {
            throw new IllegalStateException(
                    "Had already set a selected listener to button: " + bottomAction);
        }
        mActionSelectedListeners.put(bottomAction, actionSelectedListener);
    }

    /** Set back button visibility. */
    public void setBackButtonVisibility(int visibility) {
        findViewById(R.id.action_back).setVisibility(visibility);
    }

    /** Binds the cancel button to back key. */
    public void bindBackButtonToSystemBackKey(Activity activity) {
        findViewById(R.id.action_back).setOnClickListener(v -> activity.onBackPressed());
    }

    /** Returns {@code true} if visible. */
    public boolean isVisible() {
        return getVisibility() == VISIBLE;
    }

    /** Shows {@link BottomActionBar}. */
    public void show() {
        setVisibility(VISIBLE);
    }

    /** Hides {@link BottomActionBar}. */
    public void hide() {
        setVisibility(GONE);
    }

    /**
     * Adds the visibility change listener.
     *
     * @param visibilityChangeListener the listener to be notified.
     */
    public void addVisibilityChangeListener(VisibilityChangeListener visibilityChangeListener) {
        if (visibilityChangeListener == null) {
            return;
        }
        mVisibilityChangeListeners.add(visibilityChangeListener);
        visibilityChangeListener.onVisibilityChange(isVisible());
    }

    /**
     * Sets a AccessibilityCallback.
     *
     * @param accessibilityCallback the callback to be notified.
     */
    public void setAccessibilityCallback(@Nullable AccessibilityCallback accessibilityCallback) {
        mAccessibilityCallback = accessibilityCallback;
    }

    /**
     * Shows the specific actions.
     *
     * @param actions the specific actions
     */
    public void showActions(BottomAction... actions) {
        for (BottomAction action : actions) {
            mActionMap.get(action).setVisibility(VISIBLE);
        }
    }

    /**
     * Hides the specific actions.
     *
     * @param actions the specific actions
     */
    public void hideActions(BottomAction... actions) {
        for (BottomAction action : actions) {
            mActionMap.get(action).setVisibility(GONE);

            if (isExpandable(action) && mSelectedAction == action) {
                hideBottomSheetAndDeselectButtonIfExpanded();
            }
        }
    }

    /**
     * Focus the specific action.
     *
     * @param action the specific action
     */
    public void focusAccessibilityAction(BottomAction action) {
        mActionMap.get(action).sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
    }

    /**
     * Shows the specific actions only. In other words, the other actions will be hidden.
     *
     * @param actions the specific actions which will be shown. Others will be hidden.
     */
    public void showActionsOnly(BottomAction... actions) {
        final Set<BottomAction> actionsSet = new HashSet<>(Arrays.asList(actions));

        mActionMap.keySet().forEach(action -> {
            if (actionsSet.contains(action)) {
                showActions(action);
            } else {
                hideActions(action);
            }
        });
    }

    /**
     * Checks if the specific actions are shown.
     *
     * @param actions the specific actions to be verified
     * @return {@code true} if the actions are shown; {@code false} otherwise
     */
    public boolean areActionsShown(BottomAction... actions) {
        final Set<BottomAction> actionsSet = new HashSet<>(Arrays.asList(actions));
        return actionsSet.stream().allMatch(bottomAction -> {
            View view = mActionMap.get(bottomAction);
            return view != null && view.getVisibility() == VISIBLE;
        });
    }

    /**
     * All actions will be hidden.
     */
    public void hideAllActions() {
        showActionsOnly(/* No actions to show */);
    }

    /** Enables all the actions' {@link View}. */
    public void enableActions() {
        enableActions(BottomAction.values());
    }

    /** Disables all the actions' {@link View}. */
    public void disableActions() {
        disableActions(BottomAction.values());
    }

    /**
     * Enables specified actions' {@link View}.
     *
     * @param actions the specified actions to enable their views
     */
    public void enableActions(BottomAction... actions) {
        for (BottomAction action : actions) {
            mActionMap.get(action).setEnabled(true);
        }
    }

    /**
     * Disables specified actions' {@link View}.
     *
     * @param actions the specified actions to disable their views
     */
    public void disableActions(BottomAction... actions) {
        for (BottomAction action : actions) {
            mActionMap.get(action).setEnabled(false);
        }
    }

    /** Sets a default selected action button. */
    public void setDefaultSelectedButton(BottomAction action) {
        if (mSelectedAction == null) {
            mSelectedAction = action;
            updateSelectedState(mSelectedAction, /* selected= */ true);
        }
    }

    /** Deselects an action button. */
    public void deselectAction(BottomAction action) {
        if (isExpandable(action)) {
            mBottomSheetBehavior.setState(STATE_COLLAPSED);
        }
        updateSelectedState(action, /* selected= */ false);
        if (action == mSelectedAction) {
            mSelectedAction = null;
        }
    }

    public boolean isActionSelected(BottomAction action) {
        return mActionMap.get(action).isSelected();
    }

    /** Returns {@code true} if the state of bottom sheet is collapsed. */
    public boolean isBottomSheetCollapsed() {
        return mBottomSheetBehavior.getState() == STATE_COLLAPSED;
    }

    /** Resets {@link BottomActionBar} to initial state. */
    public void reset() {
        // Not visible by default, see res/layout/bottom_action_bar.xml
        hide();
        // All actions are hide and enabled by default, see res/layout/bottom_action_bar.xml
        hideAllActions();
        enableActions();
        // Clears all the actions' click listeners
        mActionMap.values().forEach(v -> v.setOnClickListener(null));
        findViewById(R.id.action_back).setOnClickListener(null);
        // Deselect all buttons.
        mActionMap.keySet().forEach(a -> updateSelectedState(a, /* selected= */ false));
        // Clear values.
        mContentViewMap.clear();
        mActionSelectedListeners.clear();
        mBottomSheetView.removeAllViews();
        mBottomSheetBehavior.reset();
        mSelectedAction = null;
    }

    /** Dynamic update color with {@code Context}. */
    public void setColor(Context context) {
        // Set bottom sheet background.
        mBottomSheetView.setBackground(context.getDrawable(R.drawable.bottom_sheet_background));
        if (mBottomSheetView.getChildCount() > 0) {
            // Update the bottom sheet content view if any.
            mBottomSheetView.removeAllViews();
            mContentViewMap.values().forEach(bottomSheetContent -> {
                bottomSheetContent.recreateView(context);
                mBottomSheetView.addView(bottomSheetContent.mContentView);
            });
        }

        // Set the bar background and action buttons.
        ViewGroup actionTabs = findViewById(R.id.action_tabs);
        actionTabs.setBackgroundColor(
                ResourceUtils.getColorAttr(context, android.R.attr.colorBackground));
        ColorStateList colorStateList = context.getColorStateList(
                R.color.bottom_action_button_color_tint);
        for (int i = 0; i < actionTabs.getChildCount(); i++) {
            View v = actionTabs.getChildAt(i);
            if (v instanceof ImageView) {
                v.setBackground(context.getDrawable(R.drawable.bottom_action_button_background));
                ImageViewCompat.setImageTintList((ImageView) v, colorStateList);
            } else if (v instanceof ProgressBar) {
                ((ProgressBar) v).setIndeterminateTintList(colorStateList);
            }
        }
    }

    /** Sets action button accessibility traversal after. */
    public void setActionAccessibilityTraversalAfter(BottomAction action, int afterId) {
        View bottomActionView = mActionMap.get(action);
        bottomActionView.setAccessibilityTraversalAfter(afterId);
    }

    /** Sets action button accessibility traversal before. */
    public void setActionAccessibilityTraversalBefore(BottomAction action, int beforeId) {
        View bottomActionView = mActionMap.get(action);
        bottomActionView.setAccessibilityTraversalBefore(beforeId);
    }

    private void updateSelectedState(BottomAction bottomAction, boolean selected) {
        View bottomActionView = mActionMap.get(bottomAction);
        if (bottomActionView.isSelected() == selected) {
            return;
        }

        OnActionSelectedListener listener = mActionSelectedListeners.get(bottomAction);
        if (listener != null) {
            listener.onActionSelected(selected);
        }
        bottomActionView.setSelected(selected);
    }

    private void hideBottomSheetAndDeselectButtonIfExpanded() {
        if (isExpandable(mSelectedAction) && mBottomSheetBehavior.getState() == STATE_EXPANDED) {
            mBottomSheetBehavior.setState(STATE_COLLAPSED);
            updateSelectedState(mSelectedAction, /* selected= */ false);
            mSelectedAction = null;
        }
    }

    private void updateContentViewFor(BottomAction action) {
        mContentViewMap.forEach((a, content) -> content.setVisibility(a.equals(action)));
    }

    private boolean isExpandable(BottomAction action) {
        return action != null && mContentViewMap.containsKey(action);
    }

    private void notifyAccessibilityCallback(int state) {
        if (mAccessibilityCallback == null) {
            return;
        }

        if (state == STATE_COLLAPSED) {
            CharSequence text = getAccessibilityText(mLastSelectedAction, /* isShown= */ false);
            if (!TextUtils.isEmpty(text)) {
                setAccessibilityPaneTitle(text);
            }
            mAccessibilityCallback.onBottomSheetCollapsed();
        } else if (state == STATE_EXPANDED) {
            CharSequence text = getAccessibilityText(mSelectedAction, /* isShown= */ true);
            if (!TextUtils.isEmpty(text)) {
                setAccessibilityPaneTitle(text);
            }
            mAccessibilityCallback.onBottomSheetExpanded();
        }
    }

    private CharSequence getAccessibilityText(BottomAction action, boolean isShown) {
        if (action == null) {
            return null;
        }
        int resId = action.getAccessibilityStringRes(isShown);
        if (resId != 0) {
            return mContext.getText(resId);
        }
        return null;
    }

    /**
     * Skip bottom action's Accessibility event.
     *
     * @param actions the {@link BottomAction} actions to be skipped.
     * @param eventTypes the {@link AccessibilityEvent} event types to be skipped.
     */
    private void skipAccessibilityEvent(BottomAction[] actions, int[] eventTypes) {
        for (BottomAction action : actions) {
            View view = mActionMap.get(action);
            view.setAccessibilityDelegate(new AccessibilityDelegate() {
                @Override
                public void sendAccessibilityEvent(View host, int eventType) {
                    if (!ArrayUtils.contains(eventTypes, eventType)) {
                        super.sendAccessibilityEvent(host, eventType);
                    }
                }
            });
        }
    }

    /** A {@link BottomSheetBehavior} that can process a queue of bottom sheet states.*/
    public static class QueueStateBottomSheetBehavior<V extends View>
            extends BottomSheetBehavior<V> {

        private final Deque<Integer> mStateQueue = new ArrayDeque<>();
        private boolean mIsQueueProcessing;

        public QueueStateBottomSheetBehavior(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            // Binds the default callback for processing queue.
            setBottomSheetCallback(null);
        }

        /** Enqueues the bottom sheet states. */
        public void enqueue(int state) {
            if (!mStateQueue.isEmpty() && mStateQueue.getLast() == state) {
                return;
            }
            mStateQueue.add(state);
        }

        /** Processes the queue of bottom sheet state that was set via {@link #enqueue}. */
        public void processQueueForStateChange() {
            if (mStateQueue.isEmpty()) {
                return;
            }
            setState(mStateQueue.getFirst());
            mIsQueueProcessing = true;
        }

        /**
         * Returns {@code true} if the queue is processing. For example, if the bottom sheet is
         * going with expanded-collapsed-expanded, it would return {@code true} until last expanded
         * state is finished.
         */
        public boolean isQueueProcessing() {
            return mIsQueueProcessing;
        }

        /** Resets the queue state. */
        public void reset() {
            mStateQueue.clear();
            mIsQueueProcessing = false;
        }

        @Override
        public void setBottomSheetCallback(BottomSheetCallback callback) {
            super.setBottomSheetCallback(new BottomSheetCallback() {
                @Override
                public void onStateChanged(@NonNull View bottomSheet, int newState) {
                    if (!mStateQueue.isEmpty()) {
                        if (newState == mStateQueue.getFirst()) {
                            mStateQueue.removeFirst();
                            if (mStateQueue.isEmpty()) {
                                mIsQueueProcessing = false;
                            } else {
                                setState(mStateQueue.getFirst());
                            }
                        } else {
                            setState(mStateQueue.getFirst());
                        }
                    }

                    if (callback != null) {
                        callback.onStateChanged(bottomSheet, newState);
                    }
                }

                @Override
                public void onSlide(@NonNull View bottomSheet, float slideOffset) {
                    if (callback != null) {
                        callback.onSlide(bottomSheet, slideOffset);
                    }
                }
            });
        }
    }
}
