/*
 * Copyright (C) 2016 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 android.support.v7.widget;

import android.content.Context;
import android.os.Build;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewPropertyAnimatorCompat;
import android.support.v4.widget.ListViewAutoScrollHelper;
import android.support.v7.appcompat.R;
import android.view.MotionEvent;
import android.view.View;

/**
 * <p>Wrapper class for a ListView. This wrapper can hijack the focus to
 * make sure the list uses the appropriate drawables and states when
 * displayed on screen within a drop down. The focus is never actually
 * passed to the drop down in this mode; the list only looks focused.</p>
 */
class DropDownListView extends ListViewCompat {

    /*
    * WARNING: This is a workaround for a touch mode issue.
    *
    * Touch mode is propagated lazily to windows. This causes problems in
    * the following scenario:
    * - Type something in the AutoCompleteTextView and get some results
    * - Move down with the d-pad to select an item in the list
    * - Move up with the d-pad until the selection disappears
    * - Type more text in the AutoCompleteTextView *using the soft keyboard*
    *   and get new results; you are now in touch mode
    * - The selection comes back on the first item in the list, even though
    *   the list is supposed to be in touch mode
    *
    * Using the soft keyboard triggers the touch mode change but that change
    * is propagated to our window only after the first list layout, therefore
    * after the list attempts to resurrect the selection.
    *
    * The trick to work around this issue is to pretend the list is in touch
    * mode when we know that the selection should not appear, that is when
    * we know the user moved the selection away from the list.
    *
    * This boolean is set to true whenever we explicitly hide the list's
    * selection and reset to false whenever we know the user moved the
    * selection back to the list.
    *
    * When this boolean is true, isInTouchMode() returns true, otherwise it
    * returns super.isInTouchMode().
    */
    private boolean mListSelectionHidden;

    /**
     * True if this wrapper should fake focus.
     */
    private boolean mHijackFocus;

    /** Whether to force drawing of the pressed state selector. */
    private boolean mDrawsInPressedState;

    /** Current drag-to-open click animation, if any. */
    private ViewPropertyAnimatorCompat mClickAnimation;

    /** Helper for drag-to-open auto scrolling. */
    private ListViewAutoScrollHelper mScrollHelper;

    /**
     * <p>Creates a new list view wrapper.</p>
     *
     * @param context this view's context
     */
    public DropDownListView(Context context, boolean hijackFocus) {
        super(context, null, R.attr.dropDownListViewStyle);
        mHijackFocus = hijackFocus;
        setCacheColorHint(0); // Transparent, since the background drawable could be anything.
    }

    /**
     * Handles forwarded events.
     *
     * @param activePointerId id of the pointer that activated forwarding
     * @return whether the event was handled
     */
    public boolean onForwardedEvent(MotionEvent event, int activePointerId) {
        boolean handledEvent = true;
        boolean clearPressedItem = false;

        final int actionMasked = MotionEventCompat.getActionMasked(event);
        switch (actionMasked) {
            case MotionEvent.ACTION_CANCEL:
                handledEvent = false;
                break;
            case MotionEvent.ACTION_UP:
                handledEvent = false;
                // $FALL-THROUGH$
            case MotionEvent.ACTION_MOVE:
                final int activeIndex = event.findPointerIndex(activePointerId);
                if (activeIndex < 0) {
                    handledEvent = false;
                    break;
                }

                final int x = (int) event.getX(activeIndex);
                final int y = (int) event.getY(activeIndex);
                final int position = pointToPosition(x, y);
                if (position == INVALID_POSITION) {
                    clearPressedItem = true;
                    break;
                }

                final View child = getChildAt(position - getFirstVisiblePosition());
                setPressedItem(child, position, x, y);
                handledEvent = true;

                if (actionMasked == MotionEvent.ACTION_UP) {
                    clickPressedItem(child, position);
                }
                break;
        }

        // Failure to handle the event cancels forwarding.
        if (!handledEvent || clearPressedItem) {
            clearPressedItem();
        }

        // Manage automatic scrolling.
        if (handledEvent) {
            if (mScrollHelper == null) {
                mScrollHelper = new ListViewAutoScrollHelper(this);
            }
            mScrollHelper.setEnabled(true);
            mScrollHelper.onTouch(this, event);
        } else if (mScrollHelper != null) {
            mScrollHelper.setEnabled(false);
        }

        return handledEvent;
    }

