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

    uBlock Origin - a browser extension to block requests.
    Copyright (C) 2014-2016 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/uBlock
*/

/* global createSet */

'use strict';

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

              +--> [[domSurveyor] --> domFilterer]
  domWatcher--|
              +--> [domCollapser]

  domWatcher:
    Watches for changes in the DOM, and notify the other components about these
    changes.

  domCollapser:
    Enforces the collapsing of DOM elements for which a corresponding
    resource was blocked through network filtering.

  domFilterer:
    Enforces the filtering of DOM elements, by feeding it cosmetic filters.

  domSurveyor:
    Surveys the DOM to find new cosmetic filters to apply to the current page.

  If page is whitelisted:
    - domWatcher: off
    - domCollapser: off
    - domFilterer: off
    - domSurveyor: off
  I verified that the code in this file is completely flushed out of memory
  when a page is whitelisted.

  If cosmetic filtering is disabled:
    - domWatcher: on
    - domCollapser: on
    - domFilterer: off
    - domSurveyor: off

  If generic cosmetic filtering is disabled:
    - domWatcher: on
    - domCollapser: on
    - domFilterer: on
    - domSurveyor: off

  Additionally, the domSurveyor can turn itself off once it decides that
  it has become pointless (repeatedly not finding new cosmetic filters).

*/

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

// Abort execution by throwing if an unexpected condition arise.
// - https://github.com/chrisaljoudi/uBlock/issues/456

if ( typeof vAPI !== 'object' ) {
    throw new Error('uBlock Origin: aborting content scripts for ' + window.location);
}
vAPI.lock();

vAPI.matchesProp = (function() {
    var docElem = document.documentElement;
    if ( typeof docElem.matches !== 'function' ) {
        if ( typeof docElem.mozMatchesSelector === 'function' ) {
            return 'mozMatchesSelector';
        } else if ( typeof docElem.webkitMatchesSelector === 'function' ) {
            return 'webkitMatchesSelector';
        }
    }
    return 'matches';
})();

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

// https://github.com/gorhill/uBlock/issues/2147

vAPI.SafeAnimationFrame = function(callback) {
    this.fid = this.tid = null;
    this.callback = callback;
};

vAPI.SafeAnimationFrame.prototype.start = function() {
    if ( this.fid !== null ) { return; }
    this.fid = requestAnimationFrame(this.callback);
    this.tid = vAPI.setTimeout(this.callback, 1200000);
};

vAPI.SafeAnimationFrame.prototype.clear = function() {
    if ( this.fid === null ) { return; }
    cancelAnimationFrame(this.fid);
    clearTimeout(this.tid);
    this.fid = this.tid = null;
};

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

// The DOM filterer is the heart of uBO's cosmetic filtering.

