/**
 * Modern Albufeira Prolog Interpreter
 *
 * Warranty & Liability
 * To the extent permitted by applicable law and unless explicitly
 * otherwise agreed upon, XLOG Technologies AG makes no warranties
 * regarding the provided information. XLOG Technologies AG assumes
 * no liability that any problems might be solved with the information
 * provided by XLOG Technologies AG.
 *
 * Rights & License
 * All industrial property rights regarding the information - copyright
 * and patent rights in particular - are the sole property of XLOG
 * Technologies AG. If the company was not the originator of some
 * excerpts, XLOG Technologies AG has at least obtained the right to
 * reproduce, change and translate the information.
 *
 * Reproduction is restricted to the whole unaltered document. Reproduction
 * of the information is only allowed for non-commercial uses. Selling,
 * giving away or letting of the execution of the library is prohibited.
 * The library can be distributed as part of your applications and libraries
 * for execution provided this comment remains unchanged.
 *
 * Restrictions
 * Only to be distributed with programs that add significant and primary
 * functionality to the library. Not to be distributed with additional
 * software intended to replace any components of the library.
 *
 * Trademarks
 * Jekejeke is a registered trademark of XLOG Technologies AG.
 */

import {
    is_logical, snapshot_peek, pred_link, is_clause,
    is_cache, resolve_link, MASK_PRED_ARITH,
    snapshot_rope, defined_pred, is_stick, is_element,
    MASK_PRED_TEST, MASK_PRED_SPECIAL, is_goal, Structure,
    engine, set_engine, Engine, Variable, is_variable,
    Compound, is_structure, is_skeleton, is_place, deref,
    VAR_MASK_ODD, VAR_MASK_STATE, VAR_MASK_SERNO,
    stack_push, stack_peek, stack_pop, is_compound, Item
} from "./store.mjs";

export const VOID_ARGS = [];

export let fs;
export let url;
export let path;
export let os;
if (typeof window === 'undefined') {
    fs = await import("node:fs");
    url = await import("node:url");
    path = await import("node:path");
    os = await import("node:os");
} else {
    fs = undefined;
    url = undefined;
    path = undefined;
    os = undefined;
}

export let trail = null;
export let redo = null;
export let call = "[]";

/**
 * Create an error term from a message.
 *
 * @param beta The message.
 * @return Compound The error term.
 */
export function make_error(beta) {
    return new Compound("error", [beta, fetch_stack(ctx)]);
}

/**
 * Retrieve the current stack.
 *
 * @param buf The context.
 * @return {string | Compound} The current stack.
 */
function fetch_stack(buf) {
    let temp = pred_link("sys_including", 3);
    if (temp === undefined || (!is_logical(temp.func) &&
        !is_stick(temp.func)))
        return "[]";
    let data = snapshot_peek(temp.func);
    let back = null;
    let res = null;
    for (let i = 0; i < data.length; i++) {
        let clause = data[i];
        let display;
        if (clause.size !== 0) {
            display = new Array(clause.size);
        } else {
            display = null;
        }
        let elem = exec_frame(clause.functor, clause.head, display);
        if (elem.args[1] !== buf)
            continue;
        elem = new Compound("sys_including", [elem.args[0], elem.args[2]]);
        elem = new Compound(".", [elem, undefined]);
        if (back === null) {
            res = elem;
        } else {
            back.args[1] = elem;
        }
        back = elem;
    }
    if (back === null) {
        res = "[]";
    } else {
        back.args[1] = "[]";
    }
    return res;
}

export function exec_frame(functor, head, display) {
    if (head.length > 0) {
        temp_display = display;
        let args = new Array(head.length);
        for (let i = 0; i < args.length; i++)
            args[i] = exec_build(head[i]);
        temp_display = null;
        return new Compound(functor, args);
    } else {
        return functor;
    }
}

/**
 * Create a Prolog compound indicator.
 *
 * @param functor The functor.
 * @param arity The arity.
 * @return Compound The Prolog compound indicator.
 */
export function make_indicator(functor, arity) {
    if (is_cache(functor))
        functor = functor.name;
    return new Compound("/", [functor, arity]);
}

/**************************************************************/
/* Frozen Terms                                               */
/**************************************************************/

/**
 * Create a Prolog frozen.
 *
 * @param functor The functor.
 * @param args The arguments.
 * @constructor The new frozen.
 */
export class Frozen extends Structure {
    constructor(functor, args) {
        super(functor, args);
        this.hash = object_hash_code(functor);
        for (let i = 0; i < args.length; i++) {
            let obj = args[i];
            if (is_frozen(obj)) {
                this.hash = this.hash * 31 + obj.hash;
            } else {
                this.hash = this.hash * 31 + object_hash_code(obj);
            }
        }
    }
}

/**
 * Check whether an object is a Prolog frozen.
 *
 * @param obj The object.
 * @return boolean True if the object is a frozen, otherwise false.
 */
