/**
 * 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_PREHASH = 0x00000010;
export const MASK_PRED_ARITH = 0x00000008;
export const MASK_PRED_SPECIAL = 0x00000004;
export const MASK_PRED_TEST = 0x00000002;
export const MASK_PRED_DYNAMIC = 0x00000001;

export const MASK_TOUCH_PREHASH = 0x00000004;
export const MASK_TOUCH_BOTTOM = 0x00000002;
export const MASK_TOUCH_DYNAMIC = 0x00000001;

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;

export const MAX_INDEX = 2;
const ROPE_LOW = 24;
const ROPE_HIGH = 40;

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

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 Prolog 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;
}

/**
 * Compute a variable serno.
 *
 * @param alpha The variable.
 * @return The serno.
 */
export function variable_serno(alpha) {
    let val = alpha.flags & VAR_MASK_SERNO;
    if (val === VAR_MASK_SERNO) {
        if (engine.low < engine.high) {
            val = engine.low;
            engine.low = val + 1;
        } else {
            val = engine.serno;
            engine.serno = val + 1;
        }
        alpha.flags &= ~VAR_MASK_SERNO;
        alpha.flags |= val;
    }
    return val;
}

/**
 * Create an element.
 *
 * @param functor The functor.
 * @param args The arguments.
 * @constructor The new element.
 */
export function Element(functor, args) {
    this.functor = functor;
    this.args = args;
}

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

/**
 * Create a Prolog structure.
 *
 * @param functor The functor.
 * @param args The arguments.
 * @constructor The new structure.
 */
export class Structure extends Element {
    constructor(functor, args) {
        super(functor, 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;
}

/**************************************************************/
/* 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 class Skeleton extends Element {
    constructor(functor, args) {
        super(functor, 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;
}

/**************************************************************/
/* 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;
    try {
        res = walk_copy(first);
    } finally {
        walk_uncopy(first);
    }
    return res;
}

function walk_copy(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_compound(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_uncopy(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_compound(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);
        }
    }
}

/**************************************************************/
/* Binary Work                                                */
/**************************************************************/

/**
 * Create a binary work item.
 *
 * @param first The first Prolog term.
 * @param second The second Prolog term.
 * @param idx The index.
 */
export function Item(first, second, idx) {
    this.first = first;
    this.second = second;
    this.idx = idx;
}

/**************************************************************/
/* Stack Array                                                */
/**************************************************************/

/**
 * The function returns the same stack extended by a work item.
 *
 * @param stack The stack.
 * @param elem The work item.
 * @return The new stack.
 */
export function stack_push(stack, elem) {
    if (stack === null)
        stack = new Array(0);
    stack.push(elem);
    return stack;
}

/**
 * The function returns the top work item or null.
 *
 * @param stack The stack.
 * @return The work item.
 */
export function stack_peek(stack) {
    if (stack === null)
        return null;
    if (stack.length === 0)
        return null;
    return stack[stack.length-1];
}

/**
 * The routine removes the top work item from the stack.
 *
 * @param stack The stack.
 */
export function stack_pop(stack) {
    stack.pop();
}

/**************************************************************/
/* 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, functor, head, body, cutvar) {
    this.size = size;
    this.functor = functor;
    this.head = head;
    this.body = body;
    this.cutvar = cutvar;
    this.creator = undefined;
    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.
 */
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;
}

/**
 * Create a stick.
 *
 * @param rope The rope.
 * @param guards The variable guards.
 * @param maps The index map.
 */
export function Stick(rope, guards, maps) {
    this.rope = rope;
    this.guards = guards;
    this.maps = maps;
}

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

export function index_value(temp, body, flags) {
    let j = 0;
    for (;;) {
        if (!is_place(temp)) {
            if ((flags & MASK_PRED_PREHASH) !== 0) {
                if (is_frozen(temp)) {
                    temp = temp.hash;
                } else if (!is_skeleton(temp)) {
                    temp = object_hash_code(temp);
                } else {
                    temp = undefined;
                }
            } else {
                if (is_element(temp))
                    temp = temp.functor;
            }
            break;
        }
        if (temp !== undefined &&
                (flags & MASK_PRED_DYNAMIC) === 0 &&
                j < body.length) {
            let peek = body[j];
            j++;
            if (is_element(peek) &&
                "=" === site_name(peek.functor) &&
                peek.args.length === 2) {
                if (temp === peek.args[0]) {
                    temp = peek.args[1];
                } else if (temp === peek.args[1]) {
                    temp = peek.args[0];
                }
                continue;
            } else if (is_element(peek) &&
                "nonvar" === site_name(peek.functor) &&
                peek.args.length === 1) {
                continue;
            }
        }
        temp = undefined;
        break;
    }
    return temp;
}

function site_name(obj) {
    if (is_cache(obj)) {
        return obj.name;
    } else {
        return obj;
    }
}

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

/**
 * Create a provable.
 *
 * @constructor The new provable.
 */
export function Provable() {
    this.flags = 0;
    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                                                     */
/*********************************************************************/

export function snapshot_peek(func) {
    if (is_stick(func)) {
        return snapshot_rope(func.rope);
    } else {
        return snapshot_rope(func);
    }
}

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

function copy_rope(res, rope) {
    if (Array.isArray(rope.data)) {
        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;
        }
    } else {
        let data = rope.data;
        let j = 0;
        for (let clause of data) {
            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;
}

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

export function resolve_link(functor, arity) {
    if (is_cache(functor)) {
        return ensure_link(functor, arity);
    } else if (is_atom(functor)) {
        return pred_link(functor, arity);
    } else if (is_provable(functor)) {
        return functor;
    } else {
        check_nonvar(functor);
        functor = copy_term(functor);
        throw make_error(new Compound("type_error",
            ["callable", functor]));
    }
}

/**
 * Assure that the object is a nonvar.
 *
 * @param beta The object.
 */
export function check_nonvar(beta) {
    if (is_variable(beta))
        throw make_error("instantiation_error");
}

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

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

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);
        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.func) && !is_stick(peek.func))
            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_PREHASH) !== 0) {
        if ((peek.flags & MASK_PRED_PREHASH) === 0) {
            if (is_stick(peek.func))
                peek.func = peek.func.rope;
            peek.flags |= MASK_PRED_PREHASH;
        }
    }
    return peek;
}