vAPI.domFilterer = (function() {

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

var shadowId = document.documentElement.shadowRoot !== undefined ?
    vAPI.randomToken():
    undefined;

var jobQueue = [
    { t: 'css-hide',  _0: [] }, // to inject in style tag
    { t: 'css-style', _0: [] }, // to inject in style tag
    { t: 'css-ssel',  _0: [] }, // to manually hide (incremental)
    { t: 'css-csel',  _0: [] }  // to manually hide (not incremental)
];

var reParserEx = /:(?:has|matches-css|matches-css-before|matches-css-after|style|xpath)\(.+?\)$/;

var allExceptions = createSet(),
    allSelectors = createSet(),
    stagedNodes = [],
    matchesProp = vAPI.matchesProp,
    userCSS = vAPI.userCSS;

// Complex selectors, due to their nature may need to be "de-committed". A
// Set() is used to implement this functionality.

var complexSelectorsOldResultSet,
    complexSelectorsCurrentResultSet = createSet('object');

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

var cosmeticFiltersActivatedTimer = null;

var cosmeticFiltersActivated = function() {
    cosmeticFiltersActivatedTimer = null;
    vAPI.messaging.send(
        'contentscript',
        { what: 'cosmeticFiltersActivated' }
    );
};

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

var runSimpleSelectorJob = function(job, root, fn) {
    if ( job._1 === undefined ) {
        job._1 = job._0.join(cssNotHiddenId + ',');
    }
    if ( root[matchesProp](job._1) ) {
        fn(root);
    }
    var nodes = root.querySelectorAll(job._1),
        i = nodes.length;
    while ( i-- ) {
        fn(nodes[i], job);
    }
};

var runComplexSelectorJob = function(job, fn) {
    if ( job._1 === undefined ) {
        job._1 = job._0.join(',');
    }
    var nodes = document.querySelectorAll(job._1),
        i = nodes.length;
    while ( i-- ) {
        fn(nodes[i], job);
    }
};

var runHasJob = function(job, fn) {
    var nodes = document.querySelectorAll(job._0),
        i = nodes.length, node;
    while ( i-- ) {
        node = nodes[i];
        if ( node.querySelector(job._1) !== null ) {
            fn(node, job);
        }
    }
};

// '/' = ascii 0x2F */

var parseMatchesCSSJob = function(raw) {
    var prop = raw.trim();
    if ( prop === '' ) { return null; }
    var pos = prop.indexOf(':'),
        v = pos !== -1 ? prop.slice(pos + 1).trim() : '',
        vlen = v.length;
    if (
        vlen > 1 &&
        v.charCodeAt(0) === 0x2F &&
        v.charCodeAt(vlen-1) === 0x2F
    ) {
        try { v = new RegExp(v.slice(1, -1)); } catch(ex) { return null; }
    }
    return { k: prop.slice(0, pos).trim(), v: v };
};

var runMatchesCSSJob = function(job, fn) {
    var nodes = document.querySelectorAll(job._0),
        i = nodes.length;
    if ( i === 0 ) { return; }
    if ( typeof job._1 === 'string' ) {
        job._1 = parseMatchesCSSJob(job._1);
    }
    if ( job._1 === null ) { return; }
    var k = job._1.k,
        v = job._1.v,
        node, style, match;
    while ( i-- ) {
        node = nodes[i];
        style = window.getComputedStyle(node, job._2);
        if ( style === null ) { continue; } /* FF */
        if ( v instanceof RegExp ) {
            match = v.test(style[k]);
        } else {
            match = style[k] === v;
        }
        if ( match ) {
            fn(node, job);
        }
    }
};

var runXpathJob = function(job, fn) {
    if ( job._1 === undefined ) {
        job._1 = document.createExpression(job._0, null);
    }
    var xpr = job._2 = job._1.evaluate(
        document,
        XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
        job._2 || null
    );
    var i = xpr.snapshotLength, node;
    while ( i-- ) {
        node = xpr.snapshotItem(i);
        if ( node.nodeType === 1 ) {
            fn(node, job);
        }
    }
};

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

var domFilterer = {
    addedNodesHandlerMissCount: 0,
    removedNodesHandlerMissCount: 0,
    commitTimer: null,
    disabledId: vAPI.randomToken(),
    enabled: true,
    excludeId: undefined,
    hiddenId: vAPI.randomToken(),
    hiddenNodeCount: 0,
    hiddenNodeEnforcer: false,
    loggerEnabled: undefined,
    styleTags: [],

    jobQueue: jobQueue,
    // Stock jobs.
    job0: jobQueue[0],
    job1: jobQueue[1],
    job2: jobQueue[2],
    job3: jobQueue[3],

    addExceptions: function(aa) {
        for ( var i = 0, n = aa.length; i < n; i++ ) {
            allExceptions.add(aa[i]);
        }
    },

    // Job:
    // Stock jobs in job queue:
    //     0 = css rules/css declaration to remove visibility
    //     1 = css rules/any css declaration
    //     2 = simple css selectors/hide
    //     3 = complex css selectors/hide
    // Custom jobs:
    //     matches-css/hide
    //     has/hide
    //     xpath/hide

    addSelector: function(s) {
        if ( allSelectors.has(s) || allExceptions.has(s) ) {
            return;
        }
        allSelectors.add(s);
        var sel0 = s, sel1 = '';
        if ( s.charCodeAt(s.length - 1) === 0x29 ) {
            var parts = reParserEx.exec(s);
            if ( parts !== null ) {
                sel1 = parts[0];
            }
        }
        if ( sel1 === '' ) {
            this.job0._0.push(sel0);
            if ( sel0.indexOf(' ') === -1 ) {
                this.job2._0.push(sel0);
                this.job2._1 = undefined;
            } else {
                this.job3._0.push(sel0);
                this.job3._1 = undefined;
            }
            return;
        }
        sel0 = sel0.slice(0, sel0.length - sel1.length);
        if ( sel1.lastIndexOf(':has', 0) === 0 ) {
            this.jobQueue.push({ t: 'has-hide', raw: s, _0: sel0, _1: sel1.slice(5, -1) });
        } else if ( sel1.lastIndexOf(':matches-css', 0) === 0 ) {
            if ( sel1.lastIndexOf(':matches-css-before', 0) === 0 ) {
                this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(20, -1), _2: ':before' });
            } else if ( sel1.lastIndexOf(':matches-css-after', 0) === 0 ) {
                this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(19, -1), _2: ':after' });
            } else {
                this.jobQueue.push({ t: 'matches-css-hide', raw: s, _0: sel0, _1: sel1.slice(13, -1), _2: null });
            }
        } else if ( sel1.lastIndexOf(':style', 0) === 0 ) {
            this.job1._0.push(sel0 + ' { ' + sel1.slice(7, -1) + ' }');
            this.job1._1 = undefined;
        } else if ( sel1.lastIndexOf(':xpath', 0) === 0 ) {
            this.jobQueue.push({ t: 'xpath-hide', raw: s, _0: sel1.slice(7, -1) });
        }
        return;
    },

    addSelectors: function(aa) {
        for ( var i = 0, n = aa.length; i < n; i++ ) {
            this.addSelector(aa[i]);
        }
    },

    addStyleTag: function(text) {
        var styleTag = document.createElement('style');
        styleTag.setAttribute('type', 'text/css');
        styleTag.textContent = text;
        if ( document.head ) {
            document.head.appendChild(styleTag);
        }
        this.styleTags.push(styleTag);
        if ( userCSS ) {
            userCSS.add(text);
        }
    },

    checkStyleTags_: function() {
        var doc = document,
            html = doc.documentElement,
            head = doc.head,
            newParent = head || html;
        if ( newParent === null ) { return; }
        this.removedNodesHandlerMissCount += 1;
        var styles = this.styleTags,
            style, oldParent;
        for ( var i = 0; i < styles.length; i++ ) {
            style = styles[i];
            oldParent = style.parentNode;
            // https://github.com/gorhill/uBlock/issues/1031
            // If our style tag was disabled, re-insert into the page.
            if (
                style.disabled &&
                oldParent !== null &&
                style.hasAttribute(this.disabledId) === false
            ) {
                oldParent.removeChild(style);
                oldParent = null;
            }
            if ( oldParent === head || oldParent === html ) { continue; }
            style.disabled = false;
            newParent.appendChild(style);
            this.removedNodesHandlerMissCount = 0;
        }
    },

    checkStyleTags: function() {
        if ( this.removedNodesHandlerMissCount < 16 ) {
            this.checkStyleTags_();
        }
    },

    commit_: function() {
        this.commitTimer.clear();

        var beforeHiddenNodeCount = this.hiddenNodeCount,
            styleText = '', i, n;

        // Stock job 0 = css rules/hide
        if ( this.job0._0.length ) {
            styleText = '\n:root ' + this.job0._0.join(',\n:root ') + '\n{ display: none !important; }';
            this.job0._0.length = 0;
        }

        // Stock job 1 = css rules/any css declaration
        if ( this.job1._0.length ) {
            styleText += '\n' + this.job1._0.join('\n');
            this.job1._0.length = 0;
        }

        // Simple selectors: incremental.

        // Stock job 2 = simple css selectors/hide
        if ( this.job2._0.length ) {
            i = stagedNodes.length;
            while ( i-- ) {
                runSimpleSelectorJob(this.job2, stagedNodes[i], hideNode);
            }
        }
        stagedNodes = [];

        // Complex selectors: non-incremental.
        complexSelectorsOldResultSet = complexSelectorsCurrentResultSet;
        complexSelectorsCurrentResultSet = createSet('object');

        // Stock job 3 = complex css selectors/hide
        // The handling of these can be considered optional, since they are
        // also applied declaratively using a style tag.
        if ( this.job3._0.length ) {
            runComplexSelectorJob(this.job3, complexHideNode);
        }

        // Custom jobs. No optional since they can't be applied in a
        // declarative way.
        for ( i = 4, n = this.jobQueue.length; i < n; i++ ) {
            this.runJob(this.jobQueue[i], complexHideNode);
        }

        // https://github.com/gorhill/uBlock/issues/1912
        //   If one or more nodes have been manually hidden, insert a style tag
        //   targeting these manually hidden nodes. For browsers supporting
        //   user styles, this allows uBO to win.
        var commitHit = this.hiddenNodeCount !== beforeHiddenNodeCount;
        if ( commitHit ) {
            if ( this.hiddenNodeEnforcer === false ) {
                styleText += '\n:root *[' + this.hiddenId + '][hidden] { display: none !important; }';
                this.hiddenNodeEnforcer = true;
            }
            this.addedNodesHandlerMissCount = 0;
        } else {
            this.addedNodesHandlerMissCount += 1;
        }

        if ( styleText !== '' ) {
            this.addStyleTag(styleText);
        }

        // Un-hide nodes previously hidden.
        i = complexSelectorsOldResultSet.size;
        if ( i !== 0 ) {
            var iter = complexSelectorsOldResultSet.values();
            while ( i-- ) {
                this.unhideNode(iter.next().value);
            }
            complexSelectorsOldResultSet.clear();
        }

        // If DOM nodes have been affected, lazily notify core process.
        if (
            this.loggerEnabled !== false &&
            commitHit &&
            cosmeticFiltersActivatedTimer === null
        ) {
            cosmeticFiltersActivatedTimer = vAPI.setTimeout(
                cosmeticFiltersActivated,
                503
            );
        }
    },

    commit: function(nodes, commitNow) {
        if ( nodes === 'all' ) {
            stagedNodes = [ document.documentElement ];
        } else if ( stagedNodes[0] !== document.documentElement ) {
            stagedNodes = stagedNodes.concat(nodes);
        }
        if ( commitNow ) {
            this.commitTimer.clear();
            this.commit_();
            return;
        }
        this.commitTimer.start();
    },

    getExcludeId: function() {
        if ( this.excludeId === undefined ) {
            this.excludeId = vAPI.randomToken();
        }
        return this.excludeId;
    },

    hideNode: function(node) {
        if ( node[this.hiddenId] !== undefined ) { return; }
        if ( this.excludeId !== undefined && node[this.excludeId] ) { return; }
        node.setAttribute(this.hiddenId, '');
        this.hiddenNodeCount += 1;
        node.hidden = true;
        node[this.hiddenId] = null;
        var style = window.getComputedStyle(node),
            display = style.getPropertyValue('display');
        if ( display !== '' && display !== 'none' ) {
            var styleAttr = node.getAttribute('style') || '';
            node[this.hiddenId] = node.hasAttribute('style') && styleAttr;
            if ( styleAttr !== '' ) { styleAttr += '; '; }
            node.setAttribute('style', styleAttr + 'display: none !important;');
        }
        if ( shadowId === undefined ) { return; }
        var shadow = node.shadowRoot;
        if ( shadow ) {
            if ( shadow[shadowId] && shadow.firstElementChild !== null ) {
                shadow.removeChild(shadow.firstElementChild);
            }
            return;
        }
        // https://github.com/gorhill/uBlock/pull/555
        // Not all nodes can be shadowed:
        //   https://github.com/w3c/webcomponents/issues/102
        try {
            shadow = node.createShadowRoot();
            shadow[shadowId] = true;
        } catch (ex) {
        }
    },

    init: function() {
        this.commitTimer = new vAPI.SafeAnimationFrame(this.commit_.bind(this));
    },

    runJob: function(job, fn) {
        switch ( job.t ) {
        case 'has-hide':
            runHasJob(job, fn);
            break;
        case 'matches-css-hide':
            runMatchesCSSJob(job, fn);
            break;
        case 'xpath-hide':
            runXpathJob(job, fn);
            break;
        }
    },

    showNode: function(node) {
        node.hidden = false;
        var styleAttr = node[this.hiddenId];
        if ( styleAttr === false ) {
            node.removeAttribute('style');
        } else if ( typeof styleAttr === 'string' ) {
            node.setAttribute('style', node[this.hiddenId]);
        }
        var shadow = node.shadowRoot;
        if ( shadow && shadow[shadowId] ) {
            if ( shadow.firstElementChild !== null ) {
                shadow.removeChild(shadow.firstElementChild);
            }
            shadow.appendChild(document.createElement('content'));
        }
    },

    toggleLogging: function(state) {
        this.loggerEnabled = state;
    },

    toggleOff: function() {
        if ( userCSS ) {
            userCSS.toggle(false);
        }
        this.enabled = false;
    },

    toggleOn: function() {
        if ( userCSS ) {
            userCSS.toggle(true);
        }
        this.enabled = true;
    },

    unhideNode: function(node) {
        if ( node[this.hiddenId] !== undefined ) {
            this.hiddenNodeCount--;
        }
        node.removeAttribute(this.hiddenId);
        node[this.hiddenId] = undefined;
        node.hidden = false;
        var shadow = node.shadowRoot;
        if ( shadow && shadow[shadowId] ) {
            if ( shadow.firstElementChild !== null ) {
                shadow.removeChild(shadow.firstElementChild);
            }
            shadow.appendChild(document.createElement('content'));
        }
    },

    unshowNode: function(node) {
        node.hidden = true;
        var styleAttr = node[this.hiddenId];
        if ( styleAttr === false ) {
            node.setAttribute('style', 'display: none !important;');
        } else if ( typeof styleAttr === 'string' ) {
            node.setAttribute('style', node[this.hiddenId] + '; display: none !important;');
        }
        var shadow = node.shadowRoot;
        if ( shadow && shadow[shadowId] && shadow.firstElementChild !== null ) {
            shadow.removeChild(shadow.firstElementChild);
        }
    },

    domChangedHandler: function(addedNodes, removedNodes) {
        this.commit(addedNodes);
        // https://github.com/gorhill/uBlock/issues/873
        // This will ensure our style elements will stay in the DOM.
        if ( removedNodes ) {
            domFilterer.checkStyleTags();
        }
    },

    start: function() {
        var domChangedHandler = this.domChangedHandler.bind(this);
        vAPI.domWatcher.addListener(domChangedHandler);
        vAPI.shutdown.add(function() {
            vAPI.domWatcher.removeListener(domChangedHandler);
        });
    }
};

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

