/**
 * 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.
 */

export const MASK_PRED_ARITH = 0x40000000;
export const MASK_PRED_SPECIAL = 0x20000000;
export const MASK_PRED_TEST = 0x10000000;
export const MASK_PRED_DYNAMIC = 0x08000000;
export const MASK_PRED_BITS = 27;
export const MASK_PRED_INDEX = (1 << MASK_PRED_BITS) - 1;

export const MASK_TOUCH_DYNAMIC = 0x00000001;
export const MASK_TOUCH_BOTTOM = 0x00000002;
export const MASK_REMOVE_REVERSE = 0x00000001;

export const VAR_MASK_SEEN = 0x40000000;
export const VAR_MASK_EVEN = 0x20000000;
export const VAR_MASK_ODD = 0x10000000;
export const VAR_MASK_STATE = VAR_MASK_EVEN | VAR_MASK_ODD;
export const VAR_MASK_BITS = 28;
export const VAR_MASK_SERNO = (1 << VAR_MASK_BITS) - 1;

export let kb = {};
export let stage = -1;

/**
 * Set the clause and predicate current stage.
 *
 * @param num The current stage.
 */
export function set_stage(num) {
    stage = num;
}

/**
 * Set the clause current partition.
 *
 * @param path The current partition.
 */
export function set_partition(path) {
    engine.partition = path;
}

/**************************************************************/
/* Engine                                                     */
/**************************************************************/

export const SYS_MASK_STATE_ASYNC = 0x00000080;

/**
 * Create a slow state engine.
 *
 * @constructor The engine.
 */
export function Engine() {
    this.signal = undefined;
    this.abort = () => {};
    this.flags = SYS_MASK_STATE_ASYNC;
    this.text_output = undefined;
    this.text_error = undefined;
    this.text_input = undefined;
    this.low = 0;
    this.high = 0;
    this.serno = 0;
    this.backtrail = null;
    this.partition = (stage === -1 ? "system" : "user");
}

export let engine = new Engine();

export function set_engine(ptr) {
    engine = ptr;
}

/**************************************************************/
/* Variable & Compound                                        */
/**************************************************************/

/**
 * Create a Prolog variable.
 *
 * @constructor The new variable.
 */
export function Variable() {
    this.instantiated = undefined;
    this.flags = VAR_MASK_SERNO;
    this.tail = null;
}

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

/**
 * Create a Prolog structure.
 *
 * @param functor The functor.
 * @param args The arguments.
 * @constructor The new structure.
 */
export function Structure(functor, args) {
    this.functor = functor;
    this.args = args;
}


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

/**
 * Create a Prolog compound.
 *
 * @param functor The functor.
 * @param args The arguments.
 * @constructor The new compound.
 */
export class Compound extends Structure {
    constructor(functor, args) {
        super(functor, args);
        this.walk = 0;
    }
}

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

/**
 * 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);
    }
}

/**
 * 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;
}

/**************************************************************/
/* Place & Skeleton                                           */
/**************************************************************/

/**
 * Create a Albufeira Code place.
 */
export function Place(index) {
    this.index = index;
}

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

/**
 * Create a Albufeira Code skeleton.
 *
 * @param functor The functor.
 * @param args    The arguments.
 */
export function Skeleton(functor, args) {
    this.functor = functor;
    this.args = args;
}

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

/**************************************************************/
/* Quote                                                      */
/**************************************************************/

/**
 * Create an Albufeira code quote.
 *
 * @param obj The Albufeira code.
 */
export function Quote(obj) {
    this.obj = obj;
}

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

export function unquote_objects(body) {
    for (let i = 0; i < body.length; i++) {
        let alpha = body[i];
        if (is_quote(alpha))
            body[i] = alpha.obj;
    }
}

/**************************************************************/
/* deref() and copy_term()                                    */
/**************************************************************/

/**
 * Dereference a Prolog term.
 *
 * @param term The Prolog term.
 * @return any The dereferenced Prolog term.
 */
export function deref(term) {
    while (is_variable(term) && term.instantiated !== undefined)
        term = term.instantiated;
    return term;
}

/**
 * Copy a Prolog term. Can handle cyclic terms.
 *
 * @param first The Prolog term.
 * @return any The copy.
 */
