// 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.tabmodel;

import android.content.Intent;
import android.os.Handler;
import android.text.TextUtils;

import org.chromium.base.SysUtils;
import org.chromium.base.TraceEvent;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.TabState;
import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabDelegateFactory;
import org.chromium.chrome.browser.tabmodel.TabModel.TabLaunchType;
import org.chromium.chrome.browser.util.IntentUtils;
import org.chromium.chrome.browser.util.UrlUtilities;
import org.chromium.components.service_tab_launcher.ServiceTabLauncher;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.Referrer;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.widget.Toast;

/**
 * This class creates various kinds of new tabs and adds them to the right {@link TabModel}.
 */
public class ChromeTabCreator extends TabCreatorManager.TabCreator {
    private static final int VISIBLE_DURATION_MS = 600;

    private final ChromeActivity mActivity;
    private final WindowAndroid mNativeWindow;
    private final TabModelOrderController mOrderController;
    private final TabPersistentStore mTabSaver;
    private final boolean mIncognito;

    private TabModel mTabModel;
    private TabContentManager mTabContentManager;

    public ChromeTabCreator(ChromeActivity activity, WindowAndroid nativeWindow,
            TabModelOrderController orderController, TabPersistentStore tabSaver,
            boolean incognito) {
        mActivity = activity;
        mNativeWindow = nativeWindow;
        mOrderController = orderController;
        mTabSaver = tabSaver;
        mIncognito = incognito;
    }

    @Override
    public boolean createsTabsAsynchronously() {
        return false;
    }

    /**
     * Creates a new tab and posts to UI.
     * @param loadUrlParams parameters of the url load.
     * @param type Information about how the tab was launched.
     * @param parent the parent tab, if present.
     * @return The new tab.
     */
    @Override
    public Tab createNewTab(LoadUrlParams loadUrlParams, TabModel.TabLaunchType type,
            Tab parent) {
        return createNewTab(loadUrlParams, type, parent, null);
    }

    /**
     * Creates a new tab and posts to UI.
     * @param loadUrlParams parameters of the url load.
     * @param type Information about how the tab was launched.
     * @param parent the parent tab, if present.
     * @param intent the source of the url if it isn't null.
     * @return The new tab.
     */
    private Tab createNewTab(LoadUrlParams loadUrlParams, TabModel.TabLaunchType type,
            Tab parent, Intent intent) {
        // If parent is in the same tab model, place the new tab next to it.
        int position = TabModel.INVALID_TAB_INDEX;
        int index = mTabModel.indexOf(parent);
        if (index != TabModel.INVALID_TAB_INDEX) position = index + 1;

        return createNewTab(loadUrlParams, type, parent, position, intent);
    }

