/*
 * Copyright 2014 A.C.R. Development
 */

package acr.browser.lightning.view;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.ArrayMap;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.webkit.CookieManager;
import android.webkit.WebSettings;
import android.webkit.WebSettings.LayoutAlgorithm;
import android.webkit.WebSettings.PluginState;
import android.webkit.WebView;

import com.anthonycr.bonsai.Schedulers;
import com.anthonycr.bonsai.Single;
import com.anthonycr.bonsai.SingleAction;
import com.anthonycr.bonsai.SingleOnSubscribe;
import com.anthonycr.bonsai.SingleSubscriber;

import java.io.File;
import java.lang.ref.WeakReference;
import java.util.Map;

import javax.inject.Inject;

import acr.browser.lightning.app.BrowserApp;
import acr.browser.lightning.constant.BookmarkPage;
import acr.browser.lightning.constant.Constants;
import acr.browser.lightning.constant.DownloadsPage;
import acr.browser.lightning.constant.StartPage;
import acr.browser.lightning.controller.UIController;
import acr.browser.lightning.dialog.LightningDialogBuilder;
import acr.browser.lightning.download.LightningDownloadListener;
import acr.browser.lightning.preference.PreferenceManager;
import acr.browser.lightning.utils.Preconditions;
import acr.browser.lightning.utils.ProxyUtils;
import acr.browser.lightning.utils.UrlUtils;
import acr.browser.lightning.utils.Utils;

/**
 * {@link LightningView} acts as a tab for the browser,
 * handling WebView creation and handling logic, as well
 * as properly initialing it. All interactions with the
 * WebView should be made through this class.
 */
public class LightningView {

    private static final String TAG = "LightningView";

    public static final String HEADER_REQUESTED_WITH = "X-Requested-With";
    public static final String HEADER_WAP_PROFILE = "X-Wap-Profile";
    private static final String HEADER_DNT = "DNT";
    private static final int API = android.os.Build.VERSION.SDK_INT;
    private static final int SCROLL_UP_THRESHOLD = Utils.dpToPx(10);

    @Nullable private static String sHomepage;
    @Nullable private static String sDefaultUserAgent;
    private static float sMaxFling;
    private static final float[] sNegativeColorArray = {
        -1.0f, 0, 0, 0, 255, // red
        0, -1.0f, 0, 0, 255, // green
        0, 0, -1.0f, 0, 255, // blue
        0, 0, 0, 1.0f, 0 // alpha
    };
    private static final float[] sIncreaseContrastColorArray = {
        2.0f, 0, 0, 0, -160.f, // red
        0, 2.0f, 0, 0, -160.f, // green
        0, 0, 2.0f, 0, -160.f, // blue
        0, 0, 0, 1.0f, 0 // alpha
    };

    @NonNull private final LightningViewTitle mTitle;
    @Nullable private WebView mWebView;
    @NonNull private final UIController mUIController;
    @NonNull private final GestureDetector mGestureDetector;
    @NonNull private final Activity mActivity;
    @NonNull private final Paint mPaint = new Paint();
    private boolean mIsNewTab;
    private final boolean mIsIncognitoTab;
    private boolean mIsForegroundTab;
    private boolean mInvertPage = false;
    private boolean mToggleDesktop = false;
    @NonNull private final WebViewHandler mWebViewHandler = new WebViewHandler(this);
    @NonNull private final Map<String, String> mRequestHeaders = new ArrayMap<>();

    @Inject PreferenceManager mPreferences;
    @Inject LightningDialogBuilder mDialogBuilder;
    @Inject ProxyUtils mProxyUtils;

    public LightningView(@NonNull Activity activity, @Nullable String url, boolean isIncognito) {
        BrowserApp.getAppComponent().inject(this);
        mActivity = activity;
        mUIController = (UIController) activity;
        mWebView = new WebView(activity);
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
            mWebView.setId(View.generateViewId());
        }
        mIsIncognitoTab = isIncognito;
        mTitle = new LightningViewTitle(activity);

        sMaxFling = ViewConfiguration.get(activity).getScaledMaximumFlingVelocity();