export function is_frozen(obj) {
    return obj instanceof Frozen;
}

/**
 * Compute the hash code of an object
 *
 * @param alpha The object.
 * @return The hash code, a signed 32-bit integer
 */
export function object_hash_code(alpha) {
    if (is_atom(alpha)) {
        let res = 0;
        for (let i = 0; i < alpha.length; i++)
            res = (res * 31 + alpha.charCodeAt(i)) | 0;
        return res;
    } else if (is_number(alpha)) {
        if (is_integer(alpha)) {
            if (!is_bigint(alpha)) {
                return alpha;
            } else {
                let sign;
                if (alpha < 0) {
                    sign = -1;
                    alpha = -alpha;
                } else {
                    sign = 1;
                }
                alpha = alpha.toString(16);
                let res = 0;
                let i = alpha.length % 8;
                if (i > 0)
                    res = parseInt(alpha.substring(0, i), 16) | 0;
                for (; i < alpha.length; i += 8)
                    res = (res * 31 + parseInt(alpha.substring(i, i + 8), 16)) | 0;
                return (sign > 0 ? res : -res);
            }
        } else {
            let beta = new Float64Array(1);
            beta[0] = Number(alpha);
            let gamma = new Int32Array(beta.buffer);
            return gamma[0] ^ gamma[1];
        }
    } else if (alpha === null) {
        return 0;
    } else if (alpha === true) {
        return 1231;
    } else if (alpha === false) {
        return 1237;
    } else {
        return 2467;
    }
}

/**************************************************************/
/* Garbage Collection                                         */
/**************************************************************/

export let gc_maxinfs = 128000;

export let gc_mask = VAR_MASK_ODD;

export let gc_last = 0.0;
export let gc_time = 0.0;
export let gc_enter = 0;
let gc_tick = gc_maxinfs;
let gc_tack = 60*gc_maxinfs;
let gc_tock = 3600*gc_maxinfs;

/**
 * Retrieve the real time.
 *
 * @return The real time.
 */
export function real_time() {
    if (fs !== undefined) {
        return (Number(process.hrtime.bigint()) / 1000000.0);
    } else {
        return performance.now();
    }
}

/**************************************************************/
/* Major Marking                                              */
/**************************************************************/

/**
 * Perform major garbage collection.
 */
export function gc_major() {
    gc_time -= real_time();
    gc_mask ^= VAR_MASK_STATE;
    engine.low = 0;
    engine.high = engine.serno;
    engine.serno = 0;
    mark_redo();
    adjust_redo();
    sweep_trail(null);
    gc_time += real_time();
}

/**
 * Major mark a term.
 *
 * @param first The term.
 */
function mark_term(first) {
    let stack = null;
    for (;;) {
        if (is_variable(first)) {
            if ((first.flags & VAR_MASK_STATE) !== gc_mask) {
                first.flags = (first.flags & ~VAR_MASK_STATE) | gc_mask;
                let val = first.flags & VAR_MASK_SERNO;
                if (val !== VAR_MASK_SERNO) {
                    if (val > engine.serno)
                        engine.serno = val + 1;
                    if (engine.low <= val && val < engine.high) {
                        if (val - engine.low > engine.high - val) {
                            engine.high = val;
                        } else {
                            engine.low = val + 1;
                        }
                    }
                }
                if (first.instantiated !== undefined) {
                    let peek = first.instantiated;
                    first.instantiated = stack;
                    stack = first;
                    first = peek;
                    continue;
                }
            }
        } else if (is_compound(first)) {
            if ((first.walk & VAR_MASK_STATE) !== gc_mask) {
                first.walk = (first.walk & ~VAR_MASK_STATE) | gc_mask;
                let peek = first.args[0];
                first.args[0] = stack;
                first.walk &= ~VAR_MASK_SERNO;
                stack = first;
                first = peek;
                continue;
            }
        }
        while (stack !== null && (is_variable(stack) ||
            (stack.walk & VAR_MASK_SERNO) === stack.args.length - 1)) {
            if (is_variable(stack)) {
                let peek = stack.instantiated;
                stack.instantiated = first;
                first = stack;
                stack = peek;
            } else {
                let peek = stack.args[stack.walk & VAR_MASK_SERNO];
                stack.args[stack.walk & VAR_MASK_SERNO] = first;
                first = stack;
                stack = peek;
            }
        }
        if (stack === null) {
            return;
        } else {
            let peek = stack.args[stack.walk & VAR_MASK_SERNO];
            stack.args[stack.walk & VAR_MASK_SERNO] = first;
            stack.walk++;
            first = stack.args[stack.walk & VAR_MASK_SERNO];
            stack.args[stack.walk & VAR_MASK_SERNO] = peek;
        }
    }
}

/**
 * Major mark the continuations.
 */
