// Copyright 2020 The Chromium Authors
// 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.paint_preview;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.Activity;
import android.graphics.Point;
import android.os.Handler;
import android.os.SystemClock;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Callback;
import org.chromium.base.TraceEvent;
import org.chromium.base.UserData;
import org.chromium.chrome.browser.browser_controls.BrowserStateBrowserControlsVisibilityDelegate;
import org.chromium.chrome.browser.paint_preview.services.PaintPreviewTabService;
import org.chromium.chrome.browser.paint_preview.services.PaintPreviewTabServiceFactory;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabHidingType;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.tab.TabViewProvider;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.components.paintpreview.player.PlayerManager;
import org.chromium.content_public.browser.RenderCoordinates;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsAccessibility;
import org.chromium.ui.base.EventForwarder;
import org.chromium.ui.base.GestureEventType;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.util.TokenHolder;

/**
 * Responsible for checking for and displaying Paint Previews that are associated with a
 * {@link Tab} by overlaying the content view.
 */
public class TabbedPaintPreview implements UserData {
    public static final Class<TabbedPaintPreview> USER_DATA_KEY = TabbedPaintPreview.class;
    private static final int CROSS_FADE_DURATION_MS = 500;
    private static final int SCROLL_DELAY_MS = 10;

    private Tab mTab;
    private TabObserver mTabObserver;
    private TabViewProvider mTabbedPaintPreviewViewProvider;
    private PaintPreviewTabService mPaintPreviewTabService;
    private PlayerManager mPlayerManager;
    private BrowserStateBrowserControlsVisibilityDelegate mBrowserVisibilityDelegate;
    private Runnable mProgressSimulatorNeededCallback;
    private Callback<Boolean> mProgressPreventionCallback;

    private boolean mIsAttachedToTab;
    private boolean mFadingOut;
    private int mPersistentToolbarToken = TokenHolder.INVALID_TOKEN;

    private static PaintPreviewTabService sPaintPreviewTabServiceForTesting;
    private boolean mWasEverShown;

    public static TabbedPaintPreview get(Tab tab) {
        if (tab.getUserDataHost().getUserData(USER_DATA_KEY) == null) {
            tab.getUserDataHost().setUserData(USER_DATA_KEY, new TabbedPaintPreview(tab));
        }
        return tab.getUserDataHost().getUserData(USER_DATA_KEY);
    }

    private TabbedPaintPreview(Tab tab) {
        mTab = tab;
        mTabbedPaintPreviewViewProvider = new TabbedPaintPreviewViewProvider();
        mPaintPreviewTabService = PaintPreviewTabServiceFactory.getServiceInstance();
        mTabObserver = new EmptyTabObserver() {
            @Override
            public void onHidden(Tab tab, @TabHidingType int hidingType) {
                releasePersistentToolbar();
                setProgressPreventionNeeded(false);
            }

            @Override
            public void onShown(Tab tab, int type) {
                if (!isShowing()) return;

                showToolbarPersistent();
                setProgressPreventionNeeded(true);
            }

            @Override
            public void onActivityAttachmentChanged(Tab tab, @Nullable WindowAndroid window) {
                // Intentionally do nothing to prevent automatic observer removal on detachment.
            }
        };
    }

    public void setBrowserVisibilityDelegate(
            BrowserStateBrowserControlsVisibilityDelegate browserVisibilityDelegate) {
        mBrowserVisibilityDelegate = browserVisibilityDelegate;
    }

    public void setProgressSimulatorNeededCallback(Runnable callback) {
        mProgressSimulatorNeededCallback = callback;
    }

    public void setProgressbarUpdatePreventionCallback(Callback<Boolean> callback) {
        mProgressPreventionCallback = callback;
    }

    void capture(Callback<Boolean> successCallback) {
        getService().captureTab(mTab, successCallback);
    }