var hideNode = domFilterer.hideNode.bind(domFilterer);

var complexHideNode = function(node) {
    complexSelectorsCurrentResultSet.add(node);
    if ( !complexSelectorsOldResultSet.delete(node) ) {
        hideNode(node);
    }
};

var cssNotHiddenId = ':not([' + domFilterer.hiddenId + '])';

domFilterer.init();

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

return domFilterer;

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

})();

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

// This is executed once, and since no hooks are left behind once the response
// is received, I expect this code to be garbage collected by the browser.

(function domIsLoading() {

    var responseHandler = function(response) {
        // cosmetic filtering engine aka 'cfe'
        var cfeDetails = response && response.specificCosmeticFilters;
        if ( !cfeDetails || !cfeDetails.ready ) {
            vAPI.domWatcher = vAPI.domCollapser = vAPI.domFilterer =
            vAPI.domSurveyor = vAPI.domIsLoaded = null;
            vAPI.unlock();
            return;
        }

        if ( response.noCosmeticFiltering ) {
            vAPI.domFilterer = null;
            vAPI.domSurveyor = null;
        } else {
            var domFilterer = vAPI.domFilterer;
            domFilterer.toggleLogging(response.loggerEnabled);
            if ( response.noGenericCosmeticFiltering || cfeDetails.noDOMSurveying ) {
                vAPI.domSurveyor = null;
            }
            if ( cfeDetails.cosmeticHide.length !== 0 || cfeDetails.cosmeticDonthide.length !== 0 ) {
                domFilterer.addExceptions(cfeDetails.cosmeticDonthide);
                domFilterer.addSelectors(cfeDetails.cosmeticHide);
                domFilterer.commit('all', true);
            }
        }

        var parent = document.head || document.documentElement;
        if ( parent ) {
            var elem, text;
            if ( cfeDetails.netHide.length !== 0 ) {
                elem = document.createElement('style');
                elem.setAttribute('type', 'text/css');
                text = cfeDetails.netHide.join(',\n');
                text += response.collapseBlocked ?
                    '\n{display:none !important;}' :
                    '\n{visibility:hidden !important;}';
                elem.appendChild(document.createTextNode(text));
                parent.appendChild(elem);
            }
            // Library of resources is located at:
            // https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt
            if ( cfeDetails.scripts ) {
                elem = document.createElement('script');
                // Have the injected script tag remove itself when execution completes:
                // to keep DOM as clean as possible.
                text = cfeDetails.scripts +
                    "\n" +
                    "(function() {\n" +
                    "    var c = document.currentScript,\n" +
                    "        p = c && c.parentNode;\n" +
                    "    if ( p ) {\n" +
                    "        p.removeChild(c);\n" +
                    "    }\n" +
                    "})();";
                elem.appendChild(document.createTextNode(text));
                parent.appendChild(elem);
                vAPI.injectedScripts = text;
            }
        }

        // https://github.com/chrisaljoudi/uBlock/issues/587
        // If no filters were found, maybe the script was injected before
        // uBlock's process was fully initialized. When this happens, pages
        // won't be cleaned right after browser launch.
        if ( document.readyState !== 'loading' ) {
            (new vAPI.SafeAnimationFrame(vAPI.domIsLoaded)).start();
        } else {
            document.addEventListener('DOMContentLoaded', vAPI.domIsLoaded);
        }
    };

    var url = window.location.href;
    vAPI.messaging.send(
        'contentscript',
        {
            what: 'retrieveContentScriptParameters',
            pageURL: url,
            locationURL: url
        },
        responseHandler
    );

})();

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

