"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Frag = exports.FragPointerEmbedding = void 0;
exports.Write = Write;
exports.WriteOpt = WriteOpt;
const tslib_1 = require("tslib");
const Crypto = tslib_1.__importStar(require("node:crypto"));
const ImpLib = tslib_1.__importStar(require("@ot-builder/common-impl"));
const errors_1 = require("@ot-builder/errors");
const buffer_writer_1 = require("./buffer-writer");
const primitive_types_1 = require("./primitive-types");
function Write(proc) {
    return { write: proc };
}
function WriteOpt(proc) {
    return { writeOpt: proc };
}
var FragPointerEmbedding;
(function (FragPointerEmbedding) {
    FragPointerEmbedding.Absolute = {
        id: "Absolute",
        getOffset: (enc, emb, to) => to
    };
    FragPointerEmbedding.Relative = {
        id: "Relative",
        getOffset: (enc, emb, to) => to - enc
    };
    FragPointerEmbedding.EmbedRelative = {
        id: "EmbedRelative",
        getOffset: (enc, emb, to) => to - enc - emb
    };
})(FragPointerEmbedding || (exports.FragPointerEmbedding = FragPointerEmbedding = {}));
class Frag {
    constructor() {
        this.bw = new buffer_writer_1.BufferWriter();
        this.pointers = [];
    }
    get size() {
        return this.bw.length;
    }
    push(builder, obj, ...args) {
        builder.write(this, obj, ...args);
        return this;
    }
    array(builder, objects, ...args) {
        for (const obj of objects) {
            builder.write(this, obj, ...args);
        }
        return this;
    }
    arrayN(builder, count, objects, ...args) {
        for (let index = 0; index < count; index++) {
            const obj = objects[index];
            builder.write(this, obj, ...args);
        }
        return this;
    }
    arrayNF(builder, count, objects, fallback, ...args) {
        for (let index = 0; index < count; index++) {
            let obj = objects[index];
            if (obj === undefined)
                obj = fallback;
            builder.write(this, obj, ...args);
        }
        return this;
    }
    reserve(builder) {
        const offset = this.bw.currentOffset;
        for (let mu = 0; mu < builder.size; mu++)
            this.uint8(0); // Fill 0
        return {
            fill: (obj, ...args) => {
                const curOffset = this.bw.seek(offset);
                this.push(builder, obj, ...args);
                this.bw.seek(curOffset);
            }
        };
    }
    bytes(buf) {
        this.bw.bytes(buf);
        return this;
    }
    uint8(x) {
        this.bw.uint8((x << 24) >>> 24);
        return this;
    }
    uint16(x) {
        this.bw.uint16((x << 16) >>> 16);
        return this;
    }
    uint32(x) {
        this.bw.uint32(x >>> 0);
        return this;
    }
    int8(x) {
        this.bw.int8((x << 24) >> 24);
        return this;
    }
    int16(x) {
        this.bw.int16((x << 16) >> 16);
        return this;
    }
    int32(x) {
        this.bw.int32(x >> 0);
        return this;
    }
    ptr16(to, embedding = FragPointerEmbedding.Relative, targetOffset = 0) {
        const offset = this.bw.currentOffset;
        this.bw.uint16(0);
        this.pointers.push({
            to,
            offset,
            createdOffset: offset,
            size: primitive_types_1.SizeofUInt16,
            embedding,
            targetOffset
        });
        return this;
    }
    ptr32(to, embedding = FragPointerEmbedding.Relative, targetOffset = 0) {
        const offset = this.bw.currentOffset;
        this.bw.uint32(0);
        this.pointers.push({
            to,
            offset,
            createdOffset: offset,
            size: primitive_types_1.SizeofUInt32,
            embedding,
            targetOffset
        });
        return this;
    }
    ptr16New(embedding = FragPointerEmbedding.Relative) {
        const to = new Frag();
        this.ptr16(to, embedding);
        return to;
    }
    ptr32New(embedding = FragPointerEmbedding.Relative) {
        const to = new Frag();
        this.ptr32(to, embedding);
        return to;
    }
    embed(frag) {
        const start = this.bw.currentOffset;
        this.bw.bytes(frag.bw.toBuffer());
        for (const ptr of frag.pointers) {
            this.pointers.push({ ...ptr, offset: ptr.offset + start });
        }
        return this;
    }
    clone() {
        const frag = new Frag();
        frag.embed(this);
        return frag;
    }
    deepClone() {
        const c = this.clone();
        for (const ptr of this.pointers) {
            if (ptr.to)
                ptr.to = ptr.to.deepClone();
        }
        return c;
    }
    getDataBuffer() {
        return this.bw.toBuffer();
    }
    // Static methods
    static pack(root) {
        return new Packing().pack(root);
    }
    static packMany(roots) {
        return new Packing().packMany(roots);
    }
    /** In-place consolidation */
    static consolidate(root) {
        const buf = new Packing().pack(root);
        root.pointers = [];
        root.bw = new buffer_writer_1.BufferWriter();
        root.bw.bytes(buf);
        return root;
    }
    // Generic initializer
    static from(builder, t, ...a) {
        return new Frag().push(builder, t, ...a);
    }
    static packFrom(builder, t, ...a) {
        return Frag.pack(new Frag().push(builder, t, ...a));
    }
    static solidFrom(builder, t, ...a) {
        const buf = Frag.pack(new Frag().push(builder, t, ...a));
        return new Frag().bytes(buf);
    }
    // Quick initializer
    static uint8(x) {
        return new Frag().uint8(x);
    }
    static uint16(x) {
        return new Frag().uint16(x);
    }
    static uint32(x) {
        return new Frag().uint32(x);
    }
    static int8(x) {
        return new Frag().int8(x);
    }
    static int16(x) {
        return new Frag().int16(x);
    }
    static int32(x) {
        return new Frag().int32(x);
    }
    // pointers
    static ptr16(x, embedding = FragPointerEmbedding.Relative) {
        return new Frag().ptr16(x, embedding);
    }
    static ptr32(x, embedding = FragPointerEmbedding.Relative) {
        return new Frag().ptr32(x, embedding);
    }
}
exports.Frag = Frag;
class Sorter {
    constructor() {
        this.marked = new Set();
        this.sorted = [];
    }
    visitUnmarked(f) {
        for (let size = 4; size > 0; size--) {
            for (let pid = f.pointers.length; pid-- > 0;) {
                const ptr = f.pointers[pid];
                if (!ptr.to || ptr.size !== size)
                    continue;
                this.dfsVisit(ptr.to);
            }
        }
        this.marked.add(f);
        this.sorted.unshift(f);
    }
    dfsVisit(f) {
        if (!this.marked.has(f)) {
            this.visitUnmarked(f);
        }
    }
    sort(root) {
        this.dfsVisit(root);
        return this.sorted;
    }
}
class Unifier {
    constructor() {
        this.hashCache = new WeakMap();
        this.hashToFragMap = new Map();
        this.unifyCache = new WeakMap();
    }
    hash(frag) {
        const cached = this.hashCache.get(frag);
        if (cached)
            return cached;
        const hr = new ImpLib.Hasher();
        hr.buffer(frag.getDataBuffer());
        for (const ptr of frag.pointers) {
            const targetHash = ptr.to ? this.hash(ptr.to) : "NULL";
            hr.string(targetHash);
            hr.integer(ptr.size, ptr.offset, ptr.createdOffset);
            hr.string(ptr.embedding.id);
        }
        const h = Crypto.createHash("sha256");
        hr.transfer(h);
        const result = h.digest("hex");
        this.hashCache.set(frag, result);
        return result;
    }
    compare(a, b) {
        if (!a) {
            return !b;
        }
        else if (!b) {
            return false;
        }
        else {
            if (a === b)
                return true;
            if (this.hash(a) !== this.hash(b))
                return false;
            return this.compareByContents(a, b);
        }
    }
    compareByContents(a, b) {
        if (Buffer.compare(a.getDataBuffer(), b.getDataBuffer()) !== 0)
            return false;
        if (a.pointers.length !== b.pointers.length)
            return false;
        for (let index = 0; index < a.pointers.length; index++) {
            const p1 = a.pointers[index], p2 = b.pointers[index];
            if (p1.size !== p2.size)
                return false;
            if (p1.offset !== p2.offset)
                return false;
            if (p1.embedding.id !== p2.embedding.id)
                return false;
            if (p1.createdOffset !== p2.createdOffset)
                return false;
            if (!this.compare(p1.to, p2.to))
                return false;
        }
        return true;
    }
    unify(frag) {
        const cached = this.unifyCache.get(frag);
        if (cached)
            return cached;
        // Unify sub-pointers
        for (const ptr of frag.pointers) {
            if (ptr.to)
                ptr.to = this.unify(ptr.to);
        }
        const h = this.hash(frag);
        let matches = this.hashToFragMap.get(h);
        if (!matches) {
            matches = [];
            this.hashToFragMap.set(h, matches);
        }
        for (const candidate of matches) {
            if (this.compare(frag, candidate)) {
                return this.cacheUnification(frag, candidate, h, matches);
            }
        }
        return this.cacheUnification(frag, frag, h, matches);
    }
    cacheUnification(frag, unifiedTo, hash, hashSink) {
        this.unifyCache.set(frag, unifiedTo);
        hashSink.push(unifiedTo);
        return unifiedTo;
    }
}
class Packing {
    shareBlocks(root) {
        const sink = new Unifier();
        sink.unify(root);
    }
    allocateOffsets(root) {
        const sorted = new Sorter().sort(root);
        const offsets = new Map();
        let currentOffset = 0;
        for (const b of sorted) {
            offsets.set(b, currentOffset);
            currentOffset += b.size;
        }
        return offsets;
    }
    getPointerOffset(offsets, frag, ptr) {
        if (!ptr.to)
            return 0;
        return ptr.embedding.getOffset(offsets.get(frag) || 0, ptr.offset - ptr.createdOffset, (offsets.get(ptr.to) || 0) + ptr.targetOffset);
    }
    deOverflow(root) {
        let rounds = 0;
        let overflows = 0;
        let offsets;
        do {
            overflows = 0;
            offsets = this.allocateOffsets(root);
            for (const frag of offsets.keys()) {
                for (const ptr of frag.pointers) {
                    const targetValue = this.getPointerOffset(offsets, frag, ptr);
                    const maxPtrValue = ptr.size === 4 ? 0x100000000 : 0x10000;
                    if (targetValue < 0 || (ptr.to && targetValue === 0)) {
                        throw errors_1.Errors.Binary.PointerUnderflow();
                    }
                    if (targetValue >= maxPtrValue) {
                        if (ptr.to)
                            ptr.to = ptr.to.clone();
                        overflows += 1;
                    }
                }
            }
            rounds += 1;
        } while (overflows && rounds < 0x100);
        if (overflows)
            throw errors_1.Errors.Binary.UnresolvableFragOverflow();
        return offsets;
    }
    writePointer(b, enclosure, ptr, value) {
        switch (ptr.size) {
            case 2:
                b.seek(enclosure + ptr.offset);
                b.uint16(value);
                break;
            case 4:
                b.seek(enclosure + ptr.offset);
                b.uint32(value);
                break;
            default:
                throw errors_1.Errors.Binary.UnknownPointerType();
        }
    }
    serialize(offsets) {
        const b = new buffer_writer_1.BufferWriter();
        for (const [block, offset] of offsets) {
            b.seek(offset);
            b.bytes(block.getDataBuffer());
            for (const ptr of block.pointers) {
                this.writePointer(b, offset, ptr, this.getPointerOffset(offsets, block, ptr));
            }
        }
        return b.toBuffer();
    }
    pack(root) {
        if (!root.pointers.length)
            return root.getDataBuffer();
        this.shareBlocks(root);
        const offsets = this.deOverflow(root);
        return this.serialize(offsets);
    }
    packMany(roots) {
        const virtualRoot = new Frag();
        for (const root of roots)
            virtualRoot.ptr32(root, FragPointerEmbedding.Absolute);
        this.shareBlocks(virtualRoot);
        const offsets = this.deOverflow(virtualRoot);
        const buffer = this.serialize(offsets);
        const rootOffsets = virtualRoot.pointers.map(ptr => offsets.get(ptr.to) - virtualRoot.size);
        return {
            buffer: buffer.slice(virtualRoot.size),
            rootOffsets
        };
    }
}
//# sourceMappingURL=fragment.js.map