/*
Copyright The Infusion copyright holders
See the AUTHORS.md file at the top-level directory of this distribution and at
https://github.com/fluid-project/infusion/raw/main/AUTHORS.md.

Licensed under the Educational Community License (ECL), Version 2.0 or the New
BSD license. You may not use this file except in compliance with one these
Licenses.

You may obtain a copy of the ECL 2.0 License and BSD License at
https://github.com/fluid-project/infusion/raw/main/Infusion-LICENSE.txt
*/

"use strict";

// unsupported, non-API function
fluid.parseTemplate = function (template, baseURL, scanStart, cutpoints_in, opts) {
    opts = opts || {};

    if (!template) {
        fluid.fail("empty template supplied to fluid.parseTemplate");
    }

    var t;
    var parser;
    var tagstack;
    var lumpindex = 0;
    var nestingdepth = 0;
    var justended = false;

    var defstart = -1;
    var defend = -1;

    var debugMode = false;

    var cutpoints = []; // list of selector, tree, id
    var simpleClassCutpoints = {};

    var cutstatus = [];

    var XMLLump = function (lumpindex, nestingdepth) {
        return {
            //rsfID: "",
            //text: "",
            //downmap: {},
            //attributemap: {},
            //finallump: {},
            nestingdepth: nestingdepth,
            lumpindex: lumpindex,
            parent: t
        };
    };

    function isSimpleClassCutpoint(tree) {
        return tree.length === 1 && tree[0].predList.length === 1 && tree[0].predList[0].clazz;
    }

    function init(baseURLin, debugModeIn, cutpointsIn) {
        t.rootlump = XMLLump(0, -1); // eslint-disable-line new-cap
        tagstack = [t.rootlump];
        lumpindex = 0;
        nestingdepth = 0;
        justended = false;
        defstart = -1;
        defend = -1;
        baseURL = baseURLin;
        debugMode = debugModeIn;
        if (cutpointsIn) {
            for (var i = 0; i < cutpointsIn.length; ++i) {
                var tree = fluid.parseSelector(cutpointsIn[i].selector, fluid.simpleCSSMatcher);
                var clazz = isSimpleClassCutpoint(tree);
                if (clazz) {
                    simpleClassCutpoints[clazz] = cutpointsIn[i].id;
                }
                else {
                    cutstatus.push([]);
                    cutpoints.push($.extend({}, cutpointsIn[i], {tree: tree}));
                }
            }
        }
    }

    function findTopContainer() {
        for (var i = tagstack.length - 1; i >= 0; --i) {
            var lump = tagstack[i];
            if (lump.rsfID !== undefined) {
                return lump;
            }
        }
        return t.rootlump;
    }

    function newLump() {
        var togo = XMLLump(lumpindex, nestingdepth); // eslint-disable-line new-cap
        if (debugMode) {
            togo.line = parser.getLineNumber();
            togo.column = parser.getColumnNumber();
        }
        //togo.parent = t;
        t.lumps[lumpindex] = togo;
        ++lumpindex;
        return togo;
    }

    function addLump(mmap, ID, lump) {
        var list = mmap[ID];
        if (!list) {
            list = [];
            mmap[ID] = list;
        }
        list[list.length] = lump;
    }

    function checkContribute(ID, lump) {
        if (ID.indexOf("scr=contribute-") !== -1) {
            var scr = ID.substring("scr=contribute-".length);
            addLump(t.collectmap, scr, lump);
        }
    }

    function debugLump(lump) {
        // TODO expand this to agree with the Firebug "self-selector" idiom
        return "<" + lump.tagname + ">";
    }

    function hasCssClass(clazz, totest) {
        if (!totest) {
            return false;
        }
        // algorithm from jQuery
        return (" " + totest + " ").indexOf(" " + clazz + " ") !== -1;
    }

    function matchNode(term, headlump, headclazz) {
        if (term.predList) {
            for (var i = 0; i < term.predList.length; ++i) {
                var pred = term.predList[i];
                if (pred.id && headlump.attributemap.id !== pred.id) {return false;}
                if (pred.clazz && !hasCssClass(pred.clazz, headclazz)) {return false;}
                if (pred.tag && headlump.tagname !== pred.tag) {return false;}
            }
            return true;
        }
    }

    function tagStartCut(headlump) {
        var togo;
        var headclazz = headlump.attributemap["class"];
        var i;
        if (headclazz) {
            var split = headclazz.split(" ");
            for (i = 0; i < split.length; ++i) {
                var simpleCut = simpleClassCutpoints[split[i].trim()];
                if (simpleCut) {
                    return simpleCut;
                }
            }
        }
        for (i = 0; i < cutpoints.length; ++i) {
            var cut = cutpoints[i];
            var cutstat = cutstatus[i];
            var nextterm = cutstat.length; // the next term for this node
            if (nextterm < cut.tree.length) {
                var term = cut.tree[nextterm];
                if (nextterm > 0) {
                    if (cut.tree[nextterm - 1].child &&
                            cutstat[nextterm - 1] !== headlump.nestingdepth - 1) {
                        continue; // it is a failure to match if not at correct nesting depth
                    }
                }
                var isMatch = matchNode(term, headlump, headclazz);
                if (isMatch) {
                    cutstat[cutstat.length] = headlump.nestingdepth;
                    if (cutstat.length === cut.tree.length) {
                        if (togo !== undefined) {
                            fluid.fail("Cutpoint specification error - node " +
                                debugLump(headlump) +
                                " has already matched with rsf:id of " + togo);
                        }
                        if (cut.id === undefined || cut.id === null) {
                            fluid.fail("Error in cutpoints list - entry at position " + i + " does not have an id set");
                        }
                        togo = cut.id;
                    }
                }
            }
        }
        return togo;
    }

    function tagEndCut() {
        if (cutpoints) {
            for (var i = 0; i < cutpoints.length; ++i) {
                var cutstat = cutstatus[i];
                if (cutstat.length > 0 && fluid.peek(cutstat) === nestingdepth) {
                    cutstat.length--;
                }
            }
        }
    }

    function processTagEnd() {
        tagEndCut();
        var endlump = newLump();
        --nestingdepth;
        endlump.text = "</" + parser.getName() + ">";
        var oldtop = fluid.peek(tagstack);
        oldtop.close_tag = t.lumps[lumpindex - 1];
        tagstack.length--;
        justended = true;
    }

    function processTagStart(isempty) {
        ++nestingdepth;
        if (justended) {
            justended = false;
            var backlump = newLump();
            backlump.nestingdepth--;
        }
        if (t.firstdocumentindex === -1) {
            t.firstdocumentindex = lumpindex;
        }
        var headlump = newLump();
        var stacktop = fluid.peek(tagstack);
        headlump.uplump = stacktop;
        var tagname = parser.getName();
        headlump.tagname = tagname;
        // NB - attribute names and values are now NOT DECODED!!
        var attrs = headlump.attributemap = parser.m_attributes;
        var ID = attrs[fluid.ID_ATTRIBUTE];
        if (ID === undefined) {
            ID = tagStartCut(headlump);
        }
        for (var attrname in attrs) {
            if (ID === undefined) {
                if (/href|src|codebase|action/.test(attrname)) {
                    ID = "scr=rewrite-url";
                }
                // port of TPI effect of IDRelationRewriter
                else if (ID === undefined && /for|headers/.test(attrname)) {
                    ID = "scr=null";
                }
            }
        }

        if (ID) {
            // TODO: ensure this logic is correct on RSF Server
            if (ID.charCodeAt(0) === 126) { // "~"
                ID = ID.substring(1);
                headlump.elide = true;
            }
            checkContribute(ID, headlump);
            headlump.rsfID = ID;
            var downreg = findTopContainer();
            if (!downreg.downmap) {
                downreg.downmap = {};
            }
            while (downreg) { // TODO: unusual fix for locating branches in parent contexts (applies to repetitive leaves)
                if (downreg.downmap) {
                    addLump(downreg.downmap, ID, headlump);
                }
                downreg = downreg.uplump;
            }
            addLump(t.globalmap, ID, headlump);
            var colpos = ID.indexOf(":");
            if (colpos !== -1) {
                var prefix = ID.substring(0, colpos);
                if (!stacktop.finallump) {
                    stacktop.finallump = {};
                }
                stacktop.finallump[prefix] = headlump;
            }
        }

        // TODO: accelerate this by grabbing original template text (requires parser
        // adjustment) as well as dealing with empty tags
        headlump.text = "<" + tagname + fluid.dumpAttributes(attrs) + (isempty && !ID ? "/>" : ">");
        tagstack[tagstack.length] = headlump;
        if (isempty) {
            if (ID) {
                processTagEnd();
            }
            else {
                --nestingdepth;
                tagstack.length--;
            }
        }
    }



    function processDefaultTag() {
        if (defstart !== -1) {
            if (t.firstdocumentindex === -1) {
                t.firstdocumentindex = lumpindex;
            }
            var text = parser.getContent().substr(defstart, defend - defstart);
            justended = false;
            var newlump = newLump();
            newlump.text = text;
            defstart = -1;
        }
    }

    /** ACTUAL BODY of fluid.parseTemplate begins here **/

    t = fluid.XMLViewTemplate();

    init(baseURL, opts.debugMode, cutpoints_in);

    var idpos = template.indexOf(fluid.ID_ATTRIBUTE);
    if (scanStart) {
        var brackpos = template.indexOf(">", idpos);
        parser = fluid.XMLP(template.substring(brackpos + 1));
    }
    else {
        parser = fluid.XMLP(template);
    }

parseloop: // eslint-disable-line indent
    while (true) {
        var iEvent = parser.next();
        switch (iEvent) {
        case fluid.XMLP._ELM_B:
            processDefaultTag();
            //var text = parser.getContent().substr(parser.getContentBegin(), parser.getContentEnd() - parser.getContentBegin());
            processTagStart(false, "");
            break;
        case fluid.XMLP._ELM_E:
            processDefaultTag();
            processTagEnd();
            break;
        case fluid.XMLP._ELM_EMP:
            processDefaultTag();
            //var text = parser.getContent().substr(parser.getContentBegin(), parser.getContentEnd() - parser.getContentBegin());
            processTagStart(true, "");
            break;
        case fluid.XMLP._PI:
        case fluid.XMLP._DTD:
            defstart = -1;
            continue; // not interested in reproducing these
        case fluid.XMLP._TEXT:
        case fluid.XMLP._ENTITY:
        case fluid.XMLP._CDATA:
        case fluid.XMLP._COMMENT:
            if (defstart === -1) {
                defstart = parser.m_cB;
            }
            defend = parser.m_cE;
            break;
        case fluid.XMLP._ERROR:
            fluid.setLogging(true);
            var message = "Error parsing template: " + parser.m_cAlt + " at line " + parser.getLineNumber();
            fluid.log(message);
            fluid.log("Just read: " + parser.m_xml.substring(parser.m_iP - 30, parser.m_iP));
            fluid.log("Still to read: " + parser.m_xml.substring(parser.m_iP, parser.m_iP + 30));
            fluid.fail(message);
            break parseloop;
        case fluid.XMLP._NONE:
            break parseloop;
        }
    }
    processDefaultTag();
    var excess = tagstack.length - 1;
    if (excess) {
        fluid.fail("Error parsing template - unclosed tag(s) of depth " + (excess) +
            ": " + fluid.transform(tagstack.splice(1, excess), function (lump) {return debugLump(lump);}).join(", "));
    }
    return t;
};

