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

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

'use strict';

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

(function() {

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

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

var reHasCSSCombinators = /[ >+~]/,
    reHasPseudoClass = /:+(?:after|before)$/,
    sanitizedSelectors = new Map(),
    matchProp = vAPI.matchesProp,
    simple = { dict: new Set(), str: undefined },
    complex = { dict: new Set(), str:  undefined },
    procedural = { dict: new Map() },
    jobQueue = [];

var DeclarativeSimpleJob = function(node) {
    this.node = node;
};
DeclarativeSimpleJob.create = function(node) {
    return new DeclarativeSimpleJob(node);
};
DeclarativeSimpleJob.prototype.lookup = function(out) {
    if ( simple.dict.size === 0 ) { return; }
    if ( simple.str === undefined ) {
        simple.str = Array.from(simple.dict).join(',\n');
    }
    if (
        (this.node === document || this.node[matchProp](simple.str) === false) &&
        (this.node.querySelector(simple.str) === null)
    ) {
        return;
    }
    for ( var selector of simple.dict ) {
        if (
            this.node !== document && this.node[matchProp](selector) ||
            this.node.querySelector(selector) !== null
        ) {
            out.push(sanitizedSelectors.get(selector) || selector);
            simple.dict.delete(selector);
            simple.str = undefined;
            if ( simple.dict.size === 0 ) { return; }
        }
    }
};

var DeclarativeComplexJob = function() {
};
DeclarativeComplexJob.instance = null;
DeclarativeComplexJob.create = function() {
    if ( DeclarativeComplexJob.instance === null ) {
        DeclarativeComplexJob.instance = new DeclarativeComplexJob();
    }
    return DeclarativeComplexJob.instance;
};
DeclarativeComplexJob.prototype.lookup = function(out) {
    if ( complex.dict.size === 0 ) { return; }
    if ( complex.str === undefined ) {
        complex.str = Array.from(complex.dict).join(',\n');
    }
    if ( document.querySelector(complex.str) === null ) { return; }
    for ( var selector of complex.dict ) {
        if ( document.querySelector(selector) !== null ) {
            out.push(sanitizedSelectors.get(selector) || selector);
            complex.dict.delete(selector);
            complex.str = undefined;
            if ( complex.dict.size === 0 ) { return; }
        }
    }
};

var ProceduralJob = function() {
};
ProceduralJob.instance = null;
ProceduralJob.create = function() {
    if ( ProceduralJob.instance === null ) {
        ProceduralJob.instance = new ProceduralJob();
    }
    return ProceduralJob.instance;
};
ProceduralJob.prototype.lookup = function(out) {
    for ( var entry of procedural.dict ) {
        if ( entry[1].test() ) {
            procedural.dict.delete(entry[0]);
            out.push(entry[1].raw);
            if ( procedural.dict.size === 0 ) { return; }
        }
    }
};

var jobQueueTimer = new vAPI.SafeAnimationFrame(function processJobQueue() {
    //console.time('dom logger/scanning for matches');
    jobQueueTimer.clear();
    var toLog = [],
        t0 = Date.now(),
        job;
    while ( (job = jobQueue.shift()) ) {
        job.lookup(toLog);
        if ( (Date.now() - t0) > 10 ) { break; }
    }
    if ( toLog.length !== 0 ) {
        vAPI.messaging.send(
            'scriptlets',
            {
                what: 'logCosmeticFilteringData',
                frameURL: window.location.href,
                frameHostname: window.location.hostname,
                matchedSelectors: toLog
            }
        );
    }
    if ( simple.dict.size === 0 && complex.dict.size === 0 ) {
        jobQueue = [];
    }
    if ( jobQueue.length !== 0 ) {
        jobQueueTimer.start(100);
    }
    //console.timeEnd('dom logger/scanning for matches');
});

var handlers = {

    onFiltersetChanged: function(type, selectors) {
        //console.time('dom logger/filterset changed');
        var selector,
            sanitized;
        if ( type === 'declarative' ) {
            var simpleSizeBefore = simple.dict.size,
                complexSizeBefore = complex.dict.size;
            for ( selector of selectors.split(',\n') ) {
                if ( reHasPseudoClass.test(selector) ) {
                    sanitized = selector.replace(reHasPseudoClass, '');
                    sanitizedSelectors.set(sanitized, selector);
                    selector = sanitized;
                }
                if ( reHasCSSCombinators.test(selector) ) {
                    complex.dict.add(selector);
                    complex.str = undefined;
                } else {
                    simple.dict.add(selector);
                    simple.str = undefined;
                }
            }
            if ( simple.dict.size !== simpleSizeBefore ) {
                jobQueue.push(DeclarativeSimpleJob.create(document));
            }
            if ( complex.dict.size !== complexSizeBefore ) {
                complex.str = Array.from(complex.dict).join(',\n');
                jobQueue.push(DeclarativeComplexJob.create());
            }
        } else if ( type === 'procedural' ) {
            for ( selector of selectors ) {
                procedural.dict.set(selector[0], selector[1]);
            }
            if ( selectors.size !== 0 ) {
                jobQueue.push(ProceduralJob.create());
            }
        }
        if ( jobQueue.length !== 0 ) {
            jobQueueTimer.start(1);
        }
        //console.timeEnd('dom logger/filterset changed');
    },

    onDOMCreated: function() {
        handlers.onFiltersetChanged(
            'declarative',
            vAPI.domFilterer.getAllDeclarativeSelectors()
        );
        handlers.onFiltersetChanged(
            'procedural',
            vAPI.domFilterer.getAllProceduralSelectors()
        );
        vAPI.domFilterer.addListener(handlers);
    },

    onDOMChanged: function(addedNodes) {
        if ( simple.dict.size === 0 && complex.dict.size === 0 ) { return; }
        // This is to guard against runaway job queue. I suspect this could
        // occur on slower devices.
        if ( jobQueue.length <= 300 ) {
            if ( simple.dict.size !== 0 ) {
                for ( var node of addedNodes ) {
                    jobQueue.push(DeclarativeSimpleJob.create(node));
                }
            }
            if ( complex.dict.size !== 0 ) {
                jobQueue.push(DeclarativeComplexJob.create());
            }
            if ( procedural.dict.size !== 0 ) {
                jobQueue.push(ProceduralJob.create());
            }
        }
        if ( jobQueue.length !== 0 ) {
            jobQueueTimer.start(100);
        }
    }

};

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

var onMessage = function(msg) {
    if ( msg.what === 'loggerDisabled' ) {
        jobQueueTimer.clear();
        vAPI.domFilterer.removeListener(handlers);
        vAPI.domWatcher.removeListener(handlers);
        vAPI.messaging.removeChannelListener('domLogger', onMessage);
    }
};
vAPI.messaging.addChannelListener('domLogger', onMessage);

vAPI.domWatcher.addListener(handlers);

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

})();
