/*******************************************************************************

    uMatrix - a browser extension to black/white list requests.
    Copyright (C) 2014-present Raymond Hill

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see {http://www.gnu.org/licenses/}.

    Home: https://github.com/gorhill/uMatrix
*/

/* global punycode, uDom, uMatrixScopeWidget */

'use strict';

/******************************************************************************/
/******************************************************************************/

{
// >>>>> start of local scope

/******************************************************************************/
/******************************************************************************/

// Stuff which is good to do very early so as to avoid visual glitches.

{
    const url = new URL(self.location.href);
    const params = url.searchParams;
    if ( params.has('tabid') || params.has('rule') ) {
        document.body.classList.add('embedded');
    }
    if ( params.has('rule') ) {
        document.body.classList.add('tabless');
    }

    const touchDevice = vAPI.localStorage.getItem('touchDevice');
    if ( touchDevice === 'true' ) {
        document.body.setAttribute('data-touch', 'true');
    } else {
        document.addEventListener('touchstart', function onTouched(ev) {
            document.removeEventListener(ev.type, onTouched);
            document.body.setAttribute('data-touch', 'true');
            vAPI.localStorage.setItem('touchDevice', 'true');
        });
    }
}

const popupWasResized = function() {
    document.body.setAttribute('data-resize-popup', '');
};

const resizePopup = (( ) => {
    let timer;

    // The purpose of `fix` is to make it so that the popup panel can still
    // function properly in a horizontally-restricted viewport: in such case
    // we need an horizontal scrollbar.
    const fix = function() {
        timer = undefined;
        document.body.classList.toggle(
            'hConstrained',
            window.innerWidth < document.body.clientWidth
        );
        popupWasResized();
    };

    // The purpose of `xobserver` is to initiate the resize handler only
    // when the popup panel is actually visible.

    // https://github.com/uBlockOrigin/uMatrix-issues/issues/235
    if ( self.IntersectionObserver instanceof Object === false ) {
        return ( ) => { };
    }
    let xobserver = new IntersectionObserver(intersections => {
        if ( intersections.length === 0 ) { return; }
        if ( intersections[0].isIntersecting === false ) { return; }
        xobserver.disconnect();
        xobserver = null;
        resizePopup();
    });
    xobserver.observe(document.body);

    return function() {
        if ( timer !== undefined ) {
            clearTimeout(timer);
        }
        if ( xobserver !== null ) { return; }
        timer = vAPI.setTimeout(fix, 97);
    };
})();

/******************************************************************************/
/******************************************************************************/

// Must be consistent with definitions in matrix.js
const Dark      = 0x80;
const Red       = 1;
const Green     = 2;
const DarkRed   = Dark | Red;
const DarkGreen = Dark | Green;

let matrixSnapshot = {};
let groupsSnapshot = [];
let allHostnamesSnapshot = 'do not leave this initial string empty';

let matrixCellHotspots = null;

const matrixHeaderPrettyNames = {
    'all': '',
    'cookie': '',
    'css': '',
    'image': '',
    'media': '',
    'script': '',
    'fetch': '',
    'frame': '',
    'other': ''
};

let firstPartyLabel = '';
let blacklistedHostnamesLabel = '';

const nodeToExpandosMap = new Map();
let expandosIdGenerator = 1;

const expandosFromNode = function(node) {
    if (
        node instanceof HTMLElement === false &&
        typeof node.nodeAt === 'function'
    ) {
        node = node.nodeAt(0);
    }
    if ( node.hasAttribute('data-expandos') === false ) {
        const expandosId = '' + (expandosIdGenerator++);
        node.setAttribute('data-expandos', expandosId);
        nodeToExpandosMap.set(expandosId, Object.create(null));
    }
    return nodeToExpandosMap.get(node.getAttribute('data-expandos'));
};

/******************************************************************************/
/******************************************************************************/

const getUserSetting = function(setting) {
    return matrixSnapshot.userSettings[setting];
};

const setUserSetting = function(setting, value) {
    matrixSnapshot.userSettings[setting] = value;
    vAPI.messaging.send('popup.js', {
        what: 'userSettings',
        name: setting,
        value: value
    });
};

/******************************************************************************/

const getUISetting = function(setting) {
    var r = vAPI.localStorage.getItem(setting);
    if ( typeof r !== 'string' ) {
        return undefined;
    }
    return JSON.parse(r);
};

const setUISetting = function(setting, value) {
    vAPI.localStorage.setItem(
        setting,
        JSON.stringify(value)
    );
};

/******************************************************************************/

const updateMatrixSnapshot = function() {
    matrixSnapshotPoller.pollNow();
};

/******************************************************************************/

// For display purpose, create four distinct groups of rows:
// 0th: literal "1st-party" row
// 1st: page domain's related
// 2nd: whitelisted
// 3rd: graylisted
// 4th: blacklisted

const getGroupStats = function() {
    // Try to not reshuffle groups around while popup is opened if
    // no new hostname added.
    const latestDomainListSnapshot = Object.keys(matrixSnapshot.rows).sort().join();
    if ( latestDomainListSnapshot === allHostnamesSnapshot ) {
        return groupsSnapshot;
    }
    allHostnamesSnapshot = latestDomainListSnapshot;

    // First, group according to whether at least one node in the domain
    // hierarchy is white or blacklisted
    const pageDomain = matrixSnapshot.domain;
    const rows = matrixSnapshot.rows;
    const anyTypeOffset = matrixSnapshot.headerIndices.get('*');
    const domainToGroupMap = {};

    // These have hard-coded position which cannot be overriden
    domainToGroupMap['1st-party'] = 0;
    domainToGroupMap[pageDomain] = 1;

    // 1st pass: domain wins if it has an explicit rule or a count
    for ( const hostname in rows ) {
        if ( rows.hasOwnProperty(hostname) === false ) { continue; }
        if ( hostname === '*' || hostname === '1st-party' ) { continue; }
        const domain = rows[hostname].domain;
        if ( domain === pageDomain || hostname !== domain ) { continue; }
        const row = rows[domain];
        const color = row.temporary[anyTypeOffset];
        if ( color === DarkGreen ) {
            domainToGroupMap[domain] = 2;
            continue;
        }
        if ( color === DarkRed ) {
            domainToGroupMap[domain] = 4;
            continue;
        }
        const count = row.counts[anyTypeOffset];
        if ( count !== 0 ) {
            domainToGroupMap[domain] = 3;
            continue;
        }
    }
    // 2nd pass: green wins
    for ( const hostname in rows ) {
        if ( rows.hasOwnProperty(hostname) === false ) { continue; }
        const row = rows[hostname];
        const domain = row.domain;
        if ( domainToGroupMap.hasOwnProperty(domain) ) { continue; }
        const color = row.temporary[anyTypeOffset];
        if ( color === DarkGreen ) {
            domainToGroupMap[domain] = 2;
        }
    }
    // 3rd pass: gray with count wins
    for ( const hostname in rows ) {
        if ( rows.hasOwnProperty(hostname) === false ) { continue; }
        const row = rows[hostname];
        const domain = row.domain;
        if ( domainToGroupMap.hasOwnProperty(domain) ) { continue; }
        const color = row.temporary[anyTypeOffset];
        const count = row.counts[anyTypeOffset];
        if ( color !== DarkRed && count !== 0 ) {
            domainToGroupMap[domain] = 3;
        }
    }
    // 4th pass: red wins whatever is left
    for ( const hostname in rows ) {
        if ( rows.hasOwnProperty(hostname) === false ) { continue; }
        const row = rows[hostname];
        const domain = row.domain;
        if ( domainToGroupMap.hasOwnProperty(domain) ) { continue; }
        const color = row.temporary[anyTypeOffset];
        if ( color === DarkRed ) {
            domainToGroupMap[domain] = 4;
        }
    }
    // 5th pass: gray wins whatever is left
    for ( const hostname in rows ) {
        if ( rows.hasOwnProperty(hostname) === false ) { continue; }
        const domain = rows[hostname].domain;
        if ( domainToGroupMap.hasOwnProperty(domain) ) { continue; }
        domainToGroupMap[domain] = 3;
    }

    // Last pass: put each domain in a group
    const groups = [ {}, {}, {}, {}, {} ];
    for ( const hostname in rows ) {
        if ( rows.hasOwnProperty(hostname) === false ) { continue; }
        if ( hostname === '*' ) { continue; }
        const domain = rows[hostname].domain;
        const groupIndex = domainToGroupMap[domain];
        const group = groups[groupIndex];
        if ( group.hasOwnProperty(domain) === false ) {
            group[domain] = {};
        }
        group[domain][hostname] = true;
    }

    groupsSnapshot = groups;

    return groups;
};

/******************************************************************************/

// helpers

const getTemporaryColor = function(hostname, type) {
    return matrixSnapshot.rows[hostname]
                         .temporary[matrixSnapshot.headerIndices.get(type)];
};

const getPermanentColor = function(hostname, type) {
    return matrixSnapshot.rows[hostname]
                         .permanent[matrixSnapshot.headerIndices.get(type)];
};

const addCellClass = function(cell, hostname, type) {
    const cl = cell.classList;
    cl.add('matCell');
    cl.add('t' + getTemporaryColor(hostname, type).toString(16));
    cl.add('p' + getPermanentColor(hostname, type).toString(16));
};

/******************************************************************************/

// This is required for when we update the matrix while it is open:
// the user might have collapsed/expanded one or more domains, and we don't
// want to lose all his hardwork.

const getCollapseState = function(domain) {
    const states = getUISetting('popupCollapseSpecificDomains');
    if ( typeof states === 'object' && states[domain] !== undefined ) {
        return states[domain];
    }
    return matrixSnapshot.collapseAllDomains === true;
};

const toggleCollapseState = function(elem) {
    if ( elem.ancestors('#matHead.collapsible').length > 0 ) {
        toggleMainCollapseState(elem);
    } else {
        toggleSpecificCollapseState(elem);
    }
    popupWasResized();
};

const toggleMainCollapseState = function(uelem) {
    const matHead = uelem.ancestors('#matHead.collapsible').toggleClass('collapsed');
    const collapsed = matrixSnapshot.collapseAllDomains = matHead.hasClass('collapsed');
    uDom('#matList .matSection.collapsible').toggleClass('collapsed', collapsed);
    setUserSetting('popupCollapseAllDomains', collapsed);
    const specificCollapseStates = getUISetting('popupCollapseSpecificDomains') || {};
    for ( const domain of Object.keys(specificCollapseStates) ) {
        if ( specificCollapseStates[domain] === collapsed ) {
            delete specificCollapseStates[domain];
        }
    }
    setUISetting('popupCollapseSpecificDomains', specificCollapseStates);
};

const toggleSpecificCollapseState = function(uelem) {
    // Remember collapse state forever, but only if it is different
    // from main collapse switch.
    const section = uelem.ancestors('.matSection.collapsible').toggleClass('collapsed');
    const domain = expandosFromNode(section).domain;
    const collapsed = section.hasClass('collapsed');
    const mainCollapseState = matrixSnapshot.collapseAllDomains === true;
    const specificCollapseStates = getUISetting('popupCollapseSpecificDomains') || {};
    if ( collapsed !== mainCollapseState ) {
        specificCollapseStates[domain] = collapsed;
        setUISetting('popupCollapseSpecificDomains', specificCollapseStates);
    } else if ( specificCollapseStates[domain] !== undefined ) {
        delete specificCollapseStates[domain];
        setUISetting('popupCollapseSpecificDomains', specificCollapseStates);
    }
};

/******************************************************************************/

// Update count value of matrix cells(s)

const updateMatrixCounts = function() {
    const matCells = uDom('.matrix .matRow.rw > .matCell');
    const headerIndices = matrixSnapshot.headerIndices;
    const rows = matrixSnapshot.rows;
    let i = matCells.length;
    while ( i-- ) {
        const matCell = matCells.nodeAt(i);
        const expandos = expandosFromNode(matCell);
        if ( expandos.hostname === '*' || expandos.reqType === '*' ) {
            continue;
        }
        const matRow = matCell.parentNode;
        const counts = matRow.classList.contains('meta') ? 'totals' : 'counts';
        const count = rows[expandos.hostname][counts][headerIndices.get(expandos.reqType)];
        if ( count === expandos.count ) { continue; }
        expandos.count = count;
        matCell.textContent = cellTextFromCount(count);
    }
};

const cellTextFromCount = function(count) {
    if ( count === 0 ) { return '\u00A0'; }
    if ( count < 100 ) { return count; }
    return '99+';
};

/******************************************************************************/

// Update color of matrix cells(s)
// Color changes when rules change

const updateMatrixColors = function() {
    const cells = uDom('.matrix .matRow.rw > .matCell').removeClass();
    for ( let i = 0; i < cells.length; i++ ) {
        const cell = cells.nodeAt(i);
        const expandos = expandosFromNode(cell);
        addCellClass(cell, expandos.hostname, expandos.reqType);
    }
    popupWasResized();
};

/******************************************************************************/

// Update behavior of matrix:
// - Whether a section is collapsible or not. It is collapsible if:
//   - It has at least one subdomain AND
//   - There is no explicit rule anywhere in the subdomain cells AND
//   - It is not part of group 3 (blacklisted hostnames)

const updateMatrixBehavior = function() {
    matrixList = matrixList || uDom('#matList');
    const sections = matrixList.descendants('.matSection');
    let i = sections.length;
    while ( i-- ) {
        const section = sections.at(i);
        const subdomainRows = section.descendants('.l2:not(.g4)');
        let j = subdomainRows.length;
        while ( j-- ) {
            const subdomainRow = subdomainRows.at(j);
            subdomainRow.toggleClass(
                'collapsible',
                subdomainRow.descendants('.t81,.t82').length === 0
            );
        }
        section.toggleClass(
            'collapsible',
            subdomainRows.filter('.collapsible').length > 0
        );
    }
};

/******************************************************************************/

// handle user interaction with filters

const getCellAction = function(hostname, type, leaning) {
    const temporaryColor = getTemporaryColor(hostname, type);
    const hue = temporaryColor & 0x03;
    // Special case: root toggle only between two states
    if ( type === '*' && hostname === '*' ) {
        return hue === Green ? 'blacklistMatrixCell' : 'whitelistMatrixCell';
    }
    // When explicitly blocked/allowed, can only graylist
    const saturation = temporaryColor & 0x80;
    if ( saturation === Dark ) {
        return 'graylistMatrixCell';
    }
    return leaning === 'whitelisting' ? 'whitelistMatrixCell' : 'blacklistMatrixCell';
};

const handleFilter = function(button, leaning) {
    // our parent cell knows who we are
    const cell = button.ancestors('div.matCell');
    const expandos = expandosFromNode(cell);
    const type = expandos.reqType;
    const desHostname = expandos.hostname;
    // https://github.com/gorhill/uMatrix/issues/24
    // No hostname can happen -- like with blacklist meta row
    if ( desHostname === '' ) { return; }
    vAPI.messaging.send('default', {
        what: getCellAction(desHostname, type, leaning),
        srcHostname: matrixSnapshot.scope,
        desHostname: desHostname,
        type: type
    }).then(( ) => {
        updateMatrixSnapshot();
    });
};

const handleWhitelistFilter = function(button) {
    handleFilter(button, 'whitelisting');
};

const handleBlacklistFilter = function(button) {
    handleFilter(button, 'blacklisting');
};

/******************************************************************************/

let matrixRowPool = [];
let matrixSectionPool = [];
let matrixGroupPool = [];
let matrixRowTemplate = null;
let matrixList = null;

const startMatrixUpdate = function() {
    matrixList =  matrixList || uDom('#matList');
    matrixList.detach();
    const rows = matrixList.descendants('.matRow');
    rows.detach();
    matrixRowPool = matrixRowPool.concat(rows.toArray());
    const sections = matrixList.descendants('.matSection');
    sections.detach();
    matrixSectionPool = matrixSectionPool.concat(sections.toArray());
    const groups = matrixList.descendants('.matGroup');
    groups.detach();
    matrixGroupPool = matrixGroupPool.concat(groups.toArray());
};

const endMatrixUpdate = function() {
    // https://github.com/gorhill/httpswitchboard/issues/246
    // If the matrix has no rows, we need to insert a dummy one, invisible,
    // to ensure the extension pop-up is properly sized. This is needed because
    // the header pane's `position` property is `fixed`, which means it doesn't
    // affect layout size, hence the matrix header row will be truncated.
    if ( matrixSnapshot.rowCount <= 1 ) {
        matrixList.append(createMatrixRow().css('visibility', 'hidden'));
    }
    updateMatrixBehavior();
    matrixList.css('display', '');
    matrixList.appendTo('.paneContent');
};

const createMatrixGroup = function() {
    const group = matrixGroupPool.pop();
    if ( group ) {
        return uDom(group).removeClass().addClass('matGroup');
    }
    return uDom(document.createElement('div')).addClass('matGroup');
};

const createMatrixSection = function() {
    const section = matrixSectionPool.pop();
    if ( section ) {
        return uDom(section).removeClass().addClass('matSection');
    }
    return uDom(document.createElement('div')).addClass('matSection');
};

const createMatrixRow = function() {
    let row = matrixRowPool.pop();
    if ( row ) {
        row.style.visibility = '';
        row = uDom(row);
        row.descendants('.matCell').removeClass().addClass('matCell');
        row.removeClass().addClass('matRow');
        return row;
    }
    if ( matrixRowTemplate === null ) {
        matrixRowTemplate = uDom('#templates .matRow');
    }
    return matrixRowTemplate.clone();
};

/******************************************************************************/

const renderMatrixHeaderRow = function() {
    const matHead = uDom('#matHead.collapsible');
    matHead.toggleClass('collapsed', matrixSnapshot.collapseAllDomains === true);
    const cells = matHead.descendants('.matCell');
    cells.removeClass();
    let cell = cells.nodeAt(0);
    let expandos = expandosFromNode(cell);
    expandos.reqType = '*';
    expandos.hostname = '*';
    addCellClass(cell, '*', '*');
    cell = cells.nodeAt(1);
    expandos = expandosFromNode(cell);
    expandos.reqType = 'cookie';
    expandos.hostname = '*';
    addCellClass(cell, '*', 'cookie');
    cell = cells.nodeAt(2);
    expandos = expandosFromNode(cell);
    expandos.reqType = 'css';
    expandos.hostname = '*';
    addCellClass(cell, '*', 'css');
    cell = cells.nodeAt(3);
    expandos = expandosFromNode(cell);
    expandos.reqType = 'image';
    expandos.hostname = '*';
    addCellClass(cell, '*', 'image');
    cell = cells.nodeAt(4);
    expandos = expandosFromNode(cell);
    expandos.reqType = 'media';
    expandos.hostname = '*';
    addCellClass(cell, '*', 'media');
    cell = cells.nodeAt(5);
    expandos = expandosFromNode(cell);
    expandos.reqType = 'script';
    expandos.hostname = '*';
    addCellClass(cell, '*', 'script');
    cell = cells.nodeAt(6);
    expandos = expandosFromNode(cell);
    expandos.reqType = 'fetch';
    expandos.hostname = '*';
    addCellClass(cell, '*', 'fetch');
    cell = cells.nodeAt(7);
    expandos = expandosFromNode(cell);
    expandos.reqType = 'frame';
    expandos.hostname = '*';
    addCellClass(cell, '*', 'frame');
    cell = cells.nodeAt(8);
    expandos = expandosFromNode(cell);
    expandos.reqType = 'other';
    expandos.hostname = '*';
    addCellClass(cell, '*', 'other');
    uDom('#matHead .matRow').css('display', '');
};

/******************************************************************************/

const renderMatrixCellDomain = function(cell, domain) {
    const expandos = expandosFromNode(cell);
    expandos.hostname = domain;
    expandos.reqType = '*';
    addCellClass(cell.nodeAt(0), domain, '*');
    const contents = cell.contents();
    contents.nodeAt(0).textContent = domain === '1st-party' ?
        firstPartyLabel :
        punycode.toUnicode(domain);
    contents.nodeAt(1).textContent = ' ';
};

const renderMatrixCellSubdomain = function(cell, domain, subomain) {
    const expandos = expandosFromNode(cell);
    expandos.hostname = subomain;
    expandos.reqType = '*';
    addCellClass(cell.nodeAt(0), subomain, '*');
    const contents = cell.contents();
    contents.nodeAt(0).textContent = punycode.toUnicode(subomain.slice(0, subomain.lastIndexOf(domain)-1)) + '.';
    contents.nodeAt(1).textContent = punycode.toUnicode(domain);
};

const renderMatrixMetaCellDomain = function(cell, domain) {
    const expandos = expandosFromNode(cell);
    expandos.hostname = domain;
    expandos.reqType = '*';
    addCellClass(cell.nodeAt(0), domain, '*');
    const contents = cell.contents();
    contents.nodeAt(0).textContent = '\u2217.' + punycode.toUnicode(domain);
    contents.nodeAt(1).textContent = ' ';
};

const renderMatrixCellType = function(cell, hostname, type, count) {
    const node = cell.nodeAt(0);
    const expandos = expandosFromNode(node);
    expandos.hostname = hostname;
    expandos.reqType = type;
    expandos.count = count;
    addCellClass(node, hostname, type);
    node.textContent = cellTextFromCount(count);
};

const renderMatrixCellTypes = function(cells, hostname, countName) {
    const counts = matrixSnapshot.rows[hostname][countName];
    const headerIndices = matrixSnapshot.headerIndices;
    renderMatrixCellType(cells.at(1), hostname, 'cookie', counts[headerIndices.get('cookie')]);
    renderMatrixCellType(cells.at(2), hostname, 'css', counts[headerIndices.get('css')]);
    renderMatrixCellType(cells.at(3), hostname, 'image', counts[headerIndices.get('image')]);
    renderMatrixCellType(cells.at(4), hostname, 'media', counts[headerIndices.get('media')]);
    renderMatrixCellType(cells.at(5), hostname, 'script', counts[headerIndices.get('script')]);
    renderMatrixCellType(cells.at(6), hostname, 'fetch', counts[headerIndices.get('fetch')]);
    renderMatrixCellType(cells.at(7), hostname, 'frame', counts[headerIndices.get('frame')]);
    renderMatrixCellType(cells.at(8), hostname, 'other', counts[headerIndices.get('other')]);
};

/******************************************************************************/

const makeMatrixRowDomain = function(domain) {
    const matrixRow = createMatrixRow().addClass('rw');
    const cells = matrixRow.descendants('.matCell');
    renderMatrixCellDomain(cells.at(0), domain);
    renderMatrixCellTypes(cells, domain, 'counts');
    return matrixRow;
};

const makeMatrixRowSubdomain = function(domain, subdomain) {
    const matrixRow = createMatrixRow().addClass('rw');
    const cells = matrixRow.descendants('.matCell');
    renderMatrixCellSubdomain(cells.at(0), domain, subdomain);
    renderMatrixCellTypes(cells, subdomain, 'counts');
    return matrixRow;
};

const makeMatrixMetaRowDomain = function(domain) {
    const matrixRow = createMatrixRow().addClass('rw');
    const cells = matrixRow.descendants('.matCell');
    renderMatrixMetaCellDomain(cells.at(0), domain);
    renderMatrixCellTypes(cells, domain, 'totals');
    return matrixRow;
};

/******************************************************************************/

const renderMatrixMetaCellType = function(cell, count) {
    // https://github.com/gorhill/uMatrix/issues/24
    // Don't forget to reset cell properties
    const node = cell.nodeAt(0);
    const expandos = expandosFromNode(node);
    expandos.hostname = '';
    expandos.reqType = '';
    expandos.count = count;
    cell.addClass('t1');
    node.textContent = cellTextFromCount(count);
};

const makeMatrixMetaRow = function(totals) {
    const headerIndices = matrixSnapshot.headerIndices;
    const matrixRow = createMatrixRow().at(0).addClass('ro');
    const cells = matrixRow.descendants('.matCell');
    const contents = cells.at(0).addClass('t81').contents();
    const expandos = expandosFromNode(cells.nodeAt(0));
    expandos.hostname = '';
    expandos.reqType = '*';
    contents.nodeAt(0).textContent = ' ';
    contents.nodeAt(1).textContent = blacklistedHostnamesLabel.replace(
        '{{count}}',
        totals[headerIndices.get('*')].toLocaleString()
    );
    renderMatrixMetaCellType(cells.at(1), totals[headerIndices.get('cookie')]);
    renderMatrixMetaCellType(cells.at(2), totals[headerIndices.get('css')]);
    renderMatrixMetaCellType(cells.at(3), totals[headerIndices.get('image')]);
    renderMatrixMetaCellType(cells.at(4), totals[headerIndices.get('media')]);
    renderMatrixMetaCellType(cells.at(5), totals[headerIndices.get('script')]);
    renderMatrixMetaCellType(cells.at(6), totals[headerIndices.get('fetch')]);
    renderMatrixMetaCellType(cells.at(7), totals[headerIndices.get('frame')]);
    renderMatrixMetaCellType(cells.at(8), totals[headerIndices.get('other')]);
    return matrixRow;
};

/******************************************************************************/

const computeMatrixGroupMetaStats = function(group) {
    const headerIndices = matrixSnapshot.headerIndices;
    const anyTypeIndex = headerIndices.get('*');
    const n = headerIndices.size;
    const totals = new Array(n);
    totals.fill(0);
    const rows = matrixSnapshot.rows;
    for ( const hostname in rows ) {
        if ( rows.hasOwnProperty(hostname) === false ) { continue; }
        const row = rows[hostname];
        if ( group.hasOwnProperty(row.domain) === false ) { continue; }
        if ( row.counts[anyTypeIndex] === 0 ) { continue; }
        totals[0] += 1;
        for ( let i = 1; i < n; i++ ) {
            totals[i] += row.counts[i];
        }
    }
    return totals;
};

/******************************************************************************/

// Compare hostname helper, to order hostname in a logical manner:
// top-most < bottom-most, take into account whether IP address or
// named hostname

const hostnameCompare = function(a,b) {
    // Normalize: most significant parts first
    if ( !a.match(/^\d+(\.\d+){1,3}$/) ) {
        const aa = a.split('.');
        a = aa.slice(-2).concat(aa.slice(0,-2).reverse()).join('.');
    }
    if ( !b.match(/^\d+(\.\d+){1,3}$/) ) {
        const bb = b.split('.');
        b = bb.slice(-2).concat(bb.slice(0,-2).reverse()).join('.');
    }
    return a.localeCompare(b);
};

/******************************************************************************/

const makeMatrixGroup0SectionDomain = function() {
    return makeMatrixRowDomain('1st-party').addClass('g0 l1');
};

const makeMatrixGroup0Section = function() {
    const domainDiv = createMatrixSection();
    expandosFromNode(domainDiv).domain = '1st-party';
    makeMatrixGroup0SectionDomain().appendTo(domainDiv);
    return domainDiv;
};

const makeMatrixGroup0 = function() {
    // Show literal "1st-party" row only if there is 
    // at least one 1st-party hostname
    if ( Object.keys(groupsSnapshot[1]).length === 0 ) {
        return;
    }
    const groupDiv = createMatrixGroup().addClass('g0');
    makeMatrixGroup0Section().appendTo(groupDiv);
    groupDiv.appendTo(matrixList);
};

/******************************************************************************/

const makeMatrixGroup1SectionDomain = function(domain) {
    return makeMatrixRowDomain(domain)
        .addClass('g1 l1');
};

const makeMatrixGroup1SectionSubomain = function(domain, subdomain) {
    return makeMatrixRowSubdomain(domain, subdomain)
        .addClass('g1 l2');
};

const makeMatrixGroup1SectionMetaDomain = function(domain) {
    return makeMatrixMetaRowDomain(domain)
        .addClass('g1 l1 meta');
};

const makeMatrixGroup1Section = function(hostnames) {
    const domain = hostnames[0];
    const domainDiv = createMatrixSection().toggleClass(
        'collapsed',
        getCollapseState(domain)
    );
    expandosFromNode(domainDiv).domain = domain;
    if ( hostnames.length > 1 ) {
        makeMatrixGroup1SectionMetaDomain(domain).appendTo(domainDiv);
    }
    makeMatrixGroup1SectionDomain(domain).appendTo(domainDiv);
    for ( let i = 1; i < hostnames.length; i++ ) {
        makeMatrixGroup1SectionSubomain(domain, hostnames[i])
            .appendTo(domainDiv);
    }
    return domainDiv;
};

const makeMatrixGroup1 = function(group) {
    const domains = Object.keys(group).sort(hostnameCompare);
    if ( domains.length ) {
        const groupDiv = createMatrixGroup().addClass('g1');
        makeMatrixGroup1Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
            .appendTo(groupDiv);
        for ( let i = 1; i < domains.length; i++ ) {
            makeMatrixGroup1Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
                .appendTo(groupDiv);
        }
        groupDiv.appendTo(matrixList);
    }
};

/******************************************************************************/

const makeMatrixGroup2SectionDomain = function(domain) {
    return makeMatrixRowDomain(domain)
        .addClass('g2 l1');
};

const makeMatrixGroup2SectionSubomain = function(domain, subdomain) {
    return makeMatrixRowSubdomain(domain, subdomain)
        .addClass('g2 l2');
};

const makeMatrixGroup2SectionMetaDomain = function(domain) {
    return makeMatrixMetaRowDomain(domain).addClass('g2 l1 meta');
};

const makeMatrixGroup2Section = function(hostnames) {
    const domain = hostnames[0];
    const domainDiv = createMatrixSection()
        .toggleClass('collapsed', getCollapseState(domain));
    expandosFromNode(domainDiv).domain = domain;
    if ( hostnames.length > 1 ) {
        makeMatrixGroup2SectionMetaDomain(domain).appendTo(domainDiv);
    }
    makeMatrixGroup2SectionDomain(domain)
        .appendTo(domainDiv);
    for ( let i = 1; i < hostnames.length; i++ ) {
        makeMatrixGroup2SectionSubomain(domain, hostnames[i])
            .appendTo(domainDiv);
    }
    return domainDiv;
};

const makeMatrixGroup2 = function(group) {
    const domains = Object.keys(group).sort(hostnameCompare);
    if ( domains.length) {
        const groupDiv = createMatrixGroup()
            .addClass('g2');
        makeMatrixGroup2Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
            .appendTo(groupDiv);
        for ( let i = 1; i < domains.length; i++ ) {
            makeMatrixGroup2Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
                .appendTo(groupDiv);
        }
        groupDiv.appendTo(matrixList);
    }
};

/******************************************************************************/

const makeMatrixGroup3SectionDomain = function(domain) {
    return makeMatrixRowDomain(domain)
        .addClass('g3 l1');
};

const makeMatrixGroup3SectionSubomain = function(domain, subdomain) {
    return makeMatrixRowSubdomain(domain, subdomain)
        .addClass('g3 l2');
};

const makeMatrixGroup3SectionMetaDomain = function(domain) {
    return makeMatrixMetaRowDomain(domain).addClass('g3 l1 meta');
};

const makeMatrixGroup3Section = function(hostnames) {
    const domain = hostnames[0];
    const domainDiv = createMatrixSection()
        .toggleClass('collapsed', getCollapseState(domain));
    expandosFromNode(domainDiv).domain = domain;
    if ( hostnames.length > 1 ) {
        makeMatrixGroup3SectionMetaDomain(domain).appendTo(domainDiv);
    }
    makeMatrixGroup3SectionDomain(domain)
        .appendTo(domainDiv);
    for ( let i = 1; i < hostnames.length; i++ ) {
        makeMatrixGroup3SectionSubomain(domain, hostnames[i])
            .appendTo(domainDiv);
    }
    return domainDiv;
};

const makeMatrixGroup3 = function(group) {
    const domains = Object.keys(group).sort(hostnameCompare);
    if ( domains.length) {
        const groupDiv = createMatrixGroup()
            .addClass('g3');
        makeMatrixGroup3Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
            .appendTo(groupDiv);
        for ( let i = 1; i < domains.length; i++ ) {
            makeMatrixGroup3Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
                .appendTo(groupDiv);
        }
        groupDiv.appendTo(matrixList);
    }
};

/******************************************************************************/

const makeMatrixGroup4SectionDomain = function(domain) {
    return makeMatrixRowDomain(domain)
        .addClass('g4 l1');
};

const makeMatrixGroup4SectionSubomain = function(domain, subdomain) {
    return makeMatrixRowSubdomain(domain, subdomain)
        .addClass('g4 l2');
};

const makeMatrixGroup4Section = function(hostnames) {
    const domain = hostnames[0];
    const domainDiv = createMatrixSection();
    expandosFromNode(domainDiv).domain = domain;
    makeMatrixGroup4SectionDomain(domain)
        .appendTo(domainDiv);
    for ( let i = 1; i < hostnames.length; i++ ) {
        makeMatrixGroup4SectionSubomain(domain, hostnames[i])
            .appendTo(domainDiv);
    }
    return domainDiv;
};

const makeMatrixGroup4 = function(group) {
    const domains = Object.keys(group).sort(hostnameCompare);
    if ( domains.length === 0 ) { return; }
    const groupDiv = createMatrixGroup().addClass('g4');
    createMatrixSection()
        .addClass('g4Meta')
        .toggleClass('g4Collapsed', !!matrixSnapshot.collapseBlacklistedDomains)
        .appendTo(groupDiv);
    makeMatrixMetaRow(computeMatrixGroupMetaStats(group), 'g4')
        .appendTo(groupDiv);
    makeMatrixGroup4Section(Object.keys(group[domains[0]]).sort(hostnameCompare))
        .appendTo(groupDiv);
    for ( let i = 1; i < domains.length; i++ ) {
        makeMatrixGroup4Section(Object.keys(group[domains[i]]).sort(hostnameCompare))
            .appendTo(groupDiv);
    }
    groupDiv.appendTo(matrixList);
};

/******************************************************************************/

const makeMenu = function() {
    const groupStats = getGroupStats();

    if ( Object.keys(groupStats).length === 0 ) { return; }

    // https://github.com/gorhill/httpswitchboard/issues/31
    if ( matrixCellHotspots ) {
        matrixCellHotspots.detach();
    }

    renderMatrixHeaderRow();

    startMatrixUpdate();
    makeMatrixGroup0(groupStats[0]);
    makeMatrixGroup1(groupStats[1]);
    makeMatrixGroup2(groupStats[2]);
    makeMatrixGroup3(groupStats[3]);
    makeMatrixGroup4(groupStats[4]);
    endMatrixUpdate();

    uMatrixScopeWidget.init(
        matrixSnapshot.domain,
        matrixSnapshot.hostname,
        matrixSnapshot.scope
    );
    updateMatrixButtons();
    resizePopup();
    recipeManager.fetch();
};

/******************************************************************************/

// Do all the stuff that needs to be done before building menu et al.

const initMenuEnvironment = function() {
    document.body.style.setProperty(
        'font-size',
        getUserSetting('displayTextSize')
    );
    uDom.nodeFromId('version').textContent = matrixSnapshot.appVersion || '';
    document.body.classList.toggle(
        'colorblind',
        getUserSetting('colorBlindFriendly')
    );
    document.body.classList.toggle(
        'noTooltips',
        getUserSetting('noTooltips')
    );

    const prettyNames = matrixHeaderPrettyNames;
    const keys = Object.keys(prettyNames);
    let i = keys.length;
    while ( i-- ) {
        const key = keys[i];
        const cell = uDom('#matHead .matCell[data-req-type="'+ key +'"]');
        const text = vAPI.i18n(key + 'PrettyName');
        cell.text(text);
        prettyNames[key] = text;
    }

    firstPartyLabel = uDom('[data-i18n="matrix1stPartyLabel"]').text();
    blacklistedHostnamesLabel = uDom('[data-i18n="matrixBlacklistedHostnames"]').text();
};

/******************************************************************************/

const scopeChangeHandler = function(ev) {
    const newScope = ev.detail.scope;
    if ( !newScope || matrixSnapshot.scope === newScope ) { return; }
    matrixSnapshot.scope = newScope;
    matrixSnapshot.tMatrixModifiedTime = undefined;
    updateMatrixSnapshot();
    dropDownMenuHide();
};

/******************************************************************************/

const updateMatrixSwitches = function() {
    const switches = matrixSnapshot.tSwitches;
    let count = 0;
    for ( const switchName in switches ) {
        if ( switches.hasOwnProperty(switchName) === false ) { continue; }
        const enabled = switches[switchName];
        if ( enabled && switchName !== 'matrix-off' ) {
            count += 1;
        }
        uDom('#mtxSwitch_' + switchName).toggleClass('switchTrue', enabled);
    }
    uDom.nodeFromId('mtxSwitch_https-strict').classList.toggle(
        'relevant',
        matrixSnapshot.hasMixedContent === true
    );
    uDom.nodeFromId('mtxSwitch_no-workers').classList.toggle(
        'relevant',
        matrixSnapshot.hasWebWorkers === true
    );
    uDom.nodeFromId('mtxSwitch_referrer-spoof').classList.toggle(
        'relevant',
        matrixSnapshot.has3pReferrer === true
    );
    uDom.nodeFromId('mtxSwitch_noscript-spoof').classList.toggle(
        'relevant',
        matrixSnapshot.hasNoscriptTags === true
    );
    uDom.nodeFromId('mtxSwitch_cname-reveal').classList.toggle(
        'relevant',
        matrixSnapshot.hasHostnameAliases === true
    );
    uDom.nodeFromSelector('#buttonMtxSwitches .fa-icon-badge').textContent =
        count.toLocaleString();
    uDom.nodeFromSelector('#mtxSwitch_matrix-off .fa-icon-badge').textContent =
        matrixSnapshot.blockedCount.toLocaleString();
    document.body.classList.toggle('powerOff', switches['matrix-off']);
};

const toggleMatrixSwitch = function(ev) {
    if ( ev.target.localName === 'a' ) { return; }
    const elem = ev.currentTarget;
    const pos = elem.id.indexOf('_');
    if ( pos === -1 ) { return; }
    const switchName = elem.id.slice(pos + 1);
    vAPI.messaging.send('popup.js', {
        what: 'toggleMatrixSwitch',
        switchName: switchName,
        srcHostname: matrixSnapshot.scope,
    }).then(( ) => {
        updateMatrixSnapshot();
    });
};

/******************************************************************************/

const updatePersistButton = function() {
    const diffCount = matrixSnapshot.diff.length;
    const button = uDom('#buttonPersist');
    button.contents()
          .filter(function(){return this.nodeType===3;})
          .first()
          .text(diffCount > 0 ? '\uf13e' : '\uf023');
    button.descendants('.fa-icon-badge').text(diffCount > 0 ? diffCount : '');
    const disabled = diffCount === 0;
    button.toggleClass('disabled', disabled);
    uDom('#buttonRevertScope').toggleClass('disabled', disabled);
};

/******************************************************************************/

const persistMatrix = function() {
    vAPI.messaging.send('popup.js', {
        what: 'applyDiffToPermanentMatrix',
        diff: matrixSnapshot.diff,
    }).then(( ) => {
        updateMatrixSnapshot();
    });
};

/******************************************************************************/

// rhill 2014-03-12: revert completely ALL changes related to the
// current page, including scopes.

const revertMatrix = function() {
    vAPI.messaging.send('popup.js', {
        what: 'applyDiffToTemporaryMatrix',
        diff: matrixSnapshot.diff,
    }).then(( ) => {
        updateMatrixSnapshot();
    });
};

/******************************************************************************/

// Buttons which are affected by any changes in the matrix

const updateMatrixButtons = function() {
    uMatrixScopeWidget.update(matrixSnapshot.scope);
    updateMatrixSwitches();
    updatePersistButton();
};

/******************************************************************************/

uDom('#buttonReload').on('click', ev => {
    vAPI.messaging.send('default', {
        what: 'forceReloadTab',
        tabId: matrixSnapshot.tabId,
        bypassCache: ev.ctrlKey || ev.metaKey || ev.shiftKey
    });
});

/******************************************************************************/

const recipeManager = (( ) => {
    const reScopeAlias = /(^|\s+)_(\s+|$)/g;
    let recipes = [];

    const createEntry = function(name, ruleset, parent) {
        const li = document.querySelector('#templates li.recipe')
                         .cloneNode(true);
        li.querySelector('.name').textContent = name;
        li.querySelector('.ruleset').textContent = ruleset;
        if ( parent ) {
            parent.appendChild(li);
        }
        return li;
    };

    const apply = function(ev) {
        if (
            ev.target.classList.contains('expander') ||
            ev.target.classList.contains('name')
        ) {
            ev.currentTarget.classList.toggle('expanded');
            return;
        }
        if (
            ev.target.classList.contains('importer') === false &&
            ev.target.classList.contains('committer') === false
        ) {
            return;
        }
        const root = ev.currentTarget;
        const ruleset = root.querySelector('.ruleset');
        const commit = ev.target.classList.contains('committer');
        vAPI.messaging.send('popup.js', {
            what: 'applyRecipe',
            ruleset: ruleset.textContent,
            commit,
        }).then(( ) => {
            updateMatrixSnapshot();
        });
        root.classList.remove('mustImport');
        if ( commit ) {
            root.classList.remove('mustCommit');
        }
        //dropDownMenuHide();
    };

    const show = function(details) {
        const root = document.querySelector('#dropDownMenuRecipes .dropdown-menu');
        const ul = document.createElement('ul');
        for ( const recipe of details.recipes ) {
            let li = createEntry(
                recipe.name,
                recipe.ruleset.replace(reScopeAlias, '$1' + details.scope + '$2'),
                ul
            );
            li.classList.toggle('mustImport', recipe.mustImport === true);
            li.classList.toggle('mustCommit', recipe.mustCommit === true);
            li.addEventListener('click', apply);
        }
        root.replaceChild(ul, root.querySelector('ul'));
        dropDownMenuShow(uDom.nodeFromId('buttonRecipes'));
    };

    const beforeShow = async function() {
        if ( recipes.length === 0 ) { return; }
        const details = await vAPI.messaging.send('popup.js', {
            what: 'fetchRecipeCommitStatuses',
            scope: matrixSnapshot.scope,
            recipes: recipes,
        });
        show(details);
    };

    const fetch = async function() {
        const desHostnames = [];
        for ( const hostname in matrixSnapshot.rows ) {
            if ( matrixSnapshot.rows.hasOwnProperty(hostname) === false ) {
                continue;
            }
            const row = matrixSnapshot.rows[hostname];
            if ( row.domain === matrixSnapshot.domain ) { continue; }
            if ( row.counts[0] !== 0 || row.domain === hostname ) {
                desHostnames.push(hostname);
            }
        }
        const response = await vAPI.messaging.send('popup.js', {
            what: 'fetchRecipes',
            srcHostname: matrixSnapshot.hostname,
            desHostnames: desHostnames
        });
        recipes = Array.isArray(response) ? response : [];
        const button = uDom.nodeFromId('buttonRecipes');
        if ( recipes.length === 0 ) {
            button.classList.add('disabled');
            return;
        }
        button.classList.remove('disabled');
        button.querySelector('.fa-icon-badge').textContent = recipes.length;
    };

    return { fetch, show: beforeShow, apply };
})();

/******************************************************************************/

const revertAll = function() {
    vAPI.messaging.send('popup.js', {
        what: 'revertTemporaryMatrix'
    }).then(( ) => {
        updateMatrixSnapshot();
    });
};

/******************************************************************************/

const mouseenterMatrixCellHandler = function(ev) {
    matrixCellHotspots.appendTo(ev.target);
};

const mouseleaveMatrixCellHandler = function() {
    matrixCellHotspots.detach();
};

/******************************************************************************/

const gotoExtensionURL = function(ev) {
    const target = ev.currentTarget;
    if ( target.hasAttribute('data-extension-url') === false ) { return; }
    let url = target.getAttribute('data-extension-url');
    if ( url === '' ) { return; }
    if (
        url === 'logger-ui.html#_' &&
        typeof matrixSnapshot.tabId === 'number'
    ) {
        url += '+' + matrixSnapshot.tabId;
    }
    vAPI.messaging.send('popup.js', {
        what: 'gotoExtensionURL',
        url,
        select: true,
        shiftKey: ev.shiftKey,
    });
    dropDownMenuHide();
    vAPI.closePopup();
};

/******************************************************************************/

const dropDownMenuShow = function(button) {
    const menuOverlay = document.getElementById(
        button.getAttribute('data-dropdown-menu')
    );
    const butnRect = button.getBoundingClientRect();
    const viewRect = document.body.getBoundingClientRect();
    const butnNormalLeft = butnRect.left / (viewRect.width - butnRect.width);
    menuOverlay.classList.add('show');
    const menu = menuOverlay.querySelector('.dropdown-menu');
    const menuRect = menu.getBoundingClientRect();
    const menuLeft = butnNormalLeft * (viewRect.width - menuRect.width);
    menu.style.top = butnRect.bottom + 'px';
    if ( menuOverlay.classList.contains('dropdown-menu-centered') === false ) {
        menu.style.left = menuLeft.toFixed(0) + 'px';
    }
};

const dropDownMenuHide = function() {
    uDom('.dropdown-menu-capture').removeClass('show');
};

/******************************************************************************/

const onMatrixSnapshotReady = function(response) {
    if ( response === 'ENOTFOUND' ) {
        uDom.nodeFromId('noTabFound').textContent =
            vAPI.i18n('matrixNoTabFound');
        document.body.classList.add('noTabFound');
        return;
    }

    // Now that tabId and pageURL are set, we can build our menu
    initMenuEnvironment();
    makeMenu();

    // After popup menu is built, check whether there is a non-empty matrix
    if ( matrixSnapshot.url === '' ) {
        uDom('#matHead').remove();
        uDom('#toolbarContainer').remove();

        // https://github.com/gorhill/httpswitchboard/issues/191
        uDom('#noNetTrafficPrompt').text(vAPI.i18n('matrixNoNetTrafficPrompt'));
        uDom('#noNetTrafficPrompt').css('display', '');
    }

    // Create a hash to find out whether the reload button needs to be
    // highlighted.
    // TODO:
};

/******************************************************************************/

const matrixSnapshotPoller = (( ) => {
    let timer;

    const preprocessMatrixSnapshot = function(snapshot) {
        if ( Array.isArray(snapshot.headerIndices) ) {
            snapshot.headerIndices = new Map(snapshot.headerIndices);
        }
        return snapshot;
    };

    const processPollResult = function(response) {
        if ( typeof response !== 'object' ) { return; }
        if (
            response.mtxContentModified === false &&
            response.mtxCountModified === false &&
            response.pMatrixModified === false &&
            response.tMatrixModified === false
        ) {
            return;
        }
        if ( response instanceof Object ) {
            matrixSnapshot = preprocessMatrixSnapshot(response);
        }
        if ( response.mtxContentModified ) {
            makeMenu();
            return;
        }
        if ( response.mtxCountModified ) {
            updateMatrixCounts();
        }
        if (
            response.pMatrixModified ||
            response.tMatrixModified ||
            response.scopeModified
        ) {
            updateMatrixColors();
            updateMatrixBehavior();
            updateMatrixButtons();
        }
    };

    const onPolled = function(response) {
        processPollResult(response);
        pollAsync();
    };

    const pollNow = function() {
        unpollAsync();
        vAPI.messaging.send('popup.js', {
            what: 'matrixSnapshot',
            tabId: matrixSnapshot.tabId,
            scope: matrixSnapshot.scope,
            mtxContentModifiedTime: matrixSnapshot.mtxContentModifiedTime,
            mtxCountModifiedTime: matrixSnapshot.mtxCountModifiedTime,
            mtxDiffCount: matrixSnapshot.diff.length,
            pMatrixModifiedTime: matrixSnapshot.pMatrixModifiedTime,
            tMatrixModifiedTime: matrixSnapshot.tMatrixModifiedTime,
        }).then(response => {
            onPolled(response);
        });
    };

    const pollAsync = function() {
        if ( timer !== undefined ) { return; }
        if ( document.defaultView === null ) { return; }
        //if ( typeof matrixSnapshot.tabId !== 'number' ) { return; }
        timer = vAPI.setTimeout(
            ( ) => {
                timer = undefined;
                pollNow();
            },
            1414
        );
    };

    const unpollAsync = function() {
        if ( timer !== undefined ) {
            clearTimeout(timer);
            timer = undefined;
        }
    };

    vAPI.messaging.send('popup.js', {
        what: 'matrixSnapshot',
        tabId: matrixSnapshot.tabId,
    }).then(response => {
        if ( response instanceof Object ) {
            matrixSnapshot = preprocessMatrixSnapshot(response);
        }
        onMatrixSnapshotReady(response);
        pollAsync();
    });

    return {
        pollNow: pollNow
    };
})();

/******************************************************************************/

// Below is UI stuff which is not key to make the menu, so this can
// be done without having to wait for a tab to be bound to the menu.

// We reuse for all cells the one and only cell hotspots.
uDom('#whitelist').on('click', function() {
        handleWhitelistFilter(uDom(this));
        return false;
    });
uDom('#blacklist').on('click', function() {
        handleBlacklistFilter(uDom(this));
        return false;
    });
uDom('#domainOnly').on('click', function() {
        toggleCollapseState(uDom(this));
        return false;
    });
matrixCellHotspots = uDom('#cellHotspots').detach();
uDom('body')
    .on('mouseenter', '.matCell', mouseenterMatrixCellHandler)
    .on('mouseleave', '.matCell', mouseleaveMatrixCellHandler);
window.addEventListener('uMatrixScopeWidgetChange', scopeChangeHandler);
uDom('#buttonMtxSwitches').on('click', function(ev) {
    dropDownMenuShow(ev.target);
});
uDom('[id^="mtxSwitch_"]').on('click', toggleMatrixSwitch);
uDom('#buttonPersist').on('click', persistMatrix);
uDom('#buttonRevertScope').on('click', revertMatrix);

uDom('#buttonRecipes').on('click', function() {
    recipeManager.show();
});

uDom('#buttonRevertAll').on('click', revertAll);
uDom('[data-extension-url]').on('click', gotoExtensionURL);
uDom('body').on('click', '.dropdown-menu-capture', dropDownMenuHide);

uDom('#matList').on('click', '.g4Meta', function(ev) {
    matrixSnapshot.collapseBlacklistedDomains =
        ev.target.classList.toggle('g4Collapsed');
    setUserSetting(
        'popupCollapseBlacklistedDomains',
        matrixSnapshot.collapseBlacklistedDomains
    );
});

/******************************************************************************/

// <<<<< end of local scope
}