    /**
     * Creates a new tab and posts to UI.
     * @param loadUrlParams parameters of the url load.
     * @param type Information about how the tab was launched.
     * @param parent the parent tab, if present.
     * @param position the requested position (index in the tab model)
     * @param intent the source of the url if it isn't null.
     * @return The new tab.
     */
    private Tab createNewTab(LoadUrlParams loadUrlParams, TabModel.TabLaunchType type,
            Tab parent, int position, Intent intent) {
        try {
            TraceEvent.begin("ChromeTabCreator.createNewTab");
            int parentId = parent != null ? parent.getId() : Tab.INVALID_TAB_ID;

            // Sanitize the url.
            loadUrlParams.setUrl(UrlUtilities.fixupUrl(loadUrlParams.getUrl()));
            loadUrlParams.setTransitionType(getTransitionType(type, intent));

            // Check if the tab is being created asynchronously.
            int assignedTabId = intent == null ? Tab.INVALID_TAB_ID : IntentUtils.safeGetIntExtra(
                    intent, IntentHandler.EXTRA_TAB_ID, Tab.INVALID_TAB_ID);
            AsyncTabParams asyncParams =
                    AsyncTabParamsManager.remove(assignedTabId);

            boolean openInForeground = mOrderController.willOpenInForeground(type, mIncognito);
            TabDelegateFactory delegateFactory = parent == null ? new TabDelegateFactory()
                    : parent.getDelegateFactory();
            Tab tab;
            if (asyncParams != null && asyncParams.getTabToReparent() != null) {
                type = TabLaunchType.FROM_REPARENTING;
                openInForeground = true;

                TabReparentingParams params = (TabReparentingParams) asyncParams;
                tab = params.getTabToReparent();
                tab.attachAndFinishReparenting(mActivity, new TabDelegateFactory(), params);
            } else if (asyncParams != null && asyncParams.getWebContents() != null) {
                openInForeground = true;
                WebContents webContents = asyncParams.getWebContents();
                // A WebContents was passed through the Intent.  Create a new Tab to hold it.
                Intent parentIntent = IntentUtils.safeGetParcelableExtra(
                        intent, IntentHandler.EXTRA_PARENT_INTENT);
                parentId = IntentUtils.safeGetIntExtra(
                        intent, IntentHandler.EXTRA_PARENT_TAB_ID, parentId);

                assert TabModelUtils.getTabIndexById(mTabModel, assignedTabId)
                        == TabModel.INVALID_TAB_INDEX;
                tab = Tab.createLiveTab(assignedTabId, mActivity, mIncognito,
                        mNativeWindow, type, parentId, !openInForeground);
                tab.initialize(
                        webContents, mTabContentManager, delegateFactory, !openInForeground, false);
                tab.setParentIntent(parentIntent);
                webContents.resumeLoadingCreatedWebContents();
            } else if (!openInForeground && SysUtils.isLowEndDevice()) {
                // On low memory devices the tabs opened in background are not loaded automatically
                // to preserve resources (cpu, memory, strong renderer binding) for the foreground
                // tab.
                // TODO(dfalcantara): Fallback Tabs created when the TabState couldn't be restored
                //                    on startup should go through this path, as well, but there's
                //                    currently no way to pipe that information to this function.
                tab = Tab.createTabForLazyLoad(mActivity, mIncognito, mNativeWindow, type,
                        parentId, loadUrlParams);
                tab.initialize(null, mTabContentManager, delegateFactory, !openInForeground, false);
                mTabSaver.addTabToSaveQueue(tab);
            } else {
                tab = Tab.createLiveTab(Tab.INVALID_TAB_ID, mActivity, mIncognito,
                        mNativeWindow, type, parentId, !openInForeground);
                tab.initialize(null, mTabContentManager, delegateFactory, !openInForeground, false);
                tab.loadUrl(loadUrlParams);
            }
            tab.getTabRedirectHandler().updateIntent(intent);

            if (intent != null && intent.hasExtra(ServiceTabLauncher.LAUNCH_REQUEST_ID_EXTRA)) {
                ServiceTabLauncher.onWebContentsForRequestAvailable(
                        intent.getIntExtra(ServiceTabLauncher.LAUNCH_REQUEST_ID_EXTRA, 0),
                        tab.getWebContents());
            }

            mTabModel.addTab(tab, position, type);

            if (type == TabLaunchType.FROM_REPARENTING) {
                TabReparentingParams params = (TabReparentingParams) asyncParams;
                if (!params.shouldStayInChrome()) {
                    new Handler().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mActivity.moveTaskToBack(true);
                            Toast.makeText(mActivity, R.string.tab_sent_to_background,
                                    Toast.LENGTH_LONG).show();
                        }
                    }, VISIBLE_DURATION_MS);
                }
            }
            return tab;
        } finally {
            TraceEvent.end("ChromeTabCreator.createNewTab");
        }
    }

    @Override
    public boolean createTabWithWebContents(Tab parent, WebContents webContents, int parentId,
            TabLaunchType type, String url) {
        // The parent tab was already closed.  Do not open child tabs.
        if (mTabModel.isClosurePending(parentId)) return false;

        // If parent is in the same tab model, place the new tab next to it.
        int position = TabModel.INVALID_TAB_INDEX;
        int index = TabModelUtils.getTabIndexById(mTabModel, parentId);
        if (index != TabModel.INVALID_TAB_INDEX) position = index + 1;

        boolean openInForeground = mOrderController.willOpenInForeground(type, mIncognito);
        TabDelegateFactory delegateFactory = parent == null ? new TabDelegateFactory()
                : parent.getDelegateFactory();
        Tab tab = Tab.createLiveTab(Tab.INVALID_TAB_ID, mActivity, mIncognito,
                mNativeWindow, type, parentId, !openInForeground);
        tab.initialize(webContents, mTabContentManager, delegateFactory, !openInForeground, false);
        mTabModel.addTab(tab, position, type);
        return true;
    }

    @Override
    public Tab launchUrl(String url, TabModel.TabLaunchType type) {
        return launchUrl(url, type, null, 0);
    }

    /**
     * Creates a new tab and loads the specified URL in it. This is a convenience method for
     * {@link #createNewTab} with the default {@link LoadUrlParams} and no parent tab.
     *
     * @param url the URL to open.
     * @param type the type of action that triggered that launch. Determines how the tab is opened
     *             (for example, in the foreground or background).
     * @param intent the source of url if it isn't null.
     * @param intentTimestamp the time the intent was received.
     * @return the created tab.
     */
    public Tab launchUrl(String url, TabModel.TabLaunchType type, Intent intent,
            long intentTimestamp) {
        LoadUrlParams loadUrlParams = new LoadUrlParams(url);
        loadUrlParams.setIntentReceivedTimestamp(intentTimestamp);
        return createNewTab(loadUrlParams, type, null, intent);
    }

    /**
     * Opens the specified URL into a tab, potentially reusing a tab. Typically if a user opens
     * several link from the same application, we reuse the same tab so as to not open too many
     * tabs.
     * @param url the URL to open
     * @param referer The referer url if provided, null otherwise.
     * @param headers HTTP headers to send alongside the URL.
     * @param appId the ID of the application that triggered that URL navigation.
     * @param forceNewTab whether the URL should be opened in a new tab. If false, an existing tab
     *                    already opened by the same app will be reused.
     * @param intent the source of url if it isn't null.
     * @param intentTimestamp the time the intent was received.
     * @return the tab the URL was opened in, could be a new tab or a reused one.
     */
    public Tab launchUrlFromExternalApp(String url, String referer, String headers,
            String appId, boolean forceNewTab, Intent intent, long intentTimestamp) {
        assert !mIncognito;
        boolean isLaunchedFromChrome = TextUtils.equals(appId, mActivity.getPackageName());

        // If an external app sends an intent for a Weblite URL and the Data Reduction Proxy is
        // using Weblite mode, then use the URL in the lite_url parameter if its scheme is HTTP.
        // This is used by ChromeTabbedActvity intents so that the user does not receive Weblite
        // pages when he or she could be served a Data Reduction Proxy preview page.
        if (url != null) url = DataReductionProxySettings.getInstance().maybeRewriteWebliteUrl(url);

        if (forceNewTab && !isLaunchedFromChrome) {
            // We don't associate the tab with that app ID, as it is assumed that if the
            // application wanted to open this tab as a new tab, it probably does not want it
            // reused either.
            LoadUrlParams loadUrlParams = new LoadUrlParams(url);
            loadUrlParams.setIntentReceivedTimestamp(intentTimestamp);
            loadUrlParams.setVerbatimHeaders(headers);
            if (referer != null) {
                loadUrlParams.setReferrer(new Referrer(referer, Referrer.REFERRER_POLICY_DEFAULT));
            }
            return createNewTab(loadUrlParams, TabLaunchType.FROM_EXTERNAL_APP, null, intent);
        }

        if (appId == null) {
            // If we have no application ID, we use a made-up one so that these tabs can be
            // reused.
            appId = TabModelImpl.UNKNOWN_APP_ID;
        }
        // Let's try to find an existing tab that was started by that app.
        for (int i = 0; i < mTabModel.getCount(); i++) {
            Tab tab = mTabModel.getTabAt(i);
            if (appId.equals(tab.getAppAssociatedWith())) {
                // We don't reuse the tab, we create a new one at the same index instead.
                // Reusing a tab would require clearing the navigation history and clearing the
                // contents (we would not want the previous content to show).
                LoadUrlParams loadUrlParams = new LoadUrlParams(url);
                loadUrlParams.setIntentReceivedTimestamp(intentTimestamp);
                Tab newTab = createNewTab(
                        loadUrlParams, TabLaunchType.FROM_EXTERNAL_APP, null, i, intent);
                newTab.setAppAssociatedWith(appId);
                mTabModel.closeTab(tab, false, false, false);
                return newTab;
            }
        }

        // No tab for that app, we'll have to create a new one.
        Tab tab = launchUrl(url, TabLaunchType.FROM_EXTERNAL_APP, intent, intentTimestamp);
        tab.setAppAssociatedWith(appId);
        return tab;
    }

    @Override
    public Tab createFrozenTab(TabState state, int id, int index) {
        Tab tab = Tab.createFrozenTabFromState(
                id, mActivity, state.isIncognito(), mNativeWindow, state.parentId, state);
        boolean selectTab = mOrderController.willOpenInForeground(TabLaunchType.FROM_RESTORE,
                state.isIncognito());
        tab.initialize(null, mTabContentManager, new TabDelegateFactory(), !selectTab, false);
        assert state.isIncognito() == mIncognito;
        mTabModel.addTab(tab, index, TabLaunchType.FROM_RESTORE);
        return tab;
    }

    /**
     * @param type Type of the tab launch.
     * @param intent The intent causing the tab launch.
     * @return The page transition type constant.
     */
    private int getTransitionType(TabLaunchType type, Intent intent) {
        int transition = PageTransition.LINK;
        switch (type) {
            case FROM_LINK:
            case FROM_EXTERNAL_APP:
                transition = PageTransition.LINK | PageTransition.FROM_API;
                break;
            case FROM_CHROME_UI:
            case FROM_LONGPRESS_FOREGROUND:
            case FROM_LONGPRESS_BACKGROUND:
                transition = PageTransition.AUTO_TOPLEVEL;
                break;
            default:
                assert false;
                break;
        }

        return IntentHandler.getTransitionTypeFromIntent(mActivity.getApplicationContext(),
                intent, transition);
    }

    /**
     * Sets the tab model and tab content manager to use.
     * @param model   The new {@link TabModel} to use.
     * @param manager The new {@link TabContentManager} to use.
     */
    public void setTabModel(TabModel model, TabContentManager manager) {
        mTabModel = model;
        mTabContentManager = manager;
    }

}