export function copy_term(first) {
    let res = walk_link(first);
    walk_unlink(first);
    return res;
}

function walk_link(first) {
    let stack = null;
    for (; ; ) {
        first = deref(first);
        if (is_variable(first)) {
            if (first.tail === null) {
                let peek = new Variable();
                first.tail = peek;
                first = peek;
            } else {
                first = first.tail;
            }
        } else if (is_compound(first)) {
            if (!is_structure(first.functor)) {
                first.functor = new Compound(first.functor,
                    new Array(first.args.length));
                first.walk &= ~VAR_MASK_SERNO;
                stack = stack_push(stack, first);
                first = first.args[0];
                continue;
            } else {
                first = first.functor;
            }
        }
        let item = stack_peek(stack);
        while (item !== null &&
        (item.walk & VAR_MASK_SERNO) === item.args.length - 1) {
            item.functor.args[item.walk & VAR_MASK_SERNO] = first;
            first = item.functor;
            stack_pop(stack);
            item = stack_peek(stack);
        }
        if (item === null) {
            return first;
        } else {
            item.functor.args[item.walk & VAR_MASK_SERNO] = first;
            item.walk++;
            first = item.args[item.walk & VAR_MASK_SERNO];
        }
    }
}

function walk_unlink(first) {
    let stack = null;
    for (; ; ) {
        first = deref(first);
        if (is_variable(first)) {
            if (first.tail !== null)
                first.tail = null;
        } else if (is_compound(first)) {
            if (is_structure(first.functor)) {
                first.functor = first.functor.functor;
                if (0 !== first.args.length - 1) {
                    first.walk &= ~VAR_MASK_SERNO;
                    stack = stack_push(stack, first);
                }
                first = first.args[0];
                continue;
            }
        }
        let item = stack_peek(stack);
        if (item === null) {
            return;
        } else {
            item.walk++;
            first = item.args[item.walk & VAR_MASK_SERNO];
            if ((item.walk & VAR_MASK_SERNO) === item.args.length - 1)
                stack_pop(stack);
        }
    }
}

export function stack_push(stack, elem) {
    if (stack === null)
        stack = new Array(0);
    stack.push(elem);
    return stack;
}

export function stack_peek(stack) {
    if (stack === null)
        return null;
    if (stack.length === 0)
        return null;
    return stack[stack.length-1];
}

export function stack_pop(stack) {
    stack.pop();
}

/**************************************************************/
/* Association Map                                            */
/**************************************************************/

export function assoc_get(map, key) {
    if (map === null)
        return undefined;
    return map.get(key);
}

export function assoc_set(map, key, value) {
    if (map === null)
        map = new Map();
    map.set(key, value);
    return map;
}

/**************************************************************/
/* Clauses Lifecycle                                          */
/**************************************************************/

export function Goal(size, body, cutvar) {
    this.size = size;
    this.body = body;
    this.cutvar = cutvar;
}

/**
 * Check whether the object is a goal.
 *
 * @param obj The object.
 * @return boolean if the object is a goal, otherwise false.
 */
export function is_goal(obj) {
    return obj instanceof Goal;
}

export function Clause(size, head, body, cutvar, idxval) {
    this.size = size;
    this.head = head;
    this.body = body;
    this.cutvar = cutvar;
    this.idxval = idxval;
    this.creator = stage;
    this.remover = undefined;
    this.shard = "";
}

/**
 * Check whether the object is a clause.
 *
 * @param obj The object.
 * @return boolean if the object is a clause, otherwise false.
 */
export function is_clause(obj) {
    return obj instanceof Clause;
}

/**
 * Create a logical.
 *
 * @param cache The cache of non-deleted clauses.
 * @param count The counter of non-deleted clauses.
 * @param data The clause list.
 * @constructor The new logical.
 */
export function Logical(cache, count, data) {
    this.cache = cache;
    this.count = count;
    this.data = data;
}

/**
 * Check whether an object is a logical.
 *
 * @param obj The object.
 * @return boolean if the object is a logical, otherwise false.
 */
export function is_logical(obj) {
    return obj instanceof Logical;
}

/**************************************************************/
/* Knowledgebase                                              */
/**************************************************************/