vAPI.domWatcher = (function() {

    var domLayoutObserver = null,
        ignoreTags = { 'head': 1, 'link': 1, 'meta': 1, 'script': 1, 'style': 1 },
        addedNodeLists = [],
        addedNodes = [],
        removedNodes = false,
        listeners = [];

    var safeObserverHandler = function() {
        safeObserverHandlerTimer.clear();
        var i = addedNodeLists.length,
            nodeList, iNode, node;
        while ( i-- ) {
            nodeList = addedNodeLists[i];
            iNode = nodeList.length;
            while ( iNode-- ) {
                node = nodeList[iNode];
                if ( node.nodeType !== 1 ) {
                    continue;
                }
                if ( ignoreTags[node.localName] === 1 ) {
                    continue;
                }
                addedNodes.push(node);
            }
        }
        addedNodeLists.length = 0;
        if ( addedNodes.length !== 0 || removedNodes ) {
            listeners[0](addedNodes, removedNodes);
            if ( listeners[1] ) {
                listeners[1](addedNodes, removedNodes);
            }
            addedNodes.length = 0;
            removedNodes = false;
        }
    };

    var safeObserverHandlerTimer = new vAPI.SafeAnimationFrame(safeObserverHandler);

    // https://github.com/chrisaljoudi/uBlock/issues/205
    // Do not handle added node directly from within mutation observer.
    var observerHandler = function(mutations) {
        var nodeList, mutation,
            i = mutations.length;
        while ( i-- ) {
            mutation = mutations[i];
            nodeList = mutation.addedNodes;
            if ( nodeList.length !== 0 ) {
                addedNodeLists.push(nodeList);
            }
            if ( mutation.removedNodes.length !== 0 ) {
                removedNodes = true;
            }
        }
        if ( addedNodeLists.length !== 0 || removedNodes ) {
            safeObserverHandlerTimer.start();
        }
    };

    var addListener = function(listener) {
        if ( listeners.indexOf(listener) !== -1 ) {
            return;
        }
        listeners.push(listener);
        if ( domLayoutObserver !== null ) {
            return;
        }
        domLayoutObserver = new MutationObserver(observerHandler);
        domLayoutObserver.observe(document.documentElement, {
            //attributeFilter: [ 'class', 'id' ],
            //attributes: true,
            childList: true,
            subtree: true
        });
    };

    var removeListener = function(listener) {
        var pos = listeners.indexOf(listener);
        if ( pos === -1 ) {
            return;
        }
        listeners.splice(pos, 1);
        if ( listeners.length !== 0 || domLayoutObserver === null ) {
            return;
        }
        domLayoutObserver.disconnect();
        domLayoutObserver = null;
    };

    var start = function() {
        vAPI.shutdown.add(function() {
            if ( domLayoutObserver !== null ) {
                domLayoutObserver.disconnect();
                domLayoutObserver = null;
            }
            safeObserverHandlerTimer.clear();
        });
    };

    return {
        addListener: addListener,
        removeListener: removeListener,
        start: start
    };
})();

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

