"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonCrdtDiff = exports.DiffError = void 0;
const tslib_1 = require("tslib");
const deepEqual_1 = require("@jsonjoy.com/util/lib/json-equal/deepEqual");
const cmpUint8Array_1 = require("@jsonjoy.com/buffers/lib/cmpUint8Array");
const json_crdt_patch_1 = require("../json-crdt-patch");
const nodes_1 = require("../json-crdt/nodes");
const str = tslib_1.__importStar(require("../util/diff/str"));
const bin = tslib_1.__importStar(require("../util/diff/bin"));
const line = tslib_1.__importStar(require("../util/diff/line"));
const structHashCrdt_1 = require("../json-hash/structHashCrdt");
const structHashSchema_1 = require("../json-hash/structHashSchema");
class DiffError extends Error {
    constructor(message = 'DIFF') {
        super(message);
    }
}
exports.DiffError = DiffError;
class JsonCrdtDiff {
    constructor(model) {
        this.model = model;
        this.builder = new json_crdt_patch_1.PatchBuilder(model.clock.clone());
    }
    diffStr(src, dst) {
        const view = src.view();
        if (view === dst)
            return;
        const builder = this.builder;
        str.apply(str.diff(view, dst), view.length, (pos, txt) => builder.insStr(src.id, !pos ? src.id : src.find(pos - 1), txt), (pos, len) => builder.del(src.id, src.findInterval(pos, len)));
    }
    diffBin(src, dst) {
        const view = src.view();
        if ((0, cmpUint8Array_1.cmpUint8Array)(view, dst))
            return;
        const builder = this.builder;
        bin.apply(bin.diff(view, dst), view.length, (pos, txt) => builder.insBin(src.id, !pos ? src.id : src.find(pos - 1), txt), (pos, len) => builder.del(src.id, src.findInterval(pos, len)));
    }
    diffArr(src, dst) {
        if (src.size() === 0) {
            const length = dst.length;
            if (length === 0)
                return;
            let after = src.id;
            for (let i = 0; i < length; i++)
                after = this.builder.insArr(src.id, after, [this.buildView(dst[i])]);
            return;
        }
        else if (dst.length === 0) {
            const spans = [];
            for (const chunk of src.chunks()) {
                if (chunk.del)
                    continue;
                const id = chunk.id;
                spans.push((0, json_crdt_patch_1.tss)(id.sid, id.time, chunk.span));
            }
            if (spans.length)
                this.builder.del(src.id, spans);
            return;
        }
        const srcLines = [];
        src.children((node) => srcLines.push((0, structHashCrdt_1.structHashCrdt)(node)));
        const dstLines = [];
        const dstLength = dst.length;
        for (let i = 0; i < dstLength; i++)
            dstLines.push((0, structHashSchema_1.structHashSchema)(dst[i]));
        const linePatch = line.diff(srcLines, dstLines);
        if (!linePatch.length)
            return;
        const inserts = [];
        const deletes = [];
        line.apply(linePatch, (posSrc) => {
            const span = src.findInterval(posSrc, 1);
            if (!span || !span.length)
                throw new DiffError();
            deletes.push(...span);
        }, (posSrc, posDst) => {
            const view = dst[posDst];
            const after = posSrc >= 0 ? src.find(posSrc) : src.id;
            if (!after)
                throw new DiffError();
            inserts.push([after, [view]]);
        }, (posSrc, posDst) => {
            const view = dst[posDst];
            try {
                this.diffAny(src.getNode(posSrc), view);
            }
            catch (error) {
                if (error instanceof DiffError) {
                    const span = src.findInterval(posSrc, 1);
                    deletes.push(...span);
                    const after = posSrc ? src.find(posSrc - 1) : src.id;
                    if (!after)
                        throw new DiffError();
                    inserts.push([after, [view]]);
                }
                else
                    throw error;
            }
        });
        const builder = this.builder;
        const length = inserts.length;
        for (let i = 0; i < length; i++) {
            const [after, views] = inserts[i];
            builder.insArr(src.id, after, views.map((view) => this.buildView(view)));
        }
        if (deletes.length)
            builder.del(src.id, deletes);
    }
    diffObj(src, dst) {
        const builder = this.builder;
        const inserts = [];
        const srcKeys = new Set();
        src.forEach((key) => {
            srcKeys.add(key);
            const dstValue = dst[key];
            if (dstValue === void 0)
                inserts.push([key, builder.con(undefined)]);
        });
        const keys = Object.keys(dst);
        const length = keys.length;
        for (let i = 0; i < length; i++) {
            const key = keys[i];
            const dstValue = dst[key];
            if (srcKeys.has(key)) {
                const child = src.get(key);
                if (child) {
                    try {
                        this.diffAny(child, dstValue);
                        continue;
                    }
                    catch (error) {
                        if (!(error instanceof DiffError))
                            throw error;
                    }
                }
            }
            inserts.push([key, this.buildConView(dstValue)]);
            // inserts.push([key, src.get(key) instanceof ConNode ? builder.con(dstValue) : this.buildConView(dstValue)]);
        }
        if (inserts.length)
            builder.insObj(src.id, inserts);
    }
    diffVec(src, dst) {
        const builder = this.builder;
        const edits = [];
        const elements = src.elements;
        const srcLength = elements.length;
        const dstLength = dst.length;
        const index = src.doc.index;
        const min = Math.min(srcLength, dstLength);
        for (let i = dstLength; i < srcLength; i++) {
            const id = elements[i];
            if (id) {
                const child = index.get(id);
                const isDeleted = !child || (child instanceof nodes_1.ConNode && child.val === void 0);
                if (isDeleted)
                    continue;
                edits.push([i, builder.con(void 0)]);
            }
        }
        CHILDREN: for (let i = 0; i < min; i++) {
            const value = dst[i];
            const child = src.get(i);
            if (child) {
                try {
                    this.diffAny(child, value);
                    continue;
                }
                catch (error) {
                    if (!(error instanceof DiffError))
                        throw error;
                }
                if (child instanceof nodes_1.ConNode && typeof value !== 'object') {
                    const valueId = builder.con(value);
                    edits.push([i, valueId]);
                    continue CHILDREN;
                }
            }
            edits.push([i, this.buildConView(value)]);
        }
        for (let i = srcLength; i < dstLength; i++)
            edits.push([i, this.buildConView(dst[i])]);
        if (edits.length)
            builder.insVec(src.id, edits);
    }
    diffVal(src, dst) {
        try {
            this.diffAny(src.node(), dst);
        }
        catch (error) {
            if (error instanceof DiffError) {
                const builder = this.builder;
                builder.setVal(src.id, this.buildConView(dst));
            }
            else
                throw error;
        }
    }
    diffAny(src, dst) {
        if (src instanceof nodes_1.ConNode) {
            if (dst instanceof json_crdt_patch_1.nodes.con)
                dst = dst.raw;
            const val = src.val;
            if (val !== dst &&
                ((val instanceof json_crdt_patch_1.Timestamp && !(dst instanceof json_crdt_patch_1.Timestamp)) ||
                    (!(val instanceof json_crdt_patch_1.Timestamp) && dst instanceof json_crdt_patch_1.Timestamp) ||
                    !(0, deepEqual_1.deepEqual)(src.val, dst)))
                throw new DiffError();
        }
        else if (src instanceof nodes_1.StrNode) {
            if (dst instanceof json_crdt_patch_1.nodes.str)
                dst = dst.raw;
            if (typeof dst !== 'string')
                throw new DiffError();
            this.diffStr(src, dst);
        }
        else if (src instanceof nodes_1.ObjNode) {
            if (dst instanceof json_crdt_patch_1.nodes.obj)
                dst = dst.opt ? { ...dst.obj, ...dst.opt } : dst.obj;
            if (dst instanceof json_crdt_patch_1.NodeBuilder)
                throw new DiffError();
            if (!dst || typeof dst !== 'object' || Array.isArray(dst))
                throw new DiffError();
            this.diffObj(src, dst);
        }
        else if (src instanceof nodes_1.ValNode) {
            if (dst instanceof json_crdt_patch_1.nodes.val)
                dst = dst.value;
            this.diffVal(src, dst);
        }
        else if (src instanceof nodes_1.ArrNode) {
            if (dst instanceof json_crdt_patch_1.nodes.arr)
                dst = dst.arr;
            if (!Array.isArray(dst))
                throw new DiffError();
            this.diffArr(src, dst);
        }
        else if (src instanceof nodes_1.VecNode) {
            if (dst instanceof json_crdt_patch_1.nodes.vec)
                dst = dst.value;
            if (!Array.isArray(dst))
                throw new DiffError();
            this.diffVec(src, dst);
        }
        else if (src instanceof nodes_1.BinNode) {
            if (dst instanceof json_crdt_patch_1.nodes.bin)
                dst = dst.raw;
            if (!(dst instanceof Uint8Array))
                throw new DiffError();
            this.diffBin(src, dst);
        }
        else {
            throw new DiffError();
        }
    }
    diff(src, dst) {
        this.diffAny(src, dst);
        return this.builder.flush();
    }
    buildView(dst) {
        const builder = this.builder;
        if (dst instanceof json_crdt_patch_1.Timestamp)
            return builder.con(dst);
        if (dst instanceof json_crdt_patch_1.nodes.con)
            return builder.con(dst.raw);
        return builder.json(dst);
    }
    buildConView(dst) {
        const builder = this.builder;
        if (dst instanceof json_crdt_patch_1.Timestamp)
            return builder.con(dst);
        if (dst instanceof json_crdt_patch_1.nodes.con)
            return builder.con(dst.raw);
        return builder.constOrJson(dst);
    }
}
exports.JsonCrdtDiff = JsonCrdtDiff;
//# sourceMappingURL=JsonCrdtDiff.js.map