1(function (root, factory) {
2    if (typeof define === 'function' && define.amd) {
3        define([], factory);
4    } else if (typeof module === 'object' && module.exports) {
5        module.exports = factory();
6    } else {
7        root.insight = factory();
8    }
9} (this, function () {
10    'use strict';
11
12    let document;
13    let strsData, mods, tagIds;
14    let domPathInput, domFuzzyMatch;
15    let domTBody;
16    let domPlaceholder = null;
17    let placeholderVisible = false;
18
19    //--------------------------------------------------------------------------
20    // DOM Helper Functions
21    //--------------------------------------------------------------------------
22    function domNewText(text) {
23        return document.createTextNode(text);
24    }
25
26    function domNewElem(type) {
27        let dom = document.createElement(type);
28        for (let i = 1; i < arguments.length; ++i) {
29            let arg = arguments[i];
30            if (typeof(arg) == 'string' || typeof(arg) == 'number') {
31                arg = domNewText(arg)
32            }
33            dom.appendChild(arg);
34        }
35        return dom;
36    }
37
38    function domNewLink(text, onClick) {
39        let dom = domNewElem('a', text);
40        dom.setAttribute('href', '#');
41        dom.addEventListener('click', onClick);
42        return dom;
43    }
44
45    //--------------------------------------------------------------------------
46    // Module Row
47    //--------------------------------------------------------------------------
48    function countDeps(deps) {
49        let direct = 0;
50        let indirect = 0;
51        if (deps.length > 0) {
52            direct = deps[0].length;
53            for (let i = 1; i < deps.length; ++i) {
54                indirect += deps[i].length;
55            }
56        }
57        return [direct, indirect];
58    }
59
60    function Module(id, modData) {
61        this.id = id;
62        this.path = strsData[modData[0]];
63        this.cls = modData[1];
64        this.tagIds = new Set(modData[2]);
65        this.deps = modData[3];
66        this.users = modData[4];
67        this.srcDirs = modData[5].map(function (x) { return strsData[x]; });
68
69        [this.numDirectDeps, this.numIndirectDeps] = countDeps(this.deps);
70        this.numUsers = this.users.length;
71
72        this.dom = null;
73        this.visible = false;
74
75        this.linkDoms = Object.create(null);
76    }
77
78    Module.prototype.isTagged = function (tagId) {
79        return this.tagIds.has(tagId);
80    }
81
82    Module.prototype.createModuleLinkDom = function (mod) {
83        let dom = domNewElem('a', mod.path);
84        dom.setAttribute('href', '#mod_' + mod.id);
85        dom.setAttribute('data-mod-id', mod.id);
86        dom.setAttribute('data-owner-id', this.id);
87        dom.addEventListener('click', onModuleLinkClicked);
88        dom.addEventListener('mouseover', onModuleLinkMouseOver);
89        dom.addEventListener('mouseout', onModuleLinkMouseOut);
90
91        this.linkDoms[mod.id] = dom;
92
93        return dom;
94    }
95
96    Module.prototype.createModuleRelationsDom = function (parent, label,
97                                                          modIds) {
98        parent.appendChild(domNewElem('h2', label));
99
100        let domOl = domNewElem('ol');
101        parent.appendChild(domOl);
102        for (let modId of modIds) {
103            domOl.appendChild(
104                    domNewElem('li', this.createModuleLinkDom(mods[modId])));
105        }
106    }
107
108    Module.prototype.createModulePathTdDom = function (parent) {
109        let domTd = domNewElem('td');
110        domTd.appendChild(domNewElem('p', this.createModuleLinkDom(this)));
111        for (let dir of this.srcDirs) {
112            let domP = domNewElem('p', 'source: ' + dir);
113            domP.setAttribute('class', 'module_src_dir');
114            domTd.appendChild(domP);
115        }
116        parent.appendChild(domTd);
117    }
118
119    Module.prototype.createTagsTdDom = function (parent) {
120        let domTd = domNewElem('td');
121        for (let tag of this.tagIds) {
122            domTd.appendChild(domNewElem('p', strsData[tag]));
123        }
124        parent.appendChild(domTd);
125    }
126
127    Module.prototype.createDepsTdDom = function (parent) {
128        let domTd = domNewElem(
129                'td', this.numDirectDeps + ' + ' + this.numIndirectDeps);
130
131        let deps = this.deps;
132        if (deps.length > 0) {
133            this.createModuleRelationsDom(domTd, 'Direct', deps[0]);
134
135            for (let i = 1; i < deps.length; ++i) {
136                this.createModuleRelationsDom(domTd, 'Indirect #' + i, deps[i]);
137            }
138        }
139
140        parent.appendChild(domTd);
141    }
142
143    Module.prototype.createUsersTdDom = function (parent) {
144        let domTd = domNewElem('td', this.numUsers);
145
146        let users = this.users;
147        if (users.length > 0) {
148            this.createModuleRelationsDom(domTd, 'Direct', users);
149        }
150
151        parent.appendChild(domTd);
152    }
153
154    Module.prototype.createDom = function () {
155        let dom = this.dom = domNewElem('tr');
156        dom.setAttribute('id', 'mod_'  + this.id);
157
158        this.createModulePathTdDom(dom);
159        this.createTagsTdDom(dom);
160        this.createDepsTdDom(dom);
161        this.createUsersTdDom(dom)
162    }
163
164    Module.prototype.showDom = function () {
165        hidePlaceholder();
166        if (this.visible) {
167            return;
168        }
169        if (this.dom === null) {
170            this.createDom();
171        }
172        domTBody.appendChild(this.dom);
173        this.visible = true;
174    }
175
176    Module.prototype.hideDom = function () {
177        if (!this.visible) {
178            return;
179        }
180        this.dom.parentNode.removeChild(this.dom);
181        this.visible = false;
182    }
183
184    function createModulesFromData(stringsData, modulesData) {
185        return modulesData.map(function (modData, id) {
186            return new Module(id, modData);
187        });
188    }
189
190    function createTagIdsFromData(stringsData, mods) {
191        let tagIds = new Set();
192        for (let mod of mods) {
193            for (let tag of mod.tagIds) {
194                tagIds.add(tag);
195            }
196        }
197
198        tagIds = Array.from(tagIds);
199        tagIds.sort(function (a, b) {
200            return strsData[a].localeCompare(strsData[b]);
201        });
202
203        return tagIds;
204    }
205
206    //--------------------------------------------------------------------------
207    // Data
208    //--------------------------------------------------------------------------
209    function init(doc, stringsData, modulesData) {
210        document = doc;
211        strsData = stringsData;
212
213        mods = createModulesFromData(stringsData, modulesData);
214        tagIds = createTagIdsFromData(stringsData, mods);
215
216        document.addEventListener('DOMContentLoaded', function (evt) {
217            createControlDom(document.body);
218            createTableDom(document.body);
219        });
220    }
221
222    //--------------------------------------------------------------------------
223    // Control
224    //--------------------------------------------------------------------------
225    function createControlDom(parent) {
226        let domTBody = domNewElem('tbody');
227
228        createSelectionTrDom(domTBody);
229        createAddByTagsTrDom(domTBody);
230        createAddByPathTrDom(domTBody);
231
232        let domTable = domNewElem('table', domTBody);
233        domTable.id = 'control';
234
235        let domFixedLink = domNewElem('a', 'Menu');
236        domFixedLink.href = '#control';
237        domFixedLink.id = 'control_menu';
238
239        parent.appendChild(domFixedLink);
240        parent.appendChild(domTable);
241    }
242
243    function createControlMenuTr(parent, label, items) {
244        let domUl = domNewElem('ul');
245        domUl.className = 'menu';
246        for (let [txt, callback] of items) {
247            domUl.appendChild(domNewElem('li', domNewLink(txt, callback)));
248        }
249
250        let domTr = domNewElem('tr',
251                               createControlLabelTdDom(label),
252                               domNewElem('td', domUl));
253
254        parent.appendChild(domTr);
255    }
256
257    function createSelectionTrDom(parent) {
258        const items = [
259            ['All', onAddAll],
260            ['32-bit', onAddAll32],
261            ['64-bit', onAddAll64],
262            ['Clear', onClear],
263        ];
264
265        createControlMenuTr(parent, 'Selection:', items);
266    }
267
268    function createAddByTagsTrDom(parent) {
269        if (tagIds.length == 0) {
270            return;
271        }
272
273        const items = tagIds.map(function (tagId) {
274            return [strsData[tagId], function (evt) {
275                evt.preventDefault(true);
276                showModulesByTagId(tagId);
277            }];
278        });
279
280        createControlMenuTr(parent, 'Add by Tags:', items);
281    }
282
283    function createAddByPathTrDom(parent) {
284        let domForm = domNewElem('form');
285        domForm.addEventListener('submit', onAddModuleByPath);
286
287        domPathInput = domNewElem('input');
288        domPathInput.type = 'text';
289        domForm.appendChild(domPathInput);
290
291        let domBtn = domNewElem('input');
292        domBtn.type = 'submit';
293        domBtn.value = 'Add';
294        domForm.appendChild(domBtn);
295
296        domFuzzyMatch = domNewElem('input');
297        domFuzzyMatch.setAttribute('id', 'fuzzy_match');
298        domFuzzyMatch.setAttribute('type', 'checkbox');
299        domFuzzyMatch.setAttribute('checked', 'checked');
300        domForm.appendChild(domFuzzyMatch);
301
302        let domFuzzyMatchLabel = domNewElem('label', 'Fuzzy Match');
303        domFuzzyMatchLabel.setAttribute('for', 'fuzzy_match');
304        domForm.appendChild(domFuzzyMatchLabel);
305
306        let domTr = domNewElem('tr',
307                               createControlLabelTdDom('Add by Path:'),
308                               domNewElem('td', domForm));
309
310        parent.appendChild(domTr);
311    }
312
313    function createControlLabelTdDom(text) {
314        return domNewElem('td', domNewElem('strong', text));
315    }
316
317
318    //--------------------------------------------------------------------------
319    // Table
320    //--------------------------------------------------------------------------
321    function createTableDom(parent) {
322        domTBody = domNewElem('tbody');
323        domTBody.id = 'module_tbody';
324
325        createTableHeaderDom(domTBody);
326
327        showPlaceholder();
328
329        let domTable  = domNewElem('table', domTBody);
330        domTable.id = 'module_table';
331
332        parent.appendChild(domTable);
333    }
334
335    function createTableHeaderDom(parent) {
336        const labels = [
337            'Name',
338            'Tags',
339            'Dependencies (Direct + Indirect)',
340            'Users',
341        ];
342
343        let domTr = domNewElem('tr');
344        for (let label of labels) {
345            domTr.appendChild(domNewElem('th', label));
346        }
347
348        parent.appendChild(domTr);
349    }
350
351    function createPlaceholder() {
352        let domTd = domNewElem('td');
353        domTd.setAttribute('colspan', 4);
354        domTd.setAttribute('id', 'no_module_placeholder');
355        domTd.appendChild(domNewText(
356            'No modules are selected.  Click the menu to select modules by ' +
357            'names or categories.'));
358        domPlaceholder = domNewElem('tr', domTd);
359    }
360
361    function showPlaceholder() {
362        if (placeholderVisible) {
363            return;
364        }
365        placeholderVisible = true;
366        if (domPlaceholder === null) {
367            createPlaceholder();
368        }
369        domTBody.appendChild(domPlaceholder);
370    }
371
372    function hidePlaceholder() {
373        if (placeholderVisible) {
374            domTBody.removeChild(domPlaceholder);
375            placeholderVisible = false;
376        }
377    }
378
379    function hideAllModules() {
380        for (let mod of mods) {
381            mod.hideDom();
382        }
383        showPlaceholder();
384    }
385
386    function showAllModules() {
387        for (let mod of mods) {
388            mod.showDom();
389        }
390    }
391
392    function showModulesByFilter(pred) {
393        let numMatched = 0;
394        for (let mod of mods) {
395            if (pred(mod)) {
396                mod.showDom();
397                ++numMatched;
398            }
399        }
400        return numMatched;
401    }
402
403    function showModulesByTagId(tagId) {
404        showModulesByFilter(function (mod) {
405            return mod.isTagged(tagId);
406        });
407    }
408
409
410    //--------------------------------------------------------------------------
411    // Events
412    //--------------------------------------------------------------------------
413
414    function onAddModuleByPath(evt) {
415        evt.preventDefault();
416
417        let path = domPathInput.value;
418        domPathInput.value = '';
419
420        function escapeRegExp(pattern) {
421            return pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1');
422        }
423
424        function createFuzzyMatcher() {
425            let parts = path.split(/\/+/g);
426            let pattern = '';
427            for (let part of parts) {
428                pattern += escapeRegExp(part) + '(?:/[^\/]*)*';
429            }
430            pattern = RegExp(pattern);
431
432            return function (mod) {
433                return pattern.test(mod.path);
434            };
435        }
436
437        function exactMatcher(mod) {
438            return mod.path == path;
439        }
440
441        let numMatched = showModulesByFilter(
442            domFuzzyMatch.checked ? createFuzzyMatcher() : exactMatcher);
443
444        if (numMatched == 0) {
445            alert('No matching modules: ' + path);
446        }
447    }
448
449    function onAddAll(evt) {
450        evt.preventDefault(true);
451        hideAllModules();
452        showAllModules();
453    }
454
455    function onAddAllClass(evt, cls) {
456        evt.preventDefault(true);
457        hideAllModules();
458        showModulesByFilter(function (mod) {
459            return mod.cls == cls;
460        });
461    }
462
463    function onAddAll32(evt) {
464        onAddAllClass(evt, 32);
465    }
466
467    function onAddAll64(evt) {
468        onAddAllClass(evt, 64);
469    }
470
471    function onClear(evt) {
472        evt.preventDefault(true);
473        hideAllModules();
474    }
475
476    function onModuleLinkClicked(evt) {
477        let modId = parseInt(evt.target.getAttribute('data-mod-id'), 10);
478        mods[modId].showDom();
479    }
480
481    function setDirectDepBackgroundColor(modId, ownerId, color) {
482        let mod = mods[modId];
483        let owner = mods[ownerId];
484        let ownerLinkDoms = owner.linkDoms;
485        if (mod.deps.length > 0) {
486            for (let depId of mod.deps[0]) {
487                if (depId in ownerLinkDoms) {
488                    ownerLinkDoms[depId].style.backgroundColor = color;
489                }
490            }
491        }
492    }
493
494    function onModuleLinkMouseOver(evt) {
495        let modId = parseInt(evt.target.getAttribute('data-mod-id'), 10);
496        let ownerId = parseInt(evt.target.getAttribute('data-owner-id'), 10);
497        setDirectDepBackgroundColor(modId, ownerId, '#ffff00');
498    }
499
500    function onModuleLinkMouseOut(evt) {
501        let modId = parseInt(evt.target.getAttribute('data-mod-id'), 10);
502        let ownerId = parseInt(evt.target.getAttribute('data-owner-id'), 10);
503        setDirectDepBackgroundColor(modId, ownerId, 'transparent');
504    }
505
506    return {
507        'init': init,
508    };
509}));
510