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