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

    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 { builtinScriptlets } from './js/resources/scriptlets.js';
import fs from 'fs/promises';
import { safeReplace } from './safe-replace.js';

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

const resourceDetails = new Map();
const resourceAliases = new Map();
const scriptletFiles = new Map();

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

function createScriptletCoreCode(scriptletToken) {
    const details = resourceDetails.get(scriptletToken);
    const components = new Map([ [ scriptletToken, details.code ] ]);
    const dependencies = details.dependencies && details.dependencies.slice() || [];
    while ( dependencies.length !== 0 ) {
        const token = dependencies.shift();
        if ( components.has(token) ) { continue; }
        const details = resourceDetails.get(token);
        if ( details === undefined ) { continue; }
        components.set(token, details.code);
        if ( Array.isArray(details.dependencies) === false ) { continue; }
        dependencies.push(...details.dependencies);
    }
    return Array.from(components.values()).join('\n\n');
}

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

export 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);
        }
    }
}

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

export function reset() {
    scriptletFiles.clear();
}

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

export function compile(assetDetails, 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; }
    const argsToken = JSON.stringify(details.args.slice(1));
    if ( resourceEntry.requiresTrust && details.trustedSource !== true ) {
        console.log(`Rejecting +js(${scriptletToken},${argsToken.slice(1,-1)}): ${assetDetails.id} is not trusted`);
        return;
    }
    if ( scriptletFiles.has(scriptletToken) === false ) {
        scriptletFiles.set(scriptletToken, {
            name: resourceEntry.name,
            code: createScriptletCoreCode(scriptletToken),
            world: resourceEntry.world,
            args: new Map(),
            hostnames: new Map(),
            exceptions: new Map(),
            hasEntities: false,
            hasAncestors: false,
            matches: new Set(),
        });
    }
    const scriptletDetails = scriptletFiles.get(scriptletToken);
    if ( scriptletDetails.args.has(argsToken) === false ) {
        scriptletDetails.args.set(argsToken, scriptletDetails.args.size);
    }
    const iArgs = scriptletDetails.args.get(argsToken);
    if ( details.matches ) {
        for ( const hn of details.matches ) {
            const isEntity = hn.endsWith('.*') || hn.endsWith('.*>>');
            scriptletDetails.hasEntities ||= isEntity;
            const isAncestor = hn.endsWith('>>')
            scriptletDetails.hasAncestors ||= isAncestor;
            if ( isEntity || isAncestor ) {
                scriptletDetails.matches.clear();
                scriptletDetails.matches.add('*');
            }
            if ( scriptletDetails.matches.has('*') === false ) {
                scriptletDetails.matches.add(hn);
            }
            if ( scriptletDetails.hostnames.has(hn) === false ) {
                scriptletDetails.hostnames.set(hn, new Set());
            }
            scriptletDetails.hostnames.get(hn).add(iArgs);
        }
    } else {
        scriptletDetails.matches.add('*');
    }
    if ( details.excludeMatches ) {
        for ( const hn of details.excludeMatches ) {
            if ( scriptletDetails.exceptions.has(hn) === false ) {
                scriptletDetails.exceptions.set(hn, []);
            }
            scriptletDetails.exceptions.get(hn).push(iArgs);
        }
    }
}

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

export async function commit(rulesetId, path, writeFn) {
    const scriptletTemplate = await fs.readFile(
        './scriptlets/scriptlet.template.js',
        { encoding: 'utf8' }
    );
    const patchHnMap = hnmap => {
        const out = Array.from(hnmap);
        out.forEach(a => {
            const values = Array.from(a[1]);
            a[1] = values.length === 1 ? values[0] : values;
        });
        return out;
    };
    const scriptletStats = [];
    for ( const [ name, details ] of scriptletFiles ) {
        let content = safeReplace(scriptletTemplate,
            'function $scriptletName$(){}',
            details.code
        );
        content = safeReplace(content, /\$rulesetId\$/, rulesetId, 0);
        content = safeReplace(content, /\$scriptletName\$/, details.name, 0);
        content = safeReplace(content,
            'self.$argsList$',
            JSON.stringify(Array.from(details.args.keys()).map(a => JSON.parse(a)))
        );
        content = safeReplace(content,
            'self.$hostnamesMap$',
            JSON.stringify(patchHnMap(details.hostnames))
        );
        content = safeReplace(content,
            'self.$hasEntities$',
            JSON.stringify(details.hasEntities)
        );
        content = safeReplace(content,
            'self.$hasAncestors$',
            JSON.stringify(details.hasAncestors)
        );
        content = safeReplace(content,
            'self.$exceptionsMap$',
            JSON.stringify(Array.from(details.exceptions))
        );
        writeFn(`${path}/${rulesetId}.${name}`, content);
        scriptletStats.push([
            name.slice(0, -3), {
                hostnames: Array.from(details.matches).sort(),
                world: details.world,
            }
        ]);
    }
    return scriptletStats;
}

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