package makulib;

import android.content.Context;
import android.content.res.AssetManager;
import android.util.Log;
import android.widget.Toast;

import org.json.JSONException;
import org.json.JSONObject;
import org.keplerproject.luajava.JavaFunction;
import org.keplerproject.luajava.LuaException;
import org.keplerproject.luajava.LuaObject;
import org.keplerproject.luajava.LuaState;
import org.keplerproject.luajava.LuaStateFactory;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Created by nmoya on 8/3/15.
 */
public class Maku implements LuaCallbacks {

    public LuaState L;
    final StringBuilder output = new StringBuilder();
    public static Context makuContext;
    public static String module;
    public static Network net;


    /* module is the starting point of the lua */
    public Maku(final Context context, final String module) {
        this.makuContext = context;
        this.module = module;
        this.net = new Network();
        File startingPoint = new File(module, "main.lua");
        String startingPointPath = startingPoint.getPath();
        Log.i("Path:", startingPointPath);

        L = LuaStateFactory.newLuaState();
        L.openLibs();

        try {
          JavaFunction httpGet = new JavaFunction(L) {
                @Override
                public int execute() throws LuaException {
                    // Get the parameters:
                    String url = null;
                    Object obj = L.toJavaObject(2);
                    url = obj.toString();

                    LuaObject luaObj = L.getLuaObject(3);

                    net.addListener(Maku.this, luaObj);
                    net.get(url);

                    return 0;
                }
            };
            httpGet.register("httpGet");

            JavaFunction httpPost = new JavaFunction(L) {
                @Override
                public int execute() throws LuaException {
                    // Get the parameters:
                    String url = L.toString(2);
                    LuaObject table = L.getLuaObject(3);
                    LuaObject luaCallback = L.getLuaObject(4);

                    HashMap<String, Object> javaTable = parseLuaTable(table);

                    net.addListener(Maku.this, luaCallback);
                    net.post(url, javaTable);

                    return 0;
                }
            };
            httpPost.register("httpPost");

            JavaFunction print = new JavaFunction(L) {
                @Override
                public int execute() throws LuaException {
                    String message = null;
                    Object obj = L.toJavaObject(2);
                    message = obj.toString();
                    Log.i("LUA PRINT", message);
                    return 0;
                }
            };
            print.register("print");

            JavaFunction assetLoader = new JavaFunction(L) {
                @Override
                public int execute() throws LuaException {
                    String name = L.toString(-1);
                    name = name.replace(".", "/");
                    File f = new File (module, name + ".lua");
                    name = f.getPath();

                    Log.i("LOADING", name);
                    AssetManager am = makuContext.getAssets();
                    try {
                        InputStream is = am.open(name);
                        byte[] bytes = IOUtils.readAll(is);
                        L.LloadBuffer(bytes, name);
                        return 1;
                    } catch (Exception e) {
                        ByteArrayOutputStream os = new ByteArrayOutputStream();
                        e.printStackTrace(new PrintStream(os));
                        L.pushString("Cannot load module " + name + ":\n" + os.toString());
                        return 1;
                    }
                }
            };

            // This create a namespace net.http and sets two functions httpPost and httpGet
            L.createTable(0, 0);
            L.pushString("http");
            L.createTable(0, 0);
            L.pushString("get");
            L.pushJavaFunction(httpGet);
            L.pushString("post");
            L.pushJavaFunction(httpPost);
            L.setTable(-5);
            L.setTable(-3);
            L.setTable(-3);
            L.setGlobal("net");

            L.getGlobal("package");            // package
            L.getField(-1, "loaders");         // package loaders
            int nLoaders = L.objLen(-1);       // package loaders

            L.pushJavaFunction(assetLoader);   // package loaders loader
            L.rawSetI(-2, nLoaders + 1);       // package loaders
            L.pop(1);                          // package

            L.getField(-1, "path");            // package path
//            String customPath = makuContext.getFilesDir() + "/?.lua";
            String customPath = "./src/main/assets/?.lua";
//            Log.i("customPath", customPath);
            L.pushString(";" + customPath);    // package path custom
            L.concat(2);                       // package pathCustom
            L.setField(-2, "path");            // package
            L.pop(1);
        } catch(LuaException le){
            le.printStackTrace();
        }
        String result1 = safeEvalLua("maku = require 'maku'");
        String result2 = safeEvalLua(IOUtils.readFile(this.makuContext, startingPointPath));
    }

