(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define([], factory);
    } else if (typeof module === 'object' && module.exports) {
        module.exports = factory();
    } else {
        root.insight = factory();
    }
} (this, function () {
    'use strict';

    let document;
    let strsData, mods, tagIds;
    let domPathInput, domFuzzyMatch;
    let domTBody;
    let domPlaceholder = null;
    let placeholderVisible = false;

    //--------------------------------------------------------------------------
    // DOM Helper Functions
    //--------------------------------------------------------------------------
    function domNewText(text) {
        return document.createTextNode(text);
    }

    function domNewElem(type) {
        let dom = document.createElement(type);
        for (let i = 1; i < arguments.length; ++i) {
            let arg = arguments[i];
            if (typeof(arg) == 'string' || typeof(arg) == 'number') {
                arg = domNewText(arg)
            }
            dom.appendChild(arg);
        }
        return dom;
    }

    function domNewLink(text, onClick) {
        let dom = domNewElem('a', text);
        dom.setAttribute('href', '#');
        dom.addEventListener('click', onClick);
        return dom;
    }

    //--------------------------------------------------------------------------
    // Module Row
    //--------------------------------------------------------------------------
    function countDeps(deps) {
        let direct = 0;
        let indirect = 0;
        if (deps.length > 0) {
            direct = deps[0].length;
            for (let i = 1; i < deps.length; ++i) {
                indirect += deps[i].length;
            }
        }
        return [direct, indirect];
    }

    function Module(id, modData) {
        this.id = id;
        this.path = strsData[modData[0]];
        this.cls = modData[1];
        this.tagIds = new Set(modData[2]);
        this.deps = modData[3];
        this.users = modData[4];
        this.srcDirs = modData[5].map(function (x) { return strsData[x]; });

        [this.numDirectDeps, this.numIndirectDeps] = countDeps(this.deps);
        this.numUsers = this.users.length;

        this.dom = null;
        this.visible = false;

        this.linkDoms = Object.create(null);
    }

    Module.prototype.isTagged = function (tagId) {
        return this.tagIds.has(tagId);
    }

    Module.prototype.createModuleLinkDom = function (mod) {
        let dom = domNewElem('a', mod.path);
        dom.setAttribute('href', '#mod_' + mod.id);
        dom.setAttribute('data-mod-id', mod.id);
        dom.setAttribute('data-owner-id', this.id);
        dom.addEventListener('click', onModuleLinkClicked);
        dom.addEventListener('mouseover', onModuleLinkMouseOver);
        dom.addEventListener('mouseout', onModuleLinkMouseOut);

        this.linkDoms[mod.id] = dom;

        return dom;
    }

    Module.prototype.createModuleRelationsDom = function (parent, label,
                                                          modIds) {
        parent.appendChild(domNewElem('h2', label));

        let domOl = domNewElem('ol');
        parent.appendChild(domOl);
        for (let modId of modIds) {
            domOl.appendChild(
                    domNewElem('li', this.createModuleLinkDom(mods[modId])));
        }
    }

    Module.prototype.createModulePathTdDom = function (parent) {
        let domTd = domNewElem('td');
        domTd.appendChild(domNewElem('p', this.createModuleLinkDom(this)));
        for (let dir of this.srcDirs) {
            let domP = domNewElem('p', 'source: ' + dir);
            domP.setAttribute('class', 'module_src_dir');
            domTd.appendChild(domP);
        }
        parent.appendChild(domTd);
    }

    Module.prototype.createTagsTdDom = function (parent) {
        let domTd = domNewElem('td');
        for (let tag of this.tagIds) {
            domTd.appendChild(domNewElem('p', strsData[tag]));
        }
        parent.appendChild(domTd);
    }

    Module.prototype.createDepsTdDom = function (parent) {
        let domTd = domNewElem(
                'td', this.numDirectDeps + ' + ' + this.numIndirectDeps);

        let deps = this.deps;
        if (deps.length > 0) {
            this.createModuleRelationsDom(domTd, 'Direct', deps[0]);

            for (let i = 1; i < deps.length; ++i) {
                this.createModuleRelationsDom(domTd, 'Indirect #' + i, deps[i]);
            }
        }

        parent.appendChild(domTd);
    }

    Module.prototype.createUsersTdDom = function (parent) {
        let domTd = domNewElem('td', this.numUsers);

        let users = this.users;
        if (users.length > 0) {
            this.createModuleRelationsDom(domTd, 'Direct', users);
        }

        parent.appendChild(domTd);
    }

    Module.prototype.createDom = function () {
        let dom = this.dom = domNewElem('tr');
        dom.setAttribute('id', 'mod_'  + this.id);

        this.createModulePathTdDom(dom);
        this.createTagsTdDom(dom);
        this.createDepsTdDom(dom);
        this.createUsersTdDom(dom)
    }

    Module.prototype.showDom = function () {
        hidePlaceholder();
        if (this.visible) {
            return;
        }
        if (this.dom === null) {
            this.createDom();
        }
        domTBody.appendChild(this.dom);
        this.visible = true;
    }

    Module.prototype.hideDom = function () {
        if (!this.visible) {
            return;
        }
        this.dom.parentNode.removeChild(this.dom);
        this.visible = false;
    }

    function createModulesFromData(stringsData, modulesData) {
        return modulesData.map(function (modData, id) {
            return new Module(id, modData);
        });
    }

    function createTagIdsFromData(stringsData, mods) {
        let tagIds = new Set();
        for (let mod of mods) {
            for (let tag of mod.tagIds) {
                tagIds.add(tag);
            }
        }

        tagIds = Array.from(tagIds);
        tagIds.sort(function (a, b) {
            return strsData[a].localeCompare(strsData[b]);
        });

        return tagIds;
    }

    //--------------------------------------------------------------------------
    // Data
    //--------------------------------------------------------------------------
    function init(doc, stringsData, modulesData) {
        document = doc;
        strsData = stringsData;

        mods = createModulesFromData(stringsData, modulesData);
        tagIds = createTagIdsFromData(stringsData, mods);

        document.addEventListener('DOMContentLoaded', function (evt) {
            createControlDom(document.body);
            createTableDom(document.body);
        });
    }

    //--------------------------------------------------------------------------
    // Control
    //--------------------------------------------------------------------------
    function createControlDom(parent) {
        let domTBody = domNewElem('tbody');

        createSelectionTrDom(domTBody);
        createAddByTagsTrDom(domTBody);
        createAddByPathTrDom(domTBody);

        let domTable = domNewElem('table', domTBody);
        domTable.id = 'control';

        let domFixedLink = domNewElem('a', 'Menu');
        domFixedLink.href = '#control';
        domFixedLink.id = 'control_menu';

        parent.appendChild(domFixedLink);
        parent.appendChild(domTable);
    }

    function createControlMenuTr(parent, label, items) {
        let domUl = domNewElem('ul');
        domUl.className = 'menu';
        for (let [txt, callback] of items) {
            domUl.appendChild(domNewElem('li', domNewLink(txt, callback)));
        }

        let domTr = domNewElem('tr',
                               createControlLabelTdDom(label),
                               domNewElem('td', domUl));

        parent.appendChild(domTr);
    }

    function createSelectionTrDom(parent) {
        const items = [
            ['All', onAddAll],
            ['32-bit', onAddAll32],
            ['64-bit', onAddAll64],
            ['Clear', onClear],
        ];

        createControlMenuTr(parent, 'Selection:', items);
    }

    function createAddByTagsTrDom(parent) {
        if (tagIds.length == 0) {
            return;
        }

        const items = tagIds.map(function (tagId) {
            return [strsData[tagId], function (evt) {
                evt.preventDefault(true);
                showModulesByTagId(tagId);
            }];
        });

        createControlMenuTr(parent, 'Add by Tags:', items);
    }

    function createAddByPathTrDom(parent) {
        let domForm = domNewElem('form');
        domForm.addEventListener('submit', onAddModuleByPath);

        domPathInput = domNewElem('input');
        domPathInput.type = 'text';
        domForm.appendChild(domPathInput);

        let domBtn = domNewElem('input');
        domBtn.type = 'submit';
        domBtn.value = 'Add';
        domForm.appendChild(domBtn);

        domFuzzyMatch = domNewElem('input');
        domFuzzyMatch.setAttribute('id', 'fuzzy_match');
        domFuzzyMatch.setAttribute('type', 'checkbox');
        domFuzzyMatch.setAttribute('checked', 'checked');
        domForm.appendChild(domFuzzyMatch);

        let domFuzzyMatchLabel = domNewElem('label', 'Fuzzy Match');
        domFuzzyMatchLabel.setAttribute('for', 'fuzzy_match');
        domForm.appendChild(domFuzzyMatchLabel);

        let domTr = domNewElem('tr',
                               createControlLabelTdDom('Add by Path:'),
                               domNewElem('td', domForm));

        parent.appendChild(domTr);
    }

    function createControlLabelTdDom(text) {
        return domNewElem('td', domNewElem('strong', text));
    }


    //--------------------------------------------------------------------------
    // Table
    //--------------------------------------------------------------------------
    function createTableDom(parent) {
        domTBody = domNewElem('tbody');
        domTBody.id = 'module_tbody';

        createTableHeaderDom(domTBody);

        showPlaceholder();

        let domTable  = domNewElem('table', domTBody);
        domTable.id = 'module_table';

        parent.appendChild(domTable);
    }

    function createTableHeaderDom(parent) {
        const labels = [
            'Name',
            'Tags',
            'Dependencies (Direct + Indirect)',
            'Users',
        ];

        let domTr = domNewElem('tr');
        for (let label of labels) {
            domTr.appendChild(domNewElem('th', label));
        }

        parent.appendChild(domTr);
    }

    function createPlaceholder() {
        let domTd = domNewElem('td');
        domTd.setAttribute('colspan', 4);
        domTd.setAttribute('id', 'no_module_placeholder');
        domTd.appendChild(domNewText(
            'No modules are selected.  Click the menu to select modules by ' +
            'names or categories.'));
        domPlaceholder = domNewElem('tr', domTd);
    }

    function showPlaceholder() {
        if (placeholderVisible) {
            return;
        }
        placeholderVisible = true;
        if (domPlaceholder === null) {
            createPlaceholder();
        }
        domTBody.appendChild(domPlaceholder);
    }

    function hidePlaceholder() {
        if (placeholderVisible) {
            domTBody.removeChild(domPlaceholder);
            placeholderVisible = false;
        }
    }

    function hideAllModules() {
        for (let mod of mods) {
            mod.hideDom();
        }
        showPlaceholder();
    }

    function showAllModules() {
        for (let mod of mods) {
            mod.showDom();
        }
    }

    function showModulesByFilter(pred) {
        let numMatched = 0;
        for (let mod of mods) {
            if (pred(mod)) {
                mod.showDom();
                ++numMatched;
            }
        }
        return numMatched;
    }

    function showModulesByTagId(tagId) {
        showModulesByFilter(function (mod) {
            return mod.isTagged(tagId);
        });
    }


    //--------------------------------------------------------------------------
    // Events
    //--------------------------------------------------------------------------

    function onAddModuleByPath(evt) {
        evt.preventDefault();

        let path = domPathInput.value;
        domPathInput.value = '';

        function escapeRegExp(pattern) {
            return pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
        }

        function createFuzzyMatcher() {
            let parts = path.split(/\/+/g);
            let pattern = '';
            for (let part of parts) {
                pattern += escapeRegExp(part) + '(?:/[^\/]*)*';
            }
            pattern = RegExp(pattern);

            return function (mod) {
                return pattern.test(mod.path);
            };
        }

        function exactMatcher(mod) {
            return mod.path == path;
        }

        let numMatched = showModulesByFilter(
            domFuzzyMatch.checked ? createFuzzyMatcher() : exactMatcher);

        if (numMatched == 0) {
            alert('No matching modules: ' + path);
        }
    }

    function onAddAll(evt) {
        evt.preventDefault(true);
        hideAllModules();
        showAllModules();
    }

    function onAddAllClass(evt, cls) {
        evt.preventDefault(true);
        hideAllModules();
        showModulesByFilter(function (mod) {
            return mod.cls == cls;
        });
    }

    function onAddAll32(evt) {
        onAddAllClass(evt, 32);
    }

    function onAddAll64(evt) {
        onAddAllClass(evt, 64);
    }

    function onClear(evt) {
        evt.preventDefault(true);
        hideAllModules();
    }

    function onModuleLinkClicked(evt) {
        let modId = parseInt(evt.target.getAttribute('data-mod-id'), 10);
        mods[modId].showDom();
    }

    function setDirectDepBackgroundColor(modId, ownerId, color) {
        let mod = mods[modId];
        let owner = mods[ownerId];
        let ownerLinkDoms = owner.linkDoms;
        if (mod.deps.length > 0) {
            for (let depId of mod.deps[0]) {
                if (depId in ownerLinkDoms) {
                    ownerLinkDoms[depId].style.backgroundColor = color;
                }
            }
        }
    }

    function onModuleLinkMouseOver(evt) {
        let modId = parseInt(evt.target.getAttribute('data-mod-id'), 10);
        let ownerId = parseInt(evt.target.getAttribute('data-owner-id'), 10);
        setDirectDepBackgroundColor(modId, ownerId, '#ffff00');
    }

    function onModuleLinkMouseOut(evt) {
        let modId = parseInt(evt.target.getAttribute('data-mod-id'), 10);
        let ownerId = parseInt(evt.target.getAttribute('data-owner-id'), 10);
        setDirectDepBackgroundColor(modId, ownerId, 'transparent');
    }

    return {
        'init': init,
    };
}));