package com.apkfuns.jsbridge;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.webkit.JsPromptResult;
import android.webkit.WebView;

import com.apkfuns.jsbridge.common.IPromptResult;
import com.apkfuns.jsbridge.common.IWebView;
import com.apkfuns.jsbridge.common.JBArgumentErrorException;
import com.apkfuns.jsbridge.module.JSArgumentType;
import com.apkfuns.jsbridge.module.JsModule;
import com.apkfuns.jsbridge.module.JsStaticModule;

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

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Created by pengwei on 2017/6/9.
 */

class JsBridgeImpl extends JsBridge {

    private Object mWebView;
    private final JsBridgeConfigImpl config;
    private final List<JsModule> loadModule;
    private final Map<JsModule, HashMap<String, JsMethod>> exposedMethods;
    private final String className;
    private String preLoad;
    private final Handler handler;
    private final Set<String> moduleLayers;
    private String newProtocol;
    private String newLoadReadyMethod;

    JsBridgeImpl(JsModule... modules) {
        this(null, null, modules);
    }

    JsBridgeImpl(String protocol, String readyMethod, JsModule... modules) {
        config = JsBridgeConfigImpl.getInstance();
        className = "JB_" + Integer.toHexString(hashCode());
        loadModule = new ArrayList<>();
        exposedMethods = new HashMap<>();
        handler = new Handler(Looper.getMainLooper());
        moduleLayers = new HashSet<>();
        this.newProtocol = protocol;
        this.newLoadReadyMethod = readyMethod;
        if (TextUtils.isEmpty(newProtocol)) {
            newProtocol = config.getProtocol();
        }
        if (TextUtils.isEmpty(newLoadReadyMethod)) {
            newLoadReadyMethod = config.getReadyFuncName();
        }
        loadingModule(modules);
        JBLog.d(String.format("Protocol:%s, LoadReadyMethod:%s, moduleSize:%s",
                newProtocol, newLoadReadyMethod, loadModule.size()));
    }

    /**
     * load module
     */
    private void loadingModule(JsModule... modules) {
        try {
            for (Class<? extends JsModule> moduleCls : config.getDefaultModule()) {
                JsModule module = moduleCls.newInstance();
                if (module != null && !TextUtils.isEmpty(module.getModuleName())) {
                    loadModule.add(module);
                }
            }
            if (modules != null) {
                for (JsModule module : modules) {
                    if (module != null && !TextUtils.isEmpty(module.getModuleName())) {
                        loadModule.add(module);
                    }
                }
            }
            if (!loadModule.isEmpty()) {
                Collections.sort(loadModule, new ModuleComparator());
                for (JsModule module : loadModule) {
                    HashMap<String, JsMethod> methodsMap = JBUtils.getAllMethod(
                            module, module.getClass(), newProtocol);
                    exposedMethods.put(module, methodsMap);
                }
            }
        } catch (Exception e) {
            JBLog.e("loadingModule error", e);
        }
    }

    @Override
    public final void injectJs(@NonNull WebView webView) {
        onInjectJs(webView.getContext(), webView);
    }

    @Override
    public final void injectJs(@NonNull IWebView webView) {
        onInjectJs(webView.getContext(), webView);
    }