    public void dispatch(String actionName, HashMap<String, Object> params)
    {
        dispatch(actionName, HashMapToLuaTable(params));
    }
    public void dispatch(String actionName, LuaObject parametersTable)
    {
        L.getGlobal("dispatch");
        LuaObject luaDispatcher = L.getLuaObject(-1);
        Object[] o = {actionName, parametersTable};
        try {
            luaDispatcher.call(o);
        } catch (LuaException le) {
            le.printStackTrace();
        }
    }
    public void dispatch(String actionName)
    {
        L.getGlobal("dispatch");
        LuaObject luaDispatcher = L.getLuaObject(-1);
        Object[] o = {actionName};
        try {
            luaDispatcher.call(o);
        } catch (LuaException le) {
            Toast toast = Toast.makeText(this.makuContext, "An error occurred", Toast.LENGTH_LONG);
            toast.show();
            le.printStackTrace();
        }
    }
    public void connect(String luaState, JavaFunction javaFnc)
    {
        L.getGlobal("connect");
        LuaObject luaConnect = L.getLuaObject(-1);
        Object[] o = {luaState, javaFnc};
        try {
            luaConnect.call(o);
        } catch (LuaException le) {
            Toast toast = Toast.makeText(this.makuContext, "An error occurred", Toast.LENGTH_LONG);
            toast.show();
            le.printStackTrace();
        }
    }
    public void connect(String luaState, final Object obj, String methodName)
    {
        Method[] LM = obj.getClass().getMethods();
        Method javaFnc = null;
        for(Method m : LM)
        {
            if (methodName.equals(m.getName()))
            {
                javaFnc = m;
                break;
            }
        }
        final Method finalJavaFnc = javaFnc;
        JavaFunction handler = new JavaFunction(L) {
            @Override
            public int execute() throws LuaException {
                int argIndex = 2;
                int type = L.type(argIndex);
                String stype = L.typeName(type);
                try {
                    if (stype.equals("number")) {
                        finalJavaFnc.invoke(obj, L.toNumber(argIndex));
                    } else if (stype.equals("string")) {
                        finalJavaFnc.invoke(obj, L.toString(argIndex));
                    } else if (stype.equals("boolean")) {
                        finalJavaFnc.invoke(obj, L.toBoolean(argIndex));
                    } else if (stype.equals("table")) {
                        LuaObject table = L.getLuaObject(argIndex);
                        HashMap<String, Object> javaTable = parseLuaTable(table);
                        finalJavaFnc.invoke(obj, javaTable);
                    } else if (stype.equals("function")) {
                        finalJavaFnc.invoke(obj, L.getLuaObject(argIndex));
                    } else {
                        finalJavaFnc.invoke(obj, L.toJavaObject(argIndex));
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return 1;
            }
        };
        this.connect(luaState, handler);
    }

    String safeEvalLua(String src) {
        String res = null;
        try {
            res = evalLua(src);
        } catch(LuaException e) {
            res = e.getMessage()+"\n";
        }
        return res;
    }
    String evalLua(String src) throws LuaException {
        L.setTop(0);
        int ok = L.LloadString(src);
        if (ok == 0) {
            L.getGlobal("debug");
            L.getField(-1, "traceback");
            L.remove(-2);
            L.insert(-2);
            ok = L.pcall(0, 0, -2);
            if (ok == 0) {
                String res = output.toString();
                output.setLength(0);
                return res;
            }
        }
        throw new LuaException(errorReason(ok) + ": " + L.toString(-1));
    }
    private String errorReason(int error) {
        switch (error) {
            case 4:
                return "Out of memory";
            case 3:
                return "Syntax error";
            case 2:
                return "Runtime error";
            case 1:
                return "Yield error";
        }
        return "Unknown error " + error;
    }
    public void pushValueIntoStack(Object value) {
        if (value instanceof Boolean)
            L.pushBoolean((boolean) value);
        else if (value instanceof Number) {
            if (value instanceof Integer)
                L.pushNumber((int) value);
            else if (value instanceof Float)
                L.pushNumber((float) value);
            else
                L.pushNumber((double) value);
        } else if (value instanceof String)
            L.pushString((String) value);
    }

    public void createInnerTableFromList(List linkedList) {
        L.createTable(0, 0);
        int counter = 1; /* Lua indexes start in 1 */
        for (Object value : linkedList) {
            L.pushNumber(counter);
            if (value instanceof Map)
                createInnerTable((HashMap<String, Object>) value);
            else if (value instanceof List)
                createInnerTableFromList((List) value);
            else
                pushValueIntoStack(value);
            L.setTable(-3);
        }
    }

    public void createInnerTable(HashMap<String, Object> map) {
        L.createTable(0, 0);
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            L.pushString(key);
            if (value instanceof Map)
                createInnerTable((HashMap<String, Object>) value);
            else if (value instanceof List)
                createInnerTableFromList((List) value);
            else
                pushValueIntoStack(value);
            L.setTable(-3);
        }
    }

    public LuaObject HashMapToLuaTable(HashMap<String, Object> map) {
        L.createTable(0, 0);
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            L.pushString(key);
            if (value instanceof Map)
                createInnerTable((HashMap<String, Object>) value);
            else if (value instanceof List)
                createInnerTableFromList((List) value);
            else
                pushValueIntoStack(value);
            L.setTable(-3);
        }
        return L.getLuaObject(-1);
    }