vAPI.domCollapser = (function() {
    var timer = null;
    var pendingRequests = Object.create(null);
    var roundtripRequests = [];
    var src1stProps = {
        'embed': 'src',
        'img': 'src',
        'object': 'data'
    };
    var src2ndProps = {
        'img': 'srcset'
    };
    var netSelectorCacheCount = 0;
    var messaging = vAPI.messaging;

    // Because a while ago I have observed constructors are faster than
    // literal object instanciations.
    var RoundtripRequest = function(tag, attr, url) {
        this.tag = tag;
        this.attr = attr;
        this.url = url;
        this.collapse = false;
    };

    var onProcessed = function(response) {
        // This can happens if uBO is restarted.
        if ( !response ) {
            return;
        }
        var requests = response.result;
        if ( requests === null || Array.isArray(requests) === false ) {
            return;
        }
        var selectors = [],
            netSelectorCacheCountMax = response.netSelectorCacheCountMax,
            aa = [ null ],
            request, key, entry, target, value;
        // Important: process in chronological order -- this ensures the
        // cached selectors are the most useful ones.
        for ( var i = 0, ni = requests.length; i < ni; i++ ) {
            request = requests[i];
            key = request.tag + ' ' + request.attr + ' ' + request.url;
            entry = pendingRequests[key];
            if ( entry === undefined ) {
                continue;
            }
            delete pendingRequests[key];
            // https://github.com/chrisaljoudi/uBlock/issues/869
            if ( !request.collapse ) {
                continue;
            }
            if ( Array.isArray(entry) === false ) {
                aa[0] = entry;
                entry = aa;
            }
            for ( var j = 0, nj = entry.length; j < nj; j++ ) {
                target = entry[j];
                // https://github.com/chrisaljoudi/uBlock/issues/399
                // Never remove elements from the DOM, just hide them
                target.style.setProperty('display', 'none', 'important');
                target.hidden = true;
                // https://github.com/chrisaljoudi/uBlock/issues/1048
                // Use attribute to construct CSS rule
                if (
                    netSelectorCacheCount <= netSelectorCacheCountMax &&
                    (value = target.getAttribute(request.attr))
                ) {
                    selectors.push(request.tag + '[' + request.attr + '="' + value + '"]');
                    netSelectorCacheCount += 1;
                }
            }
        }
        if ( selectors.length !== 0 ) {
            messaging.send(
                'contentscript',
                {
                    what: 'cosmeticFiltersInjected',
                    type: 'net',
                    hostname: window.location.hostname,
                    selectors: selectors
                }
            );
        }
    };

    var send = function() {
        timer = null;
        // https://github.com/gorhill/uBlock/issues/1927
        // Normalize hostname to avoid trailing dot of FQHN.
        var pageHostname = window.location.hostname || '';
        if (
            pageHostname.length &&
            pageHostname.charCodeAt(pageHostname.length - 1) === 0x2e
        ) {
            pageHostname = pageHostname.slice(0, -1);
        }
        messaging.send(
            'contentscript',
            {
                what: 'filterRequests',
                pageURL: window.location.href,
                pageHostname: pageHostname,
                requests: roundtripRequests
            }, onProcessed
        );
        roundtripRequests = [];
    };

    var process = function(delay) {
        if ( roundtripRequests.length === 0 ) {
            return;
        }
        if ( delay === 0 ) {
            clearTimeout(timer);
            send();
        } else if ( timer === null ) {
            timer = vAPI.setTimeout(send, delay || 20);
        }
    };

    // If needed eventually, we could listen to `src` attribute changes
    // for iframes.

    var add = function(target) {
        var tag = target.localName;
        var prop = src1stProps[tag];
        if ( prop === undefined ) {
            return;
        }
        // https://github.com/chrisaljoudi/uBlock/issues/174
        // Do not remove fragment from src URL
        var src = target[prop];
        if ( typeof src !== 'string' || src.length === 0 ) {
            prop = src2ndProps[tag];
            if ( prop === undefined ) {
                return;
            }
            src = target[prop];
            if ( typeof src !== 'string' || src.length === 0 ) {
                return;
            }
        }
        if ( src.lastIndexOf('http', 0) !== 0 ) {
            return;
        }
        var key = tag + ' ' + prop + ' ' + src,
            entry = pendingRequests[key];
        if ( entry === undefined ) {
            pendingRequests[key] = target;
            roundtripRequests.push(new RoundtripRequest(tag, prop, src));
        } else if ( Array.isArray(entry) ) {
            entry.push(target);
        } else {
            pendingRequests[key] = [ entry, target ];
        }
    };

    var addMany = function(targets) {
        var i = targets.length;
        while ( i-- ) {
            add(targets[i]);
        }
    };

    var iframeSourceModified = function(mutations) {
        var i = mutations.length;
        while ( i-- ) {
            addIFrame(mutations[i].target, true);
        }
        process();
    };
    var iframeSourceObserver = new MutationObserver(iframeSourceModified);
    var iframeSourceObserverOptions = {
        attributes: true,
        attributeFilter: [ 'src' ]
    };

    var primeLocalIFrame = function(iframe) {
        // Should probably also copy injected styles.
        // The injected scripts are those which were injected in the current
        // document, from within the `contentscript-start.js / injectScripts`,
        // and which scripts are selectively looked-up from:
        // https://github.com/gorhill/uBlock/blob/master/assets/ublock/resources.txt
        if ( vAPI.injectedScripts ) {
            var scriptTag = document.createElement('script');
            scriptTag.appendChild(document.createTextNode(vAPI.injectedScripts));
            var parent = iframe.contentDocument && iframe.contentDocument.head;
            if ( parent ) {
                parent.appendChild(scriptTag);
            }
        }
    };

    var addIFrame = function(iframe, dontObserve) {
        // https://github.com/gorhill/uBlock/issues/162
        // Be prepared to deal with possible change of src attribute.
        if ( dontObserve !== true ) {
            iframeSourceObserver.observe(iframe, iframeSourceObserverOptions);
        }

        var src = iframe.src;
        if ( src === '' || typeof src !== 'string' ) {
            primeLocalIFrame(iframe);
            return;
        }
        if ( src.lastIndexOf('http', 0) !== 0 ) {
            return;
        }
        var key = 'iframe' + ' ' + 'src' + ' ' + src,
            entry = pendingRequests[key];
        if ( entry === undefined ) {
            pendingRequests[key] = iframe;
            roundtripRequests.push(new RoundtripRequest('iframe', 'src', src));
        } else if ( Array.isArray(entry) ) {
            entry.push(iframe);
        } else {
            pendingRequests[key] = [ entry, iframe ];
        }
    };

    var addIFrames = function(iframes) {
        var i = iframes.length;
        while ( i-- ) {
            addIFrame(iframes[i]);
        }
    };

    var onResourceFailed = function(ev) {
        vAPI.domCollapser.add(ev.target);
        vAPI.domCollapser.process();
    };

    var domChangedHandler = function(nodes) {
        var node;
        for ( var i = 0, ni = nodes.length; i < ni; i++ ) {
            node = nodes[i];
            if ( node.localName === 'iframe' ) {
                addIFrame(node);
            }
            if ( node.children && node.children.length !== 0 ) {
                var iframes = node.getElementsByTagName('iframe');
                if ( iframes.length !== 0 ) {
                    addIFrames(iframes);
                }
            }
        }
        process();
    };

    var start = function() {
        // Listener to collapse blocked resources.
        // - Future requests not blocked yet
        // - Elements dynamically added to the page
        // - Elements which resource URL changes

        // https://github.com/chrisaljoudi/uBlock/issues/7
        // Preferring getElementsByTagName over querySelectorAll:
        //   http://jsperf.com/queryselectorall-vs-getelementsbytagname/145
        var elems = document.images || document.getElementsByTagName('img'),
            i = elems.length, elem;
        while ( i-- ) {
            elem = elems[i];
            if ( elem.complete ) {
                add(elem);
            }
        }
        addMany(document.embeds || document.getElementsByTagName('embed'));
        addMany(document.getElementsByTagName('object'));
        addIFrames(document.getElementsByTagName('iframe'));
        process(0);

        document.addEventListener('error', onResourceFailed, true);
        vAPI.domWatcher.addListener(domChangedHandler);

        vAPI.shutdown.add(function() {
            document.removeEventListener('error', onResourceFailed, true);
            vAPI.domWatcher.removeListener(domChangedHandler);
            if ( timer !== null ) {
                clearTimeout(timer);
            }
        });
    };

    return {
        add: add,
        addMany: addMany,
        addIFrame: addIFrame,
        addIFrames: addIFrames,
        process: process,
        start: start
    };
})();

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

