"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.s = exports.schema = exports.nodes = exports.SchemaNode = exports.NodeBuilder = void 0;
const tslib_1 = require("tslib");
const isUint8Array_1 = require("@jsonjoy.com/buffers/lib/isUint8Array");
const clock_1 = require("./clock");
const print = tslib_1.__importStar(require("../util/print"));
const tree_dump_1 = require("tree-dump");
const maybeConst = (x) => {
    switch (typeof x) {
        case 'number':
        case 'boolean':
        case 'undefined':
            return true;
        case 'object':
            return x === null || x instanceof clock_1.Timestamp;
        default:
            return false;
    }
};
/**
 * @category Patch
 */
class NodeBuilder {
    constructor(_build) {
        this._build = _build;
    }
    build(builder) {
        return this._build?.(builder) ?? builder.con(void 0);
    }
}
exports.NodeBuilder = NodeBuilder;
/**
 * @category Patch
 */
class SchemaNode extends NodeBuilder {
    toString(tab) {
        return this.type;
    }
}
exports.SchemaNode = SchemaNode;
/**
 * This namespace contains all the node builders. Each node builder is a
 * schema for a specific node type. Each node builder has a `build` method
 * that takes a {@link NodeBuilder} and returns the ID of the node.
 */
var nodes;
(function (nodes) {
    /**
     * The `con` class represents a "con" JSON CRDT node. As the generic type
     * parameter, it takes the type of the raw value.
     *
     * Example:
     *
     * ```ts
     * s.con(0);
     * s.con('');
     * s.con<number>(123);
     * s.con<0 | 1>(0);
     * ```
     */
    class con extends SchemaNode {
        constructor(raw) {
            super();
            this.raw = raw;
            this.type = 'con';
        }
        build(builder) {
            return builder.con(this.raw);
        }
        toString(tab) {
            return this.type + ' ' + print.con(this.raw);
        }
    }
    nodes.con = con;
    /**
     * The `str` class represents a "str" JSON CRDT node. As the generic type
     * parameter, it takes the type of the raw value.
     *
     * Example:
     *
     * ```ts
     * s.str('');
     * s.str('hello');
     * s.str<string>('world');
     * s.str<'' | 'hello' | 'world'>('hello');
     * ```
     */
    class str extends SchemaNode {
        constructor(raw) {
            super();
            this.raw = raw;
            this.type = 'str';
        }
        build(builder) {
            return builder.json(this.raw);
        }
        toString(tab) {
            return this.type + ' ' + print.con(this.raw);
        }
    }
    nodes.str = str;
    /**
     * The `bin` class represents a "bin" JSON CRDT node.
     */
    class bin extends SchemaNode {
        constructor(raw) {
            super();
            this.raw = raw;
            this.type = 'bin';
        }
        build(builder) {
            return builder.json(this.raw);
        }
        toString(tab) {
            return this.type + ' ' + print.bin(this.raw);
        }
    }
    nodes.bin = bin;
    /**
     * The `val` class represents a "val" JSON CRDT node. As the generic type
     * parameter, it takes the type of the inner node builder.
     *
     * Example:
     *
     * ```ts
     * s.val(s.con(0));
     * s.val(s.str(''));
     * s.val(s.str('hello'));
     * ```
     */
    class val extends SchemaNode {
        constructor(value) {
            super();
            this.value = value;
            this.type = 'val';
        }
        build(builder) {
            const valId = builder.val();
            const valueId = this.value.build(builder);
            builder.setVal(valId, valueId);
            return valId;
        }
        toString(tab) {
            return this.type + (0, tree_dump_1.printTree)(tab, [(tab) => this.value.toString(tab)]);
        }
    }
    nodes.val = val;
    /**
     * The `vec` class represents a "vec" JSON CRDT node. As the generic type
     * parameter, it takes a tuple of node builders.
     *
     * Example:
     *
     * ```ts
     * s.vec(s.con(0), s.con(1));
     * s.vec(s.str(''), s.str('hello'));
     * ```
     */
    class vec extends SchemaNode {
        constructor(value) {
            super();
            this.value = value;
            this.type = 'vec';
        }
        build(builder) {
            const vecId = builder.vec();
            const value = this.value;
            const length = value.length;
            if (length) {
                const elementPairs = [];
                for (let i = 0; i < length; i++) {
                    const element = value[i];
                    if (!element)
                        continue;
                    const elementId = element.build(builder);
                    elementPairs.push([i, elementId]);
                }
                builder.insVec(vecId, elementPairs);
            }
            return vecId;
        }
        toString(tab) {
            return (this.type +
                (0, tree_dump_1.printTree)(tab, [
                    ...this.value.map((child, i) => (tab) => `${i}: ${child ? child.toString(tab) : print.line(child)}`),
                ]));
        }
    }
    nodes.vec = vec;
    /**
     * The `obj` class represents a "obj" JSON CRDT node. As the generic type
     * parameter, it takes a record of node builders. The optional generic type
     * parameter is a record of optional keys.
     *
     * Example:
     *
     * ```ts
     * s.obj({
     *   name: s.str(''),
     *   age: s.con(0),
     * });
     * ```
     *
     * Specify optional keys as the second argument:
     *
     * ```ts
     * s.obj(
     *   {
     *     href: s.str('https://example.com'),
     *   },
     *   {
     *     title: s.str(''),
     *   },
     * )
     * ```
     *
     * Or, specify only the type, using the `optional` method:
     *
     * ```ts
     * s.obj({
     *   href: s.str('https://example.com'),
     * })
     *   .optional<nodes.obj({
     *     title: nodes.str,
     *   })>()
     * ```
     */
    class obj extends SchemaNode {
        constructor(obj, opt) {
            super();
            this.obj = obj;
            this.opt = opt;
            this.type = 'obj';
        }
        optional() {
            return this;
        }
        build(builder) {
            const objId = builder.obj();
            const keyValuePairs = [];
            const merged = { ...this.obj, ...this.opt };
            const keys = Object.keys(merged);
            const length = keys.length;
            if (length) {
                for (let i = 0; i < length; i++) {
                    const key = keys[i];
                    const valueId = merged[key].build(builder);
                    keyValuePairs.push([key, valueId]);
                }
                builder.insObj(objId, keyValuePairs);
            }
            return objId;
        }
        toString(tab = '') {
            return (this.type +
                (0, tree_dump_1.printTree)(tab, [
                    ...[...Object.entries(this.obj)].map(([key, child]) => (tab) => print.line(key) + (0, tree_dump_1.printTree)(tab + ' ', [(tab) => child.toString(tab)])),
                    ...[...Object.entries(this.opt ?? [])].map(([key, child]) => (tab) => print.line(key) + '?' + (0, tree_dump_1.printTree)(tab + ' ', [(tab) => child.toString(tab)])),
                ]));
        }
    }
    nodes.obj = obj;
    /**
     * The `arr` class represents a "arr" JSON CRDT node. As the generic type
     * parameter, it an array of node builders.
     *
     * Example:
     *
     * ```ts
     * s.arr([s.con(0), s.con(1)]);
     * s.arr([s.str(''), s.str('hello')]);
     * ```
     */
    class arr extends SchemaNode {
        constructor(arr) {
            super();
            this.arr = arr;
            this.type = 'arr';
        }
        build(builder) {
            const arrId = builder.arr();
            const arr = this.arr;
            const length = arr.length;
            if (length) {
                const valueIds = [];
                for (let i = 0; i < length; i++)
                    valueIds.push(arr[i].build(builder));
                builder.insArr(arrId, arrId, valueIds);
            }
            return arrId;
        }
        toString(tab) {
            return (this.type +
                (0, tree_dump_1.printTree)(tab, [
                    ...this.arr.map((child, i) => (tab) => `[${i}]: ${child ? child.toString(tab) : print.line(child)}`),
                ]));
        }
    }
    nodes.arr = arr;
    /**
     * Creates an extension node schema. The extension node is a tuple with a
     * sentinel header and a data node. The sentinel header is a 3-byte
     * {@link Uint8Array}, which makes this "vec" node to be treated as an
     * extension "ext" node.
     *
     * The 3-byte header consists of the extension ID, the SID of the tuple ID,
     * and the time of the tuple ID:
     *
     * - 1 byte for the extension id
     * - 1 byte for the sid of the tuple id, modulo 256
     * - 1 byte for the time of the tuple id, modulo 256
     */
    class ext extends SchemaNode {
        /**
         * @param id A unique extension ID.
         * @param data Schema of the data node of the extension.
         */
        constructor(id, data) {
            super();
            this.id = id;
            this.data = data;
            this.type = 'ext';
        }
        build(builder) {
            const buf = new Uint8Array([this.id, 0, 0]);
            const tupleId = builder.vec();
            buf[1] = tupleId.sid % 256;
            buf[2] = tupleId.time % 256;
            builder.insVec(tupleId, [
                [0, builder.constOrJson(exports.s.con(buf))],
                [1, this.data.build(builder)],
            ]);
            return tupleId;
        }
        toString(tab) {
            return this.type + '(' + this.id + ')' + (0, tree_dump_1.printTree)(tab, [(tab) => this.data.toString(tab)]);
        }
    }
    nodes.ext = ext;
})(nodes || (exports.nodes = nodes = {}));
/**
 * Schema builder. Use this to create a JSON CRDT model schema and the default
 * value.
 *
 * Example:
 *
 * ```typescript
 * const schema = s.obj({
 *   name: s.str(''),
 *   age: s.con(0),
 *   tags: s.arr<nodes.con<string>>([]),
 * });
 * ```
 */