        mWebView.setDrawingCacheBackgroundColor(Color.WHITE);
        mWebView.setFocusableInTouchMode(true);
        mWebView.setFocusable(true);
        mWebView.setDrawingCacheEnabled(false);
        mWebView.setWillNotCacheDrawing(true);
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
            //noinspection deprecation
            mWebView.setAnimationCacheEnabled(false);
            //noinspection deprecation
            mWebView.setAlwaysDrawnWithCacheEnabled(false);
        }
        mWebView.setBackgroundColor(Color.WHITE);

        mWebView.setScrollbarFadingEnabled(true);
        mWebView.setSaveEnabled(true);
        mWebView.setNetworkAvailable(true);
        mWebView.setWebChromeClient(new LightningChromeClient(activity, this));
        mWebView.setWebViewClient(new LightningWebClient(activity, this));
        mWebView.setDownloadListener(new LightningDownloadListener(activity));
        mGestureDetector = new GestureDetector(activity, new CustomGestureListener());
        mWebView.setOnTouchListener(new TouchListener());
        sDefaultUserAgent = mWebView.getSettings().getUserAgentString();
        initializeSettings();
        initializePreferences(activity);

        if (url != null) {
            if (!url.trim().isEmpty()) {
                mWebView.loadUrl(url, mRequestHeaders);
            } else {
                // don't load anything, the user is looking for a blank tab
            }
        } else {
            loadHomepage();
        }
    }

    /**
     * Sets whether this tab was the
     * result of a new intent sent
     * to the browser.
     *
     * @param isNewTab true if it's from
     *                 a new intent,
     *                 false otherwise.
     */
    public void setIsNewTab(boolean isNewTab) {
        mIsNewTab = isNewTab;
    }

    /**
     * Returns whether this tab was created
     * as a result of a new intent.
     *
     * @return true if it was a new intent,
     * false otherwise.
     */
    public boolean isNewTab() {
        return mIsNewTab;
    }

    /**
     * This method loads the homepage for the browser. Either
     * it loads the URL stored as the homepage, or loads the
     * startpage or bookmark page if either of those are set
     * as the homepage.
     */
    public void loadHomepage() {
        if (mWebView == null) {
            return;
        }

        Preconditions.checkNonNull(sHomepage);
        switch (sHomepage) {
            case Constants.SCHEME_HOMEPAGE:
                loadStartpage();
                break;
            case Constants.SCHEME_BOOKMARKS:
                loadBookmarkpage();
                break;
            default:
                mWebView.loadUrl(sHomepage, mRequestHeaders);
                break;
        }
    }

    /**
     * This method gets the startpage URL from the {@link StartPage}
     * class asynchronously and loads the URL in the WebView on the
     * UI thread.
     */
    private void loadStartpage() {
        new StartPage().getHomepage()
            .subscribeOn(Schedulers.io())
            .observeOn(Schedulers.main())
            .subscribe(new SingleOnSubscribe<String>() {
                @Override
                public void onItem(@Nullable String item) {
                    Preconditions.checkNonNull(item);
                    loadUrl(item);
                }
            });
    }

    /**
     * This method gets the bookmark page URL from the {@link BookmarkPage}
     * class asynchronously and loads the URL in the WebView on the
     * UI thread. It also caches the default folder icon locally.
     */
    public void loadBookmarkpage() {
        new BookmarkPage(mActivity).getBookmarkPage()
            .subscribeOn(Schedulers.io())
            .observeOn(Schedulers.main())
            .subscribe(new SingleOnSubscribe<String>() {
                @Override
                public void onItem(@Nullable String item) {
                    Preconditions.checkNonNull(item);
                    loadUrl(item);
                }
            });
    }

    /**
     * This method gets the bookmark page URL from the {@link BookmarkPage}
     * class asynchronously and loads the URL in the WebView on the
     * UI thread. It also caches the default folder icon locally.
     */
    public void loadDownloadspage() {
        new DownloadsPage().getDownloadsPage()
            .subscribeOn(Schedulers.io())
            .observeOn(Schedulers.main())
            .subscribe(new SingleOnSubscribe<String>() {
                @Override
                public void onItem(@Nullable String item) {
                    Preconditions.checkNonNull(item);
                    loadUrl(item);
                }
            });
    }

    /**
     * Initialize the preference driven settings of the WebView. This method
     * must be called whenever the preferences are changed within SharedPreferences.
     *
     * @param context the context in which the WebView was created, it is used
     *                to get the default UserAgent for the WebView.
     */
    @SuppressLint({"NewApi", "SetJavaScriptEnabled"})
    public synchronized void initializePreferences(@NonNull Context context) {
        if (mWebView == null) {
            return;
        }

        WebSettings settings = mWebView.getSettings();

        if (mPreferences.getDoNotTrackEnabled()) {
            mRequestHeaders.put(HEADER_DNT, "1");
        } else {
            mRequestHeaders.remove(HEADER_DNT);
        }

        if (mPreferences.getRemoveIdentifyingHeadersEnabled()) {
            mRequestHeaders.put(HEADER_REQUESTED_WITH, "");
            mRequestHeaders.put(HEADER_WAP_PROFILE, "");
        } else {
            mRequestHeaders.remove(HEADER_REQUESTED_WITH);
            mRequestHeaders.remove(HEADER_WAP_PROFILE);
        }

        settings.setDefaultTextEncodingName(mPreferences.getTextEncoding());
        sHomepage = mPreferences.getHomepage();
        setColorMode(mPreferences.getRenderingMode());

        if (!mIsIncognitoTab) {
            settings.setGeolocationEnabled(mPreferences.getLocationEnabled());
        } else {
            settings.setGeolocationEnabled(false);
        }
        if (API < Build.VERSION_CODES.KITKAT) {
            switch (mPreferences.getFlashSupport()) {
                case 0:
                    //noinspection deprecation
                    settings.setPluginState(PluginState.OFF);
                    break;
                case 1:
                    //noinspection deprecation
                    settings.setPluginState(PluginState.ON_DEMAND);
                    break;
                case 2:
                    //noinspection deprecation
                    settings.setPluginState(PluginState.ON);
                    break;
                default:
                    break;
            }
        }

        setUserAgent(context, mPreferences.getUserAgentChoice());

        if (mPreferences.getSavePasswordsEnabled() && !mIsIncognitoTab) {
            if (API < Build.VERSION_CODES.JELLY_BEAN_MR2) {
                //noinspection deprecation
                settings.setSavePassword(true);
            }
            settings.setSaveFormData(true);
        } else {
            if (API < Build.VERSION_CODES.JELLY_BEAN_MR2) {
                //noinspection deprecation
                settings.setSavePassword(false);
            }
            settings.setSaveFormData(false);
        }

        if (mPreferences.getJavaScriptEnabled()) {
            settings.setJavaScriptEnabled(true);
            settings.setJavaScriptCanOpenWindowsAutomatically(true);
        } else {
            settings.setJavaScriptEnabled(false);
            settings.setJavaScriptCanOpenWindowsAutomatically(false);
        }

        if (mPreferences.getTextReflowEnabled()) {
            settings.setLayoutAlgorithm(LayoutAlgorithm.NARROW_COLUMNS);
            if (API >= android.os.Build.VERSION_CODES.KITKAT) {
                try {
                    settings.setLayoutAlgorithm(LayoutAlgorithm.TEXT_AUTOSIZING);
                } catch (Exception e) {
                    // This shouldn't be necessary, but there are a number
                    // of KitKat devices that crash trying to set this
                    Log.e(TAG, "Problem setting LayoutAlgorithm to TEXT_AUTOSIZING");
                }
            }
        } else {
            settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);
        }

        settings.setBlockNetworkImage(mPreferences.getBlockImagesEnabled());
        if (!mIsIncognitoTab) {
            settings.setSupportMultipleWindows(mPreferences.getPopupsEnabled());
        } else {
            settings.setSupportMultipleWindows(false);
        }
        settings.setUseWideViewPort(mPreferences.getUseWideViewportEnabled());
        settings.setLoadWithOverviewMode(mPreferences.getOverviewModeEnabled());
        switch (mPreferences.getTextSize()) {
            case 0:
                settings.setTextZoom(200);
                break;
            case 1:
                settings.setTextZoom(150);
                break;
            case 2:
                settings.setTextZoom(125);
                break;
            case 3:
                settings.setTextZoom(100);
                break;
            case 4:
                settings.setTextZoom(75);
                break;
            case 5:
                settings.setTextZoom(50);
                break;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView,
                !mPreferences.getBlockThirdPartyCookiesEnabled());
        }
    }

    /**
     * Initialize the settings of the WebView that are intrinsic to Lightning and cannot
     * be altered by the user. Distinguish between Incognito and Regular tabs here.
     */
    @SuppressLint("NewApi")
    private void initializeSettings() {
        if (mWebView == null) {
            return;
        }
        final WebSettings settings = mWebView.getSettings();
        if (API < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            //noinspection deprecation
            settings.setAppCacheMaxSize(Long.MAX_VALUE);
        }
        if (API < Build.VERSION_CODES.JELLY_BEAN_MR1) {
            //noinspection deprecation
            settings.setEnableSmoothTransition(true);
        }
        if (API > Build.VERSION_CODES.JELLY_BEAN) {
            settings.setMediaPlaybackRequiresUserGesture(true);
        }
        if (API >= Build.VERSION_CODES.LOLLIPOP && !mIsIncognitoTab) {
            settings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
        } else if (API >= Build.VERSION_CODES.LOLLIPOP) {
            // We're in Incognito mode, reject
            settings.setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW);
        }
        if (!mIsIncognitoTab) {
            settings.setDomStorageEnabled(true);
            settings.setAppCacheEnabled(true);
            settings.setCacheMode(WebSettings.LOAD_DEFAULT);
            settings.setDatabaseEnabled(true);
        } else {
            settings.setDomStorageEnabled(false);
            settings.setAppCacheEnabled(false);
            settings.setDatabaseEnabled(false);
            settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
        }
        settings.setSupportZoom(true);
        settings.setBuiltInZoomControls(true);
        settings.setDisplayZoomControls(false);
        settings.setAllowContentAccess(true);
        settings.setAllowFileAccess(true);
        if (API >= Build.VERSION_CODES.JELLY_BEAN) {
            settings.setAllowFileAccessFromFileURLs(false);
            settings.setAllowUniversalAccessFromFileURLs(false);
        }

        getPathObservable("appcache").subscribeOn(Schedulers.io())
            .observeOn(Schedulers.main())
            .subscribe(new SingleOnSubscribe<File>() {
                @Override
                public void onItem(@Nullable File item) {
                    Preconditions.checkNonNull(item);
                    settings.setAppCachePath(item.getPath());
                }
            });

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            getPathObservable("geolocation").subscribeOn(Schedulers.io())
                .observeOn(Schedulers.main())
                .subscribe(new SingleOnSubscribe<File>() {
                    @Override
                    public void onItem(@Nullable File item) {
                        Preconditions.checkNonNull(item);
                        //noinspection deprecation
                        settings.setGeolocationDatabasePath(item.getPath());
                    }
                });
        }

        getPathObservable("databases").subscribeOn(Schedulers.io())
            .observeOn(Schedulers.main())
            .subscribe(new SingleOnSubscribe<File>() {
                @Override
                public void onItem(@Nullable File item) {
                    if (API < Build.VERSION_CODES.KITKAT) {
                        Preconditions.checkNonNull(item);
                        //noinspection deprecation
                        settings.setDatabasePath(item.getPath());
                    }
                }

                @Override
                public void onComplete() {
                }
            });

    }

    @NonNull
    private Single<File> getPathObservable(final String subFolder) {
        return Single.create(new SingleAction<File>() {
            @Override
            public void onSubscribe(@NonNull SingleSubscriber<File> subscriber) {
                File file = mActivity.getDir(subFolder, 0);
                subscriber.onItem(file);
                subscriber.onComplete();
            }
        });
    }

    /**
     * Getter for the {@link LightningViewTitle} of the
     * current LightningView instance.
     *
     * @return a NonNull instance of LightningViewTitle
     */
    @NonNull
    public LightningViewTitle getTitleInfo() {
        return mTitle;
    }

    /**
     * Returns whether or not the current tab is incognito or not.
     *
     * @return true if this tab is incognito, false otherwise
     */
    public boolean isIncognito() {
        return mIsIncognitoTab;
    }

    /**
     * This method is used to toggle the user agent between desktop
     * and the current preference of the user.
     *
     * @param context the Context needed to set the user agent
     */
    public void toggleDesktopUA(@NonNull Context context) {
        if (mWebView == null)
            return;
        if (!mToggleDesktop)
            mWebView.getSettings().setUserAgentString(Constants.DESKTOP_USER_AGENT);
        else
            setUserAgent(context, mPreferences.getUserAgentChoice());
        mToggleDesktop = !mToggleDesktop;
    }

    /**
     * This method sets the user agent of the current tab.
     * There are four options, 1, 2, 3, 4.
     * <p/>
     * 1. use the default user agent
     * <p/>
     * 2. use the desktop user agent
     * <p/>
     * 3. use the mobile user agent
     * <p/>
     * 4. use a custom user agent, or the default user agent
     * if none was set.
     *
     * @param context the context needed to get the default user agent.
     * @param choice  the choice of user agent to use, see above comments.
     */
    @SuppressLint("NewApi")
    private void setUserAgent(Context context, int choice) {
        if (mWebView == null) return;
        WebSettings settings = mWebView.getSettings();
        switch (choice) {
            case 1:
                if (API >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                    settings.setUserAgentString(WebSettings.getDefaultUserAgent(context));
                } else {
                    settings.setUserAgentString(sDefaultUserAgent);
                }
                break;
            case 2:
                settings.setUserAgentString(Constants.DESKTOP_USER_AGENT);
                break;
            case 3:
                settings.setUserAgentString(Constants.MOBILE_USER_AGENT);
                break;
            case 4:
                String ua = mPreferences.getUserAgentString(sDefaultUserAgent);
                if (ua == null || ua.isEmpty()) {
                    ua = " ";
                }
                settings.setUserAgentString(ua);
                break;
        }
    }

    /**
     * This method gets the additional headers that should be
     * added with each request the browser makes.
     *
     * @return a non null Map of Strings with the additional
     * request headers.
     */
    @NonNull
    Map<String, String> getRequestHeaders() {
        return mRequestHeaders;
    }

    /**
     * This method determines whether the current tab is visible or not.
     *
     * @return true if the WebView is non-null and visible, false otherwise.
     */
    public boolean isShown() {
        return mWebView != null && mWebView.isShown();
    }

    /**
     * Pause the current WebView instance.
     */
    public synchronized void onPause() {
        if (mWebView != null) {
            mWebView.onPause();
            Log.d(TAG, "WebView onPause: " + mWebView.getId());
        }
    }

    /**
     * Resume the current WebView instance.
     */
    public synchronized void onResume() {
        if (mWebView != null) {
            mWebView.onResume();
            Log.d(TAG, "WebView onResume: " + mWebView.getId());
        }
    }

    /**
     * Notify the LightningView that there is low memory and
     * for the WebView to free memory. Only applicable on
     * pre-Lollipop devices.
     */
    @Deprecated
    public synchronized void freeMemory() {
        if (mWebView != null && Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
            //noinspection deprecation
            mWebView.freeMemory();
        }
    }

    /**
     * This method sets the tab as the foreground tab or
     * the background tab.
     *
     * @param isForeground true if the tab should be set as
     *                     foreground, false otherwise.
     */
    public void setIsForegroundTab(boolean isForeground) {
        mIsForegroundTab = isForeground;
        mUIController.tabChanged(this);
    }

    /**
     * Determines if the tab is in the foreground or not.
     *
     * @return true if the tab is the foreground tab,
     * false otherwise.
     */
    public boolean isForegroundTab() {
        return mIsForegroundTab;
    }

    /**
     * Gets the current progress of the WebView.
     *
     * @return returns a number between 0 and 100 with
     * the current progress of the WebView. If the WebView
     * is null, then the progress returned will be 100.
     */
    public int getProgress() {
        if (mWebView != null) {
            return mWebView.getProgress();
        } else {
            return 100;
        }
    }

    /**
     * Notify the WebView to stop the current load.
     */
    public synchronized void stopLoading() {
        if (mWebView != null) {
            mWebView.stopLoading();
        }
    }

    /**
     * This method forces the layer type to hardware, which
     * enables hardware rendering on the WebView instance
     * of the current LightningView.
     */
    private void setHardwareRendering() {
        if (mWebView == null) {
            return;
        }
        mWebView.setLayerType(View.LAYER_TYPE_HARDWARE, mPaint);
    }

    /**
     * This method sets the layer type to none, which
     * means that either the GPU and CPU can both compose
     * the layers when necessary.
     */
    private void setNormalRendering() {
        if (mWebView == null) {
            return;
        }
        mWebView.setLayerType(View.LAYER_TYPE_NONE, null);
    }

    /**
     * This method forces the layer type to software, which
     * disables hardware rendering on the WebView instance
     * of the current LightningView and makes the CPU render
     * the view.
     */
    public void setSoftwareRendering() {
        if (mWebView == null) {
            return;
        }
        mWebView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    }

    /**
     * Sets the current rendering color of the WebView instance
     * of the current LightningView. The for modes are normal
     * rendering (0), inverted rendering (1), grayscale rendering (2),
     * and inverted grayscale rendering (3)
     *
     * @param mode the integer mode to set as the rendering mode.
     *             see the numbers in documentation above for the
     *             values this method accepts.
     */
    private void setColorMode(int mode) {
        mInvertPage = false;
        switch (mode) {
            case 0:
                mPaint.setColorFilter(null);
                // setSoftwareRendering(); // Some devices get segfaults
                // in the WebView with Hardware Acceleration enabled,
                // the only fix is to disable hardware rendering
                setNormalRendering();
                mInvertPage = false;
                break;
            case 1:
                ColorMatrixColorFilter filterInvert = new ColorMatrixColorFilter(
                    sNegativeColorArray);
                mPaint.setColorFilter(filterInvert);
                setHardwareRendering();

                mInvertPage = true;
                break;
            case 2:
                ColorMatrix cm = new ColorMatrix();
                cm.setSaturation(0);
                ColorMatrixColorFilter filterGray = new ColorMatrixColorFilter(cm);
                mPaint.setColorFilter(filterGray);
                setHardwareRendering();
                break;
            case 3:
                ColorMatrix matrix = new ColorMatrix();
                matrix.set(sNegativeColorArray);
                ColorMatrix matrixGray = new ColorMatrix();
                matrixGray.setSaturation(0);
                ColorMatrix concat = new ColorMatrix();
                concat.setConcat(matrix, matrixGray);
                ColorMatrixColorFilter filterInvertGray = new ColorMatrixColorFilter(concat);
                mPaint.setColorFilter(filterInvertGray);
                setHardwareRendering();

                mInvertPage = true;
                break;

            case 4:
                ColorMatrixColorFilter IncreaseHighContrast = new ColorMatrixColorFilter(
                    sIncreaseContrastColorArray);
                mPaint.setColorFilter(IncreaseHighContrast);
                setHardwareRendering();
                break;

        }

    }

    /**
     * Pauses the JavaScript timers of the
     * WebView instance, which will trigger a
     * pause for all WebViews in the app.
     */
    public synchronized void pauseTimers() {
        if (mWebView != null) {
            mWebView.pauseTimers();
            Log.d(TAG, "Pausing JS timers");
        }
    }

    /**
     * Resumes the JavaScript timers of the
     * WebView instance, which will trigger a
     * resume for all WebViews in the app.
     */
    public synchronized void resumeTimers() {
        if (mWebView != null) {
            mWebView.resumeTimers();
            Log.d(TAG, "Resuming JS timers");
        }
    }

    /**
     * Requests focus down on the WebView instance
     * if the view does not already have focus.
     */
    public void requestFocus() {
        if (mWebView != null && !mWebView.hasFocus()) {
            mWebView.requestFocus();
        }
    }

    /**
     * Sets the visibility of the WebView to either
     * View.GONE, View.VISIBLE, or View.INVISIBLE.
     * other values passed in will have no effect.
     *
     * @param visible the visibility to set on the WebView.
     */
    public void setVisibility(int visible) {
        if (mWebView != null) {
            mWebView.setVisibility(visible);
        }
    }

    /**
     * Tells the WebView to reload the current page.
     * If the proxy settings are not ready then the
     * this method will not have an affect as the
     * proxy must start before the load occurs.
     */
    public synchronized void reload() {
        // Check if configured proxy is available
        if (!mProxyUtils.isProxyReady(mActivity)) {
            // User has been notified
            return;
        }

        if (mWebView != null) {
            mWebView.reload();
        }
    }

    /**
     * Finds all the instances of the text passed to this
     * method and highlights the instances of that text
     * in the WebView.
     *
     * @param text the text to search for.
     */
    @SuppressLint("NewApi")
    public synchronized void find(String text) {
        if (mWebView != null) {
            if (API >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                mWebView.findAllAsync(text);
            } else {
                //noinspection deprecation
                mWebView.findAll(text);
            }
        }
    }

    /**
     * Notify the tab to shutdown and destroy
     * its WebView instance and to remove the reference
     * to it. After this method is called, the current
     * instance of the LightningView is useless as
     * the WebView cannot be recreated using the public
     * api.
     */
    // TODO fix bug where WebView.destroy is being called before the tab
    // is removed and would cause a memory leak if the parent check
    // was not in place.
    public synchronized void onDestroy() {
        if (mWebView != null) {
            // Check to make sure the WebView has been removed
            // before calling destroy() so that a memory leak is not created
            ViewGroup parent = (ViewGroup) mWebView.getParent();
            if (parent != null) {
                Log.e(TAG, "WebView was not detached from window before onDestroy");
                parent.removeView(mWebView);
            }
            mWebView.stopLoading();
            mWebView.onPause();
            mWebView.clearHistory();
            mWebView.setVisibility(View.GONE);
            mWebView.removeAllViews();
            mWebView.destroyDrawingCache();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                //this is causing the segfault occasionally below 4.2
                mWebView.destroy();
            }
            mWebView = null;
        }
    }

    /**
     * Tell the WebView to navigate backwards
     * in its history to the previous page.
     */
    public synchronized void goBack() {
        if (mWebView != null) {
            mWebView.goBack();
        }
    }

    /**
     * Tell the WebView to navigate forwards
     * in its history to the next page.
     */
    public synchronized void goForward() {
        if (mWebView != null) {
            mWebView.goForward();
        }
    }

    /**
     * Get the current user agent used
     * by the WebView.
     *
     * @return retuns the current user agent
     * of the WebView instance, or an empty
     * string if the WebView is null.
     */
    @NonNull
    private String getUserAgent() {
        if (mWebView != null) {
            return mWebView.getSettings().getUserAgentString();
        } else {
            return "";
        }
    }

    /**
     * Move the highlighted text in the WebView
     * to the next matched text. This method will
     * only have an affect after {@link LightningView#find(String)}
     * is called. Otherwise it will do nothing.
     */
    public synchronized void findNext() {
        if (mWebView != null) {
            mWebView.findNext(true);
        }
    }

    /**
     * Move the highlighted text in the WebView
     * to the previous matched text. This method will
     * only have an affect after {@link LightningView#find(String)}
     * is called. Otherwise it will do nothing.
     */
    public synchronized void findPrevious() {
        if (mWebView != null) {
            mWebView.findNext(false);
        }
    }

    /**
     * Clear the highlighted text in the WebView after
     * {@link LightningView#find(String)} has been called.
     * Otherwise it will have no affect.
     */
    public synchronized void clearFindMatches() {
        if (mWebView != null) {
            mWebView.clearMatches();
        }
    }

    /**
     * Gets whether or not the page rendering is inverted or not.
     * The main purpose of this is to indicate that JavaScript
     * should be run at the end of a page load to invert only
     * the images back to their uninverted states.
     *
     * @return true if the page is in inverted mode, false otherwise.
     */
    public boolean getInvertePage() {
        return mInvertPage;
    }

    /**
     * Handles a long click on the page and delegates the URL to the
     * proper dialog if it is not null, otherwise, it tries to get the
     * URL using HitTestResult.
     *
     * @param url the url that should have been obtained from the WebView touch node
     *            thingy, if it is null, this method tries to deal with it and find
     *            a workaround.
     */
    private void longClickPage(@Nullable final String url) {
        if (mWebView == null) {
            return;
        }
        final WebView.HitTestResult result = mWebView.getHitTestResult();
        String currentUrl = mWebView.getUrl();
        if (currentUrl != null && UrlUtils.isSpecialUrl(currentUrl)) {
            if (UrlUtils.isHistoryUrl(currentUrl)) {
                if (url != null) {
                    mDialogBuilder.showLongPressedHistoryLinkDialog(mActivity, mUIController, url);
                } else if (result != null && result.getExtra() != null) {
                    final String newUrl = result.getExtra();
                    mDialogBuilder.showLongPressedHistoryLinkDialog(mActivity, mUIController, newUrl);
                }
            } else if (UrlUtils.isBookmarkUrl(currentUrl)) {
                if (url != null) {
                    mDialogBuilder.showLongPressedDialogForBookmarkUrl(mActivity, mUIController, url);
                } else if (result != null && result.getExtra() != null) {
                    final String newUrl = result.getExtra();
                    mDialogBuilder.showLongPressedDialogForBookmarkUrl(mActivity, mUIController, newUrl);
                }
            } else if (UrlUtils.isDownloadsUrl(currentUrl)) {
                if (url != null) {
                    mDialogBuilder.showLongPressedDialogForDownloadUrl(mActivity, mUIController, url);
                } else if (result != null && result.getExtra() != null) {
                    final String newUrl = result.getExtra();
                    mDialogBuilder.showLongPressedDialogForDownloadUrl(mActivity, mUIController, newUrl);
                }
            }
        } else {
            if (url != null) {
                if (result != null) {
                    if (result.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE || result.getType() == WebView.HitTestResult.IMAGE_TYPE) {
                        mDialogBuilder.showLongPressImageDialog(mActivity, mUIController, url, getUserAgent());
                    } else {
                        mDialogBuilder.showLongPressLinkDialog(mActivity, mUIController, url);
                    }
                } else {
                    mDialogBuilder.showLongPressLinkDialog(mActivity, mUIController, url);
                }
            } else if (result != null && result.getExtra() != null) {
                final String newUrl = result.getExtra();
                if (result.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE || result.getType() == WebView.HitTestResult.IMAGE_TYPE) {
                    mDialogBuilder.showLongPressImageDialog(mActivity, mUIController, newUrl, getUserAgent());
                } else {
                    mDialogBuilder.showLongPressLinkDialog(mActivity, mUIController, newUrl);
                }
            }
        }
    }

    /**
     * Determines whether or not the WebView can go
     * backward or if it as the end of its history.
     *
     * @return true if the WebView can go back, false otherwise.
     */
    public boolean canGoBack() {
        return mWebView != null && mWebView.canGoBack();
    }

    /**
     * Determine whether or not the WebView can go
     * forward or if it is at the front of its history.
     *
     * @return true if it can go forward, false otherwise.
     */
    public boolean canGoForward() {
        return mWebView != null && mWebView.canGoForward();
    }

    /**
     * Gets the current WebView instance of the tab.
     *
     * @return the WebView instance of the tab, which
     * can be null.
     */
    @Nullable
    public synchronized WebView getWebView() {
        return mWebView;
    }

    /**
     * Gets the favicon currently in use by
     * the page. If the current page does not
     * have a favicon, it returns a default
     * icon.
     *
     * @return a non-null Bitmap with the
     * current favicon.
     */
    @NonNull
    public Bitmap getFavicon() {
        return mTitle.getFavicon(mUIController.getUseDarkTheme());
    }

    /**
     * Loads the URL in the WebView. If the proxy settings
     * are still initializing, then the URL will not load
     * as it is necessary to have the settings initialized
     * before a load occurs.
     *
     * @param url the non-null URL to attempt to load in
     *            the WebView.
     */
    public synchronized void loadUrl(@NonNull String url) {
        // Check if configured proxy is available
        if (!mProxyUtils.isProxyReady(mActivity)) {
            return;
        }

        if (mWebView != null) {
            mWebView.loadUrl(url, mRequestHeaders);
        }
    }

    /**
     * Get the current title of the page, retrieved from
     * the title object.
     *
     * @return the title of the page, or an empty string
     * if there is no title.
     */
    @NonNull
    public String getTitle() {
        return mTitle.getTitle();
    }

    /**
     * Get the current URL of the WebView, or an empty
     * string if the WebView is null or the URL is null.
     *
     * @return the current URL or an empty string.
     */
    @NonNull
    public String getUrl() {
        if (mWebView != null && mWebView.getUrl() != null) {
            return mWebView.getUrl();
        } else {
            return "";
        }
    }

    /**
     * The OnTouchListener used by the WebView so we can
     * get scroll events and show/hide the action bar when
     * the page is scrolled up/down.
     */
    private class TouchListener implements OnTouchListener {

        float mLocation;
        float mY;
        int mAction;

        @SuppressLint("ClickableViewAccessibility")
        @Override
        public boolean onTouch(@Nullable View view, @NonNull MotionEvent arg1) {
            if (view == null)
                return false;

            if (!view.hasFocus()) {
                view.requestFocus();
            }
            mAction = arg1.getAction();
            mY = arg1.getY();
            if (mAction == MotionEvent.ACTION_DOWN) {
                mLocation = mY;
            } else if (mAction == MotionEvent.ACTION_UP) {
                final float distance = (mY - mLocation);
                if (distance > SCROLL_UP_THRESHOLD && view.getScrollY() < SCROLL_UP_THRESHOLD) {
                    mUIController.showActionBar();
                } else if (distance < -SCROLL_UP_THRESHOLD) {
                    mUIController.hideActionBar();
                }
                mLocation = 0;
            }
            mGestureDetector.onTouchEvent(arg1);
            return false;
        }
    }

    /**
     * The SimpleOnGestureListener used by the {@link TouchListener}
     * in order to delegate show/hide events to the action bar when
     * the user flings the page. Also handles long press events so
     * that we can capture them accurately.
     */
    private class CustomGestureListener extends SimpleOnGestureListener {

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            int power = (int) (velocityY * 100 / sMaxFling);
            if (power < -10) {
                mUIController.hideActionBar();
            } else if (power > 15) {
                mUIController.showActionBar();
            }
            return super.onFling(e1, e2, velocityX, velocityY);
        }

        /**
         * Without this, onLongPress is not called when user is zooming using
         * two fingers, but is when using only one.
         * <p/>
         * The required behaviour is to not trigger this when the user is
         * zooming, it shouldn't matter how much fingers the user's using.
         */
        private boolean mCanTriggerLongPress = true;

        @Override
        public void onLongPress(MotionEvent e) {
            if (mCanTriggerLongPress) {
                Message msg = mWebViewHandler.obtainMessage();
                if (msg != null) {
                    msg.setTarget(mWebViewHandler);
                    if (mWebView == null) {
                        return;
                    }
                    mWebView.requestFocusNodeHref(msg);
                }
            }
        }

        /**
         * Is called when the user is swiping after the doubletap, which in our
         * case means that he is zooming.
         */
        @Override
        public boolean onDoubleTapEvent(MotionEvent e) {
            mCanTriggerLongPress = false;
            return false;
        }

        /**
         * Is called when something is starting being pressed, always before
         * onLongPress.
         */
        @Override
        public void onShowPress(MotionEvent e) {
            mCanTriggerLongPress = true;
        }
    }

    /**
     * A Handler used to get the URL from a long click
     * event on the WebView. It does not hold a hard
     * reference to the WebView and therefore will not
     * leak it if the WebView is garbage collected.
     */
    private static class WebViewHandler extends Handler {

        @NonNull private final WeakReference<LightningView> mReference;

        WebViewHandler(@NonNull LightningView view) {
            mReference = new WeakReference<>(view);
        }

        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            final String url = msg.getData().getString("url");
            LightningView view = mReference.get();
            if (view != null) {
                view.longClickPage(url);
            }
        }
    }
}
