package com.mobify.astro.plugins;

import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.View;
import android.webkit.WebView;

import com.mobify.astro.AstroActivity;
import com.mobify.astro.AstroPlugin;
import com.mobify.astro.PluginResolver;
import com.mobify.astro.messaging.EventRegistrar;
import com.mobify.astro.messaging.MessageSender;
import com.mobify.astro.messaging.annotations.RpcMethod;
import com.mobify.astro.plugins.headerbarplugin.HeaderContent;
import com.mobify.astro.plugins.headerbarplugin.HeaderContentCoordinator;
import com.mobify.astro.plugins.webviewplugin.LoadingContainerView;
import com.mobify.astro.plugins.webviewplugin.WebViewPlugin;

import org.apache.cordova.LOG;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;

public class NavigationPlugin extends AstroPlugin implements NavigationHost {
    private static final String TAG = NavigationPlugin.class.getName();
    protected static final int MAX_VIEWS = 5; // maximum number of views to keep active

    public static String loaderColorHex = "#FFFFFF";

    static final class EventNames {
        static final String POP_TO_ROOT_EVENT = "popToRoot";
    }

    protected Bundle lastWebViewState = new Bundle();
    protected LoadingContainerView containerView;
    protected HeaderContentCoordinator headerContentCoordinator;

    protected Deque<String> addressStack = new ArrayDeque<>();

    /**
     * The NavigationPlugin uses NavigationStackFrames to navigate to
     * webviews or native views.
     */
    static class DefaultNavigationStackFrame implements NavigationStackFrame {
        View view;
        boolean isActive = true;
        private NavigationHost navigationHost;

        public DefaultNavigationStackFrame(View view) {
            this.view = view;
        }

        public View getView() {
            return view;
        }

        public void saveAndDestroyView() {
            // mostly a no-op for now
            isActive = false;
        }
        public void restoreInactiveViewFromSave() {
            // mostly a no-op for now
            isActive = true;
        }
        public void saveStateToBundle(Bundle bundle) {
            // no-op
        }
        public void restoreStateFromBundle(Bundle bundle) {
            // no-op
        }

        public boolean hasActiveView() {
            return isActive;
        }

        public boolean hasWebView() {
            return false;
        }

        public void reload() {
            // no-op
        }

        public String getUrl() {
            // Provide _some_ sort of url for back navigation to native plugins.
            // Native plugins could potentially implement `description` to provide
            // a custom URL that is usable in app.js in the future.
            return "native-plugin://" + view.getId();
        }

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

        public void destroyFrame() {
            // no-op
        }
    }

    public static class PlaceholderNavigationStackFrame extends DefaultNavigationStackFrame {

        public PlaceholderNavigationStackFrame(Context context, View snapshot) {
            super(new View(context));
            this.view = snapshot;
        }
    }

    protected Deque<NavigationStackFrame> navigationStack = new ArrayDeque<>();

    public NavigationPlugin(@NonNull AstroActivity activity, @NonNull PluginResolver pluginResolver,
                            @NonNull EventRegistrar eventRegistrar, @NonNull MessageSender messageSender) {
        super(activity, pluginResolver, eventRegistrar, messageSender);

        containerView = new LoadingContainerView(activity);
    }


    protected String currentUrl() {
        NavigationStackFrame currentFrame = navigationStack.getLast();
        return currentFrame.getUrl();
    }

    //region Overrides

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

    @Override
    public void onPause() {
        if (navigationStack.size() < 1) {
            return;
        }
        NavigationStackFrame lastFrame = navigationStack.peekLast();
        lastFrame.saveStateToBundle(savedPluginState);
    }

    @Override
    public void destroy() {
        containerView.clearChildView();
        for (NavigationStackFrame frame: navigationStack) {
            frame.destroyFrame();
        }
        navigationStack.clear();
    }

    // endregion

    //region Navigation
    /**
     * De-activates and saves views' states until we have room on the
     * navigationStack for a new view.
     *
     * @see NavigationPlugin#restoreViews()
     */
    protected void makeRoomForView() {
        while (getActiveViewCount() >= MAX_VIEWS) {
            // Save the oldest (non-root) view
            for(NavigationStackFrame frame : navigationStack) {
                // don't kill the root (in order for popToRoot to work)
                if (frame.equals(navigationStack.getFirst())) {
                    continue;
                }
                if (frame.hasActiveView()) {
                    frame.saveAndDestroyView();
                    break;
                }
            }
        }
    }

