// Copyright 2013 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.infobar;

import android.animation.ObjectAnimator;
import android.app.Activity;
import android.graphics.Canvas;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

import com.google.common.annotations.VisibleForTesting;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.CalledByNative;
import org.chromium.content.browser.DeviceUtils;
import org.chromium.ui.UiUtils;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;


/**
 * A container for all the infobars of a specific tab.
 * Note that infobars creation can be initiated from Java of from native code.
 * When initiated from native code, special code is needed to keep the Java and native infobar in
 * sync, see NativeInfoBar.
 */
public class InfoBarContainer extends LinearLayout {
    private static final String TAG = "InfoBarContainer";
    private static final long REATTACH_FADE_IN_MS = 250;

    public interface InfoBarAnimationListener {
        /**
         * Notifies the subscriber when an animation is completed.
         */
        void notifyAnimationFinished(int animationType);
    }

    private static class InfoBarTransitionInfo {
        // InfoBar being animated.
        public InfoBar target;

        // View to replace the current View shown by the ContentWrapperView.
        public View toShow;

        // Which type of animation needs to be performed.
        public int animationType;

        public InfoBarTransitionInfo(InfoBar bar, View view, int type) {
            assert type >= AnimationHelper.ANIMATION_TYPE_SHOW;
            assert type < AnimationHelper.ANIMATION_TYPE_BOUNDARY;

            target = bar;
            toShow = view;
            animationType = type;
        }
    }

    private InfoBarAnimationListener mAnimationListener;

    // Native InfoBarContainer pointer which will be set by nativeInit()
    private int mNativeInfoBarContainer;

    private final Activity mActivity;

    private final AutoLoginDelegate mAutoLoginDelegate;

    // Whether the infobar are shown on top (below the location bar) or at the bottom of the screen.
    private final boolean mInfoBarsOnTop;

    // The list of all infobars in this container, regardless of whether they've been shown yet.
    private final ArrayList<InfoBar> mInfoBars = new ArrayList<InfoBar>();

    // We only animate changing infobars one at a time.
    private final ArrayDeque<InfoBarTransitionInfo> mInfoBarTransitions;

    // Animation currently moving InfoBars around.
    private AnimationHelper mAnimation;
    private final FrameLayout mAnimationSizer;

    // True when this container has been emptied and its native counterpart has been destroyed.
    private boolean mDestroyed = false;

    // The id of the tab associated with us. Set to TabBase.INVALID_TAB_ID if no tab is associated.
    private int mTabId;

    // Parent view that contains us.
    private ViewGroup mParentView;

    public InfoBarContainer(Activity activity, AutoLoginProcessor autoLoginProcessor,
            int tabId, ViewGroup parentView, int nativeWebContents) {
        super(activity);
        setOrientation(LinearLayout.VERTICAL);
        mAnimationListener = null;
        mInfoBarTransitions = new ArrayDeque<InfoBarTransitionInfo>();

        mAutoLoginDelegate = new AutoLoginDelegate(autoLoginProcessor, activity);
        mActivity = activity;
        mTabId = tabId;
        mParentView = parentView;

        mAnimationSizer = new FrameLayout(activity);
        mAnimationSizer.setVisibility(INVISIBLE);

        // The tablet has the infobars below the location bar. On the phone they are at the bottom.
        mInfoBarsOnTop = DeviceUtils.isTablet(activity);
        setGravity(determineGravity());

        // Chromium's InfoBarContainer may add an InfoBar immediately during this initialization
        // call, so make sure everything in the InfoBarContainer is completely ready beforehand.
        mNativeInfoBarContainer = nativeInit(nativeWebContents, mAutoLoginDelegate);
    }

    public void setAnimationListener(InfoBarAnimationListener listener) {
        mAnimationListener = listener;
    }

    @VisibleForTesting
    public InfoBarAnimationListener getAnimationListener() {
        return mAnimationListener;
    }


    public boolean areInfoBarsOnTop() {
        return mInfoBarsOnTop;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // Trap any attempts to fiddle with the Views while we're animating.
        return mAnimation != null;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Consume all motion events so they do not reach the ContentView.
        return true;
    }

