const toStringNode = (self, tab = '') => {
    let children = '';
    const last = self.children.size - 1;
    let i = 0;
    for (const [_index, node] of self.children) {
        const isLast = i === last;
        children += `\n${tab}${isLast ? ' └─ ' : ' ├─ '}${node.toString(tab + (isLast ? '    ' : ' │  '))}`;
        i++;
    }
    const indexFormatted = typeof self.index === 'number' ? `[${self.index}]` : `"${self.index}"`;
    const registerFormatted = self.regId === undefined
        ? ''
        : self.regId === -1
            ? ''
            : ` [${self.regId}]`;
    return `${indexFormatted} ${self.constructor.name}${registerFormatted}${children}`;
};
export class PickNode {
    regId;
    index;
    path;
    pathLength;
    parent;
    children;
    constructor(index, path, pathLength) {
        this.regId = -1;
        this.index = index;
        this.path = path;
        this.pathLength = pathLength;
        this.parent = null;
        this.children = new Map();
    }
    toString(tab = '') {
        return toStringNode(this, tab);
    }
}
export class DropNode {
    regId;
    index;
    path;
    pathLength;
    parent;
    // edits: unknown[] = [];
    children;
    constructor(index, path, pathLength) {
        this.regId = -1;
        this.index = index;
        this.path = path;
        this.pathLength = pathLength;
        this.parent = null;
        this.children = new Map();
    }
    toString(tab = '') {
        return toStringNode(this, tab);
    }
    clone() {
        const clone = new DropNode(this.index, this.path, this.pathLength);
        clone.regId = this.regId;
        for (const [index, node] of this.children) {
            const child = node.clone();
            child.parent = clone;
            clone.children.set(index, child);
        }
        return clone;
    }
}
class PickRoot extends PickNode {
    constructor() {
        super(0, [], 0);
    }
}
class DropRoot extends DropNode {
    constructor() {
        super(0, [], 0);
    }
}
export class Register {
    id;
    pick = null;
    data = undefined;
    drops = [];
    constructor(id) {
        this.id = id;
    }
    addDrop(drop) {
        this.drops.push(drop);
    }
    removeDrop(drop) {
        const index = this.drops.indexOf(drop);
        if (index > -1)
            this.drops.splice(index, 1);
    }
    toString() {
        let drops = '';
        if (this.drops.length)
            drops = this.drops.map((drop) => '/' + drop.path.slice(0, drop.pathLength).join('/')).join(', ');
        const src = this.pick
            ? '/' + this.pick.path.slice(0, this.pick.pathLength).join('/')
            : `{ ${JSON.stringify(this.data)} }`;
        const dst = drops ? `{ ${drops} }` : '∅';
        return `${this.id}: Register ${src} ┈┈┈→ ${dst}`;
    }
}
export class OpTree {
    static from(op) {
        const [test, pick = [], data = [], drop = [], _edit = []] = op;
        const tree = new OpTree();
        if (test.length)
            tree.test.push(...test);
        for (let i = 0; i < pick.length; i++) {
            const [registerId, what] = pick[i];
            tree.addPickNode(registerId, what);
        }
        for (let i = 0; i < data.length; i++) {
            const [registerId, value] = data[i];
            tree.addData(registerId, value);
        }
        for (let i = 0; i < drop.length; i++) {
            const [registerId, where] = drop[i];
            tree.addDropNode(registerId, where);
        }
        return tree;
    }
    maxRegId = -1;
    test = [];
    pick = new PickRoot();
    drop = new DropRoot();
    register = new Map();
    findPick(path, pathLength) {
        let parent = this.pick;
        for (let i = 0; i < pathLength; i++) {
            const index = path[i];
            if (!parent.children.has(index))
                return undefined;
            parent = parent.children.get(index);
        }
        return parent;
    }
    findDrop(path, pathLength) {
        let parent = this.drop;
        for (let i = 0; i < pathLength; i++) {
            const index = path[i];
            if (!parent.children.has(index))
                return undefined;
            parent = parent.children.get(index);
        }
        return parent;
    }
    setRegister(register) {
        const regId = register.id;
        if (regId > this.maxRegId)
            this.maxRegId = regId;
        this.register.set(regId, register);
    }
    addPickNode(registerId, what, length = what.length) {
        let parent = this.pick;
        if (!length) {
            parent.regId = registerId;
            const register = new Register(registerId);
            register.pick = parent;
            this.setRegister(register);
        }
        else {
            for (let i = 0; i < length; i++) {
                const index = what[i];
                const childExists = parent.children.has(index);
                if (!childExists) {
                    const child = new PickNode(index, what, i + 1);
                    parent.children.set(index, child);
                    child.parent = parent;
                }
                const child = parent.children.get(index);
                const isLast = i === length - 1;
                if (isLast) {
                    if (child.regId < 0) {
                        child.regId = registerId;
                        const register = new Register(registerId);
                        register.pick = child;
                        this.setRegister(register);
                    }
                }
                parent = child;
            }
        }
        return this.register.get(parent.regId);
    }
    addData(registerId, data) {
        const register = new Register(registerId);
        register.data = data;
        this.setRegister(register);
    }
    addDropNode(registerId, where) {
        let parent = this.drop;
        const length = where.length;
        if (!length) {
            const register = this.register.get(registerId);
            if (register instanceof Register) {
                parent.regId = register.id;
                register.addDrop(parent);
            }
        }
        else {
            for (let i = 0; i < length; i++) {
                const index = where[i];
                const childExists = parent.children.has(index);
                if (!childExists) {
                    const child = new DropNode(index, where, i + 1);
                    parent.children.set(index, child);
                    child.parent = parent;
                }
                const child = parent.children.get(index);
                const isLast = i === length - 1;
                if (isLast) {
                    child.regId = registerId;
                    const register = this.register.get(registerId);
                    register.addDrop(child);
                }
                parent = child;
            }
        }
    }
    /**
     * Composes two operations into one combined operation. This object contains
     * the result of the composition. During the composition, both operations
     * are mutated in place, hence the `other` becomes unusable after the call.
     *
     * @param other another OpTree
     */
    compose(other) {
        this.test.push(...other.test);
        // Compose deletes.
        const d1 = this.drop;
        const d2 = other.drop;
        other.register.forEach((register2) => {
            // Update pick path.
            if (register2.pick) {
                let path = register2.pick.path;
                let pathLength = register2.pick.pathLength;
                const deepestDropNodeInPath = this.findDeepestDropInPath(register2.pick.path, register2.pick.pathLength);
                if (deepestDropNodeInPath) {
                    if (deepestDropNodeInPath) {
                        const dropRegister = this.register.get(deepestDropNodeInPath.regId);
                        if (dropRegister.pick) {
                            path = [
                                ...dropRegister.pick.path.slice(0, dropRegister.pick.pathLength),
                                ...register2.pick.path.slice(0, register2.pick.pathLength).slice(deepestDropNodeInPath.pathLength),
                            ];
                            pathLength = path.length;
                            register2.pick.path = path;
                            register2.pick.pathLength = pathLength;
                        }
                    }
                }
                for (let i = 0; i < pathLength; i++) {
                    const comp = path[i];
                    if (typeof comp === 'number') {
                        const pick = this.findPick(path, i);
                        if (pick) {
                            let numberOfPickWithLowerIndex = 0;
                            pick.children.forEach((child, index) => {
                                if (+index <= comp)
                                    numberOfPickWithLowerIndex++;
                            });
                            path[i] += numberOfPickWithLowerIndex;
                        }
                    }
                }
                const isDelete = !register2.drops.length;
                if (isDelete) {
                    const op1Pick = this.findPick(register2.pick.path, register2.pick.pathLength);
                    if (op1Pick && op1Pick.regId) {
                        const register = this.register.get(op1Pick.regId);
                        const alreadyDeletedInOp1 = register && !register.drops.length;
                        if (alreadyDeletedInOp1)
                            return;
                    }
                    this.addPickNode(this.maxRegId + 1, path, pathLength);
                    const drop = this.findDrop(register2.pick.path, register2.pick.pathLength);
                    if (drop) {
                        if (drop.parent) {
                            drop.parent.children.delete(drop.index);
                            drop.parent = null;
                            const register1 = this.register.get(drop.regId);
                            if (register1 instanceof Register) {
                                register1.removeDrop(drop);
                                if (!register1.drops.length && !register1.pick)
                                    this.register.delete(drop.regId);
                            }
                        }
                        else {
                            this.drop.regId = -1;
                        }
                    }
                }
            }
        });
        this.composeDrops(d1, d2, other);
    }
    findDeepestDropInPath(path, pathLength = path.length) {
        let longest = null;
        let curr = this.drop;
        for (let i = 0; i < pathLength; i++) {
            const comp = path[i];
            const child = curr.children.get(comp);
            if (!child)
                break;
            curr = child;
            if (curr.regId >= 0)
                longest = curr;
        }
        return longest;
    }
    removeDrop(drop) {
        if (drop.regId >= 0) {
            const register = this.register.get(drop.regId);
            register.removeDrop(drop);
            if (!register.drops.length && !register.pick)
                this.register.delete(drop.regId);
        }
    }
    composeDrops(d1, d2, tree2) {
        const isDrop = d2.regId >= 0;
        if (isDrop) {
            const isRoot = !d2.parent;
            const clone = !isRoot ? d2.clone() : this.drop;
            const register2 = tree2.register.get(d2.regId);
            const isDataDrop = register2.data !== undefined;
            if (isDataDrop) {
                const newRegister = new Register(this.maxRegId + 1);
                newRegister.data = register2.data;
                newRegister.addDrop(clone);
                this.setRegister(newRegister);
                clone.regId = newRegister.id;
            }
            else {
                const samePickInOp1Exists = this.findPick(register2.pick.path, register2.pick.pathLength);
                if (samePickInOp1Exists) {
                    clone.regId = samePickInOp1Exists.regId;
                }
                else {
                    const reg = this.addPickNode(this.maxRegId + 1, register2.pick.path, register2.pick.pathLength);
                    reg.addDrop(clone);
                    clone.regId = reg.id;
                }
            }
            if (!!d1.parent && !!d2.parent) {
                const child = d1.parent.children.get(d1.index);
                if (child)
                    this.removeDrop(child);
                d1.parent.children.set(d2.index, clone);
            }
        }
        for (const [index, child2] of d2.children) {
            if (!d1.children.has(index)) {
                const child1 = new DropNode(child2.index, child2.path, child2.pathLength);
                child1.parent = d1;
                d1.children.set(child1.index, child1);
            }
            const child1 = d1.children.get(index);
            this.composeDrops(child1, child2, tree2);
        }
    }
    toJson() {
        const pick = [];
        const data = [];
        const drop = [];
        for (const [index, register] of this.register) {
            if (register.data !== undefined) {
                data.push([index, register.data]);
            }
            else {
                const pickPath = register.pick.path;
                const pickPathLength = register.pick.pathLength;
                pick.push([index, pickPath.slice(0, pickPathLength)]);
            }
        }
        this.pushDropNode(drop, this.drop);
        return [this.test, pick, data, drop, []];
    }
    pushDropNode(drop, node) {
        if (node.regId >= 0)
            drop.push([node.regId, node.path.slice(0, node.pathLength)]);
        node.children.forEach((child) => {
            this.pushDropNode(drop, child);
        });
    }
    toString(tab = '') {
        const picks = this.pick ? this.pick.toString(tab + '│  ') : ' ∅';
        let registers = 'Registers';
        const lastRegister = this.register.size - 1;
        let i = 0;
        for (const [_id, register] of this.register) {
            const isLast = i === lastRegister;
            registers += `\n${tab}${isLast ? '│  └─' : '│  ├─'} ${register}`;
            i++;
        }
        const drops = this.drop ? this.drop.toString(tab + '   ') : ' ∅';
        return `OpTree\n${tab}├─ ${picks}\n${tab}│\n${tab}├─ ${registers}\n${tab}│\n${tab}└─ ${drops}`;
    }
}
//# sourceMappingURL=tree.js.map