function mark_redo() {
    let term = call;
    let last = redo;
    for (;;) {
        mark_term(term);
        if (last === null)
            break;
        term = last.cont;
        last = last.tail;
    }
}

/**************************************************************/
/* Minor Marking                                              */
/**************************************************************/

/**
 * Perform minor garbage collection.
 */
function gc_minor() {
    gc_time -= real_time();
    mark2_redo();
    mark2_trail(engine.backtrail);
    adjust_redo();
    sweep_trail(engine.backtrail);
    let gc_now = real_time();
    gc_time += gc_now;
    let val = gc_now - gc_last;
    gc_last = gc_now;
    val = (gc_maxinfs * 1000) / val;
    gc_maxinfs = Math.round((3 * gc_maxinfs + val) / 4);
}

/**
 * Minor mark a term.
 *
 * @param first The term.
 */
export function mark2_term(first) {
    let stack = null;
    for (;;) {
        if (is_variable(first)) {
            if ((first.flags & VAR_MASK_STATE) !== gc_mask) {
                first.flags = (first.flags & ~VAR_MASK_STATE) | gc_mask;
                if (first.instantiated !== undefined) {
                    let peek = first.instantiated;
                    first.instantiated = stack;
                    stack = first;
                    first = peek;
                    continue;
                }
            }
        } else if (is_compound(first)) {
            if ((first.walk & VAR_MASK_STATE) !== gc_mask) {
                first.walk = (first.walk & ~VAR_MASK_STATE) | gc_mask;
                let peek = first.args[0];
                first.args[0] = stack;
                first.walk &= ~VAR_MASK_SERNO;
                stack = first;
                first = peek;
                continue;
            }
        }
        while (stack !== null && (is_variable(stack) ||
            (stack.walk & VAR_MASK_SERNO) === stack.args.length - 1)) {
            if (is_variable(stack)) {
                let peek = stack.instantiated;
                stack.instantiated = first;
                first = stack;
                stack = peek;
            } else {
                let peek = stack.args[stack.walk & VAR_MASK_SERNO];
                stack.args[stack.walk & VAR_MASK_SERNO] = first;
                first = stack;
                stack = peek;
            }
        }
        if (stack === null) {
            return;
        } else {
            let peek = stack.args[stack.walk & VAR_MASK_SERNO];
            stack.args[stack.walk & VAR_MASK_SERNO] = first;
            stack.walk++;
            first = stack.args[stack.walk & VAR_MASK_SERNO];
            stack.args[stack.walk & VAR_MASK_SERNO] = peek;
        }
    }
}

/**
 * Minor mark the continuations.
 */
function mark2_redo() {
    let term = call;
    let last = redo;
    for (;;) {
        mark2_term(term);
        if (last === null)
            break;
        term = last.cont;
        last = last.tail;
    }
}

/**************************************************************/
/* Variable Sweep                                             */
/**************************************************************/

/**
 * Adjust the markers into the trail.
 */
function adjust_redo() {
    let last = redo;
    while (last !== null) {
        let temp = last.mark;
        while (temp !== null) {
            if ((temp.flags & VAR_MASK_STATE) === gc_mask) {
                break;
            } else {
                temp = temp.tail;
            }
        }
        last.mark = temp;
        last = last.tail;
    }
}

/**
 * Minor mark the trail
 *
 * @param stop The stop.
 */
function mark2_trail(stop) {
    let temp = trail;
    while (temp !== stop) {
        if ((temp.flags & VAR_MASK_STATE) === VAR_MASK_STATE)
            mark2_term(temp);
        temp = temp.tail;
    }
}

/**
 * Sweep the trail.
 *
 * @param stop The stop.
 */
function sweep_trail(stop) {
    let temp = trail;
    let back = null;
    while (temp !== stop) {
        let term = temp;
        temp = term.tail;
        if ((term.flags & VAR_MASK_STATE) === gc_mask) {
            if (back !== null) {
                back.tail = term;
            } else {
                trail = term;
            }
            back = term;
        } else {
            term.instantiated = undefined;
            term.tail = null;
        }
    }
    if (back !== null) {
        back.tail = stop;
    } else {
        trail = stop;
    }
    engine.backtrail = trail;
}

/**************************************************************/
/* Signal Handling                                            */
/**************************************************************/

/**
 * Check the signal message.
 */
export function solve_signal(rope,at,choice) {
    if (engine.signal !== undefined) {
        let message = engine.signal;
        engine.signal = undefined;
        throw make_error(message);
    }
    return true;
}

/**************************************************************/
/* Clause Loops                                               */
/**************************************************************/

export const SYS_MASK_ASYNC_MODE = 0x00000001;
export const SYS_MASK_ALLOW_YIELD = 0x00000008;
export const SYS_MASK_FULL_ASYNC = SYS_MASK_ASYNC_MODE |
    SYS_MASK_ALLOW_YIELD;