function resolve_touch(functor, arity, flags) {
    if (is_cache(functor)) {
        return ensure_touch(functor, arity, flags);
    } else if (is_atom(functor)) {
        return pred_touch(functor, arity, flags);
    } else if (is_provable(functor)) {
        return functor;
    } else {
        check_nonvar(functor);
        functor = copy_term(functor);
        throw make_error(new Compound("type_error",
            ["callable", functor]));
    }
}

/*********************************************************************/
/* Index Extension                                                   */
/*********************************************************************/

/**
 * The function returns an anonymous predicate for the given clauses.
 * The index is not yet built, so only a logical is stored.
 *
 * @param data The clauses.
 * @return Provable The predicate.
 */
export function make_defined(data) {
    let peek = new Provable();
    peek.func = new_rope();
    for (let i = 0; i < data.length; i++)
        add_peek(peek.func, 0, data[i], MASK_TOUCH_BOTTOM, peek.flags);
    return peek;
}

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

function new_bouquet(rope, width) {
    let guards = new Array(width);
    let maps = new Array(width);
    return new Stick(rope, guards, maps);
}

function extend_bouquet(peek, j, offset, rope, flags) {
    peek.guards[j] = new_rope();
    peek.maps[j] = new Map();
    if (Array.isArray(rope.data)) {
        let data = rope.data;
        for (let i = 0; i < data.length; i++) {
            let clause = data[i];
            if (clause.remover === undefined)
                add_map(peek, j, offset, clause, MASK_TOUCH_BOTTOM, flags);
        }
    } else {
        let data = rope.data;
        for (let clause of data) {
            if (clause.remover === undefined)
                add_map(peek, j, offset, clause, MASK_TOUCH_BOTTOM, flags);
        }
    }
}

/**************************************************************/
/* Clauses Retrieval                                          */
/**************************************************************/

/**
 * Retrieve a logical from a provable possibly using an index.
 * If only a logical is stored, build an index before retrieval.
 *
 * @param peek The provable.
 * @param args The actual arguments.
 * @return Logical The logical.
 */
