// Copyright 2014 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 org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.chrome.browser.incognito.IncognitoNotificationManager;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;

/**
 * A TabModel implementation that handles off the record tabs.
 *
 * <p>
 * This is not thread safe and must only be operated on the UI thread.
 *
 * <p>
 * The lifetime of this object is not tied to that of the native TabModel.  This ensures the
 * native TabModel is present when at least one incognito Tab has been created and added.  When
 * no Tabs remain, the native model will be destroyed and only rebuilt when a new incognito Tab
 * is created.
 */
public class OffTheRecordTabModel implements TabModel {
    /** Creates TabModels for use in OffTheRecordModel. */
    public interface OffTheRecordTabModelDelegate {
        /** Creates a fully working TabModel to delegate calls to. */
        TabModel createTabModel();

        /** @return Whether OffTheRecord Tabs exist. */
        boolean doOffTheRecordTabsExist();
    }

    private final OffTheRecordTabModelDelegate mDelegate;
    private final ObserverList<TabModelObserver> mObservers = new ObserverList<TabModelObserver>();
    private TabModel mDelegateModel;
    private boolean mIsAddingTab;

    /**
     * Constructor for OffTheRecordTabModel.
     */
    public OffTheRecordTabModel(OffTheRecordTabModelDelegate tabModelCreator) {
        mDelegate = tabModelCreator;
        mDelegateModel = EmptyTabModel.getInstance();
    }

    /**
     * Ensures that the real TabModel has been created.
     */
    protected void ensureTabModelImpl() {
        ThreadUtils.assertOnUiThread();
        if (!(mDelegateModel instanceof EmptyTabModel)) return;

        IncognitoNotificationManager.showIncognitoNotification();
        mDelegateModel = mDelegate.createTabModel();
        for (TabModelObserver observer : mObservers) {
            mDelegateModel.addObserver(observer);
        }
    }

    /**
     * @return The TabModel that this {@link OffTheRecordTabModel} is delegating calls to.
     */
    protected TabModel getDelegateModel() {
        return mDelegateModel;
    }

    /**
     * Destroys the Incognito profile when all Incognito tabs have been closed.  Also resets the
     * delegate TabModel to be a stub EmptyTabModel.
     */
    protected void destroyIncognitoIfNecessary() {
        ThreadUtils.assertOnUiThread();
        if (!isEmpty() || mDelegateModel instanceof EmptyTabModel || mIsAddingTab) {
            return;
        }

        Profile profile = getProfile();
        mDelegateModel.destroy();

        // Only delete the incognito profile if there are no incognito tabs open in any tab
        // model selector as the profile is shared between them.
        if (profile != null && !mDelegate.doOffTheRecordTabsExist()) {
            IncognitoNotificationManager.dismissIncognitoNotification();

            profile.destroyWhenAppropriate();
        }

        mDelegateModel = EmptyTabModel.getInstance();
    }

    private boolean isEmpty() {
        return getComprehensiveModel().getCount() == 0;
    }

    @Override
    public Profile getProfile() {
        if (mDelegateModel instanceof TabModelJniBridge) {
            TabModelJniBridge tabModel = (TabModelJniBridge) mDelegateModel;
            return tabModel.isNativeInitialized() ? tabModel.getProfile() : null;
        }
        return mDelegateModel.getProfile();
    }

    @Override
    public boolean isIncognito() {
        return true;
    }

    @Override
    public boolean closeTab(Tab tab) {
        boolean retVal = mDelegateModel.closeTab(tab);
        destroyIncognitoIfNecessary();
        return retVal;
    }

    @Override
    public boolean closeTab(Tab tab, boolean animate, boolean uponExit, boolean canUndo) {
        boolean retVal = mDelegateModel.closeTab(tab, animate, uponExit, canUndo);
        destroyIncognitoIfNecessary();
        return retVal;
    }

    @Override
    public Tab getNextTabIfClosed(int id) {
        return mDelegateModel.getNextTabIfClosed(id);
    }

    @Override
    public void closeAllTabs() {
        mDelegateModel.closeAllTabs();
        destroyIncognitoIfNecessary();
    }

    @Override
    public void closeAllTabs(boolean allowDelegation, boolean uponExit) {
        mDelegateModel.closeAllTabs(allowDelegation, uponExit);
        destroyIncognitoIfNecessary();
    }

    @Override
    public int getCount() {
        return mDelegateModel.getCount();
    }

    @Override
    public Tab getTabAt(int index) {
        return mDelegateModel.getTabAt(index);
    }

    @Override
    public int indexOf(Tab tab) {
        return mDelegateModel.indexOf(tab);
    }

    @Override
    public int index() {
        return mDelegateModel.index();
    }

    @Override
    public void setIndex(int i, TabSelectionType type) {
        mDelegateModel.setIndex(i, type);
    }

    @Override
    public void moveTab(int id, int newIndex) {
        mDelegateModel.moveTab(id, newIndex);
    }

    @Override
    public void destroy() {
        mDelegateModel.destroy();
    }

    @Override
    public boolean isClosurePending(int tabId) {
        return mDelegateModel.isClosurePending(tabId);
    }

    @Override
    public boolean supportsPendingClosures() {
        return mDelegateModel.supportsPendingClosures();
    }

    @Override
    public TabList getComprehensiveModel() {
        return mDelegateModel.getComprehensiveModel();
    }

    @Override
    public void commitAllTabClosures() {
        // Return early if no tabs are open. In particular, we don't want to destroy the incognito
        // tab model, in case we are about to add a tab to it.
        if (isEmpty()) return;
        mDelegateModel.commitAllTabClosures();
        destroyIncognitoIfNecessary();
    }

    @Override
    public void commitTabClosure(int tabId) {
        mDelegateModel.commitTabClosure(tabId);
        destroyIncognitoIfNecessary();
    }

    @Override
    public void cancelTabClosure(int tabId) {
        mDelegateModel.cancelTabClosure(tabId);
    }

    @Override
    public void addTab(Tab tab, int index, TabLaunchType type) {
        mIsAddingTab = true;
        ensureTabModelImpl();
        mDelegateModel.addTab(tab, index, type);
        mIsAddingTab = false;
    }

    @Override
    public void addObserver(TabModelObserver observer) {
        mObservers.addObserver(observer);
        mDelegateModel.addObserver(observer);
    }

    @Override
    public void removeObserver(TabModelObserver observer) {
        mObservers.removeObserver(observer);
        mDelegateModel.removeObserver(observer);
    }

    @Override
    public void removeTab(Tab tab) {
        mDelegateModel.removeTab(tab);
        // Call destroyIncognitoIfNecessary() in case the last incognito tab in this model is
        // reparented to a different activity. See crbug.com/611806.
        destroyIncognitoIfNecessary();
    }

}