// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.ntp.cards;

import android.graphics.Rect;
import android.support.annotation.CallSuper;
import android.support.annotation.DrawableRes;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v7.widget.RecyclerView;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.animation.Interpolator;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ntp.ContextMenuManager;
import org.chromium.chrome.browser.ntp.ContextMenuManager.ContextMenuItemId;
import org.chromium.chrome.browser.suggestions.SuggestionsRecyclerView;
import org.chromium.chrome.browser.util.MathUtils;
import org.chromium.chrome.browser.util.ViewUtils;
import org.chromium.chrome.browser.widget.displaystyle.HorizontalDisplayStyle;
import org.chromium.chrome.browser.widget.displaystyle.MarginResizer;
import org.chromium.chrome.browser.widget.displaystyle.UiConfig;

/**
 * Holder for a generic card.
 *
 * Specific behaviors added to the cards:
 *
 * - Cards can peek above the fold if there is enough space.
 *
 * - When peeking, tapping on cards will make them request a scroll up (see
 *   {@link SuggestionsRecyclerView#interceptCardTapped}). Tap events in non-peeking state will be
 *   routed through {@link #onCardTapped()} for subclasses to override.
 *
 * - Cards will get some lateral margins when the viewport is sufficiently wide.
 *   (see {@link HorizontalDisplayStyle#WIDE})
 *
 * Note: If a subclass overrides {@link #onBindViewHolder()}, it should call the
 * parent implementation to reset the private state when a card is recycled.
 */
