/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.webkit.cts;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Picture;
import android.graphics.Rect;
import android.net.Uri;
import android.net.http.SslCertificate;
import android.os.Message;
import android.print.PrintDocumentAdapter;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.webkit.CookieManager;
import android.webkit.DownloadListener;
import android.webkit.ValueCallback;
import android.webkit.WebBackForwardList;
import android.webkit.WebChromeClient;
import android.webkit.WebMessage;
import android.webkit.WebMessagePort;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebView.HitTestResult;
import android.webkit.WebView.PictureListener;
import android.webkit.WebView.VisualStateCallback;
import android.webkit.WebViewClient;
import android.webkit.WebViewRenderProcessClient;

import com.android.compatibility.common.util.PollingCheck;

import com.google.common.util.concurrent.SettableFuture;

import java.util.concurrent.Executor;

/**
 * Many tests need to run WebView code in the UI thread. This class
 * wraps a WebView so that calls are ensured to arrive on the UI thread.
 *
 * All methods may be run on either the UI thread or test thread.
 *
 * This should remain functionally equivalent to androidx.webkit.WebViewOnUiThread.
 * Modifications to this class should be reflected in that class as necessary. See
 * http://go/modifying-webview-cts.
 */
public class WebViewOnUiThread extends WebViewSyncLoader {
    /**
     * The WebView that calls will be made on.
     */
    private WebView mWebView;

    /**
     * Wraps a WebView to ensure that methods are run on the UI thread.
     *
     * A new WebViewOnUiThread should be called during setUp so as to
     * reinitialize between calls.
     *
     * The caller is responsible for destroying the WebView instance.
     *
     * @param webView The webView that the methods should call.
     */
    public WebViewOnUiThread(WebView webView) {
        super(webView);
        mWebView = webView;
    }

    public void cleanUp() {
        super.destroy();
    }

