addons/customParser/modelEditor/modelEditor.js

const { toHankakuSpace } = require("encoding-japanese");
var CustomParser = require('www/addons/customParser/CustomParser.js');
var trans = window.opener.trans;
var nwPath = require("path");
const { title } = require("process");

var win = nw.Window.get();
win.restore(); // restore if minimized
win.show(); // show if hidden
win.setResizable(true);

var loadingScreen;
var modelEditor;

win.on('close', function() {
    var conf = confirm(t("Do you want to close Model editor?\rAll unsaved changes will be discarded."))
    if(!conf) return;

    (async ()=> {
        // Hide the window to give user the feeling of closing immediately
        this.hide();
        if (nw.process.versions["nw-flavor"] == "sdk") {
            win.closeDevTools();
            await common.wait(200);
        }
    
        /**
         * Triggered when option window is closed
         * @event ui#optionsWindowClosed
         * @since 4.5.1
         */
        window.opener.ui.trigger("modelEditorWindowClosed");
        //if (typeof window.opener.ui.windows.options !== 'undefined') window.opener.ui.windows.options = undefined;
    
        this.close(true);
    })()
});

if (win.y < 0) win.y = 0;
if (win.x < 0) win.x = 0;
//win.setResizable(false);
setTimeout(()=>{
	if (win.y < 0) win.y = 0;
	win.setMinimumSize(800, 600)
}, 200);

/**
 * @param  {Object} rootElm - JQuery instance of node as the root element or wrapper of the tabs
 * @param  {Object} options - Object of options
 * @class
 */

var SubTab = function(rootElm, options) {
    this.$elm               = rootElm;
    options                 = options || {};
    this.initialSelection   = options.initialSelection || 0;
    this.init();
}
SubTab.prototype.on = function(evt, fn) {
    this.$elm.on(evt, fn)
}
SubTab.prototype.off = function(evt, fn) {
    this.$elm.off(evt, fn)
}
SubTab.prototype.one = function(evt, fn) {
    this.$elm.one(evt, fn)
}
SubTab.prototype.trigger = function(evt, param) {
    this.$elm.trigger(evt, param)
}

SubTab.prototype.getActiveSubTab = function($rootElm) {
    $rootElm = $rootElm || this.$elm;
    return $rootElm.find(".active[data-tabid]").attr('data-tabid');
}

SubTab.prototype.select = function($tab, trigger) {
    var $rootElm = this.$elm;
    if (typeof $tab == "number") {
        $tab = $rootElm.find("[data-targettab]").eq($tab);
    } else if (typeof $tab == "string") {
        $tab = $rootElm.find(`[data-targettab="${CSS.escape($tab)}"]`);
    }
    var targetId = $tab.attr('data-targettab');
    if (trigger) {
        this.trigger("beforeSubTabChange", {
            targetId    : targetId,
            targetElm   : $rootElm.find(`[data-tabid="${targetId}"]`)
        });
    }
    
    $rootElm.find("[data-targettab]").removeClass("active");
    $rootElm.find("[data-tabid]").removeClass("active");
    $rootElm.find("[data-tabid]").addClass("hidden");
    $rootElm.find(`[data-tabid="${targetId}"]`).addClass("active");
    $rootElm.find(`[data-tabid="${targetId}"]`).removeClass("hidden");
    $tab.addClass("active");
    if (trigger) {
        this.trigger("afterSubTabChange", {
            targetId    : targetId,
            targetElm   : $rootElm.find(`[data-tabid="${targetId}"]`)
        });
    }
}

SubTab.prototype.init = function($rootElm) {
    var that = this;
    $rootElm = $rootElm || this.$elm;
    $rootElm.find("[data-targettab]").each(function() {
        var $this = $(this);
        if ($this.hasClass("tabInitialized")) return;
        $this.addClass("tabInitialized");
        $this.on("click", function() {
            var $tab = $(this);
            that.select($tab, true);
        });
    });
    this.select(this.initialSelection, false);
    $rootElm.addClass("subTabInitialized");
}


var ModelEditor = function(file, options) {
    this.file   = file;
    this.hasChange = false;
    this.option = options || {};
    this.$elm   = $("body");
    this.debugLevel = nw.App.manifest.debugLevel;
}

ModelEditor.prototype.on = function(evt, fn) {
    this.$elm.on(evt, fn)
}
ModelEditor.prototype.off = function(evt, fn) {
    this.$elm.off(evt, fn)
}
ModelEditor.prototype.one = function(evt, fn) {
    this.$elm.one(evt, fn)
}
ModelEditor.prototype.trigger = function(evt, param) {
    this.$elm.trigger(evt, param)
}

