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

    uBlock Origin - a comprehensive, efficient content blocker
    Copyright (C) 2015-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/uBlock
*/

/* globals browser */

'use strict';

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

(( ) => {
// >>>>>>>> start of private namespace

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

if ( typeof vAPI !== 'object' ) { return; }
if ( vAPI.domWatcher instanceof Object === false ) { return; }

const reHasCSSCombinators = /[ >+~]/;
const simpleDeclarativeSet = new Set();
let simpleDeclarativeStr;
const complexDeclarativeSet = new Set();
let complexDeclarativeStr;
const proceduralDict = new Map();
const exceptionDict = new Map();
let exceptionStr;
const proceduralExceptionDict = new Map();
const nodesToProcess = new Set();
const loggedSelectors = new Set();

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

const rePseudoElements = /:(?::?after|:?before|:[a-z-]+)$/;

function hasSelector(selector, context = document) {
    try {
        return context.querySelector(selector) !== null;
    }
    catch(ex) {
    }
    return false;
}

function safeMatchSelector(selector, context) {
    const safeSelector = rePseudoElements.test(selector)
        ? selector.replace(rePseudoElements, '')
        : selector;
    try {
        return context.matches(safeSelector);
    }
    catch(ex) {
    }
    return false;
}

function safeQuerySelector(selector, context = document) {
    const safeSelector = rePseudoElements.test(selector)
        ? selector.replace(rePseudoElements, '')
        : selector;
    try {
        return context.querySelector(safeSelector);
    }
    catch(ex) {
    }
    return null;
}

function safeGroupSelectors(selectors) {
    const arr = Array.isArray(selectors)
        ? selectors
        : Array.from(selectors);
    return arr.map(s => {
        return rePseudoElements.test(s)
            ? s.replace(rePseudoElements, '')
            : s;
    }).join(',\n');
}

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

function processDeclarativeSimple(node, out) {
    if ( simpleDeclarativeSet.size === 0 ) { return; }
    if ( simpleDeclarativeStr === undefined ) {
        simpleDeclarativeStr = safeGroupSelectors(simpleDeclarativeSet);
    }
    if (
        (node === document || node.matches(simpleDeclarativeStr) === false) &&
        (hasSelector(simpleDeclarativeStr, node) === false)
    ) {
        return;
    }
    for ( const selector of simpleDeclarativeSet ) {
        if (
            (node === document || safeMatchSelector(selector, node) === false) &&
            (safeQuerySelector(selector, node) === null)
        ) {
            continue;
        }
        out.push(`##${selector}`);
        simpleDeclarativeSet.delete(selector);
        simpleDeclarativeStr = undefined;
        loggedSelectors.add(selector);
    }
}

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

function processDeclarativeComplex(out) {
    if ( complexDeclarativeSet.size === 0 ) { return; }
    if ( complexDeclarativeStr === undefined ) {
        complexDeclarativeStr = safeGroupSelectors(complexDeclarativeSet);
    }
    if ( hasSelector(complexDeclarativeStr) === false ) { return; }
    for ( const selector of complexDeclarativeSet ) {
        if ( safeQuerySelector(selector) === null ) { continue; }
        out.push(`##${selector}`);
        complexDeclarativeSet.delete(selector);
        complexDeclarativeStr = undefined;
        loggedSelectors.add(selector);
    }
}

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

function processProcedural(out) {
    if ( proceduralDict.size === 0 ) { return; }
    for ( const [ raw, pselector ] of proceduralDict ) {
        if ( pselector.converted ) {
            if ( safeQuerySelector(pselector.selector) === null ) { continue; }
        } else if ( pselector.hit === false && pselector.exec().length === 0 ) {
            continue;
        }
        out.push(`##${raw}`);
        proceduralDict.delete(raw);
    }
}

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

function processExceptions(out) {
    if ( exceptionDict.size === 0 ) { return; }
    if ( exceptionStr === undefined ) {
        exceptionStr = safeGroupSelectors(exceptionDict.keys());
    }
    if ( hasSelector(exceptionStr) === false ) { return; }
    for ( const [ selector, raw ] of exceptionDict ) {
        if ( safeQuerySelector(selector) === null ) { continue; }
        out.push(`#@#${raw}`);
        exceptionDict.delete(selector);
        exceptionStr = undefined;
        loggedSelectors.add(raw);
    }
}

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

function processProceduralExceptions(out) {
    if ( proceduralExceptionDict.size === 0 ) { return; }
    for ( const exception of proceduralExceptionDict.values() ) {
        if ( exception.test() === false ) { continue; }
        out.push(`#@#${exception.raw}`);
        proceduralExceptionDict.delete(exception.raw);
    }
}

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

const processTimer = new vAPI.SafeAnimationFrame(( ) => {
    //console.time('dom logger/scanning for matches');
    processTimer.clear();
    if ( nodesToProcess.size === 0 ) { return; }

    if ( nodesToProcess.size !== 1 && nodesToProcess.has(document) ) {
        nodesToProcess.clear();
        nodesToProcess.add(document);
    }

    const toLog = [];
    if ( simpleDeclarativeSet.size !== 0 ) {
        for ( const node of nodesToProcess ) {
            processDeclarativeSimple(node, toLog);
        }
    }

    processDeclarativeComplex(toLog);
    processProcedural(toLog);
    processExceptions(toLog);
    processProceduralExceptions(toLog);

    nodesToProcess.clear();

    if ( toLog.length === 0 ) { return; }

    const location = vAPI.effectiveSelf.location;

    vAPI.messaging.send('scriptlets', {
        what: 'logCosmeticFilteringData',
        frameURL: location.href,
        frameHostname: location.hostname,
        matchedSelectors: toLog,
    });
    //console.timeEnd('dom logger/scanning for matches');
});

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

const attributeObserver = new MutationObserver(mutations => {
    if ( nodesToProcess.has(document) ) { return; }
    for ( const mutation of mutations ) {
        const node = mutation.target;
        if ( node.nodeType !== 1 ) { continue; }
        nodesToProcess.add(node);
    }
    if ( nodesToProcess.size !== 0 ) {
        processTimer.start(100);
    }
});

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

const handlers = {
    onFiltersetChanged: function(changes) {
        //console.time('dom logger/filterset changed');
        for ( const block of (changes.declarative || []) ) {
            for ( const selector of block.split(',\n') ) {
                if ( loggedSelectors.has(selector) ) { continue; }
                if ( reHasCSSCombinators.test(selector) ) {
                    complexDeclarativeSet.add(selector);
                    complexDeclarativeStr = undefined;
                } else {
                    simpleDeclarativeSet.add(selector);
                    simpleDeclarativeStr = undefined;
                }
            }
        }
        if (
            Array.isArray(changes.procedural) &&
            changes.procedural.length !== 0
        ) {
            for ( const selector of changes.procedural ) {
                proceduralDict.set(selector.raw, selector);
            }
        }
        if ( Array.isArray(changes.exceptions) ) {
            for ( const selector of changes.exceptions ) {
                if ( loggedSelectors.has(selector) ) { continue; }
                if ( selector.charCodeAt(0) !== 0x7B /* '{' */ ) {
                    exceptionDict.set(selector, selector);
                    continue;
                }
                const details = JSON.parse(selector);
                if (
                    details.action !== undefined &&
                    details.tasks === undefined &&
                    details.action[0] === 'style'
                ) {
                    exceptionDict.set(details.selector, details.raw);
                    continue;
                }
                proceduralExceptionDict.set(
                    details.raw,
                    vAPI.domFilterer.createProceduralFilter(details)
                );
            }
            exceptionStr = undefined;
        }
        nodesToProcess.clear();
        nodesToProcess.add(document);
        processTimer.start(1);
        //console.timeEnd('dom logger/filterset changed');
    },

    onDOMCreated: function() {
        if ( vAPI.domFilterer instanceof Object === false ) {
            return shutdown();
        }
        handlers.onFiltersetChanged(vAPI.domFilterer.getAllSelectors());
        vAPI.domFilterer.addListener(handlers);
        attributeObserver.observe(document.body, {
            attributes: true,
            subtree: true
        });
    },

    onDOMChanged: function(addedNodes) {
        if ( nodesToProcess.has(document) ) { return; }
        for ( const node of addedNodes ) {
            if ( node.parentNode === null ) { continue; }
            nodesToProcess.add(node);
        }
        if ( nodesToProcess.size !== 0 ) {
            processTimer.start(100);
        }
    }
};

vAPI.domWatcher.addListener(handlers);

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

const broadcastHandler = msg => {
    if ( msg.what === 'loggerDisabled' ) {
        shutdown();
    }
};

browser.runtime.onMessage.addListener(broadcastHandler);

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

function shutdown() {
    browser.runtime.onMessage.removeListener(broadcastHandler);
    processTimer.clear();
    attributeObserver.disconnect();
    if ( typeof vAPI !== 'object' ) { return; }
    if ( vAPI.domFilterer instanceof Object ) {
        vAPI.domFilterer.removeListener(handlers);
    }
    if ( vAPI.domWatcher instanceof Object ) {
        vAPI.domWatcher.removeListener(handlers);
    }
}

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

// <<<<<<<< end of private namespace
})();








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

    DO NOT:
    - Remove the following code
    - Add code beyond the following code
    Reason:
    - https://github.com/gorhill/uBlock/pull/3721
    - uBO never uses the return value from injected content scripts

**/

void 0;