public abstract class CardViewHolder
        extends NewTabPageViewHolder implements ContextMenuManager.Delegate {
    private static final Interpolator TRANSITION_INTERPOLATOR = new FastOutSlowInInterpolator();

    /** Value used for max peeking card height and padding. */
    private final int mMaxPeekPadding;

    /**
     * The card shadow is part of the drawable nine-patch and not drawn via setElevation(),
     * so it is included in the height and width of the drawable. This member contains the
     * dimensions of the shadow (from the drawable's padding), so it can be used to offset the
     * position in calculations.
     */
    private final Rect mCardShadow = new Rect();

    private final int mCardGap;

    private final int mDefaultLateralMargin;
    private final int mWideLateralMargin;

    protected final SuggestionsRecyclerView mRecyclerView;

    private final UiConfig mUiConfig;
    private final MarginResizer mMarginResizer;

    /**
     * To what extent the card is "peeking". 0 means the card is not peeking at all and spans the
     * full width of its parent. 1 means it is fully peeking and will be shown with a margin.
     */
    private float mPeekingPercentage;

    @DrawableRes
    private int mBackground;

    /**
     * @param layoutId resource id of the layout to inflate and to use as card.
     * @param recyclerView ViewGroup that will contain the newly created view.
     * @param uiConfig The NTP UI configuration object used to adjust the card UI.
     * @param contextMenuManager The manager responsible for the context menu.
     */
    public CardViewHolder(int layoutId, final SuggestionsRecyclerView recyclerView,
            UiConfig uiConfig, final ContextMenuManager contextMenuManager) {
        super(inflateView(layoutId, recyclerView));

        ApiCompatibilityUtils.getDrawable(recyclerView.getResources(), R.drawable.card_single)
                .getPadding(mCardShadow);

        mCardGap = recyclerView.getResources().getDimensionPixelSize(R.dimen.snippets_card_gap);

        mMaxPeekPadding = recyclerView.getResources().getDimensionPixelSize(
                R.dimen.snippets_padding);

        mRecyclerView = recyclerView;

        itemView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (recyclerView.interceptCardTapped(CardViewHolder.this)) return;
                onCardTapped();
            }
        });

        itemView.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
            @Override
            public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
                if (isPeeking()) return;

                contextMenuManager.createContextMenu(menu, itemView, CardViewHolder.this);
            }
        });

        mUiConfig = uiConfig;

        // Configure the resizer to use negative margins on regular display to balance out the
        // lateral shadow of the card 9-patch and avoid a rounded corner effect.
        int cardCornerRadius = recyclerView.getResources().getDimensionPixelSize(
                R.dimen.card_corner_radius);
        assert mCardShadow.left == mCardShadow.right;
        mDefaultLateralMargin = -(mCardShadow.left + cardCornerRadius);
        mWideLateralMargin = recyclerView.getResources().getDimensionPixelSize(
                R.dimen.ntp_wide_card_lateral_margins);

        mMarginResizer = MarginResizer.createWithViewAdapter(itemView, mUiConfig,
                mDefaultLateralMargin, mWideLateralMargin);
    }

    @Override
    public boolean isItemSupported(@ContextMenuItemId int menuItemId) {
        return menuItemId == ContextMenuManager.ID_REMOVE && isDismissable();
    }

    @Override
    public void removeItem() {
        getRecyclerView().dismissItemWithAnimation(this);
    }

    @Override
    public void openItem(int windowDisposition) {
        throw new UnsupportedOperationException();
    }

    @Override
    public String getUrl() {
        return null;
    }

    @Override
    public boolean isDismissable() {
        if (isPeeking()) return false;

        int position = getAdapterPosition();
        if (position == RecyclerView.NO_POSITION) return false;

        return !mRecyclerView.getNewTabPageAdapter().getItemDismissalGroup(position).isEmpty();
    }

    @Override
    public void onContextMenuCreated() {}

    /**
     * Called when the NTP cards adapter is requested to update the currently visible ViewHolder
     * with data.
     */
    @CallSuper
    protected void onBindViewHolder() {
        // Reset the transparency and translation in case a dismissed card is being recycled.
        itemView.setAlpha(1f);
        itemView.setTranslationX(0f);

        itemView.addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
            @Override
            public void onViewAttachedToWindow(View view) {}

            @Override
            public void onViewDetachedFromWindow(View view) {
                // In some cases a view can be removed while a user is interacting with it, without
                // calling ItemTouchHelper.Callback#clearView(), which we rely on for bottomSpacer
                // calculations. So we call this explicitly here instead.
                // See https://crbug.com/664466, b/32900699
                mRecyclerView.onItemDismissFinished(mRecyclerView.findContainingViewHolder(view));
                itemView.removeOnAttachStateChangeListener(this);
            }
        });

        // Make sure we use the right background.
        updateLayoutParams();

        mRecyclerView.onCardBound(this);
    }

    @Override
    public void updateLayoutParams() {
        // Nothing to do for dismissed cards.
        if (getAdapterPosition() == RecyclerView.NO_POSITION) return;

        NewTabPageAdapter adapter = mRecyclerView.getNewTabPageAdapter();

        // Each card has the full elevation effect (the shadow) in the 9-patch. If the next item is
        // a card a negative bottom margin is set so the next card is overlaid slightly on top of
        // this one and hides the bottom shadow.
        int abovePosition = getAdapterPosition() - 1;
        boolean hasCardAbove = abovePosition >= 0 && isCard(adapter.getItemViewType(abovePosition));
        int belowPosition = getAdapterPosition() + 1;
        boolean hasCardBelow = false;
        if (belowPosition < adapter.getItemCount()) {
            // The PROMO card has an empty margin and will not be right against the preceding card,
            // so we don't consider it a card from the point of view of the preceding one.
            @ItemViewType int belowViewType = adapter.getItemViewType(belowPosition);
            hasCardBelow = isCard(belowViewType) && belowViewType != ItemViewType.PROMO;
        }

        @DrawableRes
        int selectedBackground = selectBackground(hasCardAbove, hasCardBelow);
        if (mBackground == selectedBackground) return;

        mBackground = selectedBackground;
        ViewUtils.setNinePatchBackgroundResource(itemView, selectedBackground);

        // By default the apparent distance between two cards is the sum of the bottom and top
        // height of their shadows. We want |mCardGap| instead, so we set the bottom margin to
        // the difference.
        // noinspection ResourceType
        getParams().bottomMargin =
                hasCardBelow ? (mCardGap - (mCardShadow.top + mCardShadow.bottom)) : 0;
    }

    /**
     * Resets the appearance of the card to not peeking.
     */
    public void setNotPeeking() {
        setPeekingPercentage(0);
    }

    /**
     * Change the width, padding and child opacity of the card to give a smooth transition from
     * peeking to fully expanded as the user scrolls.
     * @param availableSpace space (pixels) available between the bottom of the screen and the
     *                       above-the-fold section, where the card can peek.
     */
    public void updatePeek(int availableSpace) {
        // If 1 padding unit (|mMaxPeekPadding|) is visible, the card is fully peeking. This is
        // reduced as the card is scrolled up, until 2 padding units are visible and the card is
        // not peeking anymore at all. Anything not between 0 and 1 is clamped.
        setPeekingPercentage(
                MathUtils.clamp(2f - (float) availableSpace / mMaxPeekPadding, 0f, 1f));
    }

    /**
     * @return Whether the card is peeking.
     */
    public boolean isPeeking() {
        return mPeekingPercentage > 0f;
    }

    /**
     * Override this to react when the card is tapped. This method will not be called if the card is
     * currently peeking.
     */
    protected void onCardTapped() {}

    private void setPeekingPercentage(float peekingPercentage) {
        if (mPeekingPercentage == peekingPercentage) return;

        mPeekingPercentage = peekingPercentage;

        int peekPadding = (int) (mMaxPeekPadding
                * TRANSITION_INTERPOLATOR.getInterpolation(1f - peekingPercentage));

        // Modify the padding so as the margin increases, the padding decreases, keeping the card's
        // contents in the same position. The top and bottom remain the same.
        int lateralPadding;
        if (mUiConfig.getCurrentDisplayStyle().horizontal != HorizontalDisplayStyle.WIDE) {
            lateralPadding = peekPadding;
        } else {
            lateralPadding = mMaxPeekPadding;
        }
        itemView.setPadding(lateralPadding, mMaxPeekPadding, lateralPadding, mMaxPeekPadding);

        // Adjust the margins. The shadow width is offset via the default lateral margin.
        mMarginResizer.setMargins(mDefaultLateralMargin + mMaxPeekPadding - peekPadding,
                mWideLateralMargin);

        // Set the opacity of the card content to be 0 when peeking and 1 when full width.
        int itemViewChildCount = ((ViewGroup) itemView).getChildCount();
        for (int i = 0; i < itemViewChildCount; ++i) {
            View snippetChild = ((ViewGroup) itemView).getChildAt(i);
            snippetChild.setAlpha(peekPadding / (float) mMaxPeekPadding);
        }
    }

    private static View inflateView(int resourceId, ViewGroup parent) {
        return LayoutInflater.from(parent.getContext()).inflate(resourceId, parent, false);
    }

    public static boolean isCard(@ItemViewType int type) {
        switch (type) {
            case ItemViewType.SNIPPET:
            case ItemViewType.STATUS:
            case ItemViewType.ACTION:
            case ItemViewType.PROMO:
                return true;
            case ItemViewType.ABOVE_THE_FOLD:
            case ItemViewType.TILE_GRID:
            case ItemViewType.HEADER:
            case ItemViewType.SPACING:
            case ItemViewType.PROGRESS:
            case ItemViewType.FOOTER:
            case ItemViewType.ALL_DISMISSED:
                return false;
            default:
                assert false;
        }
        return false;
    }

    @DrawableRes
    protected int selectBackground(boolean hasCardAbove, boolean hasCardBelow) {
        if (hasCardAbove && hasCardBelow) return R.drawable.card_middle;
        if (!hasCardAbove && hasCardBelow) return R.drawable.card_top;
        if (hasCardAbove && !hasCardBelow) return R.drawable.card_bottom;
        return R.drawable.card_single;
    }

    public SuggestionsRecyclerView getRecyclerView() {
        return mRecyclerView;
    }
}