ModelEditor.prototype.delete = function($li) {
    if ($li.is(this.$selected)) {
        var $rightPane = $("#rightPane")
        $rightPane.empty();
    }
    $li.remove();
    this.redrawRuleId();
    this.hasChange = true;

    if ($("#leftPane .selectable").length == 0) this.addRule();
}

ModelEditor.prototype.addRule = function($li, defaultValue) {
    if (this.debugLevel) console.log("addingRule", arguments);
    var $rule = this.$ruleTemplate.clone(true, true);
    if (!$li) {
        $("#leftPane .sortable").append($rule);
    } else {
        $li.after($rule);
    }
    
    if (defaultValue) {
        $rule.data("rule", defaultValue);
        this.drawRuleSelectorType($rule);
    }

    this.redrawRuleId();
    this.hasChange = true;
    return $rule;
}

ModelEditor.prototype.clearRules = function() {
    var $rightPane = $("#rightPane")
    $rightPane.empty();
    $("#leftPane .sortable").empty();
}

ModelEditor.prototype.resetRules = function() {
    if (this.debugLevel) console.log("reset rule");
    this.clearRules();
    this.addRule();
    this.selectRule(0);
}

ModelEditor.prototype.redrawRuleId = function() {
    var $pattern = $(".pattern");
    $pattern.each(function(index) {
        $(this).find(".patternId").text(index);
    })
}

ModelEditor.prototype.getSelectedRule = function() {
    if (empty(this.$selected.data("rule"))) this.$selected.data("rule", {})
    return this.$selected.data("rule");
}

ModelEditor.prototype.setRuleValue = function(key, value, rule) {
    rule = rule || this.$selected.data("rule") || {};
    rule[key] = value;
    this.hasChange = true;
    //this.$selected.data("rule", this.selectedRule)
}


ModelEditor.prototype.loadRules = function(value) {
    console.log("loading rules :", value);
    if (empty(value)) return this.resetRules();

    this.clearRules();
    for (var i in value) {
        this.addRule(undefined, value[i]);
    }
}



ModelEditor.prototype.applyChange = function() {
    if (!this.$activeFileGroup) return;
    if (this.$activeFileGroup.length == 0) return;
    //console.log("applying rules to FileGroup at ", this.$activeFileGroup);
    //this.$activeFileGroup.data("fileData", this.dumpPatterns());
    this.applyFileGroupData("rules", this.dumpPatterns());
    this.hasChange = true;
}

ModelEditor.prototype.getRuleData = function($elm) {
    $elm = $elm || $("#rules .selectable.selected");
    if (empty($elm.data("rule"))) $elm.data("rule", {})
    return $elm.data("rule");
}

ModelEditor.prototype.getInnermostRuleData = function($elm) {
    var rootRule = this.getRuleData($elm);
    var getRule = (rule)=> {
        rule = rule || {};
        if (rule.action == "innerRule") {
            return getRule(rule.innerRule);
        }
        return rule;
    }
    return getRule(rootRule);
}

ModelEditor.prototype.drawRuleSelectorType = function($elm) {
    $elm = $elm || $("#rules .selectable.selected");
    var rule = this.getInnermostRuleData($elm);
    $elm.attr("rule-type", "");
    try {
        $elm.attr("rule-type", rule.type);
        if (!rule.action) return;
        if (rule.action == "mask")  {
            $elm.attr("rule-type", "mask");
        } else if (rule.action == "captureMask") {
            $elm.attr("rule-type", "mask");
        }
    } catch (e) {
        // do nothing
    }
}