    private void onInjectJs(final Context context, final Object webView) {
        this.mWebView = webView;
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (preLoad == null) {
                    preLoad = getInjectJsString();
                }
                for (JsModule module : loadModule) {
                    // 监听方法不需要注入 Context
                    if (exposedMethods.get(module) == null || exposedMethods.get(module).isEmpty()) {
                        continue;
                    }
                    // 为JsModule设置context 和 WebView
                    if (module.mContext != null && module.mContext.getClass().equals(context.getClass())) {
                        break;
                    }
                    try {
                        Field contextField = module.getClass().getField("mContext");
                        if (contextField != null) {
                            contextField.set(module, context);
                        }
                        Field webViewField = module.getClass().getField("mWebView");
                        if (webViewField != null) {
                            webViewField.set(module, webView);
                        }
                    } catch (Exception e) {
                        JBLog.e("JsModule set Context Error", e);
                    }
                }
                evaluateJavascript(preLoad);
                JBLog.d("onInjectJs finish");
            }
        }, "JsBridgeThread").start();
    }

    @Override
    public final boolean callJsPrompt(@NonNull String methodArgs, @NonNull JsPromptResult result) {
        return onCallJsPrompt(methodArgs, result);
    }

    @Override
    public final boolean callJsPrompt(@NonNull String methodArgs, @NonNull IPromptResult result) {
        return onCallJsPrompt(methodArgs, result);
    }

    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        return onCallJsPrompt(message, result);
    }

    @Override
    public final void clean() {
        evaluateJavascript(newProtocol + "=undefined;");
    }

    @Override
    public final void release() {
        for (JsModule module : exposedMethods.keySet()) {
            module.mWebView = null;
            module.mContext = null;
        }
        exposedMethods.clear();
        JBLog.d("JsBridge destroy");
    }

    @Override
    public void evaluateJavascript(@NonNull final String jsCode) {
        if (mWebView == null) {
            JBLog.d("Please call injectJs first");
            return;
        }
        handler.post(new Runnable() {
            @Override
            public void run() {
                if (mWebView instanceof WebView) {
                    ((WebView) mWebView).loadUrl("javascript:" + jsCode);
                } else if (mWebView instanceof IWebView) {
                    ((IWebView) mWebView).loadUrl("javascript:" + jsCode);
                } else {
                    throw new JBArgumentErrorException("Can not cast " + mWebView.getClass().getSimpleName()
                            + " to WebView");
                }
            }
        });
    }

    @Nullable
    private JsModule getModule(String moduleName) {
        if (TextUtils.isEmpty(moduleName)) {
            return null;
        }
        for (JsModule module : exposedMethods.keySet()) {
            if (moduleName.equals(module.getModuleName())) {
                return module;
            }
        }
        return null;
    }

    private String getInjectJsString() {
        StringBuilder builder = new StringBuilder();
        builder.append("var ").append(className).append("=function(){");
        // 注入通用方法
        builder.append(JBUtilMethodFactory.getUtilMethods(newLoadReadyMethod));
        // 注入默认方法
        for (JsModule module : loadModule) {
            HashMap<String, JsMethod> methods = exposedMethods.get(module);
            if (methods == null) {
                continue;
            }
            if (module instanceof JsStaticModule) {
                for (String method : methods.keySet()) {
                    JsMethod jsMethod = methods.get(method);
                    builder.append(jsMethod.getInjectJs());
                }
            } else {
                List<String> moduleGroup = JBUtils.moduleSplit(module.getModuleName());
                if (moduleGroup.isEmpty()) {
                    continue;
                } else {
                    for (int i = 0; i < moduleGroup.size() - 1; ++i) {
                        if (!moduleLayers.contains(moduleGroup.get(i))) {
                            for (int k = i; k < moduleGroup.size() - 1; ++k) {
                                builder.append(className + ".prototype." + moduleGroup.get(k) + " = {};");
                                moduleLayers.add(moduleGroup.get(k));
                            }
                            break;
                        }
                    }
                    builder.append(className + ".prototype." + module.getModuleName() + " = {");
                    moduleLayers.add(module.getModuleName());
                }
                for (String method : methods.keySet()) {
                    JsMethod jsMethod = methods.get(method);
                    builder.append(jsMethod.getInjectJs());
                }
                builder.append("};");
            }
        }
        builder.append("};");
        builder.append("window." + newProtocol + "=new " + className + "();");
        builder.append(newProtocol + ".OnJsBridgeReady();");
        return builder.toString();
    }

    /**
     * onJsPrompt 处理
     *
     * @param methodArgs
     * @param result
     */
    private boolean onCallJsPrompt(String methodArgs, Object result) {
        JBLog.d("callJsPrompt: " + methodArgs);
        if (TextUtils.isEmpty(methodArgs) || result == null) {
            return false;
        }
        JBArgumentParser argumentParser = JBArgumentParser.parse(methodArgs);
        if (argumentParser.isSuccess() && !TextUtils.isEmpty(argumentParser.getModule())
                && !TextUtils.isEmpty(argumentParser.getMethod())) {
            JsModule findModule = getModule(argumentParser.getModule());
            if (findModule != null) {
                HashMap<String, JsMethod> methodHashMap = exposedMethods.get(findModule);
                if (methodHashMap != null && !methodHashMap.isEmpty() && methodHashMap.containsKey(
                        argumentParser.getMethod())) {
                    JsMethod method = methodHashMap.get(argumentParser.getMethod());
                    List<JBArgumentParser.Parameter> parameters = argumentParser.getParameters();
                    int length = method.getParameterType().size();
                    Object[] invokeArgs = new Object[length];
                    for (int i = 0; i < length; ++i) {
                        @JSArgumentType.Type int type = method.getParameterType().get(i);
                        if (parameters != null && parameters.size() >= i + 1) {
                            JBArgumentParser.Parameter param = parameters.get(i);
                            Object parseObject = JBUtils.parseToObject(type, param, method);
                            if (parseObject instanceof JBArgumentErrorException) {
                                setJsPromptResult(result, false, parseObject.toString());
                                return true;
                            }
                            invokeArgs[i] = parseObject;
                        }
                        if (invokeArgs[i] == null) {
                            switch (type) {
                                case JSArgumentType.TYPE_NUMBER:
                                    invokeArgs[i] = 0;
                                    break;
                                case JSArgumentType.TYPE_BOOL:
                                    invokeArgs[i] = false;
                                    break;
                                case JSArgumentType.TYPE_ARRAY:
                                case JSArgumentType.TYPE_DOUBLE:
                                case JSArgumentType.TYPE_FLOAT:
                                case JSArgumentType.TYPE_FUNCTION:
                                case JSArgumentType.TYPE_INT:
                                case JSArgumentType.TYPE_LONG:
                                case JSArgumentType.TYPE_OBJECT:
                                case JSArgumentType.TYPE_STRING:
                                case JSArgumentType.TYPE_UNDEFINE:
                                default:
                                    break;
                            }
                        }
                    }
                    try {
                        Object ret = method.invoke(invokeArgs);
                        setJsPromptResult(result, true, ret == null ? "" : ret);
                    } catch (Exception e) {
                        Throwable throwable = e;
                        if (e instanceof InvocationTargetException) {
                            throwable = ((InvocationTargetException) e).getTargetException();
                        }
                        setJsPromptResult(result, false, "Error: " + throwable.toString());
                        JBLog.e("Call JsMethod <" + method.getMethodName() + "> Error", e);
                    }
                    return true;
                }
            }
            setJsPromptResult(result, false, "JBArgument Parse error");
            return true;
        }
        JBLog.e("JBArgument error", argumentParser.getThrowable());
        setJsPromptResult(result, false, argumentParser.getErrorMsg());
        return false;
    }

    /**
     * 设置 prompt 回调结果
     *
     * @param promptResult JsPromptResult
     * @param success      bool 是否执行成功
     * @param msg          返回结果
     */
    private void setJsPromptResult(Object promptResult, boolean success, Object msg) {
        JSONObject ret = new JSONObject();
        try {
            ret.put("success", success);
            ret.put("msg", msg);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        if (promptResult instanceof JsPromptResult) {
            ((JsPromptResult) promptResult).confirm(ret.toString());
        } else if (promptResult instanceof IPromptResult) {
            ((IPromptResult) promptResult).confirm(ret.toString());
        } else {
            throw new IllegalArgumentException("JsPromptResult Type Error: " + msg);
        }
    }

    /**
     * sort by module
     */
    private static class ModuleComparator implements Comparator<JsModule> {

        @Override
        public int compare(JsModule lhs, JsModule rhs) {
            return lhs.getModuleName().split("\\.").length - rhs.getModuleName().split("\\.").length;
        }
    }
}