    /**
     * Shows a Paint Preview for the provided tab if it exists.
     * @param listener An interface used for notifying events originated from the player.
     * @return Whether a capture for this tab exists and an attempt for displaying it has started.
     */
    public boolean maybeShow(@NonNull PlayerManager.Listener listener) {
        if (mIsAttachedToTab) return true;
        TraceEvent.begin("TabbedPaintPreview.maybeShow");

        boolean allowedToShow = PaintPreviewTabService.tabAllowedForPaintPreview(mTab);
        if (!allowedToShow) {
            TraceEvent.end("TabbedPaintPreview.maybeShow");
            return false;
        }

        // Check if a capture exists. This is a quick check using a cache.
        boolean hasCapture = getService().hasCaptureForTab(mTab.getId());
        if (!hasCapture) {
            TraceEvent.end("TabbedPaintPreview.maybeShow");
            return false;
        }

        mTab.addObserver(mTabObserver);
        PaintPreviewCompositorUtils.warmupCompositor();

        mPlayerManager = new PlayerManager(mTab.getUrl(), mTab.getContext(), getService(),
                String.valueOf(mTab.getId()), listener,
                ChromeColors.getPrimaryBackgroundColor(mTab.getContext(), false),
                /*ignoreInitialScrollOffset=*/false);

        // TODO(crbug/1230021): Consider deferring/post tasking. Locally this appears to be slow.
        TraceEvent.begin("TabbedPaintPreview.maybeShow addTabViewProvider");
        mTab.getTabViewManager().addTabViewProvider(mTabbedPaintPreviewViewProvider);
        TraceEvent.end("TabbedPaintPreview.maybeShow addTabViewProvider");
        mIsAttachedToTab = true;
        mWasEverShown = true;

        TraceEvent.end("TabbedPaintPreview.maybeShow");
        return true;
    }

    public void remove(boolean animate) {
        remove(true, animate);
    }

    private void matchScrollAndScale(
            WebContents contents, Point scrollPosition, float scaleFactor) {
        if (contents == null || scaleFactor == 0f || scrollPosition == null) return;
        EventForwarder eventForwarder = contents.getEventForwarder();
        RenderCoordinates coordinates = RenderCoordinates.fromWebContents(contents);

        float scaleDelta = scaleFactor / coordinates.getPageScaleFactor();
        long timeMs = SystemClock.uptimeMillis();
        eventForwarder.onGestureEvent(GestureEventType.PINCH_BEGIN, timeMs, 0.f);
        eventForwarder.onGestureEvent(GestureEventType.PINCH_BY, timeMs, scaleDelta);
        eventForwarder.onGestureEvent(GestureEventType.PINCH_END, timeMs, 0.f);
        // Post the scroll so it occurs after the scale. This ensures positioning is correct.
        new Handler().postDelayed(() -> {
            eventForwarder.scrollTo(scrollPosition.x, scrollPosition.y);
        }, SCROLL_DELAY_MS);
    }