ModelEditor.prototype.drawSelected = function() {
    var rule    = this.getSelectedRule();
    var modelEditor = this;

    var drawBreadCrumb = ($elm, path) => {
        var activeRule  = rule;
        var thisPath    = [];
        for (var i=0; i<path.length; i++) {
            thisPath.push(path[i]);
            if (i == 0) {
                var $template = $(`<span class="breadCrumbNavigator"><i class="separator icon-home"></i>Root Rule</span>`)
            } else {
                activeRule = activeRule[path[i]];
                $elm.append(`<i class="separator icon-angle-double-right"></i>`);
                var $template = $(`<span class="breadCrumbNavigator">${path[i]}</span>`)
            }

            if (i == path.length-1) {
                $template.addClass("active");
            } else {
                (()=>{
                    var currentActiveRule = activeRule;
                    var pathCopy = common.clone(thisPath);
                    $template.on("click", ()=>{
                        console.log("thisPath:", pathCopy);
                        console.log("currentActiveRole", currentActiveRule);
                        drawForm(currentActiveRule, pathCopy);
                    })
                })()
            }
            $elm.append($template);
        }
    }



    var drawForm = (selectedRule, path = []) => {
        var $rightPane = $("#rightPane")
        $rightPane.empty();
    
        var $template   = $("#template .editorRegexWrapper").clone(true, true);       
        var that        = this;
        var switchType  = (theType) => {
            $template.find(`[data-field="type"]`).val(theType);
            $template.find(`[data-fieldGroup]`).addClass("hidden");
            $template.find(`[data-fieldGroup="${theType}"]`).removeClass("hidden");
            $template.find(`[data-fieldGroup="${theType}"]`).addClass("active");
            $template.find(".typeSelection").addClass("hidden");

            if (theType == "function") {
                // activate ACE for JS Function field
                var editor = ace.edit($template.find(".functionEditor")[0]);
                editor.setTheme("ace/theme/monokai");
                editor.session.setMode("ace/mode/javascript");
                editor.setShowPrintMargin(false);
                editor.setOptions({
                    fontSize: "12pt"
                  });
                editor.on("blur", ()=> {
                    that.setRuleValue("function", editor.getValue(), selectedRule);
                });
                editor.setValue(selectedRule.function || "");
            } else {
                var editor = ace.edit($template.find(`[data-field="pattern"]`)[0]);
                editor.setTheme("ace/theme/monokai");
                editor.session.setMode("ace/mode/javascript");
                editor.setShowPrintMargin(false);
                editor.renderer.setShowGutter(false);
                editor.setOptions({
                    fontSize: "12pt"
                });
                editor.on("change", ()=> {
                    that.setRuleValue("pattern", editor.getValue(), selectedRule);
                });
                editor.setValue(selectedRule.pattern || "");
            }
            modelEditor.drawRuleSelectorType();
        }

        drawBreadCrumb($template.find(".breadCrumb .path"), path);

        if (selectedRule.type) {
            switchType(selectedRule.type);
        }

        $template.find("a.externalLink, a[external]").on("click", function(e) {
            e.preventDefault();
            nw.Shell.openExternal($(this).attr("href"));
        });

        $template.find('.typeSelectorFld').on("click", function() {
            var thisValue =  $(this).attr("value")
            that.setRuleValue("type", thisValue, selectedRule);
            switchType(thisValue);
        });

        $template.find('[data-field="pattern"]').on("input", function() {
            that.setRuleValue("pattern", $(this).val(), selectedRule);
        });
        $template.find('[data-field="captureGroups"]').on("input", function() {
            that.setRuleValue("captureGroups", $(this).val(), selectedRule);
        });
        $template.find('[data-field="action"]').on("input", function() {
            var $this = $(this);
            $this.closest(".form-group").find(".innerRuleWrapper").addClass("hidden")
    
            that.setRuleValue("action", $this.val(), selectedRule);
            if ($this.val() == "innerRule") {
                $this.closest(".form-group").find(".innerRuleWrapper").removeClass("hidden");
            }
            that.drawRuleSelectorType();
        });

        $template.find('[data-field="function"]').on("input", function() {
            that.setRuleValue("function", $(this).val(), selectedRule);
        });
    
        $template.find('.innerRule').on("click", () => {
            selectedRule.innerRule = selectedRule.innerRule || {};
            drawForm(selectedRule.innerRule, path.concat(["innerRule"]));
        })

        console.log("selected rule:", selectedRule);
        for (var i in selectedRule) {
            if (!i) continue;
            if (!selectedRule[i]) continue;
            $template.find(`[data-field="${i}"]`).val(selectedRule[i]);
        }
        if (selectedRule.action == "innerRule") {
            $template.find(".innerRuleWrapper").removeClass("hidden");
        }
    
        $rightPane.append($template);
    }
    drawForm(rule, ["rule"]);
}

ModelEditor.prototype.selectRule = function($li, doNotApply) {
    if (typeof $li == "number") $li = $("#leftPane .selectable").eq($li);
    if (!doNotApply) this.applyChange();
    console.log("selecting", $li);
    $(".pattern").removeClass("selected");
    $li.addClass("selected");
    this.$selected = $li;
    this.drawSelected();
}

/**
 * Dump currenctly active patterns
 */
ModelEditor.prototype.dumpPatterns = function() {
    var $patterns = $("#leftPane [data-role='pattern']");
    var result = []
    for (var i=0; i<$patterns.length; i++) {
        var data = $patterns.eq(i).data('rule');
        if (empty(data)) continue;
        result.push(data);
    }
    return result;
}

ModelEditor.prototype.runSample = async function(text, model) {
    this.sampleIsRunning = true;
    text    = text || "";
    model   = model || this.loadFileGroupData()
    console.log("current model:", model);
    var options = {
        model:model
    }
    var customParser = new CustomParser(text, options);
    await customParser.parse();
    console.log("Custom parser:", customParser);
    this.gridPreviewData = customParser.transData.data;
    this.gridPreview.loadData(this.gridPreviewData);
    await this.gridPreview.render();
    this.sampleIsRunning = false;
}