    /**
     * Starts an alpha animation on the selector. When the animation ends,
     * the list performs a click on the item.
     */
    private void clickPressedItem(final View child, final int position) {
        final long id = getItemIdAtPosition(position);
        performItemClick(child, position, id);
    }

    /**
     * Sets whether the list selection is hidden, as part of a workaround for a
     * touch mode issue (see the declaration for mListSelectionHidden).
     *
     * @param hideListSelection {@code true} to hide list selection,
     *                          {@code false} to show
     */
    void setListSelectionHidden(boolean hideListSelection) {
        mListSelectionHidden = hideListSelection;
    }

    private void clearPressedItem() {
        mDrawsInPressedState = false;
        setPressed(false);
        // This will call through to updateSelectorState()
        drawableStateChanged();

        final View motionView = getChildAt(mMotionPosition - getFirstVisiblePosition());
        if (motionView != null) {
            motionView.setPressed(false);
        }

        if (mClickAnimation != null) {
            mClickAnimation.cancel();
            mClickAnimation = null;
        }
    }

    private void setPressedItem(View child, int position, float x, float y) {
        mDrawsInPressedState = true;

        // Ordering is essential. First, update the container's pressed state.
        if (Build.VERSION.SDK_INT >= 21) {
            drawableHotspotChanged(x, y);
        }
        if (!isPressed()) {
            setPressed(true);
        }

        // Next, run layout to stabilize child positions.
        layoutChildren();

        // Manage the pressed view based on motion position. This allows us to
        // play nicely with actual touch and scroll events.
        if (mMotionPosition != INVALID_POSITION) {
            final View motionView = getChildAt(mMotionPosition - getFirstVisiblePosition());
            if (motionView != null && motionView != child && motionView.isPressed()) {
                motionView.setPressed(false);
            }
        }
        mMotionPosition = position;

        // Offset for child coordinates.
        final float childX = x - child.getLeft();
        final float childY = y - child.getTop();
        if (Build.VERSION.SDK_INT >= 21) {
            child.drawableHotspotChanged(childX, childY);
        }
        if (!child.isPressed()) {
            child.setPressed(true);
        }

        // Ensure that keyboard focus starts from the last touched position.
        positionSelectorLikeTouchCompat(position, child, x, y);

        // This needs some explanation. We need to disable the selector for this next call
        // due to the way that ListViewCompat works. Otherwise both ListView and ListViewCompat
        // will draw the selector and bad things happen.
        setSelectorEnabled(false);

        // Refresh the drawable state to reflect the new pressed state,
        // which will also update the selector state.
        refreshDrawableState();
    }

    @Override
    protected boolean touchModeDrawsInPressedStateCompat() {
        return mDrawsInPressedState || super.touchModeDrawsInPressedStateCompat();
    }

    @Override
    public boolean isInTouchMode() {
        // WARNING: Please read the comment where mListSelectionHidden is declared
        return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
    }

    /**
     * <p>Returns the focus state in the drop down.</p>
     *
     * @return true always if hijacking focus
     */
    @Override
    public boolean hasWindowFocus() {
        return mHijackFocus || super.hasWindowFocus();
    }

    /**
     * <p>Returns the focus state in the drop down.</p>
     *
     * @return true always if hijacking focus
     */
    @Override
    public boolean isFocused() {
        return mHijackFocus || super.isFocused();
    }

    /**
     * <p>Returns the focus state in the drop down.</p>
     *
     * @return true always if hijacking focus
     */
    @Override
    public boolean hasFocus() {
        return mHijackFocus || super.hasFocus();
    }
}
