// Copyright 2015 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.tab;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.SystemClock;
import android.provider.Browser;
import android.text.TextUtils;

import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.util.IntentUtils;
import org.chromium.ui.base.PageTransition;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

/**
 * This class contains the logic to determine effective navigation/redirect.
 */
public class TabRedirectHandler {
    /**
     * An invalid entry index.
     */
    public static final int INVALID_ENTRY_INDEX = -1;
    private static final long INVALID_TIME = -1;

    private static final int NAVIGATION_TYPE_NONE = 0;
    private static final int NAVIGATION_TYPE_FROM_INTENT = 1;
    private static final int NAVIGATION_TYPE_FROM_USER_TYPING = 2;
    private static final int NAVIGATION_TYPE_FROM_LINK_WITHOUT_USER_GESTURE = 3;
    private static final int NAVIGATION_TYPE_FROM_RELOAD = 4;
    private static final int NAVIGATION_TYPE_OTHER = 5;

    private Intent mInitialIntent;
    // A resolver list which includes all resolvers of |mInitialIntent|.
    private final HashSet<ComponentName> mCachedResolvers = new HashSet<ComponentName>();
    private boolean mIsInitialIntentHeadingToChrome;
    private boolean mIsCustomTabIntent;

    private long mLastNewUrlLoadingTime = INVALID_TIME;
    private boolean mIsOnEffectiveRedirectChain;
    private int mInitialNavigationType;
    private int mLastCommittedEntryIndexBeforeStartingNavigation;

    private boolean mShouldNotOverrideUrlLoadingUntilNewUrlLoading;

    private final Context mContext;

    public TabRedirectHandler(Context context) {
        mContext = context;
    }

    /**
     * Updates |mIntentHistory| and |mLastIntentUpdatedTime|. If |intent| comes from chrome and
     * currently |mIsOnEffectiveIntentRedirectChain| is true, that means |intent| was sent from
     * this tab because only the front tab or a new tab can receive an intent from chrome. In that
     * case, |intent| is added to |mIntentHistory|.
     * Otherwise, |mIntentHistory| and |mPreviousResolvers| are cleared, and then |intent| is put
     * into |mIntentHistory|.
     */
    public void updateIntent(Intent intent) {
        clear();

        if (mContext == null || intent == null || !Intent.ACTION_VIEW.equals(intent.getAction())) {
            return;
        }

        mIsCustomTabIntent = ChromeLauncherActivity.isCustomTabIntent(intent);
        boolean checkIsToChrome = true;
        // All custom tabs VIEW intents are by design explicit intents, so the presence of package
        // name doesn't imply they have to be handled by Chrome explicitly. Check if external apps
        // should be checked for handling the initial redirect chain.
        if (mIsCustomTabIntent) {
            boolean sendToExternalApps = IntentUtils.safeGetBooleanExtra(intent,
                    CustomTabIntentDataProvider.EXTRA_SEND_TO_EXTERNAL_DEFAULT_HANDLER, false);
            checkIsToChrome = !(sendToExternalApps
                    && ChromeFeatureList.isEnabled(ChromeFeatureList.CCT_EXTERNAL_LINK_HANDLING));
        }

        if (checkIsToChrome) mIsInitialIntentHeadingToChrome = isIntentToChrome(mContext, intent);

        // A copy of the intent with component cleared to find resolvers.
        mInitialIntent = new Intent(intent).setComponent(null);
        Intent selector = mInitialIntent.getSelector();
        if (selector != null) selector.setComponent(null);
    }

    private static boolean isIntentToChrome(Context context, Intent intent) {
        String chromePackageName = context.getPackageName();
        return TextUtils.equals(chromePackageName, intent.getPackage())
                || TextUtils.equals(chromePackageName, IntentUtils.safeGetStringExtra(intent,
                        Browser.EXTRA_APPLICATION_ID));
    }

    private void clearIntentHistory() {
        mIsInitialIntentHeadingToChrome = false;
        mIsCustomTabIntent = false;
        mInitialIntent = null;
        mCachedResolvers.clear();
    }

    /**
     * Resets all variables except timestamps.
     */
    public void clear() {
        clearIntentHistory();
        mInitialNavigationType = NAVIGATION_TYPE_NONE;
        mIsOnEffectiveRedirectChain = false;
        mLastCommittedEntryIndexBeforeStartingNavigation = 0;
        mShouldNotOverrideUrlLoadingUntilNewUrlLoading = false;
    }

    public void setShouldNotOverrideUrlLoadingUntilNewUrlLoading() {
        mShouldNotOverrideUrlLoadingUntilNewUrlLoading = true;
    }