ModelEditor.prototype.getSampleTranslationPair = function() {
    var result = {};
    var data = modelEditor.gridPreview.getData();
    for (var r=0; r<data.length; r++) {
        var row = data[r];
        if (!Boolean(row[0])) continue;
        if (!Boolean(row[1])) continue;
        result[row[0]] = row[1];
    }
    return result;
}

ModelEditor.prototype.runSampleTranslationResult = async function(text, model) {
    text = text || this.sampleEditor.getValue();
    if (!text) return;
    this.sampleIsRunning = true;
    model   = model || this.loadFileGroupData()
    console.log("current model:", model);
    var options = {
        model:model
    }
    var customParser = new CustomParser(text, options);
    customParser.writeMode          = true;
    customParser.translationPair    = this.getSampleTranslationPair();
    await customParser.parse();
    console.log("Custom parser:", customParser);
    this.translationPreview.setValue(customParser.toString());
    this.sampleIsRunning = false;
}


ModelEditor.prototype.initSampleEditor = function() {
    var editor = ace.edit(this.$patterns.find(".sampleEditor")[0]);
    editor.setTheme("ace/theme/monokai");
    editor.session.setMode("ace/mode/text");
    editor.setShowPrintMargin(false);
    editor.setOptions({
        fontSize: "12pt"
      });
    editor.lastSampleRender = 0;
    editor.setOption("showInvisibles", true);
    editor.timeOutRender;
    editor.on("change", async ()=> {
        clearTimeout(editor.timeOutRender);
        editor.timeOutRender = setTimeout(async ()=> {
            if (this.sampleSubtab.getActiveSubTab() == "gridPreview") {
                await this.runSample(editor.getValue());
            } else {
                await this.runSampleTranslationResult(editor.getValue());
            }
        },100);
        /*
        if (this.sampleIsRunning) return;
        if (Date.now() < editor.lastSampleRender+1000) return;
        if (this.sampleSubtab.getActiveSubTab() == "gridPreview") {
            await this.runSample(editor.getValue());
        } else {
            await this.runSampleTranslationResult(editor.getValue());
        }
        editor.lastSampleRender = Date.now();
        */
    });
    editor.on("blur", async ()=> {
        localStorage.setItem("modelEditor.sampleEditor", editor.getValue());
    });
    editor.setValue(localStorage.getItem("modelEditor.sampleEditor") || "");
    this.sampleEditor = editor;
}


ModelEditor.prototype.initGridPreview = function($elm) {
    var translateSelection = () => {
        if (empty(this.gridPreviewData)) return;
        var rows = common.gridSelectedRows(this.gridPreview.getSelectedRange());
        for (var i=0; i<rows.length; i++) {
            var rowData = this.gridPreviewData[i];
            if (!rowData[0]) continue;
            var reversed = rowData[0].split("").reverse().join("")
            rowData[1] = "Translated: "+reversed;
        }
        this.gridPreview.loadData(this.gridPreviewData);
        this.gridPreview.render();
    }
    var clearTranslation = () => {
        if (empty(this.gridPreviewData)) return;
        for (var i=0; i<this.gridPreviewData.length; i++) {
            this.gridPreviewData[i][1] = null;
        }
        this.gridPreview.render();
    }


    this.gridPreviewData    = [[]];
    this.gridPreview        = new Handsontable($elm[0], {
        data                : this.gridPreviewData,
        height              : '100%',
        width               : '100%',
        manualColumnResize  : true,
        rowHeaders          : true,
        colWidths           : [176, 176],
        colHeaders          : ["Original text", "Translation"],
        columns: [
            {
                readOnly:true
            },
            {
                readOnly:false
            }
        ],
        contextMenu: {
			items: {
				'generateTranslation': {
					name: "<i class='icon-language'></i>"+t("Translate here using ")+"dummy translator",
					callback: () => {
						translateSelection();
					}
					
				},
                'clearTranslation' : {
                    name: "<i class='icon-trash-empty'></i>Clear translation",
                    callback: ()=>{
                        clearTranslation();
                    }
                }
            }
        }
    });

    $(window).on("resizeEnd.gridPreview");
    $(window).on("resizeEnd.gridPreview", ()=> {
        //if (($elm).not(":visible")) return;
        this.gridPreview.render();
    });

    // change status from hidden to visible
    this.off("startShowingGrid");
    this.on("startShowingGrid", ()=> {
        //if (($elm).not(":visible")) return;
        console.log("start showing grid");
        this.gridPreview.render();
    })
}

