# doc.js

    var katex = require('../lib/katex/katex-modified.min.js');
    var AST = require('./ast.js');
    var Symbols = require('./symbols.js');
    var Utils = require('./utils.js');
    var Parsers = require('./parser.js');
    var Version = require('./version.js');

    /**
       @class
       @classdesc A class representing a Guppy document.  To access this
       class, use `Guppy.Doc`.  To get the document for a particular guppy
       instance, say called `"guppy1"`, do `Guppy("guppy1").doc()`.
       @param {string} [doc=<m><e></e></m>] - An XML string representing the document
       @constructor
     */
    var Doc = function(doc, type){
        type = type || "xml";
        if(type == "xml") this.set_content(doc || "<m><e></e></m>");
        else if(type == "latex") this.import_latex(doc);
        else if(type == "text") this.import_text(doc);
        else if(type == "ast") this.import_ast(doc);
        if(this.root().hasAttribute("v") && this.root().getAttribute("v") != Version.DOC_VERSION)
    	throw Version.DOC_ERROR;
        else
    	this.root().setAttribute("v",Version.DOC_VERSION);
    }

    Doc.prototype.is_small = function(nn){
        var n = nn.parentNode;
        while(n != null && n.nodeName != 'm'){
            if(n.getAttribute("small") == "yes") return true;
            n = n.parentNode
            while(n != null && n.nodeName != 'c') n = n.parentNode;
        }
        return false;
    }

    Doc.prototype.ensure_text_nodes = function(){
        var l = this.base.getElementsByTagName("e");
        for(var i = 0; i < l.length; i++){
            if(!(l[i].firstChild)) l[i].appendChild(this.base.createTextNode(""));
        }
    }

    Doc.prototype.is_blank = function(){
        if(this.base.getElementsByTagName("f").length > 0) return false;
        var l = this.base.getElementsByTagName("e");
        if(l.length == 1 && (!(l[0].firstChild) || l[0].firstChild.textContent == "")) return true;
        return false;
    }

    /**
        Get the document as a DOM object
        @memberof Doc
        @returns {Element}
    */
    Doc.prototype.root = function(){
        return this.base.documentElement;
    }

    /**
        Get the content of the document as a string
        @memberof Doc
        @param {string} t - The rendering method to use ("latex", "text", "ast" (for syntax tree), or "xml" (for internal XML representation))
        @returns {string}
    */
    Doc.prototype.get_content = function(t,r){
        if(t == "xml") return (new XMLSerializer()).serializeToString(this.base);
        else if(t == "ast") return JSON.stringify(this.syntax_tree());
        else if(t == "text") return AST.to_text(this.syntax_tree());
        else if(t == "function") return AST.to_function(this.syntax_tree());
        else if(t == "eqns") return JSON.stringify(AST.to_eqlist(this.syntax_tree()));
        else return this.manual_render(t,this.root(),r);
    }

    /**
        Evaluate the document using user-supplied functions to interpret symbols
        @memberof Doc
        @param {Object} evaluators - A dictionary where each key is a node
        type in the AST ("var", "val", "sin", "cos", etc.) and the
        corresponding value is a function that takes a list of argument
        (the results of evaluating that AST node's arguments) as well as,
        optionally, a second argument for the parent AST node to the one
        currently being evaluated.
        @returns {Object}
    */
    Doc.prototype.evaluate = function(evaluators){
        return AST.eval(this.syntax_tree(), evaluators);
    }

    Doc.prototype.import_text = function(text, syms, s2n){
        var ast = Parsers.TextParser.tokenise_and_parse(text);
        this.import_ast(ast, syms, s2n);
    }

    Doc.prototype.import_latex = function(text, syms, s2n){
        var ast = Parsers.LaTeXParser.tokenise_and_parse(text);
        this.import_ast(ast, syms, s2n);
    }

    Doc.prototype.import_ast = function(ast, syms, s2n){
        syms = syms || Symbols.symbols;
        s2n = s2n || Symbols.symbol_to_node;
        var doc = AST.to_xml(ast, syms, s2n);
        this.base = doc;
        this.ensure_text_nodes();
    }

    Doc.prototype.syntax_tree = function(n){
        n = n || this.root()
        if(n.nodeName == "f"){
            var ans = {"args":[], "kwargs":{}};
            ans['value'] = n.getAttribute("type");
            ans['type'] = "function";
            if(n.hasAttribute("ast_value")) ans['value'] = n.getAttribute("ast_value");
            if(n.hasAttribute("ast_type")) ans['type'] = n.getAttribute("ast_type");
            else if(Utils.is_char(n)) ans['type'] = "name";

            var iterator = this.xpath_list("./*[name()='c' or name()='l']", n)
            for(var nn = iterator.iterateNext(); nn != null; nn = iterator.iterateNext()){
                //if(nn.hasAttribute("name")) ans.kwargs[nn.getAttribute("name")] = this.syntax_tree(nn)
                //else ans.args.push(this.syntax_tree(nn))
                ans.args.push(this.syntax_tree(nn))
            }
        }
        else if(n.nodeName == "l"){
            ans = [];
            for(nn = n.firstChild; nn != null; nn = nn.nextSibling){
            ans.push(this.syntax_tree(nn));
            }
            ans = ["list",ans];
        }
        else if(n.nodeName == "c" || n.nodeName == "m"){
            if(n.hasAttribute("mode") && n.getAttribute("mode") == "text"){
                ans = n.firstChild.firstChild.textContent;
            }
            else{
                var tokens = []
                for(nn = n.firstChild; nn != null; nn = nn.nextSibling){
                    if(nn.nodeName == "e"){
                        tokens = tokens.concat(Parsers.EParser.tokenise(nn.firstChild.textContent));
                    }
                    else if(nn.nodeName == "f"){
                        tokens.push(this.syntax_tree(nn));
                    }
                }
                ans = Parsers.EParser.parse(tokens);
            }
        }
        return ans;
    }

    Doc.prototype.xpath_node = function(xpath, node){
        node = node || this.root()
        return this.base.evaluate(xpath, node, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    }

    Doc.prototype.xpath_list = function(xpath, node){
        node = node || this.root()
        return this.base.evaluate(xpath, node, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
    }

    /**
        Get the names of symbols used in this document
        @memberof Doc
        @param {string[]} [groups] - A list of groups you want strings for
        @returns {string[]}
    */
    Doc.prototype.get_symbols = function(groups){
        var types = {};
        var ans = [];
        var groups_selector = "//f";
        if(groups) groups_selector += "[" + groups.map(function(){ return ""; }).join(" or ") + "]";
        var iterator = this.xpath_list(groups_selector)
        for(var nn = iterator.iterateNext(); nn != null; nn = iterator.iterateNext())
            types[nn.getAttribute("type")] = true;
        for(var t in types)
            ans.push(t);
        return ans;
    }

    /**
        Set the content of the document
        @memberof Doc
        @param {string} xml_data - An XML string representing the content of the document
    */
    Doc.prototype.set_content = function(xml_data){
        this.base = (new window.DOMParser()).parseFromString(xml_data, "text/xml");
        this.ensure_text_nodes();
    }

    Doc.prototype.auto_bracket = function(n){
        var e0 = n.firstChild;
        var e1 = n.lastChild;
        if(n.childElementCount == 3 && e0.firstChild.textContent == "" && e1.firstChild.textContent == ""){ // single f child, all e children empty
            var f = e0.nextSibling;
    	var cs = 0;
    	var c = null;
    	// Count immediate children of f that are c nodes in cs and store the last one in c
    	for(var nn = f.firstChild; nn; nn = nn.nextSibling) if(nn.tagName == "c"){ c = nn; cs++; }
            if(cs == 1 && c.getAttribute("is_bracket") == "yes") return false; // if the f child is a bracket, don't bracket
            if(Utils.is_char(f) && e0.getAttribute("current") != "yes" && e0.getAttribute("temp") != "yes" && e1.getAttribute("current") != "yes" && e1.getAttribute("temp") != "yes") return false; // if the f child is a character and not current or temp cursor location, don't bracket
        }
        else if(n.childElementCount == 1){ // Single e child
            var s = e0.firstChild.textContent;
            if(s.length != 1 && Number(s)+"" != s) return true; // If content is neither a single character nor a number, bracket it
            if(e0.getAttribute("current") == "yes" || e0.getAttribute("temp") == "yes") return true; // If content has the cursor or temp cursor, bracket it
            return false;
        }
        return true;
    }

    Doc.prototype.manual_render = function(t,n,r){
        var ans = "";
        var nn = null;
        var i = null;
        if(n.nodeName == "e"){
            if(t == "latex" && r){
                ans = n.getAttribute("render");
            }
            else{
                ans = n.firstChild.textContent;
            }
        }
        else if(n.nodeName == "f"){
            var real_type = (t == "latex" && this.is_small(n)) ? "small_latex" : t;
            nn = this.xpath_node("./b[@p='"+real_type+"']", n) || this.xpath_node("./b[@p='"+t+"']", n);
            if(nn) ans = this.manual_render(t,nn,r);
        }
        else if(n.nodeName == "b"){
            var cs = []
            i = 1;
            var par = n.parentNode;
            for(nn = par.firstChild; nn != null; nn = nn.nextSibling)
                if(nn.nodeName == "c" || nn.nodeName == "l") cs[i++] = this.manual_render(t,nn,r);
            for(nn = n.firstChild; nn != null; nn = nn.nextSibling){
                if(nn.nodeType == 3) ans += nn.textContent;
                else if(nn.nodeType == 1){
                    if(nn.hasAttribute("d")){
                        var dim = parseInt(nn.getAttribute("d"));
                        var joiner = function(d,l){
                            if(d > 1) for(var k = 0; k < l.length; k++) l[k] = joiner(d-1,l[k]);
                            return l.join(nn.getAttribute('sep'+(d-1)));
                        }
                    ans += joiner(dim,cs[parseInt(nn.getAttribute("ref"))]);
                    }
                    else ans += cs[parseInt(nn.getAttribute("ref"))];
                }
            }
        }
        else if(n.nodeName == "l"){
            ans = [];
            i = 0;
            for(nn = n.firstChild; nn != null; nn = nn.nextSibling){
                ans[i++] = this.manual_render(t,nn,r);
            }
        }
        else if(n.nodeName == "c" || n.nodeName == "m"){
            for(nn = n.firstChild; nn != null; nn = nn.nextSibling)
                ans += this.manual_render(t,nn,r);
            if(t == "latex" && n.getAttribute("bracket") == "yes" && this.auto_bracket(n)) {
                ans = "\\left("+ans+"\\right)";
            }
        }
        return ans;
    }

    /**
        Render all guppy documents on the page.
        @memberof Doc
    */
    Doc.render_all = function(t, delim){
        var l,i,n,d,s,ans = [];
        if(!t || t == "xml"){
            l = document.getElementsByTagName("script");
            for(i = 0; i < l.length; i++){
                if(l[i].getAttribute("type") == "text/guppy_xml"){
                    n = l[i];
                    d = new Doc(n.innerHTML);
                    s = document.createElement("span");
    		var len = ans.length;
    		var new_id = "guppy-"+t+"-render-"+len;
    		while(document.getElementById(new_id)) new_id = "guppy-xml-render-"+(++len);
                    s.setAttribute("id",new_id);
                    s.setAttribute("class","guppy-render");
                    katex.render(d.get_content("latex"), s);
                    n.parentNode.insertBefore(s, n);
                    n.parentNode.removeChild(n);
                    ans.push({"container":s, "doc":d})
                }
            }
        }
        else {
            var subs = function(node) {
                if(!node) return;
                var excludeElements = ['script', 'style', 'iframe', 'canvas'];
                do {
                    switch (node.nodeType) {
                    case 1:
                        // Don't process KaTeX elements, Guppy instances, Javascript, or CSS
                        if (excludeElements.indexOf(node.tagName.toLowerCase()) > -1 || (" "+node.getAttribute("class")+" ").indexOf(" katex ") > -1) {
                            continue;
                        }
                        subs(node.firstChild);
                        break;
                    case 3:
                        var text_node = node;
                        var offset = text_node.textContent.indexOf(delim);
                        while(offset > -1){
                            var next = text_node.textContent.substring(offset+delim.length).indexOf(delim);
                            if(next == -1) break;
                            var before = text_node.textContent.substring(0,offset);
                            var content = text_node.textContent.substring(offset+delim.length,offset+delim.length+next);
                            var after = text_node.textContent.substring(offset+delim.length+next+delim.length);

                            // Make the span to render the doc in
                            var s = document.createElement("span");
    			var l = ans.length;
    			var new_id = "guppy-"+t+"-render-"+l;
    			while(document.getElementById(new_id)) new_id = "guppy-"+t+"-render-"+(++l);
                            s.setAttribute("id",new_id);
    			s.setAttribute("class","guppy-render");

    			try {
                                // Create the document
                                d = new Doc(content,t);

                                // Render the doc
                                katex.render(d.get_content("latex"), s);
    			}
    			catch (e) {
    			    s.innerHTML = "ERROR: "+e.message;
    			}
                            var new_node = document.createTextNode(after)
                            text_node.parentNode.insertBefore(document.createTextNode(before), text_node);
                            text_node.parentNode.insertBefore(s, text_node);
                            text_node.parentNode.insertBefore(new_node, text_node);
                            text_node.parentNode.removeChild(text_node);
                            text_node = new_node;
                            ans.push({"id":new_id, "doc":d})

                            // Place the right data in the remainder of the node
                            text_node.textContent = text_node.textContent.substring(next-offset+delim.length);
                            offset = text_node.textContent.indexOf(delim);
                        }
                        break;
    		default:
                        break;
                    }
                } while ((node = node.nextSibling));

            }
            delim = delim || "$$";
            subs(document.documentElement);
        }
        return ans;
    }

    /**
        Render a given document into a specified HTML element.
        @param {string} doc - A GuppyXML string to be rendered
        @param {string} target_id - The ID of the HTML element to render into
        @memberof Doc
    */
    Doc.render = function(doc, target_id){
        var d = new Doc(doc);
        var target = document.getElementById(target_id);
        katex.render(d.get_content("latex"), target);
        return {"container":target, "doc":d};
    }

    module.exports = Doc;