/**
 * Set the continuation.
 *
 * @param term The continuation.
 */
export function cont(term) {
    call = term;
}

/**
 * Solve Prolog goals.
 *
 * @param snap The choice point boundary.
 * @param found True for call, and false for redo.
 * @return True if execution succeeds, otherwise false.
 */
export function solve(snap, found) {
    for (; ;) {
        if (found === true) {
            if (gc_enter >= gc_tick) {
                if (gc_enter >= gc_tock) {
                    gc_major();
                    gc_tock += 3600*gc_maxinfs;
                } else if (gc_enter >= gc_tack) {
                    gc_minor();
                    gc_tack += 60*gc_maxinfs;
                }
                gc_tick += gc_maxinfs;
                if ((engine.flags & SYS_MASK_ASYNC_MODE) !== 0) {
                    more(new Choice(solve_signal, null, 0, trail));
                    return immediate_promise();
                }
            }
            let goal = call;
            if (is_structure(goal)) {
                gc_enter++;
                goal = goal.args[0];
                let peek = lookup_pred(goal);
                if (peek === undefined || (peek.flags & MASK_PRED_ARITH) !== 0)
                    throw make_error(new Compound("existence_error",
                        ["procedure", make_indicator_term(goal)]));
                if (is_structure(goal)) {
                    goal = goal.args;
                } else {
                    goal = VOID_ARGS;
                }
                if ((peek.flags & MASK_PRED_TEST) !== 0) {
                    if (!peek.func(goal)) {
                        found = false;
                    } else {
                        cont(call.args[1]);
                        found = true;
                    }
                } else if ((peek.flags & MASK_PRED_SPECIAL) !== 0) {
                    found = peek.func(goal);
                } else {
                    peek = defined_pred(peek, goal);
                    found = solve2_rope(goal, snapshot_rope(peek), 0, null);
                }
            } else {
                break;
            }
        } else if (found === false) {
            if (redo !== snap) {
                let choice = redo;
                redo = choice.tail;
                unbind(choice.mark);
                call = choice.cont;
                found = choice.func(choice.data, choice.at, choice);
            } else {
                break;
            }
        } else {
            break;
        }
    }
    return found;
}

function immediate_promise() {
    return new Promise(resolve => setDelay(resolve, 0));
}

/******************************************************************/
/* Linked Provables                                               */
/******************************************************************/

/**
 * Lookup a predicate.
 *
 * @param goal The goal.
 * @return Provable The predicate or undefined.
 */
export function lookup_pred(goal) {
    let functor;
    let arity;
    if (is_element(goal)) {
        functor = goal.functor;
        arity = goal.args.length;
    } else {
        functor = goal;
        arity = 0;
    }
    return resolve_link(functor, arity);
}

/**
 * Lookup an evaluable function.
 *
 * @param expr The arithmetic expression.
 * @return Provable The evaluable function or undefined.
 */
function lookup_eval(expr) {
    let functor;
    let arity;
    if (is_element(expr)) {
        functor = expr.functor;
        arity = expr.args.length;
    } else {
        functor = expr;
        arity = 0;
    }
    return resolve_link(functor, arity + 1);
}

export function make_indicator_term(goal) {
    let functor;
    let arity;
    if (is_structure(goal)) {
        functor = goal.functor;
        arity = goal.args.length;
    } else {
        functor = goal;
        arity = 0;
    }
    return make_indicator(functor, arity);
}

function solve_rope(rope, at, choice) {
    let goal = deref(call.args[0]);
    if (is_structure(goal)) {
        goal = goal.args;
    } else {
        goal = VOID_ARGS;
    }
    return solve2_rope(goal, rope, at, choice);
}

/**
 * Search a Prolog clause and add it to the continuation.
 *
 * @param paras The called goal.
 * @param rope The clause list.
 * @param at The clause index.
 * @param choice The choice point for reuse or null.
 * @return boolean True if search succeeds, otherwise false.
 */
function solve2_rope(paras, rope,at,choice) {
    let mark = trail;
    while (at < rope.length) {
        let clause = rope[at++];
        let display;
        if (clause.size !== 0) {
            display = new Array(clause.size);
        } else {
            display = null;
        }
        if (exec_head(clause.head, display, paras)) {
            let peek = clause.cutvar;
            if (peek !== -1)
                display[peek] = redo;
            if (at < rope.length) {
                if (choice === null) {
                    choice = new Choice(solve_rope, rope, at, mark);
                } else {
                    choice.at = at;
                }
                more(choice);
                if (exec_check(clause.body, display))
                    return true;
                if (redo !== choice)
                    return false;
                more(choice.tail);
            } else {
                return exec_check(clause.body, display);
            }
        }
        unbind(mark);
    }
    return false;
}

/**
 * Sets the choice point.
 *
 * @param choice The new choice point.
 */
export function more(choice) {
    redo = choice;
}

