"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PatchBuilder = void 0;
const tslib_1 = require("tslib");
const operations = tslib_1.__importStar(require("./operations"));
const clock_1 = require("./clock");
const isUint8Array_1 = require("@jsonjoy.com/buffers/lib/isUint8Array");
const Patch_1 = require("./Patch");
const constants_1 = require("./constants");
const schema_1 = require("./schema");
const maybeConst = (x) => {
    switch (typeof x) {
        case 'number':
        case 'boolean':
            return true;
        default:
            return x === null;
    }
};
/**
 * Utility class that helps in Patch construction.
 *
 * @category Patch
 */
class PatchBuilder {
    /**
     * Creates a new PatchBuilder instance.
     *
     * @param clock Clock to use for generating timestamps.
     */
    constructor(clock) {
        this.clock = clock;
        this.patch = new Patch_1.Patch();
    }
    /**
     * Retrieve the sequence number of the next timestamp.
     *
     * @returns The next timestamp sequence number that will be used by the builder.
     */
    nextTime() {
        return this.patch.nextTime() || this.clock.time;
    }
    /**
     * Returns the current {@link Patch} instance and resets the builder.
     *
     * @returns A new {@link Patch} instance containing all operations created
     *          using this builder.
     */
    flush() {
        const patch = this.patch;
        this.patch = new Patch_1.Patch();
        return patch;
    }
    // --------------------------------------------------------- Basic operations
    /**
     * Create a new "obj" LWW-Map object.
     *
     * @returns ID of the new operation.
     */
    obj() {
        this.pad();
        const id = this.clock.tick(1);
        this.patch.ops.push(new operations.NewObjOp(id));
        return id;
    }
    /**
     * Create a new "arr" RGA-Array object.
     *
     * @returns ID of the new operation.
     */
    arr() {
        this.pad();
        const id = this.clock.tick(1);
        this.patch.ops.push(new operations.NewArrOp(id));
        return id;
    }
    /**
     * Create a new "vec" LWW-Array vector.
     *
     * @returns ID of the new operation.
     */
    vec() {
        this.pad();
        const id = this.clock.tick(1);
        this.patch.ops.push(new operations.NewVecOp(id));
        return id;
    }
    /**
     * Create a new "str" RGA-String object.
     *
     * @returns ID of the new operation.
     */
    str() {
        this.pad();
        const id = this.clock.tick(1);
        this.patch.ops.push(new operations.NewStrOp(id));
        return id;
    }
    /**
     * Create a new "bin" RGA-Binary object.
     *
     * @returns ID of the new operation.
     */
    bin() {
        this.pad();
        const id = this.clock.tick(1);
        this.patch.ops.push(new operations.NewBinOp(id));
        return id;
    }
    /**
     * Create a new immutable constant JSON value. Can be anything, including
     * nested arrays and objects.
     *
     * @param value JSON value
     * @returns ID of the new operation.
     */
    con(value) {
        this.pad();
        const id = this.clock.tick(1);
        this.patch.ops.push(new operations.NewConOp(id, value));
        return id;
    }
    /**
     * Create a new "val" LWW-Register object. Can be anything, including
     * nested arrays and objects.
     *
     * @param val Reference to another object.
     * @returns ID of the new operation.
     * @todo Rename to `newVal`.
     */
    val() {
        this.pad();
        const id = this.clock.tick(1);
        this.patch.ops.push(new operations.NewValOp(id));
        return id;
    }
    /**
     * Set value of document's root LWW-Register.
     *
     * @returns ID of the new operation.
     */
    root(val) {
        return this.setVal(constants_1.ORIGIN, val);
    }
    /**
     * Set fields of an "obj" object.
     *
     * @returns ID of the new operation.
     */
    insObj(obj, data) {
        if (!data.length)
            throw new Error('EMPTY_TUPLES');
        this.pad();
        const id = this.clock.tick(1);
        const op = new operations.InsObjOp(id, obj, data);
        const span = op.span();
        if (span > 1)
            this.clock.tick(span - 1);
        this.patch.ops.push(op);
        return id;
    }
    /**
     * Set elements of a "vec" object.
     *
     * @returns ID of the new operation.
     */
    insVec(obj, data) {
        if (!data.length)
            throw new Error('EMPTY_TUPLES');
        this.pad();
        const id = this.clock.tick(1);
        const op = new operations.InsVecOp(id, obj, data);
        const span = op.span();
        if (span > 1)
            this.clock.tick(span - 1);
        this.patch.ops.push(op);
        return id;
    }
    /**
     * Set value of a "val" object.
     *
     * @returns ID of the new operation.
     * @todo Rename to "insVal".
     */
    setVal(obj, val) {
        this.pad();
        const id = this.clock.tick(1);
        const op = new operations.InsValOp(id, obj, val);
        this.patch.ops.push(op);
        return id;
    }
    /**
     * Insert a substring into a "str" object.
     *
     * @returns ID of the new operation.
     */
    insStr(obj, ref, data) {
        if (!data.length)
            throw new Error('EMPTY_STRING');
        this.pad();
        const id = this.clock.tick(1);
        const op = new operations.InsStrOp(id, obj, ref, data);
        const span = op.span();
        if (span > 1)
            this.clock.tick(span - 1);
        this.patch.ops.push(op);
        return id;
    }
    /**
     * Insert binary data into a "bin" object.
     *
     * @returns ID of the new operation.
     */
    insBin(obj, ref, data) {
        if (!data.length)
            throw new Error('EMPTY_BINARY');
        this.pad();
        const id = this.clock.tick(1);
        const op = new operations.InsBinOp(id, obj, ref, data);
        const span = op.span();
        if (span > 1)
            this.clock.tick(span - 1);
        this.patch.ops.push(op);
        return id;
    }
    /**
     * Insert elements into an "arr" object.
     *
     * @returns ID of the new operation.
     */
    insArr(arr, ref, data) {
        this.pad();
        const id = this.clock.tick(1);
        const op = new operations.InsArrOp(id, arr, ref, data);
        const span = op.span();
        if (span > 1)
            this.clock.tick(span - 1);
        this.patch.ops.push(op);
        return id;
    }
    /**
     * Update an element in an "arr" object.
     *
     * @returns ID of the new operation.
     */
    updArr(arr, ref, val) {
        this.pad();
        const id = this.clock.tick(1);
        const op = new operations.UpdArrOp(id, arr, ref, val);
        this.patch.ops.push(op);
        return id;
    }
    /**
     * Delete a span of operations.
     *
     * @param obj Object in which to delete something.
     * @param what List of time spans to delete.
     * @returns ID of the new operation.
     */
    del(obj, what) {
        this.pad();
        const id = this.clock.tick(1);
        this.patch.ops.push(new operations.DelOp(id, obj, what));
        return id;
    }
    /**
     * Operation that does nothing just skips IDs in the patch.
     *
     * @param span Length of the operation.
     * @returns ID of the new operation.
     *
     */
    nop(span) {
        this.pad();
        const id = this.clock.tick(span);
        this.patch.ops.push(new operations.NopOp(id, span));
        return id;
    }
    // --------------------------------------- JSON value construction operations
    /**
     * Run the necessary builder commands to create an arbitrary JSON object.
     */
    jsonObj(obj) {
        const id = this.obj();
        const keys = Object.keys(obj);
        if (keys.length) {
            const tuples = [];
            for (const k of keys) {
                const value = obj[k];
                const valueId = value instanceof clock_1.Timestamp ? value : maybeConst(value) ? this.con(value) : this.json(value);
                tuples.push([k, valueId]);
            }
            this.insObj(id, tuples);
        }
        return id;
    }
    /**
     * Run the necessary builder commands to create an arbitrary JSON array.
     */
    jsonArr(arr) {
        const id = this.arr();
        if (arr.length) {
            const values = [];
            for (const el of arr)
                values.push(this.json(el));
            this.insArr(id, id, values);
        }
        return id;
    }
    /**
     * Run builder commands to create a JSON string.
     */
    jsonStr(str) {
        const id = this.str();
        if (str)
            this.insStr(id, id, str);
        return id;
    }
    /**
     * Run builder commands to create a binary data type.
     */
    jsonBin(bin) {
        const id = this.bin();
        if (bin.length)
            this.insBin(id, id, bin);
        return id;
    }
    /**
     * Run builder commands to create a JSON value.
     */
    jsonVal(value) {
        const valId = this.val();
        const id = this.con(value);
        this.setVal(valId, id);
        return valId;
    }
    /**
     * Run the necessary builder commands to create any arbitrary JSON value.
     */
    json(json) {
        if (json instanceof clock_1.Timestamp)
            return json;
        if (json === undefined)
            return this.con(json);
        if (json instanceof Array)
            return this.jsonArr(json);
        if ((0, isUint8Array_1.isUint8Array)(json))
            return this.jsonBin(json);
        if (json instanceof schema_1.NodeBuilder)
            return json.build(this);
        switch (typeof json) {
            case 'object':
                return json === null ? this.jsonVal(json) : this.jsonObj(json);
            case 'string':
                return this.jsonStr(json);
            case 'number':
            case 'boolean':
                return this.jsonVal(json);
        }
        throw new Error('INVALID_JSON');
    }
    /**
     * Given a JSON `value` creates the necessary builder commands to create
     * JSON CRDT Patch operations to construct the value. If the `value` is a
     * timestamp, it is returned as-is. If the `value` is a JSON primitive is
     * a number, boolean, or `null`, it is converted to a "con" data type. Otherwise,
     * the `value` is converted using the {@link PatchBuilder.json} method.
     *
     * @param value A JSON value for which to create JSON CRDT Patch construction operations.
     * @returns ID of the root constructed CRDT object.
     */
    constOrJson(value) {
        if (value instanceof clock_1.Timestamp)
            return value;
        return maybeConst(value) ? this.con(value) : this.json(value);
    }
    /**
     * Creates a "con" data type unless the value is already a timestamp, in which
     * case it is returned as-is.
     *
     * @param value Value to convert to a "con" data type.
     * @returns ID of the new "con" object.
     */
    maybeConst(value) {
        return value instanceof clock_1.Timestamp ? value : this.con(value);
    }
    // ------------------------------------------------------------------ Private
    /**
     * Add padding "noop" operation if clock's time has jumped. This method checks
     * if clock has advanced past the ID of the last operation of the patch and,
     * if so, adds a "noop" operation to the patch to pad the gap.
     */
    pad() {
        const nextTime = this.patch.nextTime();
        if (!nextTime)
            return;
        const drift = this.clock.time - nextTime;
        if (drift > 0) {
            const id = (0, clock_1.ts)(this.clock.sid, nextTime);
            const padding = new operations.NopOp(id, drift);
            this.patch.ops.push(padding);
        }
    }
}
exports.PatchBuilder = PatchBuilder;
//# sourceMappingURL=PatchBuilder.js.map