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

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

import { hostnameCompare, isHnRegexOrPath } from './make-utils.js';
import { builtinScriptlets } from '../resources/scriptlets.js';
import { literalStrFromRegex } from './regex-analyzer.js';
import { safeReplace } from './safe-replace.js';

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

const resourceDetails = new Map();
const resourceAliases = new Map();
const worldTemplate = {
    scriptletFunctions: new Map(),
    allFunctions: new Map(),
    args: new Map(),
    arglists: new Map(),
    hostnames: new Map(),
    regexesOrPaths: new Map(),
    matches: new Set(),
    hasEntities: false,
    hasAncestors: false,
};
const worlds = {
    ISOLATED: structuredClone(worldTemplate),
    MAIN: structuredClone(worldTemplate),
};

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

function createScriptletCoreCode(worldDetails, resourceEntry) {
    const { allFunctions } = worldDetails;
    allFunctions.set(resourceEntry.name, resourceEntry.code);
    const dependencies = resourceEntry.dependencies &&
        resourceEntry.dependencies.slice() || [];
    while ( dependencies.length !== 0 ) {
        const token = dependencies.shift();
        const details = resourceDetails.get(token);
        if ( details === undefined ) { continue; }
        if ( allFunctions.has(details.name) ) { continue; }
        allFunctions.set(details.name, details.code);
        if ( Array.isArray(details.dependencies) === false ) { continue; }
        dependencies.push(...details.dependencies);
    }
}

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

export function reset() {
    worlds.ISOLATED = structuredClone(worldTemplate);
    worlds.MAIN = structuredClone(worldTemplate);
}

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

export function compile(rulesetId, details) {
    if ( details.args[0].endsWith('.js') === false ) {
        details.args[0] += '.js';
    }
    if ( resourceAliases.has(details.args[0]) ) {
        details.args[0] = resourceAliases.get(details.args[0]);
    }
    const scriptletToken = details.args[0];
    const resourceEntry = resourceDetails.get(scriptletToken);
    if ( resourceEntry === undefined ) { return; }
    if ( resourceEntry.requiresTrust && details.trustedSource !== true ) {
        console.log(`Rejecting +js(${details.args.join()}): ${rulesetId} is not trusted`);
        return;
    }
    const worldDetails = worlds[resourceEntry.world];
    const { scriptletFunctions } = worldDetails;
    if ( scriptletFunctions.has(resourceEntry.name) === false ) {
        scriptletFunctions.set(resourceEntry.name, scriptletFunctions.size);
        createScriptletCoreCode(worldDetails, resourceEntry);
    }
    // Convert args to arg indices
    const arglist = details.args.slice();
    arglist[0] = scriptletFunctions.get(resourceEntry.name);
    for ( let i = 1; i < arglist.length; i++ ) {
        const arg = arglist[i];
        if ( worldDetails.args.has(arg) === false ) {
            worldDetails.args.set(arg, worldDetails.args.size);
        }
        arglist[i] = worldDetails.args.get(arg);
    }
    const arglistKey = JSON.stringify(arglist).slice(1, -1);
    if ( worldDetails.arglists.has(arglistKey) === false ) {
        worldDetails.arglists.set(arglistKey, worldDetails.arglists.size);
    }
    const arglistIndex = worldDetails.arglists.get(arglistKey);
    if ( details.matches ) {
        for ( const hn of details.matches ) {
            if ( isHnRegexOrPath(hn) ) {
                worldDetails.matches.clear();
                worldDetails.matches.add('*');
                if ( worldDetails.regexesOrPaths.has(hn) === false ) {
                    worldDetails.regexesOrPaths.set(hn, new Set());
                }
                worldDetails.regexesOrPaths.get(hn).add(arglistIndex);
                continue;
            }
            const isEntity = hn.endsWith('.*') || hn.endsWith('.*>>');
            worldDetails.hasEntities ||= isEntity;
            const isAncestor = hn.endsWith('>>')
            worldDetails.hasAncestors ||= isAncestor;
            if ( isEntity || isAncestor ) {
                worldDetails.matches.clear();
                worldDetails.matches.add('*');
            }
            if ( worldDetails.matches.has('*') === false ) {
                worldDetails.matches.add(hn);
            }
            if ( worldDetails.hostnames.has(hn) === false ) {
                worldDetails.hostnames.set(hn, new Set());
            }
            worldDetails.hostnames.get(hn).add(arglistIndex);
        }
    } else {
        worldDetails.matches.add('*');
    }
    if ( details.excludeMatches ) {
        for ( const hn of details.excludeMatches ) {
            if ( isHnRegexOrPath(hn) ) {
                if ( worldDetails.regexesOrPaths.has(hn) === false ) {
                    worldDetails.regexesOrPaths.set(hn, new Set());
                }
                worldDetails.regexesOrPaths.get(hn).add(~arglistIndex);
                continue;
            }
            if ( worldDetails.hostnames.has(hn) === false ) {
                worldDetails.hostnames.set(hn, new Set());
            }
            worldDetails.hostnames.get(hn).add(~arglistIndex);
        }
    }
}

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

