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

    uBlock Origin - a comprehensive, efficient content blocker
    Copyright (C) 2019-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 { proxyApplyFn } from './proxy-apply.js';
import { registerScriptlet } from './base.js';
import { safeSelf } from './safe-self.js';

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

class RangeParser {
    constructor(s) {
        this.not = s.charAt(0) === '!';
        if ( this.not ) { s = s.slice(1); }
        if ( s === '' ) { return; }
        const pos = s.indexOf('-');
        if ( pos !== 0 ) {
            this.min = this.max = parseInt(s, 10) || 0;
        }
        if ( pos !== -1 ) {
            this.max = parseInt(s.slice(1), 10) || Number.MAX_SAFE_INTEGER;
        }
    }
    unbound() {
        return this.min === undefined && this.max === undefined;
    }
    test(v) {
        const n = Math.min(Math.max(Number(v) || 0, 0), Number.MAX_SAFE_INTEGER);
        if ( this.min === this.max ) {
            return (this.min === undefined || n === this.min) !== this.not;
        }
        if ( this.min === undefined ) {
            return (n <= this.max) !== this.not;
        }
        if ( this.max === undefined ) {
            return (n >= this.min) !== this.not;
        }
        return (n >= this.min && n <= this.max) !== this.not;
    }
}
registerScriptlet(RangeParser, {
    name: 'range-parser.fn',
});

/**
 * @scriptlet prevent-setTimeout
 * 
 * @description
 * Conditionally prevent execution of the callback function passed to native
 * setTimeout method. With no parameters, all calls to setTimeout will be
 * shown in the logger.
 * 
 * @param [needle]
 * A pattern to match against the stringified callback. The pattern can be a
 * plain string, or a regex. Prepend with `!` to reverse the match condition.
 * 
 * @param [delay]
 * A value to match against the delay. Can be a single value for exact match,
 * or a range:
 * - `min-max`: matches if delay >= min and delay <= max
 * - `min-`: matches if delay >= min
 * - `-max`: matches if delay <= max
 * No delay means to match any delay value.
 * Prepend with `!` to reverse the match condition.
 * 
 * */

export function preventSetTimeout(
    needleRaw = '',
    delayRaw = ''
) {
    const safe = safeSelf();
    const logPrefix = safe.makeLogPrefix('prevent-setTimeout', needleRaw, delayRaw);
    const needleNot = needleRaw.charAt(0) === '!';
    const reNeedle = safe.patternToRegex(needleNot ? needleRaw.slice(1) : needleRaw);
    const range = new RangeParser(delayRaw);
    proxyApplyFn('setTimeout', function(context) {
        const { callArgs } = context;
        const a = callArgs[0] instanceof Function
            ? String(safe.Function_toString(callArgs[0]))
            : String(callArgs[0]);
        const b = callArgs[1];
        if ( needleRaw === '' && range.unbound() ) {
            safe.uboLog(logPrefix, `Called:\n${a}\n${b}`);
            return context.reflect();
        }
        if ( reNeedle.test(a) !== needleNot && range.test(b) ) {
            callArgs[0] = function(){};
            safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`);
        }
        return context.reflect();
    });
}
registerScriptlet(preventSetTimeout, {
    name: 'prevent-setTimeout.js',
    aliases: [
        'no-setTimeout-if.js',
        'nostif.js',
        'setTimeout-defuser.js',
    ],
    dependencies: [
        proxyApplyFn,
        RangeParser,
        safeSelf,
    ],
});

/**
 * @scriptlet prevent-setInterval
 * 
 * @description
 * Conditionally prevent execution of the callback function passed to native
 * setInterval method. With no parameters, all calls to setInterval will be
 * shown in the logger.
 * 
 * @param [needle]
 * A pattern to match against the stringified callback. The pattern can be a
 * plain string, or a regex. Prepend with `!` to reverse the match condition.
 * No pattern means to match anything.
 * 
 * @param [delay]
 * A value to match against the delay. Can be a single value for exact match,
 * or a range:
 * - `min-max`: matches if delay >= min and delay <= max
 * - `min-`: matches if delay >= min
 * - `-max`: matches if delay <= max
 * No delay means to match any delay value.
 * Prepend with `!` to reverse the match condition.
 * 
 * */

export function preventSetInterval(
    needleRaw = '',
    delayRaw = ''
) {
    const safe = safeSelf();
    const logPrefix = safe.makeLogPrefix('prevent-setInterval', needleRaw, delayRaw);
    const needleNot = needleRaw.charAt(0) === '!';
    const reNeedle = safe.patternToRegex(needleNot ? needleRaw.slice(1) : needleRaw);
    const range = new RangeParser(delayRaw);
    proxyApplyFn('setInterval', function(context) {
        const { callArgs } = context;
        const a = callArgs[0] instanceof Function
            ? String(safe.Function_toString(callArgs[0]))
            : String(callArgs[0]);
        const b = callArgs[1];
        if ( needleRaw === '' && range.unbound() ) {
            safe.uboLog(logPrefix, `Called:\n${a}\n${b}`);
            return context.reflect();
        }
        if ( reNeedle.test(a) !== needleNot && range.test(b) ) {
            callArgs[0] = function(){};
            safe.uboLog(logPrefix, `Prevented:\n${a}\n${b}`);
        }
        return context.reflect();
    });
}
registerScriptlet(preventSetInterval, {
    name: 'prevent-setInterval.js',
    aliases: [
        'no-setInterval-if.js',
        'nosiif.js',
        'setInterval-defuser.js',
    ],
    dependencies: [
        proxyApplyFn,
        RangeParser,
        safeSelf,
    ],
});

/**
 * @scriptlet prevent-requestAnimationFrame
 * 
 * @description
 * Conditionally prevent execution of the callback function passed to native
 * requestAnimationFrame method. With no parameters, all calls to
 * requestAnimationFrame will be shown in the logger.
 * 
 * @param [needle]
 * A pattern to match against the stringified callback. The pattern can be a
 * plain string, or a regex.
 * Prepend with `!` to reverse the match condition.
 * 
 * */

export function preventRequestAnimationFrame(
    needleRaw = ''
) {
    const safe = safeSelf();
    const logPrefix = safe.makeLogPrefix('prevent-requestAnimationFrame', needleRaw);
    const needleNot = needleRaw.charAt(0) === '!';
    const reNeedle = safe.patternToRegex(needleNot ? needleRaw.slice(1) : needleRaw);
    proxyApplyFn('requestAnimationFrame', function(context) {
        const { callArgs } = context;
        const a = callArgs[0] instanceof Function
            ? String(safe.Function_toString(callArgs[0]))
            : String(callArgs[0]);
        if ( needleRaw === '' ) {
            safe.uboLog(logPrefix, `Called:\n${a}`);
        } else if ( reNeedle.test(a) !== needleNot ) {
            callArgs[0] = function(){};
            safe.uboLog(logPrefix, `Prevented:\n${a}`);
        }
        return context.reflect();
    });
}
registerScriptlet(preventRequestAnimationFrame, {
    name: 'prevent-requestAnimationFrame.js',
    aliases: [
        'no-requestAnimationFrame-if.js',
        'norafif.js',
    ],
    dependencies: [
        proxyApplyFn,
        safeSelf,
    ],
});