    /**
     * Restore the newest frame with a web view so we can keep our fast back
     *
     * @see NavigationPlugin#makeRoomForView()
     */
    protected void restoreViews() {
        int activeViewOnStack = 0;

        // Always restoreInactiveViewFromSave the first view to support popToRoot
        NavigationStackFrame firstFrame = navigationStack.getFirst();
        firstFrame.restoreInactiveViewFromSave();
        activeViewOnStack++;

        Iterator<NavigationStackFrame> iterator = navigationStack.descendingIterator();
        while(iterator.hasNext() && activeViewOnStack < MAX_VIEWS) {
            iterator.next().restoreInactiveViewFromSave();
            activeViewOnStack++;
        }
    }

    protected int getActiveViewCount() {
        int count = 0;

        for (NavigationStackFrame frame: navigationStack) {
            if (frame.hasActiveView()) {
                count++;
            }
        }
        return count;
    }

    @RpcMethod(methodName="canGoBack")
    public boolean canGoBack() {
        if (navigationStack.size() < 2) {
            return false;
        }
        return true;
    }

    //endregion

    protected void setContainerView(LoadingContainerView view) {
        containerView = view;
    }

    @RpcMethod(methodName="navigateToPlugin", parameterNames = {"address", "options"})
    public void navigateToPlugin(String address, JSONObject options) throws JSONException {
        AstroPlugin plugin = pluginResolver.instanceForAddress(address);
        NavigationStackFrame nextFrame;

        if (navigationStack.size() > 0) {
            NavigationStackFrame currentFrame = navigationStack.getLast();
            if (currentFrame.hasWebView()) {
                currentFrame.saveStateToBundle(lastWebViewState);
            }
        }

        if (plugin instanceof NavigationStackFrame && ((NavigationStackFrame) plugin).hasWebView()) {
            nextFrame = (NavigationStackFrame) plugin;

            if (!lastWebViewState.isEmpty()) {
                // Share history, cookies, and local storage by serializing the web view's state to
                // the newly created web view. Session storage does not seem to get brought over.
                nextFrame.restoreStateFromBundle(lastWebViewState);
            }
        } else {
            View view = plugin.getView();
            nextFrame = new DefaultNavigationStackFrame(view);
        }
        this.addressStack.addLast(address);

        pushStackFrame(nextFrame, options);
    }

    private void pushStackFrame(NavigationStackFrame stackFrame, JSONObject options) throws JSONException {
        activity.hideKeyboard();
        makeRoomForView();

        pushHeaderContent(options);
        navigationStack.addLast(stackFrame);
        stackFrame.setNavigationHost(this);
        containerView.setChildView(stackFrame.getView());
    }

    /**
     * Reloads the top web view.
     */
    @RpcMethod(methodName = "reload")
    public void reload() {
        NavigationStackFrame lastFrame = navigationStack.getLast();
        if (lastFrame != null) {
            lastFrame.reload();
        }
    }