export function defined_pred(peek, args) {
    let width = Math.min(MAX_INDEX, args.length);
    let offset = 0;
    let func = peek.func;
    let flags = peek.flags;
    let val = null;
    let k = 0;
    for (;;) {
        let rope;
        if (is_stick(func)) {
            rope = func.rope;
        } else {
            rope = func;
        }
        if (rope.count < 2)
            return rope;
        let i = 0;
        for (; i < width; i++) {
            let key = key_value(args[i+offset], flags);
            if (key === undefined)
                continue;
            if (!is_stick(func)) {
                func = new_bouquet(rope, width);
                if (is_stick(peek)) {
                    if (val === undefined) {
                        peek.guards[k] = func;
                    } else {
                        peek.maps[k].set(val, func);
                    }
                } else {
                    peek.func = func;
                }
            }
            peek = func;
            if (peek.maps[i] === undefined)
                extend_bouquet(peek, i, offset, rope, flags);
            if (peek.maps[i].size === 0)
                continue;
            func = peek.maps[i].get(key);
            if (func === undefined) {
                func = peek.guards[i];
                val = undefined;
            } else {
                val = key;
            }
            k = i;
            width = width-i-1;
            offset = i+offset+1;
            i = -1;
            break;
        }
        if (!(i < width))
            return rope;
    }
}

function key_value(term, flags) {
    term = deref(term);
    if (!is_variable(term)) {
        if ((flags & MASK_PRED_PREHASH) !== 0) {
            if (is_frozen(term)) {
                term = term.hash;
            } else if (!is_compound(term)) {
                term = object_hash_code(term);
            } else {
                term = undefined;
            }
        } else {
            if (is_structure(term))
                term = term.functor;
        }
    } else {
        term = undefined;
    }
    return term;
}

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

/*********************************************************************/
/* 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.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.func) || is_stick(peek.func))
                rollback_peek(peek.func, true);
        }
        if (trim_arities(temp))
            delete kb[functor]
    }
}

function rollback_peek(peek, update) {
    if (is_stick(peek)) {
        if (!has_action(peek.rope))
            return;
        for (let i = 0; i < peek.maps.length; i++) {
            let temp = peek.maps[i];
            if (temp === undefined)
                continue;
            for (let [key, val] of temp) {
                rollback_peek(val, false);
                if (size_peek(val) === 0)
                    temp.delete(key);
            }
            rollback_peek(peek.guards[i], false);
        }
        rollback_rope(peek.rope, update);
    } else {
        if (!has_action(peek))
            return;
        rollback_rope(peek, update);
    }
}

export function size_peek(func) {
    let rope;
    if (is_stick(func)) {
        rope = func.rope;
    } else {
        rope = func;
    }
    if (Array.isArray(rope.data)) {
        return rope.data.length;
    } else {
        return rope.data.size;
    }
}

/**
 * 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) {
    if (Array.isArray(rope.data)) {
        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;
    } else {
        let data = rope.data;
        for (let clause of data) {
            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) {
    if (Array.isArray(rope.data)) {
        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;
    } else {
        let data = rope.data;
        for (let clause of data) {
            if (clause.creator === stage) {
                rope.count--;
                data.delete(clause);
            } else {
                if (clause.remover === stage) {
                    if (update)
                        clause.remover = undefined;
                    rope.count++;
                }
            }
        }
        rope.cache = null;
    }
}

/*********************************************************************/
/* Provable Addition                                                 */
/*********************************************************************/

/**
 * Enhance the knowledge base by a provable.
 *
 * @param functor The functor.
 * @param arity The arity.
 * @param pred The provable.
 */
export function set(functor, arity, pred) {
    let temp = kb[functor];
    if (temp === undefined) {
        temp = new Array(0);
        kb[functor] = temp;
    }
    let peek = temp[arity];
    if (peek === undefined || peek.remover !== undefined) {
        if (peek !== undefined)
            pred.overlay = peek;
        temp[arity] = pred;
    } else {
        throw make_error(new Compound("permission_error",
            ["coerce", "procedure", make_indicator(functor, arity)]));
    }
}

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

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

export function add_clause(clause, flags) {
    if (clause.remover !== undefined)
        throw make_error(new Compound("permission_error",
            ["modify", "static_procedure",
                make_indicator(clause.functor, clause.head.length)]));
    if (clause.creator !== undefined)
        return;
    let peek = resolve_touch(clause.functor, clause.head.length, flags);
    add_peek(peek.func, 0, clause, flags, peek.flags);
    clause.creator = stage;
    if ((flags & MASK_TOUCH_DYNAMIC) === 0)
        clause.shard = engine.partition;
}

function add_peek(peek, offset, clause, flags, flags2) {
    if (is_stick(peek)) {
        add_rope(peek.rope, clause, flags);
        for (let i = 0; i < peek.maps.length; i++)
            if (peek.maps[i] !== undefined)
                add_map(peek, i, offset, clause, flags, flags2);
    } else {
        add_rope(peek, clause, flags);
    }
}

