package org.bdware.sc.trace;

import org.bdware.sc.boundry.JavaScriptEntry;
import org.bdware.sc.engine.DesktopEngine;
import wrp.jdk.nashorn.api.scripting.NashornScriptEngine;
import wrp.jdk.nashorn.api.scripting.ScriptObjectMirror;
import wrp.jdk.nashorn.internal.objects.Global;
import wrp.jdk.nashorn.internal.runtime.Context;
import wrp.jdk.nashorn.internal.runtime.ScriptFunction;
import wrp.jdk.nashorn.internal.runtime.ScriptObject;
import wrp.jdk.nashorn.internal.scripts.JO;

import javax.script.Bindings;
import javax.script.ScriptContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.GZIPInputStream;

public class TraceRecoverUtil {
    DesktopEngine engine;
    NashornScriptEngine nashornEngine;

    private ArrayList<TraceRecord> traceRecords;

    private int recoverFlag; // 表示当前traceRecords中第i个已经恢复

    // 存储恢复过程中的局部变量
    private Map<Integer, ScriptObjectMirror> recoverMap;

    public TraceRecoverUtil(DesktopEngine de) {
        this.engine = de;
        recoverFlag = -1;
        nashornEngine = engine.getNashornEngine();
    }

    public Map<Integer, ScriptObjectMirror> getMap() {
        return recoverMap;
    }

    public ArrayList<TraceRecord> getTraceRecords() {
        if (traceRecords == null) {
            return null;
        }
        return this.traceRecords;
    }

    public TraceRecord getTraceRecord(int c) {
        if (traceRecords == null) return null;
        else if (traceRecords.size() <= c) return null;
        else return traceRecords.get(c);
    }

    public String getTraceRecordsContent() {
        StringBuilder str = new StringBuilder();

        for (int i = 0; i < traceRecords.size(); i++) {
            str.append("No." + i + "record : \n" + traceRecords.get(i).toString());
        }

        return str.toString();
    }