    /**
     * 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;
        }

        if (headerContentCoordinator != null) {
            headerContentCoordinator.popHeaderContent();
        }

        NavigationStackFrame currentFrame = navigationStack.removeLast();
        NavigationStackFrame secondLastFrame = navigationStack.peekLast();

        if (secondLastFrame instanceof PlaceholderNavigationStackFrame) {
            navigationStack.removeLast();
            assert(currentFrame instanceof WebViewPlugin);

            // Get image from placeholder
            View snapshot = secondLastFrame.getView();
            // Add to webview
            ((WebViewPlugin) currentFrame).showEphemeralSnapshot(snapshot);

            ((WebViewPlugin)currentFrame).getWebView().evaluateJavascript("if (window.Progressive && window.Progressive.api && window.Progressive.api.navigateBack) {window.Progressive.api.navigateBack();} else {window.history.back();}", null);
            navigationStack.addLast(currentFrame);
            popBackToFrame(currentFrame);
            secondLastFrame.destroyFrame();
        } else {
            popBackToFrame(secondLastFrame);
            this.addressStack.pollLast();
            currentFrame.destroyFrame();
        }

        restoreViews();
    }

    @RpcMethod(methodName="setHeaderBar", parameterNames= {"address"})
    public void setHeaderBar(String address) throws Exception {
        AstroPlugin plugin = pluginResolver.instanceForAddress(address);

        if (!(plugin instanceof HeaderContentCoordinator)) {
            throw new Exception("Plugin passed to `setHeaderBar` must be a header bar!");
        }

        headerContentCoordinator = (HeaderContentCoordinator)plugin;
    }

    @RpcMethod(methodName="popToRoot", parameterNames= {"options"})
    public void popToRoot(JSONObject options) {
        if (!canGoBack()) {
            return;
        }

        NavigationStackFrame firstFrame = navigationStack.peek();
        // If the first VC is a placeholder, grab its webview and then navigate back
        // the number of placeholders that it was nested in
        if (firstFrame instanceof PlaceholderNavigationStackFrame) {
            // Start at 1 because the while loop always does 1 extra loop (the last loop is the one
            // that pops the webview from the nav stack)
            int numberOfBackTransitions = 1;
            while (firstFrame instanceof PlaceholderNavigationStackFrame) {
                // We subtract from the number of transitions because the `go()` JS API takes a
                // negative number for back navigation
                numberOfBackTransitions -= 1;
                firstFrame = navigationStack.pop();
            }
            assert (firstFrame instanceof WebViewPlugin);
            WebView rootWebView = ((WebViewPlugin) firstFrame).getWebView();
            rootWebView.evaluateJavascript("window.history.go("+ numberOfBackTransitions +");", null);
            //Re-add the last "firstFrame" as it's now the root frame
            navigationStack.push(firstFrame);
        }
        popBackToFrame(firstFrame);
        popNavigationStackToRoot();

        String firstElement = this.addressStack.removeFirst();
        this.addressStack.clear();
        this.addressStack.push(firstElement);

        popHeaderToRoot();

        triggerPopToRoot();
    }

    private void popNavigationStackToRoot() {
        while (navigationStack.size() > 1) {
            NavigationStackFrame currentFrame = navigationStack.removeLast();
            currentFrame.destroyFrame();
        }
    }

    private void popHeaderToRoot() {
        if (headerContentCoordinator != null) {
            headerContentCoordinator.popToRootHeaderContent();
        }
    }

    protected void popBackToFrame(NavigationStackFrame frame) {
        activity.hideKeyboard();
        containerView.transitionToShowingWithView(frame.getView());
        triggerBackEvent(frame.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(WebViewPlugin.EventNames.BACK, params);
    }

    protected void triggerPopToRoot() {
        JSONObject params = new JSONObject();
        try {
            params.put("url", currentUrl());
        } catch (Exception e) {
            Log.d(TAG, "Error adding params to popToRoot json", e);
        }

        triggerEvent(EventNames.POP_TO_ROOT_EVENT, params);
    }

    @RpcMethod(methodName="enableBackGesture")
    public void enableBackGesture() {
        // No-op -- this is not standard on Android nor recommended in design guidelines
        Log.d(TAG, "enableBackGesture is a no-op by design on Android");
    }

    @RpcMethod(methodName="disableBackGesture")
    public void disableBackGesture() {
        // No-op -- this is not standard on Android nor recommended in design guidelines
        Log.d(TAG, "disableBackGesture is a no-op by design on Android");
    }

    private void pushHeaderContent(JSONObject options) throws JSONException {
        if (headerContentCoordinator != null) {
            HeaderContent headerContent;

            if (options != null && options.has("header")) {
                headerContent = HeaderContent.fromJson(pluginResolver, activity, options.getJSONObject("header"));
            } else {
                headerContent = headerContentCoordinator.getLatestHeaderContent() != null ? headerContentCoordinator.getLatestHeaderContent() : new HeaderContent();
            }

            headerContentCoordinator.pushHeaderContent(headerContent);
        }
    }

    @Override
    public void pwaNavigating(final WebViewPlugin webViewPlugin, final JSONObject headerParam) {
        this.activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                PlaceholderNavigationStackFrame stackFrame = new PlaceholderNavigationStackFrame(activity, webViewPlugin.snapview);

                NavigationStackFrame topStackFrame = navigationStack.removeLast();
                assert(topStackFrame == webViewPlugin);

                navigationStack.addLast(stackFrame);

                try {
                    pushStackFrame(webViewPlugin, headerParam);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    @RpcMethod(methodName="getTopPluginAddress")
    public String getTopPluginAddress() {
        return this.addressStack.peekLast();
    }

    @RpcMethod(methodName="setLoaderViewBackgroundColor", parameterNames= {"options"})
    public void setLoaderViewBackgroundColor(JSONObject options) {
        if (options != null && options.has("color")) {
            try {
                NavigationPlugin.loaderColorHex = options.getString("color");
            } catch (JSONException e) {
                LOG.e(TAG, "Called setLoaderViewBackgroundColor but no color in options JSONObject");
            }
        }
    }
}
