"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonPatch = void 0;
const deepEqual_1 = require("@jsonjoy.com/util/lib/json-equal/deepEqual");
const nodes_1 = require("../nodes");
const util_1 = require("@jsonjoy.com/json-pointer/lib/util");
const clock_1 = require("../../json-crdt-patch/clock");
class JsonPatch {
    constructor(model, pfx = [], base) {
        this.model = model;
        this.pfx = pfx;
        this.base = base;
    }
    apply(ops) {
        const length = ops.length;
        this.model.api.transaction(() => {
            for (let i = 0; i < length; i++)
                this.applyOp(ops[i]);
        });
        return this;
    }
    applyOp(op) {
        switch (op.op) {
            case 'add':
                this.add(op.path, op.value);
                break;
            case 'remove':
                this.remove(op.path);
                break;
            case 'replace':
                this.replace(op.path, op.value);
                break;
            case 'move':
                this.move(op.path, op.from);
                break;
            case 'copy':
                this.copy(op.path, op.from);
                break;
            case 'test':
                this.test(op.path, op.value);
                break;
            case 'str_ins':
                this.strIns(op.path, op.pos, op.str);
                break;
            case 'str_del':
                this.strDel(op.path, op.pos, op.len ?? 0, op.str);
                break;
            default:
                throw new Error('UNKNOWN_OP');
        }
        this.model.api.apply();
        return this;
    }
    builder() {
        return this.model.api.builder;
    }
    toPath(path) {
        return this.pfx.concat((0, util_1.toPath)(path));
    }
    add(path, value) {
        const builder = this.builder();
        const steps = this.toPath(path);
        if (!steps.length)
            this.setRoot(value);
        else {
            const objSteps = steps.slice(0, steps.length - 1);
            const node = this.base?.find(objSteps) ?? this.model.api.find(objSteps);
            const key = steps[steps.length - 1];
            if (node instanceof nodes_1.ObjNode) {
                builder.insObj(node.id, [[String(key), builder.json(value)]]); // TODO: see if "con" nodes can be used here in some cases.
            }
            else if (node instanceof nodes_1.ArrNode) {
                const builderValue = builder.json(value);
                if (key === '-') {
                    const length = node.length();
                    const after = node.find(length - 1) || node.id;
                    builder.insArr(node.id, after, [builderValue]);
                }
                else {
                    const index = ~~key;
                    if ('' + index !== key)
                        throw new Error('INVALID_INDEX');
                    if (!index)
                        builder.insArr(node.id, node.id, [builderValue]);
                    else {
                        const after = node.find(index - 1);
                        if (!after)
                            throw new Error('NOT_FOUND');
                        builder.insArr(node.id, after, [builderValue]);
                    }
                }
            }
            else
                throw new Error('NOT_FOUND');
        }
    }
    remove(path) {
        const builder = this.builder();
        const steps = this.toPath(path);
        if (!steps.length)
            this.setRoot(null);
        else {
            const objSteps = steps.slice(0, steps.length - 1);
            const node = this.base?.find(objSteps) ?? this.model.api.find(objSteps);
            const key = steps[steps.length - 1];
            if (node instanceof nodes_1.ObjNode) {
                const stringKey = String(key);
                const valueNode = node.get(stringKey);
                if (valueNode === undefined)
                    throw new Error('NOT_FOUND');
                if (valueNode instanceof nodes_1.ConNode && valueNode.val === undefined)
                    throw new Error('NOT_FOUND');
                builder.insObj(node.id, [[stringKey, builder.con(undefined)]]);
            }
            else if (node instanceof nodes_1.ArrNode) {
                const key = steps[steps.length - 1];
                const index = ~~key;
                if (typeof key === 'string' && '' + index !== key)
                    throw new Error('INVALID_INDEX');
                const id = node.find(index);
                if (!id)
                    throw new Error('NOT_FOUND');
                builder.del(node.id, [(0, clock_1.interval)(id, 0, 1)]);
            }
            else
                throw new Error('NOT_FOUND');
        }
    }
    replace(path, value) {
        this.remove(path);
        this.add(path, value);
    }
    move(path, from) {
        path = (0, util_1.toPath)(path);
        from = (0, util_1.toPath)(from);
        if ((0, util_1.isChild)(from, path))
            throw new Error('INVALID_CHILD');
        const json = this.json(this.toPath(from));
        this.remove(from);
        this.add(path, json);
    }
    copy(path, from) {
        path = (0, util_1.toPath)(path);
        const json = this.json(this.toPath(from));
        this.add(path, json);
    }
    test(path, value) {
        path = this.toPath(path);
        const json = this.json(path);
        if (!(0, deepEqual_1.deepEqual)(json, value))
            throw new Error('TEST');
    }
    strIns(path, pos, str) {
        path = this.toPath(path);
        const node = this.base?.find(path) ?? this.model.api.find(path);
        if (!(node instanceof nodes_1.StrNode))
            throw new Error('NOT_FOUND');
        const length = node.length();
        const after = pos ? node.find(length < pos ? length - 1 : pos - 1) : node.id;
        if (!after)
            throw new Error('OUT_OF_BOUNDS');
        this.builder().insStr(node.id, after, str);
    }
    strDel(path, pos, len, str = '') {
        path = this.toPath(path);
        const node = this.base?.find(path) ?? this.model.api.find(path);
        if (!(node instanceof nodes_1.StrNode))
            throw new Error('NOT_FOUND');
        const length = node.length();
        if (length <= pos)
            return;
        const deletionLength = Math.min(len ?? str.length, length - pos);
        const range = node.findInterval(pos, deletionLength);
        if (!range)
            throw new Error('OUT_OF_BOUNDS');
        this.builder().del(node.id, range);
    }
    get(path) {
        return (this.base ?? this.model.api).read(this.toPath(path));
    }
    json(steps) {
        const json = (this.base ?? this.model.api).read(steps);
        if (json === undefined)
            throw new Error('NOT_FOUND');
        return json;
    }
    setRoot(json) {
        const root = this.base?.node ?? this.model.root;
        const builder = this.builder();
        builder.setVal(root.id, builder.json(json));
    }
}
exports.JsonPatch = JsonPatch;
//# sourceMappingURL=JsonPatch.js.map