    public void setTraceRecords(String fileName) {
        File file = new File(fileName);
        ObjectInputStream reader;
        try {
            FileInputStream fileout = new FileInputStream(file);
            GZIPInputStream gzin = new GZIPInputStream(fileout);
            reader = new ObjectInputStream(gzin);
            traceRecords = (ArrayList<TraceRecord>) reader.readObject();
            reader.close();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public void recoverInit() {
        this.recoverMap = new HashMap();
        Bindings bindings = nashornEngine.getBindings(ScriptContext.ENGINE_SCOPE);
        System.out.println(bindings);

        for (String key : bindings.keySet()) {
            Object obj = bindings.get(key);
            if (obj instanceof ScriptObjectMirror) initRecoverMap((ScriptObjectMirror) obj);
        }
        Context.setGlobal(JavaScriptEntry.getEngineGlobal());
    }

    public String recoverFromTraceRecord() {
        System.out.println("[TraceRecoverUtil] recoverFromTraceRecord : ");

        recoverInit();
        Global oldGlobal = Context.getGlobal();
        boolean globalChanged = (oldGlobal != engine.getDesktopGlobal());
        try {
            if (globalChanged) {
                Context.setGlobal(engine.getDesktopGlobal());
            }

            for (int i = 0; i < traceRecords.size(); i++) {
                TraceRecord traceRecord = traceRecords.get(i);
                System.out.println(traceRecord.toString());

                for (int j = 0; j < traceRecord.length(); j++) {
                    // System.out.println("trace <" + j + ">");

                    Trace trace = traceRecord.traces.get(j);

                    if (trace instanceof TraceSetup) {
                        recoverSetup((TraceSetup) trace);
                    } else if (trace instanceof TraceSet) {
                        recoverSet((TraceSet) trace);
                    } else if (trace instanceof TraceDone) {
                        // System.out.println("[DesktopEngine] recover TraceDone, before set:" +
                        // ScriptObject.getAllocID()
                        //		+ " -->" + ((TraceDone) trace).getID());
                        ScriptObject.setAllocID(((TraceDone) trace).getID());
                    } else if (trace instanceof TraceInitArray) {
                        recoverInitArray((TraceInitArray) trace);
                    } else if (trace instanceof TraceInitObject) { // un use
                        recoverInitObject((TraceInitObject) trace);
                    }
                }

                recoverFlag = i;
            }
        } catch (Exception e) {

        } finally {
            Context.setGlobal(oldGlobal);
        }

        return "[recoverFromTraceRecord] recover all";
    }
    /*
     * 通过traceRecord进行恢复 从当前状态恢复到第c次执行之后的状态
     */
    public String recoverFromTraceRecord(int c) {
        int oldflag = recoverFlag;

        if (recoverFlag < 0) recoverInit();

        if (recoverFlag > c) {
            System.out.println(
                    "[recoverFromTraceRecord] recoverFlag now is "
                            + recoverFlag
                            + " ,can't recover to "
                            + c
                            + " !");
            return "recover from trace failed!";
        }

        if (c >= traceRecords.size()) {
            System.out.println(
                    "[recoverFromTraceRecord] traceRecords' size now is "
                            + traceRecords.size()
                            + " ,can't recover to "
                            + c
                            + " !");
            return "recover from trace failed!";
        }

        Global oldGlobal = Context.getGlobal();
        boolean globalChanged = (oldGlobal != engine.getDesktopGlobal());
        try {
            if (globalChanged) {
                Context.setGlobal(engine.getDesktopGlobal());
            }

            for (int i = recoverFlag + 1; i <= c; i++) {
                TraceRecord traceRecord = traceRecords.get(i);

                for (int j = 0; j < traceRecord.length(); j++) {
                    // System.out.println("trace <" + j + ">");

                    Trace trace = traceRecord.traces.get(j);

                    if (trace instanceof TraceSetup) {
                        recoverSetup((TraceSetup) trace);
                    } else if (trace instanceof TraceSet) {
                        recoverSet((TraceSet) trace);
                    } else if (trace instanceof TraceDone) {
                        // System.out.println("[DesktopEngine] recover TraceDone, before set:" +
                        // ScriptObject.getAllocID()
                        //		+ " -->" + ((TraceDone) trace).getID());
                        ScriptObject.setAllocID(((TraceDone) trace).getID());
                    } else if (trace instanceof TraceInitArray) {
                        recoverInitArray((TraceInitArray) trace);
                    } else if (trace instanceof TraceInitObject) { // un use
                        recoverInitObject((TraceInitObject) trace);
                    }
                }

                recoverFlag = i;
            }
        } catch (Exception e) {

        } finally {
            Context.setGlobal(oldGlobal);
        }

        recoverFlag = c;

        return "[recoverFromTraceRecord] recover from " + oldflag + " to " + c;
    }

    public void recoverSetup(TraceSetup trace) {
        // System.out.println("[recoverSetup]\n" + trace.traceContent());

        ScriptObjectMirror obj = null;
        int id = -1;

        if (trace.getObj() instanceof JSArray) {
            id = ((JSArray) trace.getObj()).getObjID();
            obj =
                    (ScriptObjectMirror)
                            ScriptObjectMirror.wrap(
                                    Global.allocate(new int[0]), engine.getDesktopGlobal());
            obj.setObjectID(id);
        } else if (trace.getObj() instanceof JSObject) {

            id = ((JSObject) trace.getObj()).getObjID();
            JO so = new JO(JO.getInitialMap());
            so.setObjectID(id);
            obj = (ScriptObjectMirror) ScriptObjectMirror.wrap(so, engine.getDesktopGlobal());

            /*
             * System.out.println("[recover setup JSObject] : "); id = ((JSObject)
             * trace.getObj()).getObjID();
             *
             * PropertyMap map = PropertyMap.newMap(); for(int i = 0;i <
             * trace.proLength();i++) { map.addProperty(trace.get(i)); } ScriptObject so =
             * new JO(map);
             *
             *
             *
             *
             * so.setObjectID(id); obj = (ScriptObjectMirror) ScriptObjectMirror.wrap(so,
             * JavaScriptEntry.getEngineGlobal());
             */

        }
        recoverMap.put(id, obj);
    }

    public void recoverInitArray(TraceInitArray trace) {
        // System.out.println("[recoverInitArray]\n" + trace.traceContent());

        for (int i = 0; i < trace.getLength(); i++) {
            TraceSet trace2 = new TraceSet(trace.getArrayId(), trace.getKey(i), trace.getValue(i));
            recoverSet(trace2);
        }
    }

    public void recoverInitObject(TraceInitObject trace) {
        // System.out.println("[recoverInitObject]\n" + trace.traceContent());
        // System.out.println("id=" + trace.getId() + " id2=" + trace.getId2());
        ScriptObject so = getScriptObjectMirrorById(trace.getId()).getScriptObject();
        ScriptObject so2 = (getScriptObjectMirrorById(trace.getId2())).getScriptObject();
        // System.out.println("so=" + so.toString() + " so2=" + so2.toString());
        so.setGlobalObjectProto(so2);
    }

    public void recoverSet(TraceSet trace) {
        // System.out.println("[recoverSet]\n" + trace.traceContent());

        ScriptObjectMirror owner = getScriptObjectMirrorById(trace.getOwner());
        Object key = trace.getKey();
        Object value = trace.getValue();

        // 把JS类型转化为ScriptObjectMirror
        if (value instanceof JSObject)
            value = getScriptObjectMirrorById(((JSObject) value).getObjID());
        // 目前JSArray和JSObject一样，可能可以简化
        else if (value instanceof JSArray)
            value = getScriptObjectMirrorById(((JSArray) value).getObjID());
        else if (value instanceof String || value instanceof Integer || value instanceof Double) ;
        else if (value instanceof JSNull) // 数组trace中可能存在null和Undifined类型，赋值时跳过
        return;
        else if (value instanceof JSUndifined) {
            return;
        } else System.out.println("[recoverSet] encounter new value type!" + value.toString());

        // 修改ScriptObjectMirror中的setMemeber，使它支持所有类型
        owner.setMember2(key, value);
    }

    /*
     * 将bindings中的变量放入recoverMap 对于函数的scope中的对象以及对象中的对象的情况可以通过递归将所有需要的都放入recoverMap中
     */
    private void initRecoverMap(ScriptObjectMirror obj) {
        if (obj == null) return;
        if (recoverMap.containsKey(obj.getObjectID())) return;
        recoverMap.put(obj.getObjectID(), obj);

        // 全局变量从bindings中获得
        for (String key : obj.getOwnKeys(true)) {
            try {
                Object value = obj.getMember(key);
                if (value instanceof ScriptObjectMirror) {
                    ScriptObjectMirror svalue = (ScriptObjectMirror) value;
                    initRecoverMap(svalue);
                    // 从函数的Scope中获得
                    if (svalue.isFunction()) {
                        ScriptFunction sf = (ScriptFunction) svalue.getScriptObject();
                        ScriptObject s = sf.getScope();
                        ScriptObjectMirror obj2 =
                                (ScriptObjectMirror)
                                        ScriptObjectMirror.wrap(
                                                s, JavaScriptEntry.getEngineGlobal());
                        initRecoverMap(obj2);
                    }
                }
            } catch (Exception e) {
                // e.printStackTrace();
            }
        }
    }

    public ScriptObjectMirror getScriptObjectMirrorById(int id) {
        ScriptObjectMirror so = null;

        // 从recoverMap中获得
        if (recoverMap.containsKey(id)) return recoverMap.get(id);

        System.out.println("[getScriptObjectMirrorById] can't find the ScriptObjectMirror by id!");
        return so;
    }
}