    public void setWebViewClient(final WebViewClient webViewClient) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.setWebViewClient(webViewClient);
        });
    }

    public void setWebChromeClient(final WebChromeClient webChromeClient) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.setWebChromeClient(webChromeClient);
        });
    }

    /**
     * Set the webview renderer client for {@code mWebView}, on the UI thread.
     */
    public void setWebViewRenderProcessClient(
            final WebViewRenderProcessClient webViewRenderProcessClient) {
        setWebViewRenderProcessClient(mWebView, webViewRenderProcessClient);
    }

    /**
     * Set the webview renderer client for {@code webView}, on the UI thread.
     */
    public static void setWebViewRenderProcessClient(
            final WebView webView,
            final WebViewRenderProcessClient webViewRenderProcessClient) {
        WebkitUtils.onMainThreadSync(() ->
                webView.setWebViewRenderProcessClient(webViewRenderProcessClient)
        );
    }

    /**
     * Set the webview renderer client for {@code mWebView}, on the UI thread, with callbacks
     * executed by {@code executor}
     */
    public void setWebViewRenderProcessClient(
            final Executor executor, final WebViewRenderProcessClient webViewRenderProcessClient) {
        setWebViewRenderProcessClient(mWebView, executor, webViewRenderProcessClient);
    }

    /**
     * Set the webview renderer client for {@code webView}, on the UI thread, with callbacks
     * executed by {@code executor}
     */
    public static void setWebViewRenderProcessClient(
            final WebView webView,
            final Executor executor,
            final WebViewRenderProcessClient webViewRenderProcessClient) {
        WebkitUtils.onMainThreadSync(() ->
                webView.setWebViewRenderProcessClient(executor, webViewRenderProcessClient)
        );
    }

    /**
     * Get the webview renderer client currently set on {@code mWebView}, on the UI thread.
     */
    public WebViewRenderProcessClient getWebViewRenderProcessClient() {
        return getWebViewRenderProcessClient(mWebView);
    }

    /**
     * Get the webview renderer client currently set on {@code webView}, on the UI thread.
     */
    public static WebViewRenderProcessClient getWebViewRenderProcessClient(
            final WebView webView) {
        return WebkitUtils.onMainThreadSync(() -> {
            return webView.getWebViewRenderProcessClient();
        });
    }

    public void setPictureListener(final PictureListener pictureListener) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.setPictureListener(pictureListener);
        });
    }

    public void setNetworkAvailable(final boolean available) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.setNetworkAvailable(available);
        });
    }

    public void setDownloadListener(final DownloadListener listener) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.setDownloadListener(listener);
        });
    }

    public void setBackgroundColor(final int color) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.setBackgroundColor(color);
        });
    }

    public void clearCache(final boolean includeDiskFiles) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.clearCache(includeDiskFiles);
        });
    }

    public void clearHistory() {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.clearHistory();
        });
    }

    public void requestFocus() {
        new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
            @Override
            protected boolean check() {
                requestFocusOnUiThread();
                return hasFocus();
            }
        }.run();
    }

    private void requestFocusOnUiThread() {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.requestFocus();
        });
    }

    private boolean hasFocus() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.hasFocus();
        });
    }

    public boolean canZoomIn() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.canZoomIn();
        });
    }

    public boolean canZoomOut() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.canZoomOut();
        });
    }

    public boolean zoomIn() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.zoomIn();
        });
    }

    public boolean zoomOut() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.zoomOut();
        });
    }

    public void zoomBy(final float zoomFactor) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.zoomBy(zoomFactor);
        });
    }

    public void setFindListener(final WebView.FindListener listener) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.setFindListener(listener);
        });
    }

    public void removeJavascriptInterface(final String interfaceName) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.removeJavascriptInterface(interfaceName);
        });
    }

    public WebMessagePort[] createWebMessageChannel() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.createWebMessageChannel();
        });
    }

    public void postWebMessage(final WebMessage message, final Uri targetOrigin) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.postWebMessage(message, targetOrigin);
        });
    }

    public void addJavascriptInterface(final Object object, final String name) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.addJavascriptInterface(object, name);
        });
    }

    public void flingScroll(final int vx, final int vy) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.flingScroll(vx, vy);
        });
    }

    public void requestFocusNodeHref(final Message hrefMsg) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.requestFocusNodeHref(hrefMsg);
        });
    }

    public void requestImageRef(final Message msg) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.requestImageRef(msg);
        });
    }

    public void setInitialScale(final int scaleInPercent) {
        WebkitUtils.onMainThreadSync(() -> {
                mWebView.setInitialScale(scaleInPercent);
        });
    }

    public void clearSslPreferences() {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.clearSslPreferences();
        });
    }

    public void clearClientCertPreferences(final Runnable onCleared) {
        WebkitUtils.onMainThreadSync(() -> {
            WebView.clearClientCertPreferences(onCleared);
        });
    }

    public void resumeTimers() {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.resumeTimers();
        });
    }

    public void findNext(final boolean forward) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.findNext(forward);
        });
    }

    public void clearMatches() {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.clearMatches();
        });
    }

    public void loadUrl(final String url) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.loadUrl(url);
        });
    }

    public void stopLoading() {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.stopLoading();
        });
    }

    /**
     * Reload the previous URL.
     */
    public void reload() {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.reload();
        });
    }

    public String getTitle() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.getTitle();
        });
    }

    public WebSettings getSettings() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.getSettings();
        });
    }

    public WebBackForwardList copyBackForwardList() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.copyBackForwardList();
        });
    }

    public Bitmap getFavicon() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.getFavicon();
        });
    }

    public String getUrl() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.getUrl();
        });
    }

    public int getProgress() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.getProgress();
        });
    }

    public int getHeight() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.getHeight();
        });
    }

    public int getContentHeight() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.getContentHeight();
        });
    }

    public boolean pageUp(final boolean top) {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.pageUp(top);
        });
    }

    public boolean pageDown(final boolean bottom) {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.pageDown(bottom);
        });
    }

    /**
     * Post a visual state listener callback for mWebView on the UI thread.
     */
    public void postVisualStateCallback(final long requestId, final VisualStateCallback callback) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.postVisualStateCallback(requestId, callback);
        });
    }

    public int[] getLocationOnScreen() {
        final int[] location = new int[2];
        return WebkitUtils.onMainThreadSync(() -> {
            mWebView.getLocationOnScreen(location);
            return location;
        });
    }

    public float getScale() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.getScale();
        });
    }

    public boolean requestFocus(final int direction,
            final Rect previouslyFocusedRect) {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.requestFocus(direction, previouslyFocusedRect);
        });
    }

    public HitTestResult getHitTestResult() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.getHitTestResult();
        });
    }

    public int getScrollX() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.getScrollX();
        });
    }

    public int getScrollY() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.getScrollY();
        });
    }

    public final DisplayMetrics getDisplayMetrics() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.getContext().getResources().getDisplayMetrics();
        });
    }

    public boolean requestChildRectangleOnScreen(final View child,
            final Rect rect,
            final boolean immediate) {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.requestChildRectangleOnScreen(child, rect,
                    immediate);
        });
    }

    public int findAll(final String find) {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.findAll(find);
        });
    }

    public Picture capturePicture() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.capturePicture();
        });
    }

    /**
     * Execute javascript synchronously, returning the result.
     */
    public String evaluateJavascriptSync(final String script) {
        final SettableFuture<String> future = SettableFuture.create();
        evaluateJavascript(script, result -> future.set(result));
        return WebkitUtils.waitForFuture(future);
    }

    public void evaluateJavascript(final String script, final ValueCallback<String> result) {
        WebkitUtils.onMainThread(() -> mWebView.evaluateJavascript(script, result));
    }

    public void saveWebArchive(final String basename, final boolean autoname,
                               final ValueCallback<String> callback) {
        WebkitUtils.onMainThreadSync(() -> {
            mWebView.saveWebArchive(basename, autoname, callback);
        });
    }

    public SslCertificate getCertificate() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.getCertificate();
        });
    }

    public WebView createWebView() {
        return WebkitUtils.onMainThreadSync(() -> {
            return new WebView(mWebView.getContext());
        });
    }

    public PrintDocumentAdapter createPrintDocumentAdapter() {
        return WebkitUtils.onMainThreadSync(() -> {
            return mWebView.createPrintDocumentAdapter();
        });
    }

    public void setLayoutHeightToMatchParent() {
        WebkitUtils.onMainThreadSync(() -> {
            ViewParent parent = mWebView.getParent();
            if (parent instanceof ViewGroup) {
                ((ViewGroup) parent).getLayoutParams().height =
                    ViewGroup.LayoutParams.MATCH_PARENT;
            }
            mWebView.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
            mWebView.requestLayout();
        });
    }

    public void setLayoutToMatchParent() {
        WebkitUtils.onMainThreadSync(() -> {
            setMatchParent((View) mWebView.getParent());
            setMatchParent(mWebView);
            mWebView.requestLayout();
        });
    }

    public void setAcceptThirdPartyCookies(final boolean accept) {
        WebkitUtils.onMainThreadSync(() -> {
            CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView, accept);
        });
    }

    public boolean acceptThirdPartyCookies() {
        return WebkitUtils.onMainThreadSync(() -> {
            return CookieManager.getInstance().acceptThirdPartyCookies(mWebView);
        });
    }

    /**
     * Accessor for underlying WebView.
     * @return The WebView being wrapped by this class.
     */
    public WebView getWebView() {
        return mWebView;
    }

    /**
     * Wait for the current state of the DOM to be ready to render on the next draw.
     */
    public void waitForDOMReadyToRender() {
        final SettableFuture<Void> future = SettableFuture.create();
        postVisualStateCallback(0, new VisualStateCallback() {
            @Override
            public void onComplete(long requestId) {
                future.set(null);
            }
        });
        WebkitUtils.waitForFuture(future);
    }

    /**
     * Capture a bitmap representation of the current WebView state.
     *
     * This synchronises so that the bitmap contents reflects the current DOM state, rather than
     * potentially capturing a previously generated frame.
     */
    public Bitmap captureBitmap() {
        getSettings().setOffscreenPreRaster(true);
        waitForDOMReadyToRender();
        return WebkitUtils.onMainThreadSync(() -> {
            Bitmap bitmap = Bitmap.createBitmap(mWebView.getWidth(), mWebView.getHeight(),
                    Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bitmap);
            mWebView.draw(canvas);
            return bitmap;
        });
    }

    /**
     * Set LayoutParams to MATCH_PARENT.
     *
     * @param view Target view
     */
    private void setMatchParent(View view) {
        ViewGroup.LayoutParams params = view.getLayoutParams();
        params.height = ViewGroup.LayoutParams.MATCH_PARENT;
        params.width = ViewGroup.LayoutParams.MATCH_PARENT;
        view.setLayoutParams(params);
    }
}