ModelEditor.prototype.initTranslationPreview = function() {
    //this.initSubTab();
    var editor = ace.edit(this.$patterns.find(".translationPreview")[0]);
    editor.setTheme("ace/theme/monokai");
    editor.session.setMode("ace/mode/text");
    editor.setShowPrintMargin(false);
    editor.setReadOnly(true);
    editor.setOptions({
        fontSize: "12pt"
      });
    this.translationPreview = editor;

    this.sampleSubtab.off("beforeSubTabChange.transPreview");
    this.sampleSubtab.on("beforeSubTabChange.transPreview", (e, info) => {
        console.log(info);
        if (info.targetId == "translationPreview") {
            this.runSampleTranslationResult();
        }
    });

    this.sampleSubtab.off("afterSubTabChange.transPreview");
    this.sampleSubtab.on("afterSubTabChange.transPreview", (e, info) => {
        if (this.sampleSubtab.getActiveSubTab() == "gridPreview") this.trigger("startShowingGrid");
    });
}
/**
 * Initializing Testing Tab
 * If testing tab is initialized, then refresh the content
 */
ModelEditor.prototype.initTestingTab = async function() {
    if (this.$patterns.hasClass("testingTabInitialized")) {
        if (this.sampleSubtab.getActiveSubTab() == "gridPreview") {
            await this.runSample(this.sampleEditor.getValue());
        } else {
            await this.runSampleTranslationResult(this.sampleEditor.getValue());
        }
        return;
    }
    this.sampleSubtab = new SubTab(this.$elm.find(".translationPreviewer"));
    this.initSampleEditor();
    this.initGridPreview(this.$patterns.find(".gridPreview"));
    this.initTranslationPreview();
    this.$patterns.addClass("testingTabInitialized");
}

ModelEditor.prototype.initHooksTab = function($rootElm) {
    var that = this;
    $rootElm = $rootElm || this.$elm;
    if ($rootElm.hasClass("subTabInitialized")) return;
    // init navigator
    this.hooksTab = new SubTab($rootElm);

    // init field
    $rootElm.find(".editor").each(function() {
        var fieldName = $(this).attr("data-field");
        var editor = ace.edit($(this)[0]);
        editor.setTheme("ace/theme/monokai");
        editor.session.setMode("ace/mode/javascript");
        editor.setShowPrintMargin(false);
        editor.setOptions({
            fontSize: "12pt"
          });
        editor.on("blur", ()=> {
            that.applyFileGroupData(fieldName, editor.getValue())
        });
        editor.setValue(that.loadFileGroupData(undefined, fieldName) || "");
    });
    Prism.highlightAll();
}

var setValueRecursive = function(rootObj, stringVar, value, force) {
	try {
		var autosetPath = stringVar.split(".")
		if (autosetPath.length < 1) return false;

		rootObj ||= window;
		for (var i=0; i<autosetPath.length-1; i++) {
			if (force) {
				if (typeof rootObj[autosetPath[i]] == 'undefined') {
					rootObj[autosetPath[i]] = {};	
				}
			}
			rootObj = rootObj[autosetPath[i]];

			
		}
		rootObj[autosetPath[autosetPath.length-1]] = value;

	} catch (e) {
		return false;
	}
	return true;
}

ModelEditor.prototype.setOptionValue = function(key, value) {
    var fileGroupData = this.loadFileGroupData(undefined, undefined, true);
    fileGroupData.options ||= {};
    setValueRecursive(fileGroupData, `options.${key}`, value, true);
    console.log("data after set:", JSON.stringify(fileGroupData, undefined, 2));
}

ModelEditor.prototype.getOptionValue = function(key) {
    var fileGroupData = this.loadFileGroupData();
    console.log("loading path", key);
    try {
		var x = eval("fileGroupData.options."+key);
		return x;
	} catch (e) {
        console.log("can not find value");
		return;
	}
}

ModelEditor.prototype.initOptionsTab = function($rootElm) {
    var that = this;
    $rootElm = $rootElm || this.$elm;
    console.log("initializing options tab");

    var $options = $rootElm.find("#options");
    $options.find("[data-fld]").each(function() {
        var $this = $(this);
        var thisValue = that.getOptionValue($this.attr("data-fld")) || $this.attr("data-default");
        $this.val(thisValue)
    });


    if ($options.hasClass("eventInitialized")) return;
    console.log("initializing event's option");
    $options.find("[data-fld]").on("change", function() {
        console.log("saving options");
        var $this = $(this);
        that.setOptionValue($this.attr("data-fld"), $this.val())
    })
    
    $options.addClass("eventInitialized")
}