vAPI.domSurveyor = (function() {
    var domFilterer = null,
        messaging = vAPI.messaging,
        surveyPhase3Nodes = [],
        cosmeticSurveyingMissCount = 0,
        highGenerics = null,
        lowGenericSelectors = [],
        queriedSelectors = createSet(),
        surveyCost = 0;

    // Handle main process' response.

    var surveyPhase3 = function(response) {
        var result = response && response.result,
            firstSurvey = highGenerics === null;

        if ( result ) {
            if ( result.hide.length ) {
                processLowGenerics(result.hide);
            }
            if ( result.highGenerics ) {
                highGenerics = result.highGenerics;
            }
        }

        if ( highGenerics ) {
            var t0 = window.performance.now();
            if ( highGenerics.hideLowCount ) {
                processHighLowGenerics(highGenerics.hideLow);
            }
            if ( highGenerics.hideMediumCount ) {
                processHighMediumGenerics(highGenerics.hideMedium);
            }
            if ( highGenerics.hideHighSimpleCount || highGenerics.hideHighComplexCount ) {
                processHighHighGenerics();
            }
            surveyCost += window.performance.now() - t0;
        }

        // Need to do this before committing DOM filterer, as needed info
        // will no longer be there after commit.
        if ( firstSurvey || domFilterer.job0._0.length ) {
            messaging.send(
                'contentscript',
                {
                    what: 'cosmeticFiltersInjected',
                    type: 'cosmetic',
                    hostname: window.location.hostname,
                    selectors: domFilterer.job0._0,
                    first: firstSurvey,
                    cost: surveyCost
                }
            );
        }

        // Shutdown surveyor if too many consecutive empty resultsets.
        if ( domFilterer.job0._0.length === 0 ) {
            cosmeticSurveyingMissCount += 1;
        } else {
            cosmeticSurveyingMissCount = 0;
        }

        domFilterer.commit(surveyPhase3Nodes);
        surveyPhase3Nodes = [];
    };

    // Query main process.

    var surveyPhase2 = function(addedNodes) {
        surveyPhase3Nodes = surveyPhase3Nodes.concat(addedNodes);
        if ( lowGenericSelectors.length !== 0 || highGenerics === null ) {
            messaging.send(
                'contentscript',
                {
                    what: 'retrieveGenericCosmeticSelectors',
                    pageURL: window.location.href,
                    selectors: lowGenericSelectors,
                    firstSurvey: highGenerics === null
                },
                surveyPhase3
            );
            lowGenericSelectors = [];
        } else {
            surveyPhase3(null);
        }
    };

    // Low generics:
    // - [id]
    // - [class]

    var processLowGenerics = function(generics) {
        domFilterer.addSelectors(generics);
    };

    // High-low generics:
    // - [alt="..."]
    // - [title="..."]

    var processHighLowGenerics = function(generics) {
        var attrs = ['title', 'alt'];
        var attr, attrValue, nodeList, iNode, node;
        var selector;
        while ( (attr = attrs.pop()) ) {
            nodeList = selectNodes('[' + attr + ']', surveyPhase3Nodes);
            iNode = nodeList.length;
            while ( iNode-- ) {
                node = nodeList[iNode];
                attrValue = node.getAttribute(attr);
                if ( !attrValue ) { continue; }
                // Candidate 1 = generic form
                // If generic form is injected, no need to process the
                // specific form, as the generic will affect all related
                // specific forms.
                selector = '[' + attr + '="' + attrValue + '"]';
                if ( generics.hasOwnProperty(selector) ) {
                    domFilterer.addSelector(selector);
                    continue;
                }
                // Candidate 2 = specific form
                selector = node.localName + selector;
                if ( generics.hasOwnProperty(selector) ) {
                    domFilterer.addSelector(selector);
                }
            }
        }
    };

    // High-medium generics:
    // - [href^="http"]

    var processHighMediumGenerics = function(generics) {
        var stagedNodes = surveyPhase3Nodes,
            i = stagedNodes.length;
        if ( i === 1 && stagedNodes[0] === document.documentElement ) {
            processHighMediumGenericsForNodes(document.links, generics);
            return;
        }
        var aa = [ null ],
            node, nodes;
        while ( i-- ) {
            node = stagedNodes[i];
            if ( node.localName === 'a' ) {
                aa[0] = node;
                processHighMediumGenericsForNodes(aa, generics);
            }
            nodes = node.getElementsByTagName('a');
            if ( nodes.length !== 0 ) {
                processHighMediumGenericsForNodes(nodes, generics);
            }
        }
    };

    var processHighMediumGenericsForNodes = function(nodes, generics) {
        var i = nodes.length,
            node, href, pos, entry, j, selector;
        while ( i-- ) {
            node = nodes[i];
            href = node.getAttribute('href');
            if ( !href ) { continue; }
            pos = href.indexOf('://');
            if ( pos === -1 ) { continue; }
            entry = generics[href.slice(pos + 3, pos + 11)];
            if ( entry === undefined ) { continue; }
            if ( typeof entry === 'string' ) {
                if ( href.lastIndexOf(entry.slice(8, -2), 0) === 0 ) {
                    domFilterer.addSelector(entry);
                }
                continue;
            }
            j = entry.length;
            while ( j-- ) {
                selector = entry[j];
                if ( href.lastIndexOf(selector.slice(8, -2), 0) === 0 ) {
                    domFilterer.addSelector(selector);
                }
            }
        }
    };

    var highHighSimpleGenericsCost = 0,
        highHighSimpleGenericsInjected = false,
        highHighComplexGenericsCost = 0,
        highHighComplexGenericsInjected = false;

    var processHighHighGenerics = function() {
        var tstart;
        // Simple selectors.
        if (
            highHighSimpleGenericsInjected === false &&
            highHighSimpleGenericsCost < 50 &&
            highGenerics.hideHighSimpleCount !== 0
        ) {
            tstart = window.performance.now();
            var matchesProp = vAPI.matchesProp,
                nodes = surveyPhase3Nodes,
                i = nodes.length, node;
            while ( i-- ) {
                node = nodes[i];
                if (
                    node[matchesProp](highGenerics.hideHighSimple) ||
                    node.querySelector(highGenerics.hideHighSimple) !== null
                ) {
                    highHighSimpleGenericsInjected = true;
                    domFilterer.addSelectors(highGenerics.hideHighSimple.split(',\n'));
                    break;
                }
            }
            highHighSimpleGenericsCost += window.performance.now() - tstart;
        }
        // Complex selectors.
        if (
            highHighComplexGenericsInjected === false &&
            highHighComplexGenericsCost < 50 &&
            highGenerics.hideHighComplexCount !== 0
        ) {
            tstart = window.performance.now();
            if ( document.querySelector(highGenerics.hideHighComplex) !== null ) {
                highHighComplexGenericsInjected = true;
                domFilterer.addSelectors(highGenerics.hideHighComplex.split(',\n'));
            }
            highHighComplexGenericsCost += window.performance.now() - tstart;
        }
    };

    // Extract and return the staged nodes which (may) match the selectors.

    var selectNodes = function(selector, nodes) {
        var stagedNodes = nodes,
            i = stagedNodes.length;
        if ( i === 1 && stagedNodes[0] === document.documentElement ) {
            return document.querySelectorAll(selector);
        }
        var targetNodes = [],
            node, nodeList, j;
        while ( i-- ) {
            node = stagedNodes[i];
            targetNodes.push(node);
            nodeList = node.querySelectorAll(selector);
            j = nodeList.length;
            while ( j-- ) {
                targetNodes.push(nodeList[j]);
            }
        }
        return targetNodes;
    };

    // Extract all classes/ids: these will be passed to the cosmetic
    // filtering engine, and in return we will obtain only the relevant
    // CSS selectors.

    // https://github.com/gorhill/uBlock/issues/672
    // http://www.w3.org/TR/2014/REC-html5-20141028/infrastructure.html#space-separated-tokens
    // http://jsperf.com/enumerate-classes/6

    var surveyPhase1 = function(addedNodes) {
        var t0 = window.performance.now(),
            nodes = selectNodes('[class],[id]', addedNodes),
            qq = queriedSelectors,
            ll = lowGenericSelectors,
            node, v, vv, j,
            i = nodes.length;
        while ( i-- ) {
            node = nodes[i];
            if ( node.nodeType !== 1 ) { continue; }
            v = node.id;
            if ( v !== '' && typeof v === 'string' ) {
                v = '#' + v.trim();
                if ( v !== '#' && !qq.has(v) ) { ll.push(v); qq.add(v); }
            }
            vv = node.className;
            if ( vv === '' || typeof vv !== 'string' ) { continue; }
            if ( /\s/.test(vv) === false ) {
                v = '.' + vv;
                if ( !qq.has(v) ) { ll.push(v); qq.add(v); }
            } else {
                vv = node.classList;
                j = vv.length;
                while ( j-- ) {
                    v = '.' + vv[j];
                    if ( !qq.has(v) ) { ll.push(v); qq.add(v); }
                }
            }
        }
        surveyCost += window.performance.now() - t0;
        surveyPhase2(addedNodes);
    };

    var domChangedHandler = function(addedNodes, removedNodes) {
        if ( cosmeticSurveyingMissCount > 255 ) {
            vAPI.domWatcher.removeListener(domChangedHandler);
            vAPI.domSurveyor = null;
            domFilterer.domChangedHandler(addedNodes, removedNodes);
            domFilterer.start();
            return;
        }

        surveyPhase1(addedNodes);
        if ( removedNodes ) {
            domFilterer.checkStyleTags();
        }
    };

    var start = function() {
        domFilterer = vAPI.domFilterer;
        domChangedHandler([ document.documentElement ]);
        vAPI.domWatcher.addListener(domChangedHandler);
        vAPI.shutdown.add(function() {
            vAPI.domWatcher.removeListener(domChangedHandler);
        });
    };

    return {
        start: start
    };
})();

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