/**
 * Create a provable.
 *
 * @constructor The new provable.
 */
export function Provable() {
    this.flags = 0;
    this.rope = undefined;
    this.nonguard = undefined;
    this.idxmap = undefined;
    this.func = undefined;
    this.creator = stage;
    this.remover = undefined;
    this.overlay = undefined;
}

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

/**
 * Create a cache node.
 *
 * @param name The functor.
 * @constructor The new cache node.
 */
export function Cache(name) {
    this.link = undefined;
    this.name = name;
}

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

/*********************************************************************/
/* Snapshot Data                                                     */
/*********************************************************************/

/**
 * Make snapshot of a logical.
 *
 * @param rope The logical.
 * @return array The clause list snapshot.
 */
export function snapshot_data(rope) {
    let res = rope.cache;
    if (res === null) {
        res = new Array(rope.count);
        rope_copy(res, rope);
        rope.cache = res;
    }
    return res;
}

function rope_copy(res, rope) {
    let data = rope.data;
    let j = 0;
    for (let i = 0; i < data.length; i++) {
        let clause = data[i];
        if (clause.remover === undefined)
            res[j++] = clause;
    }
}

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

/**
 * Retrieve a provable from monomorphic cache.
 *
 * @param cache The cache.
 * @param arity The arity.
 * @return The provable or undefined.
 */
export function ensure_link(cache, arity) {
    let peek = cache.link;
    if (peek === undefined || peek.remover !== undefined) {
        peek = pred_link(cache.name, arity);
        cache.link = peek;
    }
    return peek;
}

/**
 * Retrieve a provable by predicate indicator.
 *
 * @param functor The functor.
 * @param arity The arity.
 * @return The provable or undefined.
 */
export function pred_link(functor, arity) {
    let temp = kb[functor];
    if (temp === undefined)
        return undefined;
    let peek = temp[arity];
    if (peek === undefined || peek.remover !== undefined)
        return undefined;
    return peek;
}

/**
 * The function returns an anonymous predicate for the given clauses.
 *
 * @param rope The clauses.
 * @param offset The selected indexing argument.
 * @return Provable The predicate.
 */
export function make_defined(rope, offset) {
    let peek = new Provable();
    peek.flags |= offset;
    peek.rope = new_rope();
    peek.nonguard = new_rope();
    peek.idxmap = new Map();
    for (let i = 0; i < rope.length; i++)
        add_peek(peek, rope[i], MASK_TOUCH_BOTTOM);
    return peek;
}

function new_rope() {
    return new Logical(null, 0, new Array(0));
}

/*********************************************************************/
/* Dynamic Predicates                                                */
/*********************************************************************/

import {
    make_error, make_indicator, VOID_ARGS
} from "./machine.mjs";

/**
 * Create a new dynamic predicate entry.
 *
 * @param functor The functor.
 * @param arity The arity.
 * @param flags The flags.
 */
export function pred_touch(functor, arity, flags) {
    let temp = kb[functor];
    if (temp === undefined) {
        temp = new Array(0);
        kb[functor] = temp;
    }
    let peek = temp[arity];
    if (peek === undefined || peek.remover !== undefined) {
        let res = make_defined(VOID_ARGS, 1);
        if (peek !== undefined)
            res.overlay = peek;
        if ((flags & MASK_TOUCH_DYNAMIC) !== 0)
            res.flags |= MASK_PRED_DYNAMIC;
        temp[arity] = res;
    } else {
        if (!is_logical(peek.rope))
            throw make_error(new Compound("permission_error",
                ["modify", "static_procedure",
                    make_indicator(functor, arity)]));
        if ((flags & MASK_TOUCH_DYNAMIC) !== 0)
            if ((peek.flags & MASK_PRED_DYNAMIC) === 0)
                throw make_error(new Compound("permission_error",
                    ["modify", "static_procedure",
                        make_indicator(functor, arity)]));
    }
}

/*********************************************************************/
/* Destroy Provable                                                 */
/*********************************************************************/

/**
 * Remove a predicate from the knowledge base.
 *
 * @param functor The functor.
 * @param arity The arity.
 */