    /**
     * Updates new url loading information to trace navigation.
     * A time based heuristic is used to determine if this loading is an effective redirect or not
     * if core of |pageTransType| is LINK.
     *
     * http://crbug.com/322567 : Trace navigation started from an external app.
     * http://crbug.com/331571 : Trace navigation started from user typing to do not override such
     * navigation.
     * http://crbug.com/426679 : Trace every navigation and the last committed entry index right
     * before starting the navigation.
     *
     * @param pageTransType page transition type of this loading.
     * @param isRedirect whether this loading is http redirect or not.
     * @param hasUserGesture whether this loading is started by a user gesture.
     * @param lastUserInteractionTime time when the last user interaction was made.
     * @param lastCommittedEntryIndex the last committed entry index right before this loading.
     */
    public void updateNewUrlLoading(int pageTransType, boolean isRedirect, boolean hasUserGesture,
            long lastUserInteractionTime, int lastCommittedEntryIndex) {
        long prevNewUrlLoadingTime = mLastNewUrlLoadingTime;
        mLastNewUrlLoadingTime = SystemClock.elapsedRealtime();

        int pageTransitionCore = pageTransType & PageTransition.CORE_MASK;

        boolean isNewLoadingStartedByUser = false;
        boolean isFromIntent = pageTransitionCore == PageTransition.LINK
                && (pageTransType & PageTransition.FROM_API) != 0;
        if (!isRedirect) {
            if ((pageTransType & PageTransition.FORWARD_BACK) != 0) {
                isNewLoadingStartedByUser = true;
            } else if (pageTransitionCore != PageTransition.LINK) {
                isNewLoadingStartedByUser = true;
            } else if (prevNewUrlLoadingTime == INVALID_TIME || isFromIntent
                    || lastUserInteractionTime > prevNewUrlLoadingTime) {
                isNewLoadingStartedByUser = true;
            }
        }

        if (isNewLoadingStartedByUser) {
            // Updates mInitialNavigationType for a new loading started by a user's gesture.
            if (isFromIntent && mInitialIntent != null) {
                mInitialNavigationType = NAVIGATION_TYPE_FROM_INTENT;
            } else {
                clearIntentHistory();
                if (pageTransitionCore == PageTransition.TYPED) {
                    mInitialNavigationType = NAVIGATION_TYPE_FROM_USER_TYPING;
                } else if (pageTransitionCore == PageTransition.RELOAD
                        || (pageTransType & PageTransition.FORWARD_BACK) != 0) {
                    mInitialNavigationType = NAVIGATION_TYPE_FROM_RELOAD;
                } else if (pageTransitionCore == PageTransition.LINK && !hasUserGesture) {
                    mInitialNavigationType = NAVIGATION_TYPE_FROM_LINK_WITHOUT_USER_GESTURE;
                } else {
                    mInitialNavigationType = NAVIGATION_TYPE_OTHER;
                }
            }
            mIsOnEffectiveRedirectChain = false;
            mLastCommittedEntryIndexBeforeStartingNavigation = lastCommittedEntryIndex;
            mShouldNotOverrideUrlLoadingUntilNewUrlLoading = false;
        } else if (mInitialNavigationType != NAVIGATION_TYPE_NONE) {
            // Redirect chain starts from the second url loading.
            mIsOnEffectiveRedirectChain = true;
        }
    }

    /**
     * @return whether on effective intent redirect chain or not.
     */
    public boolean isOnEffectiveIntentRedirectChain() {
        return mInitialNavigationType == NAVIGATION_TYPE_FROM_INTENT && mIsOnEffectiveRedirectChain;
    }

    /**
     * @param hasExternalProtocol whether the destination URI has an external protocol or not.
     * @return whether we should stay in Chrome or not.
     */
    public boolean shouldStayInChrome(boolean hasExternalProtocol) {
        return (mIsInitialIntentHeadingToChrome && !hasExternalProtocol)
                || shouldNavigationTypeStayInChrome();

    }

    /**
     * @return Whether the current navigation is of the type that should always stay in Chrome.
     */
    public boolean shouldNavigationTypeStayInChrome() {
        return mInitialNavigationType == NAVIGATION_TYPE_FROM_LINK_WITHOUT_USER_GESTURE
                || mInitialNavigationType == NAVIGATION_TYPE_FROM_RELOAD;
    }

    /**
     * @return Whether this navigation is initiated by a Custom Tabs {@link Intent}.
     */
    public boolean isFromCustomTabIntent() {
        return mIsCustomTabIntent;
    }

    /**
     * @return whether navigation is from a user's typing or not.
     */
    public boolean isNavigationFromUserTyping() {
        return mInitialNavigationType == NAVIGATION_TYPE_FROM_USER_TYPING;
    }

    /**
     * @return whether we should stay in Chrome or not.
     */
    public boolean shouldNotOverrideUrlLoading() {
        return mShouldNotOverrideUrlLoadingUntilNewUrlLoading;
    }

    /**
     * @return whether on navigation or not.
     */
    public boolean isOnNavigation() {
        return mInitialNavigationType != NAVIGATION_TYPE_NONE;
    }

    /**
     * @return the last committed entry index which was saved before starting this navigation.
     */
    public int getLastCommittedEntryIndexBeforeStartingNavigation() {
        return mLastCommittedEntryIndexBeforeStartingNavigation;
    }

    private static List<ComponentName> getIntentHandlers(Context context, Intent intent) {
        List<ResolveInfo> list = context.getPackageManager().queryIntentActivities(intent, 0);
        List<ComponentName> nameList = new ArrayList<ComponentName>();
        for (ResolveInfo r : list) {
            nameList.add(new ComponentName(r.activityInfo.packageName, r.activityInfo.name));
        }
        return nameList;
    }

    /**
     * @return whether |intent| has a new resolver against |mIntentHistory| or not.
     */
    public boolean hasNewResolver(Intent intent) {
        if (mInitialIntent == null) {
            return intent != null;
        } else if (intent == null) {
            return false;
        }

        List<ComponentName> newList = getIntentHandlers(mContext, intent);
        if (mCachedResolvers.isEmpty()) {
            mCachedResolvers.addAll(getIntentHandlers(mContext, mInitialIntent));
        }
        for (ComponentName name : newList) {
            if (!mCachedResolvers.contains(name)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return The initial intent of a redirect chain, if available.
     */
    public Intent getInitialIntent() {
        return mInitialIntent;
    }
}