    /**
     * Removes the view containing the Paint Preview from the most recently shown {@link Tab}. Does
     * nothing if there is no view showing.
     */
    public void remove(boolean matchScroll, boolean animate) {
        PaintPreviewCompositorUtils.stopWarmCompositor();
        if (mTab == null || mPlayerManager == null || mFadingOut) return;
        TraceEvent.begin("TabbedPaintPreview.remove");

        mFadingOut = true;
        mPlayerManager.setAcceptUserInput(false);
        mTab.removeObserver(mTabObserver);
        Point scrollPosition = mPlayerManager.getScrollPosition();
        float scale = mPlayerManager.getScale();
        final boolean supportsAccessibility = mPlayerManager.supportsAccessibility();
        // Destroy early to free up resource, but don't null until faded out so view sticks around.
        mPlayerManager.destroy();
        if (matchScroll) {
            matchScrollAndScale(mTab.getWebContents(), scrollPosition, scale);
        }
        mTabbedPaintPreviewViewProvider.getView()
                .animate()
                .alpha(0f)
                .setDuration(animate ? CROSS_FADE_DURATION_MS : 0)
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        if (mTab != null) {
                            mTab.getTabViewManager().removeTabViewProvider(
                                    mTabbedPaintPreviewViewProvider);
                        }
                        if (mPlayerManager != null) {
                            mPlayerManager = null;
                        }
                        // WebContentsAccessibilityImpl gets its focus stuck on the root ID. Clear
                        // focus here to solve this problem.
                        if (supportsAccessibility) clearFocus();

                        mIsAttachedToTab = false;
                        mFadingOut = false;
                    }
                });
        // Ensure the progress update occur during the animation.
        setProgressPreventionNeeded(false);

        if (mProgressSimulatorNeededCallback != null) mProgressSimulatorNeededCallback.run();
        TraceEvent.end("TabbedPaintPreview.remove");
    }

    /**
     * Clears focus and accessibility focus.
     */
    private void clearFocus() {
        WebContents webContents = mTab != null ? mTab.getWebContents() : null;
        if (webContents == null || webContents.isDestroyed()) return;

        // Clear input focus. This is required due to a bug where the root view is treated as
        // focused for input on exit causing talkback to attempt to return focus to the root view.
        // TODO(crbug/1197693): this approach could cause loss of focus in a menu, omnibox, etc.
        // is there a less heavy-handed option here?
        WindowAndroid window = webContents.getTopLevelNativeWindow();
        Activity activity = window != null ? window.getActivity().get() : null;
        View v = activity != null ? activity.getCurrentFocus() : null;
        if (v != null) v.clearFocus();

        // Clear accessibility focus.
        WebContentsAccessibility wcax = WebContentsAccessibility.fromWebContents(webContents);
        if (wcax != null) wcax.resetFocus();
    }

    public boolean isShowing() {
        if (mTab == null) return false;

        return mTab.getTabViewManager().isShowing(mTabbedPaintPreviewViewProvider);
    }

    public boolean isAttached() {
        return mIsAttachedToTab;
    }

    /**
     * Persistently shows the toolbar and avoids hiding it on scrolling down.
     */
    private void showToolbarPersistent() {
        if (mBrowserVisibilityDelegate == null
                || mPersistentToolbarToken != TokenHolder.INVALID_TOKEN) {
            return;
        }

        mPersistentToolbarToken = mBrowserVisibilityDelegate.showControlsPersistent();
    }

    private void releasePersistentToolbar() {
        if (mBrowserVisibilityDelegate == null) return;

        mBrowserVisibilityDelegate.releasePersistentShowingToken(mPersistentToolbarToken);
        mPersistentToolbarToken = TokenHolder.INVALID_TOKEN;
    }

    /**
     * @param progressPrevention Whether progress updates shown in the progress bar should be
     *                           suppressed.
     */
    private void setProgressPreventionNeeded(boolean progressPrevention) {
        if (mProgressPreventionCallback == null) return;

        mProgressPreventionCallback.onResult(progressPrevention);
    }

    @Override
    public void destroy() {
        mTab.removeObserver(mTabObserver);
        mTab = null;
    }

    private PaintPreviewTabService getService() {
        if (sPaintPreviewTabServiceForTesting == null) return mPaintPreviewTabService;

        return sPaintPreviewTabServiceForTesting;
    }

    static void overridePaintPreviewTabServiceForTesting(PaintPreviewTabService service) {
        sPaintPreviewTabServiceForTesting = service;
    }

    @VisibleForTesting
    boolean wasEverShown() {
        return mWasEverShown;
    }

    View getViewForTesting() {
        return mTabbedPaintPreviewViewProvider.getView();
    }

    PlayerManager getPlayerManagerForTesting() {
        return mPlayerManager;
    }

    private class TabbedPaintPreviewViewProvider implements TabViewProvider {
        @Override
        public int getTabViewProviderType() {
            return Type.PAINT_PREVIEW;
        }

        @Override
        public View getView() {
            return mPlayerManager == null ? null : mPlayerManager.getView();
        }

        @Override
        public void onShown() {
            showToolbarPersistent();
            setProgressPreventionNeeded(true);
        }

        @Override
        public void onHidden() {
            releasePersistentToolbar();
            setProgressPreventionNeeded(false);
        }
    }
}