export function pred_destroy(functor, arity) {
    let temp = kb[functor];
    if (temp === undefined)
        return;
    let peek = temp[arity];
    if (peek === undefined || peek.remover !== undefined)
        return;
    if (peek.creator === stage) {
        peek = clear_pop(peek);
        temp[arity] = peek;
        if (peek === undefined && trim_arities(temp))
            delete kb[functor];
    } else {
        peek.remover = stage;
    }
}

/**
 * Clear and pop a provable.
 *
 * @param peek The provable.
 * @return The parent provable.
 */
function clear_pop(peek) {
    peek.remover = stage;
    peek.rope = undefined;
    peek.idxmap = undefined;
    peek.nonguard = undefined;
    peek.func = undefined;

    let back = peek;
    peek = back.overlay;
    back.overlay = undefined;
    return peek;
}

/**
 * Trim the arities array.
 *
 * @param peek The arities array.
 * @return boolean True if empty, otherwise fale.
 */
function trim_arities(peek) {
    let pos = peek.length;
    while (pos > 0 && peek[pos - 1] === undefined)
        pos--;
    if (pos === 0)
        return true;
    if (pos !== peek.length)
        peek.splice(pos, peek.length - pos);
    return false;
}

/**************************************************************/
/* Clauses Transactions                                       */
/**************************************************************/

/**
 * Rollback the clauses.
 */
export function clear() {
    for (let functor in kb) {
        let temp = kb[functor];
        for (let i = 0; i < temp.length; i++) {
            let peek = temp[i];
            if (peek === undefined)
                continue;
            if (peek.creator === stage) {
                peek = clear_pop(peek);
                temp[i] = peek;
                if (peek === undefined)
                    continue;
            }
            if (peek.remover === stage)
                peek.remover = undefined;
            if (is_logical(peek.rope))
                rollback_peek(peek);
        }
        if (trim_arities(temp))
            delete kb[functor]
    }
}

/**
 * Rollback the clauses of a predicate.
 *
 * @param peek The predicate.
 */
function rollback_peek(peek) {
    if (!has_action(peek.rope))
        return;
    for (let [key,value] of peek.idxmap) {
        if (has_action(value)) {
            rollback_rope(value, false);
            if (value.data.length === 0)
                peek.idxmap.delete(key);
        }
    }
    if (has_action(peek.nonguard))
        rollback_rope(peek.nonguard, false);
    rollback_rope(peek.rope, true);
}

/**
 * Check whether a logical has some action.
 *
 * @param rope The logical.
 * @return boolean if the logical has some action, false otherwise.
 */
function has_action(rope) {
    let data = rope.data;
    for (let i = 0; i < data.length; i++) {
        let clause = data[i];
        if (clause.creator === stage)
            return true;
        if (clause.remover === stage)
            return true;
    }
    return false;
}

/**
 * Rollback clauses from a logical.
 *
 * @param rope The logical.
 * @param update The update flag.
 */
function rollback_rope(rope, update) {
    let data = rope.data;
    let j = 0;
    for (let i = 0; i < data.length; i++) {
        let clause = data[i];
        if (clause.creator === stage) {
            rope.count--;
        } else {
            if (clause.remover === stage) {
                if (update)
                    clause.remover = undefined;
                rope.count++;
            }
            data[j++] = clause;
        }
    }
    data.splice(j);
    rope.cache = null;
}

/*********************************************************************/
/* Clause Addition                                                   */
/*********************************************************************/

/**
 * Enhance the knowledge base by a claus or predicate.
 *
 * @param functor The functor.
 * @param arity The arity.
 * @param clause_or_pred The clause or predicate.
 */
export function add(functor, arity, clause_or_pred) {
    add_clause(functor, arity, clause_or_pred, MASK_TOUCH_BOTTOM);
}

/**
 * Enhance the knowledge base by a claus or predicate.
 *
 * @param functor The functor.
 * @param arity The arity.
 * @param clause_or_pred The clause or predicate.
 * @param flags The flags.
 */
