package com.mobify.astro.plugins.webviewplugin;

import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Trace;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.CookieManager;
import android.webkit.JavascriptInterface;
import android.webkit.WebBackForwardList;
import android.webkit.WebHistoryItem;
import android.webkit.WebView;
import android.widget.ImageView;

import com.mobify.astro.AstroActivity;
import com.mobify.astro.PluginResolver;
import com.mobify.astro.messaging.EventRegistrar;
import com.mobify.astro.messaging.Exceptions;
import com.mobify.astro.messaging.MessageSender;
import com.mobify.astro.messaging.annotations.RpcMethod;
import com.mobify.astro.plugins.AstroNative;
import com.mobify.astro.plugins.NavigationHost;
import com.mobify.astro.AstroWorker;
import com.mobify.astro.plugins.NavigationPlugin;
import com.mobify.astro.plugins.NavigationStackFrame;
import com.mobify.astro.plugins.loaders.LoaderPlugin;
import com.mobify.astro.utilities.AstroFileUtilities;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;

/*
 * Please see the NavigationPlugin when making changes.  It is the one that
 * really needs the stacking support.  We should move it to that plugin.
 */
public class WebViewPlugin
        extends AstroWebViewPlugin
        implements AstroWebViewClient.WebClientListener, NavigationStackFrame {

    private static final String TAG = WebViewPlugin.class.getName();

    protected static int pageTimeoutDuration = 10;
    private static long fadeDuration = 100L;
    private static long foregroundViewRemovalDelay = 100L;
    private static long foregroundViewBackupRemovalDelay = 1000L;

    private Handler handler = new Handler();
    private int foregroundViewId = View.NO_ID;
    private Timer foregroundViewBackupRemoval;
    protected LoadingContainerView containerView;
    protected AstroWebView webView;
    protected ArrayList<String> manualShowPageHosts = new ArrayList<>();
    protected boolean areScrollbarsVisible = true;
    protected CookieManager cookieManager;
    protected Bundle webViewState;
    protected AstroWebViewClient webViewClient;
    protected boolean disposeWhenPopped;
    private NavigationHost navigationHost;
    public View snapview = null;

    protected Runnable triggerPageTimeout = new Runnable() {
        @Override
        public void run() {
            WebView webView = getWebView();
            if (webView == null) {
                return;
            }

            webView.stopLoading();
            JSONObject error = new JSONObject();
            try {
                error.put("description", "Page load timed out");
                error.put("code", WebViewErrorCodes.PAGE_TIMEOUT);
            } catch (JSONException e) {
                e.printStackTrace();
            }

            deleteMobifyPathCookieIfEmpty();
            triggerNavigationResult(EventNames.NAVIGATION_FAILED, webView.getUrl(), error);
        }
    };

    private static final class WebViewErrorCodes {
        //These are defined with values that match NSURLError for consistency with iOS.
        private static final int PAGE_TIMEOUT = -1001;
        private static final int NO_INTERNET_CONNECTION = -1009;
    }

    public static final class EventNames {
        public static final String NAVIGATE = "navigate";
        public static final String NAVIGATION_COMPLETED = "navigationCompleted";
        public static final String NAVIGATION_FAILED = "navigationFailed";
        public static final String BACK = "back";
    }

    public WebViewPlugin(@NonNull AstroActivity activity, @NonNull PluginResolver pluginResolver,
                         @NonNull EventRegistrar eventRegistrar, @NonNull MessageSender messageSender) {
        this(activity, pluginResolver, eventRegistrar, messageSender, null);
    }

    public WebViewPlugin(@NonNull AstroActivity activity, @NonNull PluginResolver pluginResolver,
                         @NonNull EventRegistrar eventRegistrar, @NonNull MessageSender messageSender,
                         @Nullable JSONObject options) {
        super(activity, pluginResolver, eventRegistrar, messageSender);

        containerView = new LoadingContainerView(activity);
        manualShowPageHosts = new ArrayList<>();
        webViewClient = new AstroWebViewClient(this);
        webView = makeAstroWebView();

        disposeWhenPopped = false;
        if (options != null) {
            try {
                disposeWhenPopped = options.getBoolean("disposeWhenPopped");
            } catch (Exception e) {
                // no-op, default already set
            }
        }
    }

    protected AstroWebView makeAstroWebView() {
        AstroWebView webView = new AstroWebView(activity);
        webView.setScrollBarVisibility(areScrollbarsVisible);
        webView.addJavascriptInterface(this, "Astro");
        webView.addJavascriptInterface(new AstroNative(), "AstroNativeObject");
        webView.setAstroWebViewClient(webViewClient);
        return webView;
    }

    //region Overrides

    @Override
    @Nullable
    public WebView getWebView() {
        return webView;
    }

    @Override
    public View getView() {
        return containerView;
    }

    @Override
    public void onPause() {
        super.onPause();

        if (webView != null) {
            webView.saveState(savedPluginState);
        }
    }

    public void destroyFrame() {
        if (disposeWhenPopped) {
            pluginResolver.removePluginFromList(this);
            destroy();
        }
    }

    @Override
    public void destroy() {
        super.destroy();
        // We want to stop any timeout timers here to prevent
        // events triggering from a deallocated plugin.
        clearTimeoutTimer();

        containerView.clearChildView();
        if (webView != null) {
            webView.destroy();
        }
    }

    // region NavigationStackFrame

    public void saveAndDestroyView() {
        if (webView == null) {
            return;
        }

        webViewState = new Bundle();
        webView.saveState(webViewState);
        webView.destroy();
        webView = null;
    }

    public void restoreInactiveViewFromSave() {
        // if there is no saved state or the webView is currently active, short-circuit
        if (webViewState == null || webView != null) {
            return;
        }

        webView = makeAstroWebView();
        webView.restoreState(webViewState);
        webViewState = null;
    }

    public void saveStateToBundle(Bundle bundle) {
        if (webView == null || bundle == null) {
            return;
        }

        webView.saveState(bundle);
    }

    public void restoreStateFromBundle(Bundle bundle) {
        if (webView == null || bundle == null) {
            return;
        }

        // Restoring state from a bundle on older versions of Chrome
        // causes the web view to immediately complete a full page
        // lifecycle. Tell the web view client to ignore it.
        webView.getAstroWebViewClient().ignoreNextPageLifecycle();
        webView.restoreState(bundle);
    }

    public boolean hasActiveView() {
        return hasWebView();
    }

    public boolean hasWebView() {
        return webView != null;
    }

    public String getUrl() {
        return webView.getUrl();
    }

    @Override
    public void setNavigationHost(NavigationHost host) {
        this.navigationHost = host;
    }

    private void deleteMobifyPathCookieIfEmpty() {
        String cookieName = "mobify-path";
        String cookieValue = getCookie(cookieName);
        if ("".equals(cookieValue)) {
            deleteCookie(cookieName);
        }
    }

    //region WebClientListener

    @Override
    public void webClientPageStarted(WebView view, String url) {
        deleteMobifyPathCookieIfEmpty();

        setJsLoaded(false);
        startTimeoutTimer();

        containerView.transitionToLoadingWithView(view, url);
    }

    private void triggerNavigationResult(String eventName, String url) {
        triggerNavigationResult(eventName, url, null);
    }

    private void triggerNavigationResult(String eventName, String url, JSONObject error) {
        // Send navigation events to JavaScript.
        JSONObject navigationData = new JSONObject();
        try {
            navigationData.put("url", url);
            if (error != null) {
                navigationData.put("error", error);
            }
        } catch (JSONException e) {
            Log.e(TAG, e.getMessage(), e);
        }
        triggerEvent(eventName, navigationData);
    }

    @Override
    public void webClientPageFinished(WebView view, String url) {
        // Flush queued JavaScript invocations.
        setJsLoaded(true);

        // Clear the timeout timer before checking isShowPageManualForURL since we don't want a
        // timeout due to a slow adaptive/web page, or an intentional delay to calling showPage.
        clearTimeoutTimer();

        if (isShowPageManualForURL(url)) {
            // If the host for this url is in the list of overridden hosts,
            // defer control of showing the page to app.js.
        } else {
            showPage();
        }

        // Send navigation completed event to JavaScript.
        triggerNavigationResult(EventNames.NAVIGATION_COMPLETED, url);
    }

    public void webClientPageFailed(WebView view, String url) {
        // Flush queued JavaScript invocations.
        setJsLoaded(true);

        clearTimeoutTimer();
        showPage();

        // Send navigation failed event to JavaScript.
        triggerNavigationResult(EventNames.NAVIGATION_FAILED, url);
    }

    @Override
    public boolean webClientNavigate(WebView view, String url, boolean isCurrentlyLoading) {
        if (url.startsWith("tel:") || url.startsWith("mailto:")) {
            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
            activity.startActivity(intent);
            return true;
        }
        // Send navigation events to JavaScript.
        JSONObject navigationData = new JSONObject();
        try {
            navigationData.put("url", url);
            navigationData.put("isCurrentlyLoading", isCurrentlyLoading);
        } catch (JSONException e) {
            Log.e(TAG, e.getMessage(), e);
        }
        triggerEvent(EventNames.NAVIGATE, navigationData);

        // Allow JavaScript to override url loading for non-redirects. If this is a redirect,
        // then we let the browser process it so that we don't get multiple calls to `pageStarted`
        // and `pageFinished`.
        return !isCurrentlyLoading;
    }

    @Override
    @JavascriptInterface
    public void exec(String address, String jsonData) throws
            JSONException,
            Exceptions.PayloadMissingException,
            Exceptions.MalformedRpcMessageException {
        String nativeAddress = transformJsAddressToNative(address);

        // Check recipient address
        if (!nativeAddress.equals(AstroWorker.ADDRESS) &&
                !nativeAddress.equals(getEventsAddress())) {
            Log.e(TAG, "JavaScript in WebViewPlugin with address " + getInstanceAddress() +
                    " attempted to send message " + jsonData + " to " + address +
                    ", the message was not sent. JavaScript in WebViewPlugin instances may only" +
                    "send messages" + "to the Application address, or their own events address.");
            return;
        }

        super.exec(address, jsonData);
    }

    //endregion

    //endregion

    protected void startTimeoutTimer() {
        handler.postDelayed(triggerPageTimeout, pageTimeoutDuration * 1000);
    }

    protected void clearTimeoutTimer() {
        handler.removeCallbacks(triggerPageTimeout);
    }

    @RpcMethod(methodName="canGoBack")
    public boolean canGoBack() {
        return webView.canGoBack();
    }

    //region Connectivity

    protected boolean ensureConnectivity(Uri uri) {
        if (!AstroFileUtilities.isFileUri(uri) && isOffline()) {
            triggerNoInternetConnectionError(uri);
            return false;
        }

        return true;
    }

    protected void triggerNoInternetConnectionError(Uri uri) {

        JSONObject error = new JSONObject();
        try {
            error.put("description", "No internet connection");
            error.put("code", WebViewErrorCodes.NO_INTERNET_CONNECTION);
        } catch (Exception e) {
            Log.d(TAG, "Error adding url failed to load to JSON object", e);
        }
        triggerNavigationResult(EventNames.NAVIGATION_FAILED, uri.toString(), error);
    }

    //endregion

    protected boolean isShowPageManualForURL(String url) {
        try {
            URL myUrl = new URL(url);

            for (String host : manualShowPageHosts) {
                if (myUrl.getHost().equals(host)) {
                    return true;
                }
            }
        } catch (MalformedURLException e) {
            Log.e(TAG, "Could not parse URL: " + e.getMessage(), e);
        }

        return false;
    }

    protected void setScrollBarVisibility(boolean visible) {
        areScrollbarsVisible = visible;
        webView.setScrollBarVisibility(visible);
    }

    /**
     * Navigates the web view to the given url.
     * @param url the url to navigate to
     */
    @RpcMethod(methodName="navigate", parameterNames= {"url"})
    public void navigate(String url) throws Exception {
        Uri uriObject = Uri.parse(Uri.decode(url));

        if (!ensureConnectivity(uriObject)) {
            // We clear the timeout timer here to prevent triggering
            // a page timeout event alongside a no connectivity event.
            clearTimeoutTimer();
            return;
        }

        // Using Astro Resource Url to specify a local webpage
        // Expect these webpages to be in the android assets directory
        // So change url to point to filesystem location
        if (AstroFileUtilities.isFileUri(uriObject)) {
            uriObject = AstroFileUtilities.createAndroidAssetUri(uriObject);
        }

        // Navigate.
        String navigationUrl = uriObject.toString();
        webView.loadUrl(navigationUrl);
    }

    /**
     * Gets the url one back of the current index in the web views history
     */
    protected String getBackUrlFromWebViewHistory(WebView webView) {
        WebBackForwardList history = webView.copyBackForwardList();
        WebHistoryItem item = history.getItemAtIndex(history.getCurrentIndex() - 1);
        return item.getUrl();
    }

    /**
     * Triggers an event letting the JS know what url is being navigated back to
     */
    protected void triggerBackEvent(String url) {
        JSONObject params = new JSONObject();
        try {
            params.put("url", url);
            params.put("canGoBack", canGoBack());
        } catch (Exception e) {
            Log.d(TAG, "Error adding url failed to load to JSON object", e);
        }
        triggerEvent(EventNames.BACK, params);
    }

    /**
     * Reloads the top web view.
     */
    @RpcMethod(methodName = "reload")
    public void reload() {
        webView.reload();
    }

    /**
     * Shows the web view.
     */
    @RpcMethod(methodName = "showPage")
    public void showPage() {
        containerView.transitionToShowing();
    }

    /**
     * Navigates the web view up the history stack to the last page that wasn't a redirect.
     */
    @RpcMethod(methodName = "back")
    public void goBack() {
        // Short circuit if we don't expect to be able to go back.
        if (!canGoBack()) {
            return;
        }
        // clear existing timer since we are no longer trying
        // to load the current page, and instead are going back.
        clearTimeoutTimer();

        String url = getBackUrlFromWebViewHistory(webView);
        triggerBackEvent(url);

        // Web views are not stacked or there is only one left.
        webView.goBack();
    }

    /**
     * Changes the color of the web view's container view
     * @param color The string value of the color
     */
    @RpcMethod(methodName="setBackgroundColor", parameterNames= {"color"})
    public void setBackgroundColor(String color) {
        containerView.setBackgroundColor(Color.parseColor(color));
    }

    /**
     * Shows scrollbars on all web views.
     */
    @RpcMethod(methodName="showScrollBars")
    public void showScrollBars() {
        setScrollBarVisibility(true);
    }

    /**
     * Hides scrollbars on all web views.
     */
    @RpcMethod(methodName="hideScrollBars")
    public void hideScrollBars() {
        setScrollBarVisibility(false);
    }

    /**
     * Suppresses the pageFinished behaviour for the given hosts.
     * @param hosts JSONArray of the host schemes that should be overridden
     */
    @RpcMethod(methodName="manuallyShowPageForHosts", parameterNames = {"hosts"})
    public void manuallyShowPageForHosts(JSONArray hosts) {
        // TODO: We will want more granularity here. This was just an MVP
        //       if there are already hosts in this array, clear it for now.
        manualShowPageHosts.clear();

        // Convert the JSONArray to an ArrayList
        for (int i = 0; i < hosts.length(); i++) {
            try {
                manualShowPageHosts.add(hosts.getString(i));
            } catch (JSONException e) {
                Log.e(TAG, "Could not get string from JSON Array: " + e.getMessage(), e);
            }
        }
    }

    /**
     * On iOS adds url patterns to let webView handle some urls navigation (i.e. open in frame)
     * Not supported on Android since webView handles frames navigation with target appropriately there.
     */
    @RpcMethod(methodName="allowExplicitNavigation", parameterNames = {"urlPatterns"})
    public void allowExplicitNavigation(JSONArray urlPatterns) {
        Log.d(TAG, "allowExplicitNavigation() is not supported on Android!");
    }

    /**
     * Override the default page timeout length.
     * @param timeoutDuration the timeout duration in seconds
     */
    @RpcMethod(methodName="setPageTimeoutDuration", parameterNames = {"timeoutDuration"})
    public void setPageTimeoutDuration(int timeoutDuration) {
        // Note: this modifies a static property.
        pageTimeoutDuration = timeoutDuration;
    }

    @RpcMethod(methodName="setLoaderPlugin", parameterNames = {"address"})
    public void setLoaderPlugin(String address) {
        LoaderPlugin plugin = (LoaderPlugin)pluginResolver.instanceForAddress(address);
        containerView.setLoaderPlugin(plugin);
    }

    @RpcMethod(methodName="enableScrollBounce")
    public void enableScrollBounce() {
        Log.d(TAG, "enableScrollBounce() is not supported on Android!");
    }

    @RpcMethod(methodName="disableScrollBounce")
    public void disableScrollBounce() {
        Log.d(TAG, "disableScrollBounce() is not supported on Android!");
    }

    @RpcMethod(methodName="enableScrolling")
    public void enableScrolling() {
        webView.setScrolling(true);
    }

    @RpcMethod(methodName="disableScrolling")
    public void disableScrolling() {
//        webView.setScrolling(false);
    }

    @RpcMethod(methodName="enableLoader")
    public void enableLoader() {
        containerView.setLoaderEnabled(true);
    }

    @RpcMethod(methodName="disableLoader")
    public void disableLoader() {
        containerView.setLoaderEnabled(false);
    }

    /**
     * @return null if cookieName isn't found, value of cookie (possibly "") otherwise
     */
    @RpcMethod(methodName="getCookie", parameterNames={"cookieName"})
    public String getCookie(String cookieName) {
        WebView webView = getWebView();
        if (webView == null) {
            return null;
        }

        String url = webView.getUrl();
        if (url == null) {
            return null;
        }

        cookieManager = CookieManager.getInstance();
        String cookieString = cookieManager.getCookie(url);
        if (cookieString == null) {
            return null;
        }
        String[] cookies = cookieString.split(";");
        for (int i = 0; i < cookies.length; i++) {
            String cookie = cookies[i].trim();
            if (cookie.startsWith(cookieName)) {
                String[] cookiePieces = cookie.split("=");
                // If we have a cookie with no value, return ""
                if (cookiePieces.length == 1) {
                    return "";
                }
                return cookiePieces[1];
            }
        }
        return null;
    }

    public void deleteCookie(String cookieName) {
        WebView webView = getWebView();
        if (webView == null) {
            return;
        }

        String url = webView.getUrl();
        if (url == null) {
            return;
        }

        cookieManager = CookieManager.getInstance();
        cookieManager.setCookie(url, cookieName + "=; expires=Thu, 01 Jan 1970 00:00:01 GMT");
    }

    @Override
    protected void pwaNavigate(JSONObject params) {
        if (navigationHost != null) {
            snapshotWebview();
            insertPwaLoaderView();
            navigationHost.pwaNavigating(this, params);
        }
    }

    @Override
    protected void pwaRendered() {
        // Delay removal event by 0.1 seconds to give enough time for View.draw(Canvas) event
        // to finish rendering snapshot view during pwaNavigate
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // Fade for 0.1 seconds
                final View foregroundView = containerView.findViewById(foregroundViewId);

                if (foregroundView != null) {
                    foregroundView.animate().alpha(1).setDuration(fadeDuration).withEndAction(new Runnable() {
                        @Override
                        public void run() {
                            containerView.removeView(foregroundView);
                            foregroundViewId = View.NO_ID;
                        }
                    });
                }

                if (foregroundViewBackupRemoval != null) {
                    foregroundViewBackupRemoval.cancel();
                    foregroundViewBackupRemoval.purge();
                    foregroundViewBackupRemoval = null;
                }
            }
        }, foregroundViewRemovalDelay);
    }

    private void insertPwaLoaderView() {
        final View pwaLoaderViewView = new View(activity);
        pwaLoaderViewView.setBackgroundColor(Color.parseColor(NavigationPlugin.loaderColorHex));
        addForegroundViewContainer(pwaLoaderViewView);
    }

    public void showEphemeralSnapshot(final View snapshot) {
        addForegroundViewContainer(snapshot);
    }

    private void addForegroundViewContainer(final View view) {
        if (view.getId() == View.NO_ID) {
            view.setId(View.generateViewId());
        }
        foregroundViewId = view.getId();
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                containerView.addView(view, -1, new ViewGroup.LayoutParams(
                        ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT
                ));
            }
        });

        // Backup removal after one second in case pwaRendered is not called
        TimerTask timeTask = new TimerTask() {
            @Override
            public void run() {
                pwaRendered();
            }
        };

        foregroundViewBackupRemoval = new Timer();
        foregroundViewBackupRemoval.schedule(timeTask, foregroundViewBackupRemovalDelay);
    }

    /**
     * Takes a snapshot of this webview and returns it as an ImageView
     * @return an imageview corresponding to the webview contents at the time the snapshot was taken
     */
    public void snapshotWebview() {
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = Bitmap.createBitmap(containerView.getWidth(), containerView.getHeight(), Bitmap.Config.RGB_565);
                Canvas canvas = new Canvas(bitmap);
                containerView.layout(0, 0, containerView.getLayoutParams().width, containerView.getLayoutParams().height);
                containerView.draw(canvas);
                ImageView imageView = new ImageView(activity.getApplicationContext());
                imageView.setImageBitmap(bitmap);
                snapview = imageView;
            }
        });
    }
}