/**
 * Create a choice point.
 *
 * @param func The choice point handler.
 * @param data The choice point data.
 * @param at The choice point index.
 * @param mark The trail mark.
 * @constructor The choice point.
 */
export function Choice(func, data, at, mark) {
    this.func = func;
    this.data = data;
    this.at = at;
    this.mark = mark;
    this.cont = call;
    this.tail = redo;
}

/**************************************************************/
/* Directives                                                 */
/**************************************************************/

export let CTX_MAIN = "main";

/**
 * Run a compiled goal once. The goal is run with auto-yield
 * disabled and promises are not accepted.
 *
 * @param goal The compiled goal.
 * @throw If false.
 */
export function run(goal) {
    if (!launch(goal, CTX_MAIN, VOID_ARGS))
        throw make_error(new Compound("syntax_error", ["directive_failed"]));
}

export function snap_setup() {
    redo = new Choice(solve_setup, null, 0, trail);
    return redo;
}

function solve_setup(rope,at,choice) {
    return false;
}

export function snap_cleanup(snap) {
    more(snap.tail);
    unbind(snap.mark);
    call = snap.cont;
}

/**************************************************************/
/* Terms                                                      */
/**************************************************************/

/**
 * Check whether an object is an atom.
 *
 * @param obj The object.
 * @return boolean True if the object is an atom, otherwise false.
 */
export function is_atom(obj) {
    return typeof obj === "string";
}

/**
 * Check whether an object is a number.
 *
 * @param obj The object.
 * @return boolean True if the object is a number, otherwise false.
 */
export function is_number(obj) {
    if (typeof obj === "number")
        return true;
    if (typeof obj === "bigint")
        return true;
    return false;
}

/**
 * Check whether an object is an integer.
 *
 * @param obj The object.
 * @return boolean True if the object is an integer, otherwise false.
 */
export function is_integer(obj) {
    if (typeof obj === "number" &&
        Number.isInteger(obj) &&
        ((-94906266 <= obj) && (obj <= 94906266)))
        return true;
    if (typeof obj === "bigint" &&
        !((-94906266 <= obj) && (obj <= 94906266)))
        return true;
    return false;
}

/**
 * Check whether an object is a float.
 *
 * @param obj The object.
 * @return boolean True if the object is a float, otherwise false.
 */
export function is_float(obj) {
    if (typeof obj === "number" &&
        (!Number.isInteger(obj) ||
            !((-94906266 <= obj) && (obj <= 94906266))))
        return true;
    if (typeof obj === "bigint" &&
        ((-94906266 <= obj) && (obj <= 94906266)))
        return true;
    return false;
}

/**
 * Check whether an object is a bigint.
 *
 * @param alpha The object.
 * @return boolean True if the object is a bignum, otherwise false.
 */
export function is_bigint(alpha) {
    return typeof alpha === "bigint";
}

/**
 * Check whether an object is a special value.
 *
 * @param alpha The object.
 * @return boolean True if the object is a special value, otherwise false.
 */
export function is_special(alpha) {
    return (typeof alpha === "number" &&
                !Number.isFinite(alpha));
}

/**************************************************************/
/* Albufeira Lazy                                             */
/**************************************************************/

let temp_display = null;

export function set_temp_display(display) {
    temp_display = display;
}

export function is_pending(template) {
    if (template === undefined) {
        return true;
    } else if (is_place(template)) {
        return true;
    } else {
        return false;
    }
}

export function exec_deref(template) {
    if (template === undefined) {
        return template;
    } else if (is_place(template)) {
        let index = template.index;
        let peek = temp_display[index];
        if (peek === undefined) {
            return template;
        } else {
            return deref(peek);
        }
    } else {
        return deref(template);
    }
}

/**************************************************************/
/* Albufeira Modes                                            */
/**************************************************************/

export function exec_build(template) {
    let back = null;
    for (; ;) {
        if (template === undefined) {
            template = new Variable();
            break;
        } else if (is_place(template)) {
            let index = template.index;
            template = temp_display[index];
            if (template === undefined) {
                template = new Variable();
                temp_display[index] = template;
            } else {
                template = deref(template);
            }
            break;
        } else if (is_skeleton(template)) {
            let args = new Array(template.args.length);
            args[args.length - 1] = back;
            back = new Compound(template.functor, args);
            template = template.args;
            let i = 0;
            for (; i < args.length - 1; i++)
                args[i] = exec_build(template[i]);
            template = template[i];
        } else {
            template = deref(template);
            break;
        }
    }
    while (back !== null) {
        let peek = back.args[back.args.length - 1];
        back.args[back.args.length - 1] = template;
        template = back;
        back = peek;
    }
    return template;
}