exports.schema = {
    /**
     * Creates a "con" node schema and the default value.
     *
     * @param raw Raw default value.
     */
    con: (raw) => new nodes.con(raw),
    /**
     * Creates a "str" node schema and the default value.
     *
     * @param str Default value.
     */
    str: (str) => new nodes.str(str || ''),
    /**
     * Creates a "bin" node schema and the default value.
     *
     * @param bin Default value.
     */
    bin: (bin) => new nodes.bin(bin),
    /**
     * Creates a "val" node schema and the default value.
     *
     * @param val Default value.
     */
    val: (val) => new nodes.val(val),
    /**
     * Creates a "vec" node schema and the default value.
     *
     * @param vec Default value.
     */
    vec: (...vec) => new nodes.vec(vec),
    /**
     * Creates a "obj" node schema and the default value.
     *
     * @param obj Default value, required object keys.
     * @param opt Default value of optional object keys.
     */
    obj: (obj, opt) => new nodes.obj(obj, opt),
    /**
     * This is an alias for {@link schema.obj}. It creates a "map" node schema,
     * which is an object where a key can be any string and the value is of the
     * same type.
     *
     * @param obj Default value.
     */
    map: (obj) => exports.schema.obj(obj),
    /**
     * Creates an "arr" node schema and the default value.
     *
     * @param arr Default value.
     */
    arr: (arr) => new nodes.arr(arr),
    /**
     * Recursively creates a node tree from any POJO.
     */
    json: (value) => {
        switch (typeof value) {
            case 'object': {
                if (!value)
                    return exports.s.val(exports.s.con(value));
                if (value instanceof NodeBuilder)
                    return value;
                else if (Array.isArray(value))
                    return exports.s.arr(value.map((v) => exports.s.json(v)));
                else if ((0, isUint8Array_1.isUint8Array)(value))
                    return exports.s.bin(value);
                else if (value instanceof clock_1.Timestamp)
                    return exports.s.val(exports.s.con(value));
                else {
                    const obj = {};
                    const keys = Object.keys(value);
                    for (const key of keys)
                        obj[key] = exports.s.jsonCon(value[key]);
                    return exports.s.obj(obj);
                }
            }
            case 'string':
                return exports.s.str(value);
            default:
                return exports.s.val(exports.s.con(value));
        }
    },
    /**
     * Recursively creates a schema node tree from any POJO. Same as {@link json}, but
     * converts constant values to {@link nodes.con} nodes, instead wrapping them into
     * {@link nodes.val} nodes.
     *
     * @todo Remove this once "arr" RGA supports in-place updates.
     */
    jsonCon: (value) => {
        return maybeConst(value) ? exports.s.con(value) : exports.s.json(value);
    },
    /**
     * Creates an extension node schema.
     *
     * @param id A unique extension ID.
     * @param data Schema of the data node of the extension.
     */
    ext: (id, data) => new nodes.ext(id, data),
};
/**
 * Schema builder. Use this to create a JSON CRDT model schema and the default
 * value. Alias for {@link schema}.
 */
exports.s = exports.schema;
//# sourceMappingURL=schema.js.map