// unsupported, non-API function
fluid.debugLump = function (lump) {
    var togo = lump.text;
    togo += " at ";
    togo += "lump line " + lump.line + " column " + lump.column + " index " + lump.lumpindex;
    togo += lump.parent.href === null ? "" : " in file " + lump.parent.href;
    return togo;
};

// Public definitions begin here

fluid.ID_ATTRIBUTE = "rsf:id";

// unsupported, non-API function
fluid.getPrefix = function (id) {
    var colpos = id.indexOf(":");
    return colpos === -1 ? id : id.substring(0, colpos);
};

// unsupported, non-API function
fluid.SplitID = function (id) {
    var that = {};
    var colpos = id.indexOf(":");
    if (colpos === -1) {
        that.prefix = id;
    }
    else {
        that.prefix = id.substring(0, colpos);
        that.suffix = id.substring(colpos + 1);
    }
    return that;
};

// unsupported, non-API function
fluid.XMLViewTemplate = function () {
    return {
        globalmap: {},
        collectmap: {},
        lumps: [],
        firstdocumentindex: -1
    };
};

// TODO: find faster encoder
fluid.XMLEncode = function (text) {
    return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\"/g, "&quot;");
};

// unsupported, non-API function
fluid.dumpAttributes = function (attrcopy) {
    var togo = "";
    for (var attrname in attrcopy) {
        var attrvalue = attrcopy[attrname];
        if (attrvalue !== null && attrvalue !== undefined) {
            togo += " " + attrname + "=\"" + attrvalue + "\"";
        }
    }
    return togo;
};

// unsupported, non-API function
fluid.aggregateMMap = function (target, source) {
    for (var key in source) {
        var targhas = target[key];
        if (!targhas) {
            target[key] = [];
        }
        target[key] = target[key].concat(source[key]);
    }
};

/* Returns a "template structure", with globalmap in the root, and a list
 * of entries {url, template, cutpoints} for each parsed template.
 */
fluid.parseTemplates = function (resourceSpec, templateList, opts) {
    var togo = [];
    opts = opts || {};
    togo.globalmap = {};
    for (var i = 0; i < templateList.length; ++i) {
        var resource = resourceSpec[templateList[i]];
        var lastslash = resource.url.lastIndexOf("/");
        var baseURL = lastslash === -1 ? "" : resource.url.substring(0, lastslash + 1);

        var template = fluid.parseTemplate(resource.resourceText, baseURL,
            opts.scanStart && i === 0, resource.cutpoints, opts);
        if (i === 0) {
            fluid.aggregateMMap(togo.globalmap, template.globalmap);
        }
        template.url = resource.url;
        template.baseURL = baseURL;
        template.resourceKey = resource.resourceKey;

        togo[i] = template;
        fluid.aggregateMMap(togo.globalmap, template.rootlump.downmap);
    }
    return togo;
};
