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

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

'use strict';

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

(( ) => {
// >>>>> start of local scope

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

// Worker context

if (
    self.WorkerGlobalScope instanceof Object &&
    self instanceof self.WorkerGlobalScope
) {
    let content = '';

    const doSearch = function(details) {
        const reEOLs = /\n\r|\r\n|\n|\r/g;
        const t1 = Date.now() + 750;

        let reSearch;
        try {
            reSearch = new RegExp(details.pattern, details.flags);
        } catch(ex) {
            return;
        }

        const response = [];
        const maxOffset = content.length;
        let iLine = 0;
        let iOffset = 0;
        let size = 0;
        while ( iOffset < maxOffset ) {
            // Find next match
            const match = reSearch.exec(content);
            if ( match === null ) { break; }
            // Find number of line breaks between last and current match.
            reEOLs.lastIndex = 0;
            const eols = content.slice(iOffset, match.index).match(reEOLs);
            if ( Array.isArray(eols) ) {
                iLine += eols.length;
            }
            // Store line
            response.push(iLine);
            size += 1;
            // Find next line break.
            reEOLs.lastIndex = reSearch.lastIndex;
            const eol = reEOLs.exec(content);
            iOffset = eol !== null
                ? reEOLs.lastIndex
                : content.length;
            reSearch.lastIndex = iOffset;
            iLine += 1;
            // Quit if this takes too long
            if ( (size & 0x3FF) === 0 && Date.now() >= t1 ) { break; }
        }

        return response;
    };

    self.onmessage = function(e) {
        const msg = e.data;

        switch ( msg.what ) {
        case 'setHaystack':
            content = msg.content;
            break;

        case 'doSearch':
            const response = doSearch(msg);
            self.postMessage({ id: msg.id, response });
            break;
        }
    };

    return;
}

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

// Main context

{
    const workerTTL = 5 * 60 * 1000;
    const pendingResponses = new Map();

    let worker;
    let workerTTLTimer;
    let messageId = 1;

    const onWorkerMessage = function(e) {
        const msg = e.data;
        const resolver = pendingResponses.get(msg.id);
        if ( resolver === undefined ) { return; }
        pendingResponses.delete(msg.id);
        resolver(msg.response);
    };

    const cancelPendingTasks = function() {
        for ( const resolver of pendingResponses.values() ) {
            resolver();
        }
        pendingResponses.clear();
    };

    const destroy = function() {
        shutdown();
        self.searchThread = undefined;
    };

    const shutdown = function() {
        if ( workerTTLTimer !== undefined ) {
            clearTimeout(workerTTLTimer);
            workerTTLTimer = undefined;
        }
        if ( worker === undefined ) { return; }
        worker.terminate();
        worker.onmessage = undefined;
        worker = undefined;
        cancelPendingTasks();
    };

    const init = function() {
        if ( self.searchThread instanceof Object === false ) { return; }
        if ( worker === undefined ) {
            worker = new Worker('js/codemirror/search-thread.js');
            worker.onmessage = onWorkerMessage;
        }
        if ( workerTTLTimer !== undefined ) {
            clearTimeout(workerTTLTimer);
        }
        workerTTLTimer = vAPI.setTimeout(shutdown, workerTTL);
    };

    const needHaystack = function() {
        return worker instanceof Object === false;
    };

    const setHaystack = function(content) {
        init();
        worker.postMessage({ what: 'setHaystack', content });
    };

    const search = function(query, overwrite = true) {
        init();
        if ( worker instanceof Object === false ) {
            return Promise.resolve();
        }
        if ( overwrite ) {
            cancelPendingTasks();
        }
        const id = messageId++;
        worker.postMessage({
            what: 'doSearch',
            id,
            pattern: query.source,
            flags: query.flags,
            isRE: query instanceof RegExp
        });
        return new Promise(resolve => {
            pendingResponses.set(id, resolve);
        });
    };

    self.addEventListener(
        'beforeunload',
        ( ) => { destroy(); },
        { once: true }
    );

    self.searchThread = { needHaystack, setHaystack, search, shutdown };
}

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

// <<<<< end of local scope
})();

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

void 0;