export function exec_unify(template, alpha) {
    for (; ;) {
        if (template === undefined) {
            return true;
        } else if (is_place(template)) {
            let index = template.index;
            template = temp_display[index];
            if (template === undefined) {
                temp_display[index] = deref(alpha);
                return true;
            } else {
                return unify(alpha, template);
            }
        } else if (is_skeleton(template)) {
            alpha = deref(alpha);
            if (is_variable(alpha)) {
                let args = new Array(template.args.length);
                bind(new Compound(template.functor, args), alpha);
                template = template.args;
                for (let i = 0; i < args.length; i++)
                    args[i] = exec_build(template[i]);
                return true;
            } else if (is_structure(alpha)) {
                if (template.args.length !== alpha.args.length)
                    return false;
                if (template.functor !== alpha.functor)
                    return false;
                template = template.args;
                alpha = alpha.args;
                let i = 0;
                for (; i < template.length - 1; i++)
                    if (!exec_unify(template[i], alpha[i]))
                        return false;
                template = template[i];
                alpha = alpha[i];
            } else {
                return false;
            }
        } else {
            return unify(template, alpha);
        }
    }
}

/**************************************************************/
/* Albufeira Clauses                                          */
/**************************************************************/

export function exec_body(code, display) {
    temp_display = display;
    let back = null;
    let res = null;
    for (let i = 0; i < code.length; i++) {
        let goal = exec_build(code[i]);
        let temp = new Compound(".", [goal, undefined]);
        if (back === null) {
            res = temp;
        } else {
            back.args[1] = temp;
        }
        back = temp;
    }
    if (back === null) {
        res = "[]";
    } else {
        back.args[1] = "[]";
    }
    temp_display = null;
    return res;
}

export function exec_head(code, display, aux) {
    if (aux.length !== code.length)
        return false;
    temp_display = display;
    for (let i = 0; i < code.length; i++) {
        if (!exec_unify(code[i], aux[i])) {
            temp_display = null;
            return false;
        }
    }
    temp_display = null;
    return true;
}

/**************************************************************/
/* Head Check                                                 */
/**************************************************************/

export function exec_eval(template) {
    if (template === undefined) {
        throw make_error("instantiation_error");
    } else if (is_place(template)) {
        let index = template.index;
        template = temp_display[index];
        if (template === undefined)
            throw make_error("instantiation_error");
    }
    template = deref(template);
    if (is_number(template))
        return template;
    let peek = lookup_eval(template);
    if (peek === undefined || (peek.flags & MASK_PRED_ARITH) === 0)
        throw make_error(new Compound("type_error",
            ["evaluable", make_indicator_term(template)]));
    if (is_element(template)) {
        template = template.args;
    } else {
        template = VOID_ARGS;
    }
    return peek.func(template);
}

function exec_test(template) {
    let peek = lookup_pred(template);
    if (peek === undefined || (peek.flags & MASK_PRED_TEST) === 0) {
        return exec_build(template);
    } else {
        if (is_element(template)) {
            template = template.args;
        } else {
            template = VOID_ARGS;
        }
        gc_enter++;
        return peek.func(template);
    }
}

export function exec_check(code, display) {
    temp_display = display;
    let check = true;
    let back = null;
    let res = null;
    for (let i = 0; i < code.length; i++) {
        let goal = (check ? exec_test(code[i]) : exec_build(code[i]));
        if (true === goal)
            continue;
        if (false === goal) {
            temp_display = null;
            return false;
        }
        goal = new Compound(".", [goal, undefined]);
        if (back === null) {
            res = goal;
        } else {
            back.args[1] = goal;
        }
        back = goal;
        check = false;
    }
    if (back === null) {
        res = call.args[1];
    } else {
        back.args[1] = call.args[1];
    }
    temp_display = null;
    cont(res);
    return true;
}

/**************************************************************/
/* Unification                                                */
/**************************************************************/

/**
 * Determine whether two terms unify.
 * As a side effect the trail is extended, even if unification fails.
 * Can handle cyclic terms and deep recursion.
 *
 * @param first The first term.
 * @param second The second term.
 * @return boolean True if the two terms unify, otherwise false.
 */
export function unify(first, second) {
    let stack = null;
    let log = null;
    try {
        for (; ;) {
            first = deref(first);
            second = deref(second);
            if (is_variable(first)) {
                if (!is_variable(second) || first !== second)
                    bind(second, first);
            } else if (is_variable(second)) {
                bind(first, second);
            } else if (!is_structure(first)) {
                if (!Object.is(first, second))
                    break;
            } else if (!is_structure(second)) {
                break;
            } else if (first.args.length !== second.args.length) {
                break;
            } else {
                first = union_find(first);
                second = union_find(second);
                if (first !== second) {
                    if (is_frozen(first) && is_frozen(second) &&
                        first.hash !== second.hash)
                        break;
                    if (first.functor !== second.functor)
                        break;
                    log = union_add(log, first, second);
                    if (0 !== first.args.length - 1) {
                        let item2 = new Item(first, second, 0);
                        stack = stack_push(stack, item2);
                    }
                    first = first.args[0];
                    second = second.args[0];
                    continue;
                }
            }
            let item = stack_peek(stack);
            if (item === null) {
                return true;
            } else {
                item.idx++;
                first = item.first.args[item.idx];
                second = item.second.args[item.idx];
                if (item.idx === item.first.args.length - 1)
                    stack_pop(stack);
            }
        }
        return false;
    } finally {
        union_undo(log);
    }
}