function add_map(peek, i, offset, clause, flags, flags2) {
    let key = index_value(clause.head[i+offset], clause.body, flags2);
    let temp = peek.maps[i];
    if (key === undefined) {
        for (let [key, val] of temp)
            add_peek(val, i+offset+1, clause, flags, flags2);
        add_peek(peek.guards[i], i+offset+1, clause, flags, flags2);
    } else {
        let val = temp.get(key);
        if (val === undefined) {
            val = clone_peek(peek.guards[i]);
            temp.set(key, val);
        }
        add_peek(val, i+offset+1, clause, flags, flags2);
    }
}

function add_rope(rope, clause, flags) {
    if (Array.isArray(rope.data)) {
        let data = rope.data;
        if ((flags & MASK_TOUCH_BOTTOM) !== 0) {
            data.push(clause);
        } else {
            data.unshift(clause);
        }
        if (data.length >= ROPE_HIGH)
            rope.data = new Set(data);
        rope.count++;
        rope.cache = null;
    } else {
        let data = rope.data;
        if ((flags & MASK_TOUCH_BOTTOM) !== 0) {
            data.add(clause);
        } else {
            rope.data = new Set((function *() {yield clause; yield * data})());
        }
        rope.count++;
        rope.cache = null;
    }
}

function clone_peek(func) {
    let rope;
    if (is_stick(func)) {
        rope = func.rope;
    } else {
        rope = func;
    }
    if (Array.isArray(rope.data)) {
        return new Logical(rope.cache, rope.count, rope.data.slice());
    } else {
        return new Logical(rope.cache, rope.count, new Set(rope.data));
    }
}

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

/**
 * Remove a clause from the knowledge base.
 *
 * @param clause The clause.
 * @param flags The flags.
 * @return boolean True if the clause was removed, false otherwise.
 */
export function remove_clause(clause, flags) {
    if (clause.remover !== undefined)
        return false;
    if (clause.creator === undefined)
        return false;
    let peek = resolve_link(clause.functor, clause.head.length);
    if (peek === undefined)
        return false;
    if (!is_logical(peek.func) && !is_stick(peek.func))
        throw make_error(new Compound("permission_error",
            ["modify", "static_procedure",
                make_indicator(clause.functor, clause.head.length)]));
    remove_peek(peek.func, 0, clause, flags, peek.flags);
    if (clause.creator !== stage) {
        clause.remover = stage;
    } else {
        clause.creator = undefined;
    }
    return true;
}

function remove_peek(peek, offset, clause, flags, flags2) {
    if (is_stick(peek)) {
        remove_rope(peek.rope, clause, flags);
        for (let i = 0; i < peek.maps.length; i++) {
            let temp = peek.maps[i];
            if (temp === undefined)
                continue;
            let key = index_value(clause.head[i+offset], clause.body, flags2);
            if (key === undefined) {
                for (let [key, val] of temp) {
                    remove_peek(val, i+offset+1, clause, flags, flags2);
                    if (size_peek(val) === 0)
                        temp.delete(key);
                }
                remove_peek(peek.guards[i], i+offset+1, clause, flags, flags2);
            } else {
                let val = temp.get(key);
                remove_peek(val, i+offset+1, clause, flags, flags2);
                if (size_peek(val) === 0)
                    temp.delete(key);
            }
        }
    } else {
        remove_rope(peek, clause, flags);
    }
}

function remove_rope(rope, clause, flags) {
    if (Array.isArray(rope.data)) {
        let data = rope.data;
        let index;
        if ((flags & MASK_REMOVE_REVERSE) === 0) {
            index = data.indexOf(clause);
        } else {
            index = data.lastIndexOf(clause);
        }
        if (clause.creator === stage) {
            if (index === -1)
                throw new Error("rope corrupted");
            data.splice(index, 1);
        } else {
            if (index === -1)
                throw new Error("rope corrupted");
        }
        rope.count--;
        rope.cache = null;
    } else {
        let data = rope.data;
        if (clause.creator === stage) {
            if (!data.delete(clause))
                throw new Error("rope corrupted");
        } else {
            if (!data.has(clause))
                throw new Error("rope corrupted");
        }
        if (data.size < ROPE_LOW)
            rope.data = [...data];
        rope.count--;
        rope.cache = null;
    }

}