    private void addToParentView() {
        if (mParentView != null && mParentView.indexOfChild(this) == -1) {
            mParentView.addView(this, createLayoutParams());
        }
    }

    private int determineGravity() {
        return mInfoBarsOnTop ? Gravity.TOP : Gravity.BOTTOM;
    }

    private FrameLayout.LayoutParams createLayoutParams() {
        return new FrameLayout.LayoutParams(
                LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, determineGravity());
    }

    public void removeFromParentView() {
        if (getParent() != null) {
            ((ViewGroup) getParent()).removeView(this);
        }
    }

    /**
     * Called when the parent {@link android.view.ViewGroup} has changed for
     * this container.
     */
    public void onParentViewChanged(int tabId, ViewGroup parentView) {
        mTabId = tabId;
        mParentView = parentView;

        if (getParent() != null) {
            removeFromParentView();
            addToParentView();
        }
    }

    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        if (mAnimation == null || child != mAnimation.getTarget()) {
            return super.drawChild(canvas, child, drawingTime);
        }
        // When infobars are on top, the new infobar Z-order is greater than the previous infobar,
        // which means it shows on top during the animation. We cannot change the Z-order in the
        // linear layout, it is driven by the insertion index.
        // So we simply clip the children to their bounds to make sure the new infobar does not
        // paint over.
        boolean retVal;
        canvas.save();
        canvas.clipRect(mAnimation.getTarget().getClippingRect());
        retVal = super.drawChild(canvas, child, drawingTime);
        canvas.restore();
        return retVal;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        ObjectAnimator.ofFloat(this, "alpha", 0.f, 1.f).setDuration(REATTACH_FADE_IN_MS).start();
        setVisibility(VISIBLE);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        setVisibility(INVISIBLE);
    }

    public InfoBar findInfoBar(int nativeInfoBar) {
        for (InfoBar infoBar : mInfoBars) {
            if (infoBar.ownsNativeInfoBar(nativeInfoBar)) {
                return infoBar;
            }
        }
        return null;
    }

    /**
     * Adds an InfoBar to the view hierarchy.
     * @param infoBar InfoBar to add to the View hierarchy.
     */
    @CalledByNative
    public void addInfoBar(InfoBar infoBar) {
        assert !mDestroyed;
        if (infoBar == null) {
            return;
        }
        if (mInfoBars.contains(infoBar)) {
            assert false : "Trying to add an info bar that has already been added.";
            return;
        }

        // We add the infobar immediately to mInfoBars but we wait for the animation to end to
        // notify it's been added, as tests rely on this notification but expects the infobar view
        // to be available when they get the notification.
        mInfoBars.add(infoBar);
        infoBar.setContext(mActivity);
        infoBar.setInfoBarContainer(this);

        enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_SHOW);
    }

    /**
     * Returns the latest InfoBarTransitionInfo that deals with the given InfoBar.
     * @param toFind InfoBar that we're looking for.
     */
    public InfoBarTransitionInfo findLastTransitionForInfoBar(InfoBar toFind) {
        Iterator<InfoBarTransitionInfo> iterator = mInfoBarTransitions.descendingIterator();
        while (iterator.hasNext()) {
            InfoBarTransitionInfo info = iterator.next();
            if (info.target == toFind) return info;
        }
        return null;
    }

    /**
     * Animates swapping out the current View in the {@code infoBar} with {@code toShow} without
     * destroying or dismissing the entire InfoBar.
     * @param infoBar InfoBar that is having its content replaced.
     * @param toShow View representing the InfoBar's new contents.
     */
    public void swapInfoBarViews(InfoBar infoBar, View toShow) {
        assert !mDestroyed;

        if (!mInfoBars.contains(infoBar)) {
            assert false : "Trying to swap an InfoBar that is not in this container.";
            return;
        }

        InfoBarTransitionInfo transition = findLastTransitionForInfoBar(infoBar);
        if (transition != null && transition.toShow == toShow) {
            assert false : "Tried to enqueue the same swap twice in a row.";
            return;
        }

        enqueueInfoBarAnimation(infoBar, toShow, AnimationHelper.ANIMATION_TYPE_SWAP);
    }

    /**
     * Removes an InfoBar from the view hierarchy.
     * @param infoBar InfoBar to remove from the View hierarchy.
     */
    public void removeInfoBar(InfoBar infoBar) {
        assert !mDestroyed;

        if (!mInfoBars.remove(infoBar)) {
            assert false : "Trying to remove an InfoBar that is not in this container.";
            return;
        }

        // If an InfoBar is told to hide itself before it has a chance to be shown, don't bother
        // with animating any of it.
        boolean collapseAnimations = false;
        ArrayDeque<InfoBarTransitionInfo> transitionCopy =
                new ArrayDeque<InfoBarTransitionInfo>(mInfoBarTransitions);
        for (InfoBarTransitionInfo info : transitionCopy) {
            if (info.target == infoBar) {
                if (info.animationType == AnimationHelper.ANIMATION_TYPE_SHOW) {
                    // We can assert that two attempts to show the same InfoBar won't be in the
                    // deque simultaneously because of the check in addInfoBar().
                    assert !collapseAnimations;
                    collapseAnimations = true;
                }
                if (collapseAnimations) {
                    mInfoBarTransitions.remove(info);
                }
            }
        }

        if (!collapseAnimations) {
            enqueueInfoBarAnimation(infoBar, null, AnimationHelper.ANIMATION_TYPE_HIDE);
        }
    }

    /**
     * Enqueue a new animation to run and kicks off the animation sequence.
     */
    private void enqueueInfoBarAnimation(InfoBar infoBar, View toShow, int animationType) {
        InfoBarTransitionInfo info = new InfoBarTransitionInfo(infoBar, toShow, animationType);
        mInfoBarTransitions.add(info);
        processPendingInfoBars();
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // Hide the infobars when the keyboard is showing.
        boolean isShowing = (getVisibility() == View.VISIBLE);
        if (UiUtils.isKeyboardShowing(mActivity, this)) {
            if (isShowing) {
                setVisibility(View.INVISIBLE);
            }
        } else {
            if (!isShowing) {
                setVisibility(View.VISIBLE);
            }
        }
        super.onLayout(changed, l, t, r, b);
    }

    /**
     * @return True when this container has been emptied and its native counterpart has been
     *         destroyed.
     */
    public boolean hasBeenDestroyed() {
        return mDestroyed;
    }

    private void processPendingInfoBars() {
        if (mAnimation != null || mInfoBarTransitions.isEmpty()) return;

        // Start animating what has to be animated.
        InfoBarTransitionInfo info = mInfoBarTransitions.remove();
        View toShow = info.toShow;
        ContentWrapperView targetView;

        addToParentView();

        if (info.animationType == AnimationHelper.ANIMATION_TYPE_SHOW) {
            targetView = info.target.getContentWrapper(true);
            assert mInfoBars.contains(info.target);
            toShow = targetView.detachCurrentView();
            addView(targetView, mInfoBarsOnTop ? getChildCount() : 0,
                    new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
        } else {
            targetView = info.target.getContentWrapper(false);
        }

        // Kick off the animation.
        mAnimation = new AnimationHelper(this, targetView, info.target, toShow, info.animationType);
        mAnimation.start();
    }

    // Called by the tab when it has started loading a new page.
    public void onPageStarted(String url) {
        LinkedList<InfoBar> barsToRemove = new LinkedList<InfoBar>();

        for (InfoBar infoBar : mInfoBars) {
            if (infoBar.shouldExpire(url)) {
                barsToRemove.add(infoBar);
            }
        }

        for (InfoBar infoBar : barsToRemove) {
            infoBar.dismissJavaOnlyInfoBar();
        }
    }

    /**
     * Returns the id of the tab we are associated with.
     */
    public int getTabId() {
        return mTabId;
    }

    public void destroy() {
        mDestroyed = true;
        removeAllViews();
        if (mNativeInfoBarContainer != 0) {
            nativeDestroy(mNativeInfoBarContainer);
        }
        mInfoBarTransitions.clear();
    }

    /**
     * @return all of the InfoBars held in this container.
     */
    @VisibleForTesting
    public ArrayList<InfoBar> getInfoBars() {
        return mInfoBars;
    }

    /**
     * Dismisses all {@link AutoLoginInfoBar}s in this {@link InfoBarContainer} that are for
     * {@code accountName} and {@code authToken}.  This also resets all {@link InfoBar}s that are
     * for a different request.
     * @param accountName The name of the account request is being accessed for.
     * @param authToken The authentication token access is being requested for.
     * @param success Whether or not the authentication attempt was successful.
     * @param result The resulting token for the auto login request (ignored if {@code success} is
     *               {@code false}.
     */
    public void processAutoLogin(String accountName, String authToken, boolean success,
            String result) {
        mAutoLoginDelegate.dismissAutoLogins(accountName, authToken, success, result);
    }

    /**
     * Dismiss all auto logins infobars without processing any result.
     */
    public void dismissAutoLoginInfoBars() {
        mAutoLoginDelegate.dismissAutoLogins("", "", false, "");
    }

    public void prepareTransition(View toShow) {
        if (toShow != null) {
            // In order to animate the addition of the infobar, we need a layout first.
            // Attach the child to invisible layout so that we can get measurements for it without
            // moving everything in the real container.
            ViewGroup parent = (ViewGroup) toShow.getParent();
            if (parent != null) parent.removeView(toShow);

            assert mAnimationSizer.getParent() == null;
            mParentView.addView(mAnimationSizer, createLayoutParams());
            mAnimationSizer.addView(toShow, 0,
                    new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
            mAnimationSizer.requestLayout();
        }
    }

    public void startTransition() {
        if (mInfoBarsOnTop) {
            // We need to clip this view to its bounds while it is animated because the layout's
            // z-ordering puts it on top of other infobars as it's being animated.
            ApiCompatibilityUtils.postInvalidateOnAnimation(this);
        }
    }

    /**
     * Finishes off whatever animation is running.
     */
    public void finishTransition() {
        assert mAnimation != null;

        // If the InfoBar was hidden, get rid of its View entirely.
        if (mAnimation.getAnimationType() == AnimationHelper.ANIMATION_TYPE_HIDE) {
            removeView(mAnimation.getTarget());
        }

        // Reset all translations and put everything where they need to be.
        for (int i = 0; i < getChildCount(); ++i) {
            View view = getChildAt(i);
            view.setTranslationY(0);
        }
        requestLayout();

        // If there are no infobars shown, there is no need to keep the infobar container in the
        // view hierarchy.
        if (getChildCount() == 0) {
            removeFromParentView();
        }

        if (mAnimationSizer.getParent() != null) {
            ((ViewGroup) mAnimationSizer.getParent()).removeView(mAnimationSizer);
        }

        // Notify interested parties and move on to the next animation.
        if (mAnimationListener != null) {
            mAnimationListener.notifyAnimationFinished(mAnimation.getAnimationType());
        }
        mAnimation = null;
        processPendingInfoBars();
    }

    /**
     * Searches a given view's child views for an instance of {@link InfoBarContainer}.
     *
     * @param parentView View to be searched for
     * @return {@link InfoBarContainer} instance if it's one of the child views;
     *     otherwise {@code null}.
     */
    public static InfoBarContainer childViewOf(ViewGroup parentView) {
        for (int i = 0; i < parentView.getChildCount(); i++) {
            if (parentView.getChildAt(i) instanceof InfoBarContainer) {
                return (InfoBarContainer) parentView.getChildAt(i);
            }
        }
        return null;
    }

    public int getNative() {
        return mNativeInfoBarContainer;
    }

    private native int nativeInit(int webContentsPtr, AutoLoginDelegate autoLoginDelegate);

    private native void nativeDestroy(int nativeInfoBarContainerAndroid);
}