/**
 * The function returns the representative of a structure.
 *
 * @param obj The structure.
 * @return The representative.
 */
export function union_find(obj) {
    while (is_structure(obj.functor))
        obj = obj.functor;
    return obj;
}

/**
 * The function merges respresentatives and returns a new log.
 *
 * @param log The log.
 * @param from The first representative.
 * @param to The second representative.
 * @return The new log.
 */
export function union_add(log, from, to) {
    from.functor = to;
    if (log === null)
        log = new Array(0);
    log.push(from);
    return log;
}

/**
 * The routine uses the log L to undo modifications.
 *
 * @param log The log.
 */
export function union_undo(log) {
    if (log === null)
        return;
    for (let i = log.length - 1; i >= 0; i--) {
        let elem = log[i];
        elem.functor = elem.functor.functor;
    }
}

/**
 * Bind a variable to a term.
 *
 * @param source The Prolog term.
 * @param term The variable.
 */
export function bind(source, term) {
    term.instantiated = source;
    term.tail = trail;
    if ((term.flags & VAR_MASK_STATE) === gc_mask)
        term.flags |= VAR_MASK_STATE;
    trail = term;
}

/**
 * Unbind variable binds.
 *
 * @param mark The trail mark.
 */
export function unbind(mark) {
    while (mark !== trail) {
        let term = trail;
        if (engine.backtrail === term)
            engine.backtrail = term.tail;
        trail = term.tail;
        term.instantiated = undefined;
        term.tail = null;
    }
}

/**************************************************************/
/* Context                                                    */
/**************************************************************/

export let tasks = new Array(0);

let notify = () => {};

export function Context() {
    this.trail = null;
    this.redo = null;
    this.call = "[]";
    this.gc_mask = VAR_MASK_ODD;
    this.engine = new Engine();
    this.engine.text_output = engine.text_output;
    this.engine.text_error = engine.text_error;
    this.engine.text_input = engine.text_input;
    tasks.push(this);
}

function ctx_ended(buf) {
    let k = tasks.lastIndexOf(buf);
    if (k !== -1)
        tasks.splice(k, 1);
    notify();
}

/**************************************************************/
/* Group                                                      */
/**************************************************************/

export function group_teardown() {
    for (let i = 0; i < tasks.length; i++) {
        let buf = tasks[i];
        let msg = new Compound("system_error", ["user_exit"]);
        register_signal(buf, msg);
        invoke_interrupt(buf);
    }
}

export async function group_gather_async() {
    while (tasks.length > 0) {
        solve_signal(null, 0, null);
        let prom = wait_promise(CTX_MAIN);
        await prom;
    }
    solve_signal(null, 0, null);

}

function wait_promise(buf) {
    return new Promise(resolve => {
        function handler() {
            notify = () => {};
            register_interrupt(buf, () => {});
            resolve();
        }
        notify = handler;
        register_interrupt(buf, () => {
            notify = () => {};
            register_interrupt(buf, () => {});
            resolve();
        });
    });
}

/**************************************************************/
/* Switching                                                  */
/**************************************************************/

export let ctx = CTX_MAIN;

/**
 * Set the task context.
 *
 * @param buf The context.
 */
export function ctx_set(buf) {
    ctx = buf;
}

/**
 * Switch the task context.
 *
 * @param buf The context.
 */
export function ctx_switch(buf) {
    let temp = trail;
    trail = buf.trail;
    buf.trail = temp;
    temp = redo;
    redo = buf.redo;
    buf.redo = temp;
    temp = call;
    call = buf.call;
    buf.call = temp;
    temp = gc_mask;
    gc_mask = buf.gc_mask;
    buf.gc_mask = temp;
    temp = engine;
    set_engine(buf.engine);
    buf.engine = temp;
}

/**************************************************************/
/* Callback                                                   */
/**************************************************************/

/**
 * Run a callback once, i.e. no choice point or trailing left
 * behind. Callbacks are run with auto-yield disabled and
 * promises are not accepted, i.e. run "stackless" on top of the
 * given main stack or side stack. "stackless" because completion,
 * i.e. return or exception by the callback, is the only context switch.
 *
 * @param form The goal or closure.
 * @param buf The context or "main".
 * @param paras The actual parameters.
 * @return boolean True or false.
 */