ModelEditor.prototype.newRulesPane = function(defaultValue) {
    console.log("Creating new rules pane");
    this.clearRulesPane();
    this.$patterns = this.$patternsTemplate.clone(true, true);
    this.$rulesRootElm.append(this.$patterns);
    this.$rulesRootElm.find(".patternEditor").tabs({
		active: 0,
		activate: function(e, thisUi) {
            
		}
	});
    this.$rulesRootElm.find(".patternEditor").on("tabsactivate", async (e, ui)=> {
        console.log(ui);
        if (ui.newPanel.attr("id") == "hooks") {
            console.log("hooks tab activated");
            this.initHooksTab(ui.newPanel);
        } else if (ui.newPanel.attr("id") == "options") {
            console.log("options tab activated");
            await this.initOptionsTab();
        } else if (ui.newPanel.attr("id") == "testing") {
            console.log("testing tab activated");
            await this.initTestingTab();
        }
    });

    this.$rulesRootElm.find(".sortable").sortable(); 
    this.loadRules(defaultValue.rules);
    this.selectRule(0, true);
    console.log("End of creating new rules pane");

    /*
    // initializing testing tab
    this.sampleSubtab = new SubTab(this.$elm.find(".translationPreviewer"));
    this.initSampleEditor();
    this.initGridPreview(this.$patterns.find(".gridPreview"));
    this.initTranslationPreview();
   */
}

ModelEditor.prototype.clearRulesPane = function() {
    this.$rulesRootElm.empty();
}

ModelEditor.prototype.clearFileGroup = function() {
    this.$fileGroups.empty();
}

ModelEditor.prototype.drawNoFileGroupWarning = function() {
    this.$rulesRootElm.append($("#template .noFileGroup").clone(true, true));
}

ModelEditor.prototype.getActiveFileGroup = function() {
    return this.$fileGroups.find(":selected");
}

ModelEditor.prototype.applyFileGroupData = function(key, data, $elm) {
    //console.log("applyFileGroupData", arguments);
    $elm = $elm || this.$activeFileGroup
    if (!$elm) return;
    if ($elm.length == 0) return;
    if (empty($elm.data("fileData"))) $elm.data("fileData", {});
    console.log("Applying key", key, JSON.stringify(data, undefined, 2), $elm.attr("value"));
    var fileData    = $elm.data("fileData");
    fileData[key]   = data;
    $elm.data("fileData", fileData);    
}

ModelEditor.prototype.loadFileGroupData = function($elm, key, doNotSave) {
    console.log("loadFileGroupData", arguments)
    $elm = $elm || this.getActiveFileGroup();
    if (!doNotSave) this.applyChange();
    if (empty($elm.data("fileData"))) $elm.data("fileData", {});
    if (!key) return $elm.data("fileData");
    return $elm.data("fileData")[key];
}

ModelEditor.prototype.selectFileGroup = function($elm) {
    loadingScreen.show();
    $(".patternEditor").addClass("hidden");
    if (typeof $elm == "number") $elm = $("#fileNavSelector option").eq($elm);
    console.log("Selecting fileGroup", $elm.attr("value"));
    console.log("Applying any change on previous file group");
    this.applyChange();

    console.log("moving to selected group");
    $elm.prop("selected", true);
    this.$activeFileGroup = $elm;
    var defaultValue = this.loadFileGroupData($elm, undefined, true);
    this.newRulesPane(defaultValue);
    loadingScreen.hide();
}

ModelEditor.prototype.newFileGroup = function(group, defaultData) {
    console.log("creating new file group");

    defaultData         = defaultData || {};
    defaultData.pattern = group;
    var $template = $(`<option></option>`);
    $template.attr("value", group);
    $template.text(group);
    $template.data("fileData", defaultData)
    
    this.$fileGroups.append($template);
    this.enableSave();
    this.hasChange = true;
    console.log("file group creation done");
    return $template;
}

ModelEditor.prototype.editFileGroup = function(group, $elm) {
    $elm = $elm || this.getActiveFileGroup();
    $elm.attr("value", group);
    $elm.text(group);
    this.applyFileGroupData("pattern", group, $elm);
    this.hasChange = true;
    return $elm;
}

ModelEditor.prototype.removeFileGroup = function($elm) {
    $elm = $elm || this.getActiveFileGroup();
    console.log("removing FileGroup", $elm);
    console.log("removing group with active data:", $elm.data("fileData"));
    console.log($elm.is(":selected"));
    if ($elm.is(":selected")) this.clearRulesPane();
    if ($("#fileNavSelector option").length > 1) {
        var $prev = $elm.prev();
        $elm.remove();
        if ($prev.length == 0) this.selectFileGroup(0);
        this.selectFileGroup($prev);
        this.hasChange = true;
    } else {
        this.initialize();
    }
}