    public List<String> getKeysFromTable(LuaObject table) {
        List<String> keys = new LinkedList<String>();

        LuaState LS = table.getLuaState();
        table.push();
        LS.pushNil();
        while (LS.next(-2) != 0) {
            L.pop(1);
            String key = L.toString(-1);
            keys.add(key);
        }
        LS.pop(1);
        return keys;
    }

    public HashMap<String, Object> parseLuaTable(LuaObject table) {
        HashMap<String, Object> node = new HashMap<String, Object>();
        List<String> keys = new LinkedList<String>();
        LuaState LS = table.getLuaState();
        keys = getKeysFromTable(table);
        for (String k : keys) {
            try {
                LuaObject tmp = table.getField(k);
                if (tmp.isTable()) {
                    node.put(k, parseLuaTable(tmp));
                } else {
                    tmp.push();
//                    Object tmp = LS.toJavaObject(-1);
//                    Log.i("Key", tmp.toString());
                    node.put(k, LS.toJavaObject(-1));
                    LS.pop(1);
                }
            } catch (LuaException le) {
                le.printStackTrace();
            }
        }
        return node;
    }

    public static void printMap(Map mp) {
        Iterator it = mp.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry pair = (Map.Entry) it.next();
            Log.i("HASHMAP", pair.getKey() + " = " + pair.getValue());
        }
    }

    @Override
    public void onGetResponseToLua(String response, LuaObject luaCallback) {
        Object[] o = {response};
        try {
            luaCallback.call(o);
        } catch (LuaException le) {
            le.printStackTrace();
        }
    }

    @Override
    public void onPostResponseToLua(String response, LuaObject luaCallback) {
        Object[] o = new Object[1];
        JSONObject jObject = null;
        HashMap<String, Object> convertedHM = new HashMap<String, Object>();
        try {
            jObject = new JSONObject(response.trim());
            convertedHM = (HashMap<String, Object>) JsonUtils.jsonToMap(jObject);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        try {
            o[0] = HashMapToLuaTable(convertedHM);
            luaCallback.call(o);
        } catch (LuaException le) {
            Toast toast = Toast.makeText(this.makuContext, "Invalid username/password.", Toast.LENGTH_LONG);
            toast.show();
            le.printStackTrace();
        }
    }

    /* Create a decorator to connect the objects */
}
//			JavaFunction print = new JavaFunction(L) {
//				@Override
//				public int execute() throws LuaException {
//					for (int i = 2; i <= L.getTop(); i++) {
//						int type = L.type(i);
//						String stype = L.typeName(type);
//						String val = null;
//						if (stype.equals("userdata")) {
//							Object obj = L.toJavaObject(i);
//							if (obj != null)
//								val = obj.toString();
//						} else if (stype.equals("boolean")) {
//							val = L.toBoolean(i) ? "true" : "false";
//						} else {
//							val = L.toString(i);
//						}
//						if (val == null)
//							val = stype;
//						output.append(val);
//						output.append("\t");
//					}
//					output.append("\n");
//					return 0;
//				}
//			};
//			print.register("print");