export function launch(form, buf, paras) {
    if (buf !== CTX_MAIN) {
        ctx_set(buf);
        ctx_switch(buf);
    }
    let back = engine.flags & SYS_MASK_FULL_ASYNC;
    engine.flags &= ~SYS_MASK_FULL_ASYNC;
    let snap = snap_setup();
    if (is_clause(form)) {
        call = melt_clause(form, paras);
    } else if (is_goal(form)) {
        call = melt_directive(form);
    } else {
        call = form;
    }
    let found;
    try {
        found = solve(snap, true);
    } finally {
        snap_cleanup(snap);
        engine.flags &= ~SYS_MASK_FULL_ASYNC;
        engine.flags |= back;
        if (buf !== CTX_MAIN) {
            ctx_switch(buf);
            ctx_set(CTX_MAIN);
        }
    }
    return found;
}

export function callback(form, buf, paras) {
    try {
        return launch(form, buf, paras);
    } catch (err) {
        return false;
    }
}

export function melt_directive(goal) {
    let display;
    if (goal.size !== 0) {
        display = new Array(goal.size);
    } else {
        display = null;
    }
    let peek = goal.cutvar;
    if (peek !== -1)
        display[peek] = redo;
    return exec_body(goal.body, display);
}

export function melt_clause(clause, paras) {
    let display;
    if (clause.size !== 0) {
        display = new Array(clause.size);
    } else {
        display = null;
    }
    if (exec_head(clause.head, display, paras)) {
        let peek = clause.cutvar;
        if (peek !== -1)
            display[peek] = redo;
        return exec_body(clause.body, display);
    } else {
        return "[]";
    }
}

/**************************************************************/
/* Task                                                       */
/**************************************************************/

/**
 * Run a task once, i.e. no choice point or trailing left
 * behind. Tasks are run with auto-yield enabled and promises are
 * accepted, i.e. run "stackfull" on top of the given main stack
 * or side stack. "stackfull" because not only completion, i.e.
 * return or exception by the task, cause a context switch, but
 * also await of an auto-yield or promise.
 *
 * @param form The goal or closure.
 * @param buf  The context or "main".
 * @param paras   The actual parameters.
 * @return True or false.
 */
export async function launch_async(form, buf, paras) {
    gc_last += real_time();
    if (buf !== CTX_MAIN) {
        ctx_set(buf);
        ctx_switch(buf);
    }
    let back = engine.flags & SYS_MASK_FULL_ASYNC;
    engine.flags |= SYS_MASK_FULL_ASYNC;
    let found = true;
    let snap = snap_setup();
    if (is_clause(form)) {
        call = melt_clause(form, paras);
    } else if (is_goal(form)) {
        call = melt_directive(form);
    } else {
        call = form;
    }
    try {
        for (;;) {
            found = solve(snap, found);
            if (found === false) {
                break;
            } else if (found !== true) {
                if (buf !== CTX_MAIN) {
                    ctx_switch(buf);
                    ctx_set(CTX_MAIN);
                }
                gc_last -= real_time();
                await found;
                gc_last += real_time();
                if (buf !== CTX_MAIN) {
                    ctx_set(buf);
                    ctx_switch(buf);
                }
                found = false;
            } else {
                break;
            }
        }
    } finally {
        snap_cleanup(snap);
        engine.flags &= ~SYS_MASK_FULL_ASYNC;
        engine.flags |= back;
        if (buf !== CTX_MAIN) {
            ctx_switch(buf);
            ctx_set(CTX_MAIN);
            ctx_ended(buf);
        }
    }
    gc_last -= real_time();
    return found;
}

export async function task_async(form, buf, paras) {
    try {
        await launch_async(form, buf, paras);
    } catch (err) {
        /* */
    }
}

/**
 * Delay a coroutine by some milliseconds.
 *
 * @param proc The coroutine.
 * @param delay The milliseconds.
 * @return The timer.
 */
export function setDelay(proc, delay) {
    if (delay === 0 && fs !== undefined) {
        return setImmediate(proc);
    } else {
        return setTimeout(proc, delay);
    }
}

/**
 * Register an abort function in a context.
 *
 * @param buf The context.
 * @param func The function.
 */
export function register_interrupt(buf, func) {
    let en = determine_engine(buf);
    en.abort = func;
}

/**
 * Register a signal in a context.
 *
 * @param buf The context.
 * @param msg The signal.
 */
export function register_signal(buf, msg) {
    let en = determine_engine(buf);
    en.signal = msg;
}

/**
 * Invoke the abort handler of a context.
 *
 * @param buf The context.
 */
export function invoke_interrupt(buf) {
    let en = determine_engine(buf);
    en.abort();
}

function determine_engine(buf) {
    if (buf !== CTX_MAIN) {
        if (buf !== ctx) {
            return buf.engine;
        } else {
            return engine;
        }
    } else {
        if (ctx !== CTX_MAIN) {
            return ctx.engine;
        } else {
            return engine;
        }
    }
}
