export class FlowTree {
    constructor(flow) {
        this.orphans = [];
        this._nodes = new Map();
        this._indexes = new Map();
        this.flow = flow;
        this.root = this.parse(flow.start, flow);
        for (const orphan of flow.orphans || [])
            this.orphans.push(this.parse(orphan, flow));
        // Add indexes
        let i = 1;
        for (const node of this.nodes) {
            if (!node.id)
                continue;
            const id = FlowTree.encode(node.id, node.version);
            if (!this._indexes.has(id))
                this._indexes.set(id, i++);
        }
    }
    get nodes() {
        const nodes = [];
        const iterated = new Set();
        for (const node of this.root.nodes) {
            if (iterated.has(node))
                continue;
            iterated.add(node);
            nodes.push(node);
        }
        for (const orphan of this.orphans) {
            for (const node of orphan.nodes) {
                if (iterated.has(node))
                    continue;
                iterated.add(node);
                nodes.push(node);
            }
        }
        return nodes;
    }
    static encode(arg1, arg2) {
        const { id, version } = typeof arg1 === 'string' ? { id: arg1, version: arg2 } : arg1;
        return `${id}@${version}`;
    }
    static decode(encoded) {
        const [id, version] = encoded.split('@');
        return { id, version };
    }
    canLink(source, target, once = false) {
        const encoded = FlowTree.encode(source);
        const src = this._nodes.get(encoded);
        if (!src)
            return false;
        // Can not link nodes if the target node is already a child of the source
        // node or the other way around
        for (const node of src.nodes)
            if (node.id === target.id)
                return false;
        return once ? true : this.canLink(target, source, true);
    }
    getIndex(arg1, arg2) {
        const encoded = arg2 != undefined ? FlowTree.encode(arg1, arg2) : arg1;
        const index = this._indexes.get(encoded);
        return index == undefined ? -1 : index;
    }
    getLabel(arg1, arg2) {
        const index = this.getIndex(arg1, arg2);
        return index >= 0 ? ('000' + index).slice(-3) : '';
    }
    add(target, id) {
        const prev = this.flow.blocks[target.id] || [];
        const blocks = Object.assign({}, this.flow.blocks, { [target.id]: [...prev, { id, version: 'v1' }] });
        return Object.assign({}, this.flow, { blocks });
    }
    delete(target) {
        if (this.flow.start.id === target.id)
            return this.flow;
        // Remove every reference of the node from the blocks chain
        const blocks = Object.keys(this.flow.blocks).reduce((blocks, id) => {
            if (id === target.id)
                return blocks;
            const next = this.flow.blocks[id];
            blocks[id] =
                next && next.findIndex(n => n.id === target.id) >= 0
                    ? [
                        ...next.filter(n => n.id !== target.id),
                        ...(this.flow.blocks[target.id] || [])
                    ]
                    : next;
            return blocks;
        }, {});
        // Filter block from orphans
        const filtered = this.flow.orphans.filter(o => o.id !== target.id);
        if (this.flow.orphans.length === filtered.length)
            return Object.assign({}, this.flow, { blocks });
        // An orphaned block was deleted, gather the new orphans
        const newOrphans = (this.flow.blocks[target.id] || []).filter(n => {
            return Object.keys(blocks).every(id => blocks[id].every(c => c.id !== n.id));
        });
        const orphans = [...filtered, ...newOrphans];
        return Object.assign({}, this.flow, { blocks, orphans });
    }
    unlink(source, target) {
        // Unlink blocks
        const next = this.flow.blocks[source];
        const detached = next.find(n => n.id === target);
        const blocks = Object.assign({}, this.flow.blocks, { [source]: next.filter(n => n.id !== target) });
        // Add target to orphaned blocks in case there is no way to reach the block
        const orphaned = Object.keys(blocks).every(id => blocks[id].every(n => n.id !== target));
        const orphans = orphaned
            ? [...this.flow.orphans, detached]
            : this.flow.orphans;
        return Object.assign({}, this.flow, { blocks, orphans });
    }
    parse(current, flow) {
        const encoded = FlowTree.encode(current);
        const existing = this._nodes.get(encoded);
        if (existing)
            return existing;
        const { id, version } = current;
        const children = this.parseChildren(current, flow);
        const node = new FlowTreeNode(id, version, children);
        this._nodes.set(encoded, node);
        return node;
    }
    parseChildren(current, flow) {
        const next = (flow.blocks || {})[current.id];
        return next ? next.map(n => this.parse(n, flow)).filter(n => !!n) : [];
    }
}
export class FlowTreeNode {
    get nodes() {
        const nestedNodes = this.children.map(n => n.nodes);
        const nodes = [].concat(...nestedNodes);
        return [this, ...nodes];
    }
    constructor(id, version, children) {
        this.id = id;
        this.version = version;
        this.children = children;
    }
}