ModelEditor.prototype.dump = function() {
    this.applyChange();
    var fileGroups = $("#fileNavSelector option");
    var parserModel = {
        files:[]
    }
    for (var i=0; i<fileGroups.length; i++ ) {
        /*
        var thisData = {
            pattern : fileGroups.eq(i).attr("value"),
            rules   : this.loadFileGroupData(fileGroups.eq(i))
        }
        */
        parserModel.files.push(this.loadFileGroupData(fileGroups.eq(i)))
    }
    return parserModel;
}

ModelEditor.prototype.disableSave = function() {
    this.isSaveEnable = false;
    $(".menu-button.button-save").prop("disabled", true);
    $(".menu-button.button-save-as").prop("disabled", true);
}

ModelEditor.prototype.enableSave = function() {
    this.isSaveEnable = true;
    $(".menu-button.button-save").prop("disabled", false);
    $(".menu-button.button-save-as").prop("disabled", false);
}

ModelEditor.prototype.saveAs = async function() {
    if (!this.isSaveEnable) return;
    var $elm = $(`<input type="file" class="saveAs hidden" nwsaveas="new parser model.tpm" accept=".tpm,application/tpm" autocomplete="off"  />`);
    return new Promise(async (resolve, reject)=>{
        $elm.one("input", async ()=> {
            var file = $elm.val();
            if (!$elm.val()) resolve;
            try {
                $(".button-save img").addClass("rotating");
                await common.filePutContents(file, JSON.stringify(this.dump()));
                this.hasChange = false;
            } catch (e) {
                $(".button-save img").removeClass("rotating");
                console.warn(e);
                alert("Error when trying to save file to:"+file+"\n"+e.toString());
                resolve();
            }
            $(".button-save img").removeClass("rotating");
            resolve(file);
        });
        $elm.trigger("click");

    })

}

ModelEditor.prototype.saveTo = async function(file) {
    if (!this.isSaveEnable) return;
    file = file || this.file
    if (!this.file) {
        return this.saveAs();
    }
    try {
        $(".button-save img").addClass("rotating");
        await common.filePutContents(file, JSON.stringify(this.dump()));
        this.hasChange = false;

    } catch (e) {
        $(".button-save img").removeClass("rotating");
        alert("Error when trying to save file to:"+file+"\n"+e.toString());
        return false;
    }
    $(".button-save img").removeClass("rotating");
    return file;
}

ModelEditor.prototype.loadFromJson = async function(json) {
    this.initialize();
    if (!common.isJSON(json)) return alert("Invalid JSON");
    var data = JSON.parse(json);
    data.files = data.files || [];
    for (var i=0; i<data.files.length; i++) {
        this.newFileGroup(data.files[i].pattern, data.files[i]);
    }
    this.selectFileGroup(0);
    this.trigger("jsonLoaded");
}

ModelEditor.prototype.loadFromFile = async function(file) {
    var openFileDialog = async () => {
        return new Promise(async (resolve, reject) => {
            var $elm = $(`<input type="file" class="openFile hidden" accept=".tpm,application/tpm" autocomplete="off" />`);
            $elm.one("input", async ()=> {
                resolve($elm.val());
            })
            $elm.trigger("click");
        })
    }

    if (typeof file == "undefined") {
        file = await openFileDialog();
    }

    if (!file) return;
    
    try {
        var fileContent = await common.fileGetContents(file);
        await this.loadFromJson(fileContent);

        this.file = nwPath.resolve(file);
        this.setWindowTitle(nwPath.basename(this.file));
        this.trigger("fileLoaded")
    } catch (e) {
        console.error(e);
        alert("Error when trying to open file :"+file+"\n"+e.toString());
        return false;
    }
}

ModelEditor.prototype.setWindowTitle = function(string) {
    string = string || ""
    const baseTitle = "Parser Model Creator - Translator++"
    var title = baseTitle;
    if (string) {
        title = string + " - " + baseTitle;
    }
    $("title").text(title)
    return title;
}

ModelEditor.prototype.initialize = function() {
    var $template   = $("#template");
    var that        = this;
    this.hasChange  = false;
    this.file       = "";
    // fileGroup
    this.$fileGroups    = $("#fileNavSelector");
    this.$fileGroups.off("change");
    this.$fileGroups.on("change", function() {
        that.selectFileGroup($(this).find(":selected"));
    });
    this.clearFileGroup();
    this.$activeFileGroup   = undefined;

    // rules
    if (!ModelEditor.$patternEditor) {
        ModelEditor.$patternEditor = $template.find(".patternEditor").clone(true, true);
        // remove the original to avoid confusion of the query
        $template.find(".patternEditor").remove();
    }
    this.$rulesRootElm      = $("#patternEditorWrapper");
    this.$patternsTemplate  = ModelEditor.$patternEditor.clone(true, true);
    this.$ruleTemplate      = ModelEditor.$patternEditor.find(".pattern").clone(true, true);



    this.$ruleTemplate.on("mouseup", function() {
        that.selectRule($(this));
    })

    this.$ruleTemplate.find(".buttons .add").on("click", function() {
        that.addRule($(this).closest(".selectable"));
    })

    this.$ruleTemplate.find(".buttons .delete").on("click", function() {
        that.delete($(this).closest(".selectable"));
    })

    this.clearRulesPane();
    this.drawNoFileGroupWarning();

    this.disableSave();

    this.setWindowTitle("");
}



