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

    uMatrix - a Chromium browser extension to black/white list requests.
    Copyright (C) 2018 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/uMatrix
*/

/* global punycode */

'use strict';

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

µMatrix.recipeManager = (function() {
    let rawRecipes = [];
    let recipeIdGenerator = 1;
    let recipeBook = new Map();
    let reValidRecipeFile = /^! uMatrix: Ruleset recipes [0-9.]+\n/;
    let reNoUnicode = /^[\x00-\x7F]$/;

    var authorFromHeader = function(header) {
        let match = /^! +maintainer: +([^\n]+)/im.exec(header);
        return match !== null ? match[1].trim() : '';
    };

    var conditionMatch = function(condition, srcHostname, desHostnames) {
        let i = condition.indexOf(' ');
        if ( i === -1 ) { return false; }
        let targetHostname = condition.slice(0, i).trim();
        if ( targetHostname !== '*' ) {
            let hn = srcHostname;
            if ( targetHostname.endsWith('.*') ) {
                let domain = µMatrix.URI.domainFromHostname(hn);
                let pos = domain.indexOf('.');
                if ( pos !== -1 ) {
                    hn = hn.slice(0, pos + hn.length - domain.length) + '.*';
                }
            }
            if ( hn.endsWith(targetHostname) === false ) {
                return false;
            }
        }
        targetHostname = condition.slice(i + 1).trim();
        if ( targetHostname === '*' ) { return true; }
        for ( let hn of desHostnames ) {
            if ( hn === srcHostname ) { continue; }
            if ( hn.endsWith(targetHostname) ) { return true; }
        }
        return false;
    };

    var toASCII = function(rule) {
        if ( reNoUnicode.test(rule) ) { return rule; }
        let parts = rule.split(/\s+/);
        for ( let i = 0; i < parts.length; i++ ) {
            parts[i] = punycode.toASCII(parts[i]);
        }
        return parts.join(' ');
    };

    var compareLength = function(a, b) {
        return b.length - a.length;
    };

    var getTokens = function(s) {
        let tokens = s.match(/[a-z0-9]+/gi);
        if ( tokens === null ) { return []; }
        return tokens;
    };

    var addRecipe = function(recipe) {
        let tokens = getTokens(recipe.condition);
        tokens.sort(compareLength);
        let token = tokens[0];
        let recipes = recipeBook.get(token);
        if ( recipes === undefined ) {
            recipeBook.set(token, recipes = []);
        }
        recipes.push(recipe);
    };

    var fromString = function(raw) {
        var recipeName,
            recipeCondition,
            recipeRuleset;
        let rawHeader = raw.slice(0, 1024);
        if ( reValidRecipeFile.test(rawHeader) === false ) { return; }
        let maintainer = authorFromHeader(rawHeader);
        let lineIter = new µMatrix.LineIterator(raw);
        for (;;) {
            let line = lineIter.next().trim();
            if ( line.length === 0 ) {
                if (
                    recipeName !== undefined &&
                    recipeCondition !== undefined &&
                    recipeRuleset.length !== 0
                ) {
                    addRecipe({
                        id: recipeIdGenerator++,
                        name: recipeName,
                        maintainer: maintainer,
                        condition: recipeCondition,
                        ruleset: recipeRuleset
                    });
                }
                recipeName = undefined;
            }
            if ( lineIter.eot() && recipeName === undefined ) { break; }
            if ( line.length === 0 ) { continue; }
            let c = line.charCodeAt(0);
            if ( c === 0x23 /* '#' */ || c === 0x21 /* '!' */ ) { continue; }
            if ( recipeName === undefined ) {
                recipeName = line;
                recipeCondition = undefined;
                continue;
            }
            if ( recipeCondition === undefined ) {
                recipeCondition = toASCII(line);
                recipeRuleset = '';
                continue;
            }
            if ( recipeRuleset.length !== 0 ) {
                recipeRuleset += '\n';
            }
            recipeRuleset += toASCII(line);
        }
    };

    var fromPendingStrings = function() {
        if ( rawRecipes.length === 0 ) { return; }
        for ( var raw of rawRecipes ) {
            fromString(raw);
        }
        rawRecipes = [];
    };

    return {
        apply: function(details) {
            let µm = µMatrix;
            let tMatrix = µm.tMatrix;
            let pMatrix = µm.pMatrix;
            let mustPersist = false;
            for ( let rule of details.ruleset.split('\n') ) {
                let parts = rule.split(/\s+/);
                if ( parts.length < 2 ) { continue; }
                let f0 = parts[0];
                let f1 = parts[1];
                // Switch
                if ( f0.endsWith(':') ) {
                    f0 = f0.slice(0, -1);
                    if ( tMatrix.evaluateSwitchZ(f0, f1) !== false ) {
                        tMatrix.setSwitchZ(f0, f1, false);
                        if ( details.commit ) {
                            pMatrix.setSwitchZ(f0, f1, false);
                            mustPersist = true;
                        }
                    }
                    continue;
                }
                // Rule
                if ( parts.length < 3 ) { continue; }
                let f2 = parts[2];
                let action = tMatrix.evaluateCellZXY(f0, f1, f2);
                if ( (action & 3) === 1 ) {
                    tMatrix.whitelistCell(f0, f1, f2);
                }
                if ( details.commit !== true ) { continue; }
                action = pMatrix.evaluateCellZXY(f0, f1, f2);
                if ( (action & 3) === 1 ) {
                    pMatrix.whitelistCell(f0, f1, f2);
                    mustPersist = true;
                }
            }
            if ( mustPersist ) {
                µm.saveMatrix();
            }
        },
        fetch: function(srcHostname, desHostnames, callback) {
            fromPendingStrings();
            let out = [];
            let fetched = new Set();
            let tokens = getTokens(srcHostname + ' ' + desHostnames.join(' '));
            for ( let token of tokens ) {
                let recipes = recipeBook.get(token);
                if ( recipes === undefined ) { continue; }
                for ( let recipe of recipes ) {
                    if ( fetched.has(recipe.id) ) { continue; }
                    if (
                        conditionMatch(
                            recipe.condition,
                            srcHostname,
                            desHostnames
                        )
                    ) {
                        out.push(recipe);
                        fetched.add(recipe.id);
                    }
                }
            }
            callback(out);
        },
        commitStatuses: function(details) {
            let matrix = µMatrix.pMatrix;
            for ( let recipe of details.recipes ) {
                let ruleIter = new µMatrix.LineIterator(recipe.ruleset);
                while ( ruleIter.eot() === false ) {
                    let parts = ruleIter.next().split(/\s+/);
                    if (
                        matrix.evaluateCellZXY(
                            details.scope,
                            parts[1],
                            parts[2]
                        ) === 1
                    ) {
                        recipe.mustCommit = true;
                        break;
                    }
                }
            }
            return details;
        },
        fromString: function(raw) {
            rawRecipes.push(raw);
        },
        reset: function() {
            rawRecipes.length = 0;
            recipeBook.clear();
        }
    };
})();

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