package com.mobify.astro.plugins.webviewplugin;

import android.support.annotation.NonNull;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;

import com.mobify.astro.AstroActivity;
import com.mobify.astro.AstroPlugin;
import com.mobify.astro.BuildConfig;
import com.mobify.astro.PluginResolver;
import com.mobify.astro.messaging.EventMessage;
import com.mobify.astro.messaging.EventRegistrar;
import com.mobify.astro.messaging.Exceptions;
import com.mobify.astro.messaging.JsonMessageFactory;
import com.mobify.astro.messaging.Message;
import com.mobify.astro.messaging.MessageSender;
import com.mobify.astro.messaging.RpcMessage;
import com.mobify.astro.messaging.RpcResponse;
import com.mobify.astro.messaging.SenderMessage;
import com.mobify.astro.utilities.AstroWebUtilities;

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

import java.util.LinkedList;
import java.util.Queue;

public abstract class AstroWebViewPlugin extends AstroPlugin {
    private static final String TAG = AstroWebViewPlugin.class.getName();
    public static final String RECEIVE_MESSAGE_FORMAT_STRING = "Astro.receiveMessage('%s', %s);";
    protected static final String SELF_KEYWORD = "self";

    public abstract WebView getWebView();

    // A FIFO list of JavaScript statements to invoke once the web view becomes ready.
    protected Queue<String> jsQueue = new LinkedList<String>();
    protected boolean isJsLoaded = false;

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

    @Override
    public void onPause() {
        triggerEvent("pause", new JSONObject());
        super.onPause();
    }

    protected void setJsLoaded(boolean isLoaded) {
        isJsLoaded = isLoaded;

        if (isJsLoaded) {
            flushJsQueue();
        }
    }

    /**
     * Flushes queued JavaScript statements to the web view.
     */
    protected void flushJsQueue() {
        while (!jsQueue.isEmpty()) {
            String javaScript = jsQueue.remove();

            if (!invokeJavaScript(javaScript)) {
                // Invocation failed, the web view is likely still not ready.
                return;
            }
        }
    }

    /**
     * Invokes JavaScript.
     */
    public boolean invokeJavaScript(String javaScript) {
        WebView invocationTargetWebView = getWebView();

        if (invocationTargetWebView == null || !isJsLoaded) {
            // Web view is not ready, queue invocation.
            jsQueue.add(javaScript);
            return false;
        }

        if (BuildConfig.DEBUG) {
            Log.d(TAG, "Invoking: " + javaScript);
        }

        // Use this method to evaluate JS since encoded urls are undesirably
        // decoded using the old method for invoking JS: loadUrl("javascript: ...")
        invocationTargetWebView.evaluateJavascript(javaScript, null);
        return true;
    }

    /**
     * Invokes `Astro.receiveMessage(address, message)` in the JavaScript.
     */
    protected void invokeReceiveMessageJs(String address, Message message) {
        address = transformNativeAddressToJs(address);

        String javaScript = String.format(RECEIVE_MESSAGE_FORMAT_STRING, address, message.toString());
        invokeJavaScript(javaScript);
    }

    /**
     * Transforms addresses going to JavaScript by replacing this plugin's address with "self".
     */
    protected String transformNativeAddressToJs(String address) {
        if (address.equals(getInstanceAddress()) || address.startsWith(getInstanceAddress() + ":")) {
            address = address.replaceFirst(getInstanceAddress(), SELF_KEYWORD);
        }
        return address;
    }

    /**
     * Transforms addresses coming from JavaScript by replacing "self" to this plugin's address.
     */
    protected String transformJsAddressToNative(String address) {
        if (address.equals(SELF_KEYWORD) || address.startsWith(SELF_KEYWORD + ":")) {
            address = address.replaceFirst(SELF_KEYWORD, this.getInstanceAddress());
        }
        return address;
    }

    /**
     * Relays messages from native to JavaScript.
     */
    @Override
    protected void onMessage(String address, SenderMessage message) {
        if (messageIsForJs(message)) {
            invokeReceiveMessageJs(address, message);
            return;
        }
        super.onMessage(address, message);
    }

    /**
     * Determines if a message should be routed to the web view.
     * @return true if the message is a JsRpcMessage or an Event
     */
    boolean messageIsForJs(Message message) {
        boolean isJsRpc = (message instanceof RpcMessage) && message.hasHeader(RpcMessage.KeyNames.JS_RPC_HEADER);
        boolean isRpcResponse = message instanceof RpcResponse;
        boolean isEvent = message instanceof EventMessage;
        return isJsRpc || isRpcResponse || isEvent;
    }

    private void returnRpcReponse(SenderMessage message) throws JSONException {
        RpcResponse response = new RpcResponse(getInstanceAddress(), this, message.getHeader("id"));
        response.setResult(null);
        messageSender.sendRpcResponse(response);
    }
    private static boolean isMessageMethodType(SenderMessage message, String methodType) throws
            JSONException {
        return message.getPayload().get("method").toString().equals(methodType);
    }

    /**
     * Relays messages from JavaScript to native.
     */
    @JavascriptInterface
    public void exec(String address, String jsonData) throws
            JSONException,
            Exceptions.PayloadMissingException,
            Exceptions.MalformedRpcMessageException {

        address = transformJsAddressToNative(address);

        SenderMessage message = JsonMessageFactory.senderMessageFromJSONString(jsonData, this);
        JSONObject messagePayload = message.getPayload();

        //FIXME: find a more sustainable way of triggering PWA-navigations
        if (messagePayload.has("method")) {
            if (isMessageMethodType(message,"pwa-navigate")) {
                // Get params
                JSONObject params = null;
                if (messagePayload.has("params")) {
                    params = messagePayload.getJSONObject("params");
                }
                // Swallow the message if we're doing a PWA navigation.
                // Webview will just navigate and we don't need to stack.
                pwaNavigate(params);
                returnRpcReponse(message);
                return;
            }
            if (isMessageMethodType(message,"pwa-rendered")) {
                pwaRendered();
                returnRpcReponse(message);
                return;
            }
        }

        messageSender.sendMessageToAddress(message, address);
    }

    protected void pwaNavigate(JSONObject params) { /* Empty */ }
    protected void pwaRendered() { /* Empty */ }
}