$(document).ready(function() {
    loadingScreen    = new LoadingScreen();
    modelEditor      = new ModelEditor()
    if (nw.process.versions["nw-flavor"] == "sdk") {
        $(".button-debugger").removeClass("hidden");
    }


    window.dropFileToWindow = new (require("www/js/DropFileToWindow.js"))(document.querySelector("body"), {
        hoverText: "Drop .tpm file here!",
        supportedFiles: [".tpm"],
        onSupportedFileDrop: (filePath)=> {
            modelEditor.loadFromFile(filePath);
        }
    });
    window.dropFileToWindow.init()
    
    // MENU
    $(".addFileGroup").on("click", function() {
        var fileGroup = prompt(t("Please specify what kind of file you want to parse.\nYou can use semicolon separated glob pattens. For example: *.txt;*.js;*.json"));
        if (!fileGroup) return;
        var newOpt = modelEditor.newFileGroup(fileGroup);
        if (!newOpt) alert("Failed to create new file group. Please check your pattern!");
        modelEditor.selectFileGroup(newOpt);
    })

    $(".editFileGroup").on("click", function() {
        var currentFileGroup = modelEditor.getActiveFileGroup();
        if (currentFileGroup.length < 1) return;
        var defaultFileGroup = currentFileGroup.attr("value");
        var fileGroup = prompt(t("Please specify what kind of file you want to parse.\nYou can use semicolon separated glob pattens. For example: *.txt;*.js;*.json"), defaultFileGroup);
        if (!fileGroup) return;
        var newOpt = modelEditor.editFileGroup(fileGroup);
        if (!newOpt) alert("Failed to create new file group. Please check your pattern!");
    });

    $(".removeFileGroup").on("click", function() {
        var conf = confirm(t("Do you wish to remove current file group?\nYou can not undo this action!"));
        if (!conf) return;
        modelEditor.removeFileGroup();
    });

    $(".menu-button.button-new").on("click", function() {
        if (modelEditor.hasChange) {
            var conf = confirm(t("Do you want to discard your changes and create a new blank model?"));
            if (!conf) return;
        }
        modelEditor.initialize();
    })
    $(".menu-button.button-open").on("click", function() {
        if (modelEditor.hasChange) {
            var conf = confirm(t("Do you want to discard your changes and open a file?"));
            if (!conf) return;
        }
        modelEditor.loadFromFile();
    })
    $(".menu-button.button-save").on("click", function() {
        modelEditor.saveTo();
    })
    $(".menu-button.button-save-as").on("click", function() {
        modelEditor.saveAs();
    })

    $(".button-debugger").on("click", function() {
        win.showDevTools();
    });

    $(".menu-button.button-help").on("click", function() {
        nw.Shell.openExternal("https://dreamsavior.net/docs/translator/parser-model-creator/");
    })

    $(window).on("resize", function() {
        clearTimeout(this.windowResizeTimeout);
        this.windowResizeTimeout = setTimeout(()=>{
            $(window).trigger("resizeEnd");
        }, 100);
    });


    $(window).on("resizeEnd", function() {
        console.log("resizeEnd");
    });

    $(document).on('keydown', function(e) {
		var keyCode = e.keyCode || e.which;

		switch (keyCode) {

			case 112 : //F1, about
				e.preventDefault();
                nw.Shell.openExternal("https://dreamsavior.net/docs/translator/parser-model-creator/");
                break;		
			case 122 : //F11, Full screen
				var win = win || nw.Window.get();
				win.maximize();
			break;		
		}

		// EDITING COMMAND
		if (e.ctrlKey) {
			switch(keyCode) {
				case 79 : //o
                    if (modelEditor.hasChange) {
                        var conf = confirm(t("Do you want to discard your changes and open a file?"));
                        if (!conf) return;
                    }
                    modelEditor.loadFromFile();
				break;
				case 83 : //s
					e.preventDefault();
					console.log("Pressing CTRL+s");
					modelEditor.saveTo();;
					//saveData();
				break;
			}
		} else if (e.altKey) {
			switch(keyCode) {
			}			
		} else if (e.shiftKey) {
			switch(keyCode) {
			}
		}
	});	

    // create tempalte
    modelEditor.initialize();
})