// The following code contains the implementation of the behavior trees and 
// unit tests for it.
// https://hexdocs.pm/behavior_tree/BehaviorTree.html#content
// https://www.gamasutra.com/blogs/ChrisSimpson/20140717/221339/Behavior_trees_for_AI_How_they_work.php

// Possible states of a task
const State =
{
    Undefined: "Undefined",
    Failure: "Failure",
    Success: "Success",
    Running: "Running"
}

// A simple class to log the execution of the tasks in a string variable.
class Log {
    constructor() {
        this.text = '';
    }

    reset() {
        this.text = '';
    }

    append(text) {
        this.text += text + '\n';
    }

    toString() {
        return this.text;
    }
}

// Base class for all tasks. It has an id and a state. The run method is used
// to execute the task and log the execution if a log object is provided.
class Task {

    constructor(id, state = State.Undefined) {
        this.id = id;
        this.state = state;
    }

    run(log = null, level = '') {
        if (log) log.append(level + 'Task(' + this.id + ')');

        if (log) log.append(level + '└ ' + this.state);
        return this;
    }
}

// Check the state of a task by comparing it with a given state. It returns 
// success if the states match, and failure otherwise.
class CheckState extends Task {

    constructor(checktask = null, checkstate = State.Success) {
        super('CheckState');
        this.checktask = checktask;
        this.checkstate = checkstate;
    }

    run(log = null, level = '') {
        if (log) log.append(level + 'CheckState(' + this.checktask.id + ',' + this.checkstate + ')');

        this.state = (this.checktask.state == this.checkstate) ? State.Success : State.Failure;

        if (log) log.append(level + '└ ' + this.state);
        return this;
    }
}

// Selector composite. It returns immediately with a success status code when one 
// of its children runs successfully.
class Selector extends Task {

    constructor(tasks = []) {
        super('Selector');
        this.tasks = tasks;
    }

    run(log = null, level = '') {
        if (log) log.append(level + this.id + '()');

        this.state = State.Failure;
        for (let task of this.tasks) {
            this.state = task.run(log, level + '│ ').state;
            if (this.state != State.Failure)
                break;
        }

        if (log) log.append(level + '└ ' + this.state);
        return this;
    }
}

// Sequence composite. It returns immediately with a failure status code when
// one of its children fails.
class Sequence extends Task {

    constructor(tasks = []) {
        super('Sequence');
        this.tasks = tasks;
    }

    run(log = null, level = '') {
        if (log) log.append(level + this.id + '()');

        this.state = State.Success;
        for (let task of this.tasks) {
            this.state = task.run(log, level + '│ ').state;
            if (this.state != State.Success)
                break;
        }

        if (log) log.append(level + '└ ' + this.state);
        return this;
    }
}

// Random selector composite. It tries a random child and returns its state.
class RandomSelector extends Task {

    constructor(tasks = []) {
        super('RandomSelector');
        this.tasks = tasks;
    }

    run(log = null, level = '') {
        if (log) log.append(level + this.id + '()');

        this.state = State.Failure;
        if (this.tasks.length) {
            let i = Math.floor(Math.random() * this.tasks.length);
            this.state = this.tasks[i].run(log, level + '│ ').state;
        }

        if (log) log.append(level + '└ ' + this.state);
        return this;
    }
}


// The following classes are Decorators that modify the behavior of a single 
// child task.

// Inverter decorator. It inverts the value returned by a task when the value 
// is not "Running".
class Inverter extends Task {

    constructor(task = null) {
        super('Inverter');
        this.task = task;
    }

    run(log = null, level = '') {
        if (log) log.append(level + this.id + '()');

        if (this.task) {
            this.task.run(log, level + '│ ');
            switch (this.task.state) {
                case State.Running:
                    this.state = State.Running;
                    break;
                case State.Failure:
                    this.state = State.Success;
                    break;
                case State.Success:
                    this.state = State.Failure;
                    break;
                default:
                    this.state = State.Undefined;
            }
        }

        if (log) log.append(level + '└ ' + this.state);
        return this;
    }
}

// Succeeder decorator. It will always return success, irrespective of what the 
// child node actually returned.
class Succeeder extends Task {

    constructor(task = null) {
        super('Succeeder');
        this.task = task;
    }

    run(log = null, level = '') {
        if (log) log.append(level + this.id + '()');
        this.state = State.Success;

        if (this.task)
            this.task.run(log, level + '│ ');

        if (log) log.append(level + '└ ' + this.state);
        return this;
    }
}

// Following classes repeatedly execute their child tasks until a certain 
// condition is met.

// Repeater. A repeater will reprocess its child node each time its child 
// returns a result. These are often used at the very base of the tree, to make 
// the tree to run continuously. Repeaters may optionally run their children a 
// set number of times before returning to their parent.
class Repeater extends Task {

    constructor(task = null, counter = 0) {
        super('Repeater');
        this.task = task;
        this.counter = counter;
    }

    run(log = null, level = '') {
        if (log) log.append(level + this.id + '(' + this.counter + ')');
        this.state = State.Success;

        if (this.task)
        {
            let remaining = this.counter;
            while ((remaining--) > 0)
                this.task.run(log, level + '│ ');
        }
        
        if (log) log.append(level + '└ ' + this.state);
        return this;
    }
}

// Repeat_until_fail. Like a repeater, these decorators will continue to 
// reprocess their child. That is until the child finally returns a failure, 
// at which point the repeater will return success to its parent.
class Repeat_until_fail extends Task {

    constructor(task = null) {
        super('Repeat_until_fail');
        this.task = task;
    }

    run(log = null, level = '') {
        if (log) log.append(level + this.id + '()');
        this.state = State.Success;

        if (this.task)
            while (this.task.run(log, level + '│ ').state == State.Success) { }

        if (log) log.append(level + '└ ' + this.state);
        return this;
    }
}