vAPI.domIsLoaded = function(ev) {
    // This can happen on Firefox. For instance:
    // https://github.com/gorhill/uBlock/issues/1893
    if ( window.location === null ) {
        return;
    }

    var slowLoad = ev instanceof Event;
    if ( slowLoad ) {
        document.removeEventListener('DOMContentLoaded', vAPI.domIsLoaded);
    }
    vAPI.domIsLoaded = null;

    vAPI.domWatcher.start();
    vAPI.domCollapser.start();

    if ( vAPI.domFilterer ) {
        // https://github.com/chrisaljoudi/uBlock/issues/789
        // https://github.com/gorhill/uBlock/issues/873
        // Be sure our style tags used for cosmetic filtering are still
        // applied.
        vAPI.domFilterer.checkStyleTags();
        // To avoid neddless CPU overhead, we commit existing cosmetic filters
        // only if the page loaded "slowly", i.e. if the code here had to wait
        // for a DOMContentLoaded event -- in which case the DOM may have
        // changed a lot since last time the domFilterer acted on it.
        if ( slowLoad ) {
            vAPI.domFilterer.commit('all');
        }
        if ( vAPI.domSurveyor ) {
            vAPI.domSurveyor.start();
        } else {
            vAPI.domFilterer.start();
        }
    }

    // To send mouse coordinates to main process, as the chrome API fails
    // to provide the mouse position to context menu listeners.
    // https://github.com/chrisaljoudi/uBlock/issues/1143
    // Also, find a link under the mouse, to try to avoid confusing new tabs
    // as nuisance popups.
    // Ref.: https://developer.mozilla.org/en-US/docs/Web/Events/contextmenu

    var onMouseClick = function(ev) {
        var elem = ev.target;
        while ( elem !== null && elem.localName !== 'a' ) {
            elem = elem.parentElement;
        }
        vAPI.messaging.send(
            'contentscript',
            {
                what: 'mouseClick',
                x: ev.clientX,
                y: ev.clientY,
                url: elem !== null ? elem.href : ''
            }
        );
    };

    (function() {
        if ( window !== window.top || !vAPI.domFilterer ) {
            return;
        }
        document.addEventListener('mousedown', onMouseClick, true);

        // https://github.com/gorhill/uMatrix/issues/144
        vAPI.shutdown.add(function() {
            document.removeEventListener('mousedown', onMouseClick, true);
        });
    })();
};

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