/// <reference path="../../Functions.ts" />
interface RulebookOptions<T> {
    noun? : T;
}

/**
 * Without the Runner, Rulebooks were not "thread-safe", so we need the runner.
 */
class RulebookRunner<T> {
    public noun : T;
    private rulesToExecute : Array<Rule> = [];
    public rulebook : Rulebook<T>;
    public rule : Rule;
    private ruleRunner : number = -1;

    public constructor (rulebook : Rulebook<T>, noun : T) {
        this.rulebook = rulebook;
        this.noun = noun;
    }

    public addRulebooks (...rulebooks : Array<Rulebook<T>>) {
        rulebooks = arrayUniqueNewArray(rulebooks);
        rulebooks.forEach((rulebook) => {
            this.addRules(...rulebook.rules);
        });
    }

    public addRules (...rules) {
        if (this.ruleRunner < 0) {
            this.rulesToExecute.push(...rules);
        }
    }

    public skipRule (rule : Rule) {
        if (this.ruleRunner >= 0) {
            let index = this.rulesToExecute.indexOf(rule);
            if (index > this.ruleRunner) {
                this.rulesToExecute.splice(index, 1);
                console.debug("[Rulebook]" + this.rulebook.name + ", skipping Rule " + rule.name + " due to request.");
            } else {
                console.warn("[Rulebook]" + this.rulebook.name + ": uname to skip Rule" + rule.name + " due to it being too late to stop it.");
            }
        }
    }

    public async execute () {
        arrayUnique(this.rulesToExecute).sort(function (a: Rule, b: Rule) {
            return a.compareTo(b);
        });

        for (this.ruleRunner = 0; this.ruleRunner < this.rulesToExecute.length; this.ruleRunner++) {
            this.rule = this.rulesToExecute[this.ruleRunner];
            let result = await this.rule.execute(this);

            if (result != undefined) {
                return result;
            }
        }
    }
}

class Rulebook<T> {
    public rules : Array<Rule> = [];
    public name : string;
    private static indentantionSpaces = 2;
    public static rulebookStack : Array<any> = [];

    public static getStack () {
        let stack = [];
        Rulebook.rulebookStack.forEach((rl) => {
            if (rl instanceof Object && rl.name != undefined) {
                stack.push(rl.name);
            } else if (typeof rl == "string") {
                stack.push(rl);
            } else if (rl instanceof Object && rl.id != undefined) {
                stack.push(rl.id);
            } else {
                stack.push("Undefined");
            }
        });
        return stack;
    }

    public static getIndentation () {
        return " ".repeat(Rulebook.indentantionSpaces).repeat(Rulebook.rulebookStack.length);
    }

    public static increaseIndentation (rulebook : any) {
        Rulebook.rulebookStack.push(rulebook);
    }

    public static decreaseIndentation () {
        Rulebook.rulebookStack.pop();
    }

    public static isRunning (r : Rule | Rulebook<any>) {
        return Rulebook.rulebookStack.indexOf(r) != -1;
    }

    public isRunning () {
        return Rulebook.isRunning(this);
    }

    public constructor (name : string) {
        this.name = name;
    }

    public async execute (options : RulebookOptions<T>, ...rulebooks) : Promise<any>{
        options = options == undefined ? {} : options;

        let runner = new RulebookRunner<T>(this, options.noun); // duplicate array
        runner.addRulebooks(this, ...rulebooks);

        var names = [];
        for (let i = 0; i < rulebooks.length; i++) {
            if (rulebooks[i] === this) continue;
            names.push(rulebooks[i].name);
        }

        console.debug(Rulebook.getIndentation() + "[RULEBOOK] " + this.name + (names.length > 0 ? (" merged with " + names.join(", ")) : ""));
        Rulebook.increaseIndentation(this);

        let result = await runner.execute();

        Rulebook.decreaseIndentation();
        return result;
    }

    public createAndAddRule (r : RuleOptions<T>) : Rule {
        let rule = new Rule(r);
        this.addRule(rule);
        return rule;
    }

    public addRule (r : Rule) {
        this.rules.push(r);
    }

    public sortRules () {
        this.rules.sort(function (a : Rule, b : Rule) {
            return a.compareTo(b);
        });
    }
}