export function commit(rulesetId, template) {
    const stats = {};
    for ( const world of Object.keys(worlds) ) {
        const worldDetails = worlds[world];
        const { scriptletFunctions, allFunctions, args, arglists } = worldDetails;
        if ( scriptletFunctions.size === 0 ) { continue; }
        const hostnames = Array.from(worldDetails.hostnames).toSorted((a, b) =>
            hostnameCompare(a[0], b[0])
        ).map(a => ([ a[0], JSON.stringify(Array.from(a[1]).map(a => JSON.parse(a))).slice(1,-1)]));
        const scriptletFromRegexes = Array.from(worldDetails.regexesOrPaths)
            .filter(a => a[0].startsWith('/') && a[0].endsWith('/'))
            .map(a => {
                const restr = a[0].slice(1,-1);
                return [
                    literalStrFromRegex(restr).slice(0,8),
                    restr,
                    JSON.stringify(Array.from(a[1])).slice(1,-1),
                ];
            }).flat();
        let content = safeReplace(template, 'self.$hasEntities$', JSON.stringify(worldDetails.hasEntities));
        content = safeReplace(content, 'self.$hasAncestors$', JSON.stringify(worldDetails.hasAncestors));
        content = safeReplace(content, 'self.$hasRegexes$', JSON.stringify(scriptletFromRegexes.length !== 0));
        content = safeReplace(content,
            'self.$scriptletFromRegexes$',
            `/* ${worldDetails.regexesOrPaths.size} */ ${JSON.stringify(scriptletFromRegexes)}`
        );
        content = safeReplace(content,
            'self.$scriptletHostnames$',
            `/* ${hostnames.length} */ ${JSON.stringify(hostnames.map(a => a[0]))}`
        );
        content = safeReplace(content,
            'self.$scriptletArglistRefs$',
            `/* ${hostnames.length} */ ${JSON.stringify(hostnames.map(a => a[1]).join(';'))}`
        );
        content = safeReplace(content,
            'self.$scriptletArglists$',
            `/* ${arglists.size} */ ${JSON.stringify(Array.from(arglists.keys()).join(';'))}`
        );
        content = safeReplace(content,
            'self.$scriptletArgs$',
            `/* ${args.size} */ ${JSON.stringify(Array.from(args.keys()))}`
        );
        content = safeReplace(content,
            'self.$scriptletFunctions$',
            `/* ${scriptletFunctions.size} */\n[${Array.from(scriptletFunctions.keys()).join(',')}]`
        );
        content = safeReplace(content,
            'self.$scriptletCode$',
            Array.from(allFunctions.values()).sort().join('\n\n')
        );
        content = safeReplace(content, /\$rulesetId\$/, rulesetId, 0);
        stats[world] = {
            code: content,
            hostnames: Array.from(worldDetails.matches).sort(),
        };
    }
    return stats;
}

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

function init() {
    for ( const scriptlet of builtinScriptlets ) {
        const { name, aliases, fn } = scriptlet;
        const entry = {
            name: fn.name,
            code: fn.toString(),
            world: scriptlet.world || 'MAIN',
            dependencies: scriptlet.dependencies,
            requiresTrust: scriptlet.requiresTrust === true,
        };
        resourceDetails.set(name, entry);
        if ( Array.isArray(aliases) === false ) { continue; }
        for ( const alias of aliases ) {
            resourceAliases.set(alias, name);
        }
    }
}

init();

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