export function add_clause(functor, arity, clause_or_pred, flags) {
    let temp = kb[functor];
    if (temp === undefined) {
        temp = new Array(0);
        kb[functor] = temp;
    }
    let peek = temp[arity];
    if (is_clause(clause_or_pred)) {
        if (peek === undefined || peek.remover !== undefined) {
            let res = make_defined(VOID_ARGS, 1);
            if (peek !== undefined)
                res.overlay = peek;
            if ((flags & MASK_TOUCH_DYNAMIC) !== 0)
                res.flags |= MASK_PRED_DYNAMIC;
            temp[arity] = res;
            peek = res;
        } else {
            if (!is_logical(peek.rope))
                throw make_error(new Compound("permission_error",
                    ["modify", "static_procedure",
                        make_indicator(functor, arity)]));
            if ((flags & MASK_TOUCH_DYNAMIC) !== 0)
                if ((peek.flags & MASK_PRED_DYNAMIC) === 0)
                    throw make_error(new Compound("permission_error",
                        ["modify", "static_procedure",
                            make_indicator(functor, arity)]));
        }
        if ((flags & MASK_TOUCH_DYNAMIC) === 0)
            clause_or_pred.shard = engine.partition;
        add_peek(peek, clause_or_pred, flags);
    } else if (is_provable(clause_or_pred)) {
        if (peek === undefined || peek.remover !== undefined) {
            if (peek !== undefined)
                clause_or_pred.overlay = peek;
            temp[arity] = clause_or_pred;
        } else {
            throw make_error(new Compound("permission_error",
                ["coerce", "procedure", make_indicator(functor, arity)]));
        }
    } else {
        throw make_error(new Compound("permission_error",
            ["coerce", "procedure", make_indicator(functor, arity)]));
    }
}

export function add_peek(peek, clause, flags) {
    add_rope(peek.rope, clause, flags);
    let idxval = clause.idxval;
    if (idxval === undefined) {
        for (let [key, value] of peek.idxmap)
            add_rope(value, clause, flags);
        add_rope(peek.nonguard, clause, flags);
    } else {
        let value = peek.idxmap.get(idxval);
        if (value === undefined) {
            value = clone_rope(peek.nonguard);
            peek.idxmap.set(idxval, value);
        }
        add_rope(value, clause, flags);
    }
}

function add_rope(rope, clause, flags) {
    if ((flags & MASK_TOUCH_BOTTOM) !== 0) {
        rope.data.push(clause);
    } else {
        rope.data.unshift(clause);
    }
    rope.count++;
    rope.cache = null;
}

function clone_rope(rope) {
    return new Logical(rope.cache, rope.count, rope.data.slice());
}

/*********************************************************************/
/* Clause Removal                                                    */
/*********************************************************************/

/**
 * Remove a clause from the knowledge base.
 *
 * @param functor The functor.
 * @param arity The arity.
 * @param clause The clause.
 * @param flags The flags.
 * @return boolean True if the clause was removed, false otherwise.
 */
export function remove_clause(functor, arity, clause, flags) {
    let temp = kb[functor];
    if (temp === undefined)
        return false;
    let peek = temp[arity];
    if (peek === undefined || peek.remover !== undefined)
        return false;
    if (!is_logical(peek.rope))
        throw make_error(new Compound("permission_error",
            ["modify", "static_procedure", make_indicator(functor, arity)]));
    return remove_peek(peek, clause, flags);
}

function remove_peek(peek, clause, flags) {
    if (clause.remover !== undefined)
        return false;
    if (!remove_rope(peek.rope, clause, flags))
        return false;
    let idxval = clause.idxval;
    if (idxval === undefined) {
        for (let [key,value] of peek.idxmap) {
            remove_rope(value, clause, flags);
            if (value.data.length === 0)
                peek.idxmap.delete(key);
        }
        remove_rope(peek.nonguard, clause, flags);
    } else {
        let value = peek.idxmap.get(idxval);
        remove_rope(value, clause, flags);
        if (value.data.length === 0)
            peek.idxmap.delete(idxval);
    }
    clause.remover = stage;
    return true;
}

function remove_rope(rope, clause, flags) {
    let data = rope.data;
    let index;
    if ((flags & MASK_REMOVE_REVERSE) === 0) {
        index = data.indexOf(clause);
    } else {
        index = data.lastIndexOf(clause);
    }
    if (index  === -1)
        return false;
    if (clause.creator === stage)
        data.splice(index, 1);
    rope.count--;
    rope.cache = null;
    return true;
}
