import { UUID } from '@bcase/core';
import { faLink, faPlus, faTrash } from '@fortawesome/free-solid-svg-icons';
import { library } from '@fortawesome/fontawesome-svg-core';
import { h } from "@stencil/core";
import { ModuleFlow } from '../../models/module';
import { FlowGraph } from '../../utils/flow-graph';
import { FlowTree } from '../../utils/flow-tree';
import { NativeEvent } from '../../utils/native-event';
library.add(faLink, faPlus, faTrash);
export class BceFlowGraph {
    constructor() {
        this.value = Object.assign({}, ModuleFlow.empty(), { start: { id: '<start>', version: 'v1' } });
        this.current = '';
        this.editable = false;
        this.createId = () => UUID.v4();
        this.edgeHover = -1;
        this.linking = null;
        this.handleMouseEnter = (event) => {
            const str = event.target.dataset.index;
            this.edgeHover = parseInt(str || '-1', 10);
        };
        this.handleMouseLeave = () => {
            this.edgeHover = -1;
        };
        this.handleUnlink = (event) => {
            const ref = this.findEdgeReference(event);
            if (ref)
                this.tree.unlink(ref.head, ref.tail);
        };
        this.handleInsert = (event) => {
            const ref = this.findEdgeReference(event);
            if (!ref)
                return;
            const { head, tail } = ref;
            const id = this.createId();
            const copy = [...this.value.blocks[head]];
            const index = copy.findIndex(n => n.id === tail);
            const next = copy.splice(index, 1, { id, version: 'v1' });
            const blocks = Object.assign({}, this.value.blocks, { [head]: copy, [id]: next });
            this.value = Object.assign({}, this.value, { blocks });
            this.el.dispatchEvent(new NativeEvent('input'));
        };
        this.handleNode = (event) => {
            const ref = this.findNodeReference(event);
            if (!ref)
                return;
            if (this.linking)
                this.handleLinkEnd(ref);
            else
                this.onNode.emit(ref);
        };
        this.handleAdd = (event) => {
            const ref = this.findNodeReference(event);
            if (ref)
                this.addBlock(ref);
        };
        this.handleDelete = (event) => {
            const ref = this.findNodeReference(event);
            if (ref)
                this.deleteBlock(ref);
        };
        this.handleLinkStart = (event) => {
            const ref = this.findNodeReference(event);
            if (ref)
                this.linking = Object.assign({}, ref);
        };
        this.handleLinkEnd = (ref) => {
            if (this.linking && this.tree.canLink(this.linking, ref)) {
                const { id } = this.linking;
                const link = [...(this.value.blocks[id] || []), Object.assign({}, ref)];
                const blocks = Object.assign({}, this.value.blocks, { [id]: link });
                this.value = Object.assign({}, this.value, { blocks });
                this.el.dispatchEvent(new NativeEvent('input'));
            }
            this.linking = null;
        };
    }
    componentWillLoad() {
        this.initialize();
    }
    async addBlock(target) {
        const id = this.createId();
        this.value = this.tree.add(target, id);
        this.el.dispatchEvent(new NativeEvent('input'));
    }
    async deleteBlock(target) {
        // Also emits a delete event to allow database cleaning
        this.onDelete.emit(target);
        this.value = this.tree.delete(target);
        this.el.dispatchEvent(new NativeEvent('input'));
    }
    async unlink(source, target) {
        this.value = this.tree.unlink(source, target);
        this.el.dispatchEvent(new NativeEvent('input'));
    }
    initialize() {
        this.tree = new FlowTree(this.value);
        this.graph = new FlowGraph(this.tree);
        if (!this.tree.flow.start.id) {
            const message = '[bce-flow-graph] value should contain a non-empty start id.';
            throw new Error(message);
        }
    }
    findEdgeReference(event) {
        const selector = '[data-edge-control]';
        const target = event.target && event.target;
        const node = target && target.closest(selector);
        if (!node)
            return undefined;
        const { head, tail } = node.dataset;
        return head && tail ? { head, tail } : undefined;
    }
    findNodeReference(event) {
        const target = event.target && event.target;
        const node = target && target.closest('[data-node]');
        if (!node)
            return undefined;
        const { id, version } = node.dataset;
        return id && version ? { id, version } : undefined;
    }
    renderEdgeControl(edge) {
        const control = !this.linking && !edge.start && !edge.end && this.editable;
        if (!control)
            return;
        const width = '60px';
        const height = '30px';
        const top = edge.center.y + 'px';
        const left = edge.center.x - 30 + 'px';
        const style = { width, height, top, left };
        return (h("div", { style: style, "data-edge-control": true, "data-hover": this.edgeHover === edge.index, "data-index": edge.index, "data-head": edge.head, "data-tail": edge.tail, onMouseEnter: this.handleMouseEnter, onMouseLeave: this.handleMouseLeave },
            h("bce-button", { type: "text", icon: "fas:trash", "icon-only": true, onClick: this.handleUnlink }),
            h("bce-button", { type: "text", icon: "fas:plus", "icon-only": true, onClick: this.handleInsert })));
    }
    renderEdge(edge) {
        const control = !this.linking && !edge.start && !edge.end && this.editable;
        return (h("g", { "data-edge": true, "data-hover": this.edgeHover === edge.index },
            h("polyline", { points: edge.points }),
            control && (h("polyline", { points: edge.points, "stroke-width": "20", "stroke-opacity": "0", "data-index": edge.index, onMouseEnter: this.handleMouseEnter, onMouseLeave: this.handleMouseLeave }))));
    }
    renderNodeControl(node) {
        const control = !this.linking && !node.start && !node.end && this.editable;
        if (!control)
            return null;
        return (h("div", { "data-node-control": true },
            node.id !== this.value.start.id && (h("bce-button", { icon: "fas:trash", type: "text", "icon-only": true, onClick: this.handleDelete })),
            h("bce-button", { icon: "fas:link", type: "text", "icon-only": true, onClick: this.handleLinkStart }),
            h("bce-button", { icon: "fas:plus", type: "text", "icon-only": true, onClick: this.handleAdd })));
    }
    renderNode(node) {
        const { existing, linking } = this;
        const width = node.width + 'px';
        const height = node.height + 'px';
        const top = node.y + 'px';
        const left = node.x + 'px';
        const style = { width, height, top, left };
        return (h("span", { "data-node": true, "data-id": node.id, "data-version": node.version, style: style },
            h("div", { "data-blueprint": existing && existing.indexOf(node.id) < 0, "data-linkable": linking && this.tree.canLink(linking, node), "data-linking": linking && node.id === linking.id, "data-current": this.current === node.id, "data-start": node.start, "data-end": node.end, onClick: this.handleNode }, node.label),
            this.renderNodeControl(node)));
    }
    render() {
        const { width, height } = this.graph.dimensions;
        const style = { width: width + 'px', height: height + 'px' };
        return (h("div", { "data-linking": !!this.linking, style: style },
            h("svg", { width: width, height: height }, this.graph.edges.map(edge => this.renderEdge(edge))),
            this.graph.nodes.map(node => this.renderNode(node)),
            this.graph.edges.map(edge => this.renderEdgeControl(edge))));
    }
    static get is() { return "bce-flow-graph"; }
    static get originalStyleUrls() { return {
        "$": ["bce-flow-graph.scss"]
    }; }
    static get styleUrls() { return {
        "$": ["bce-flow-graph.css"]
    }; }
    static get properties() { return {
        "value": {
            "type": "unknown",
            "mutable": false,
            "complexType": {
                "original": "ModuleFlow",
                "resolved": "ModuleFlow",
                "references": {
                    "ModuleFlow": {
                        "location": "import",
                        "path": "../../models/module"
                    }
                }
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": ""
            },
            "defaultValue": "{\r\n    ...ModuleFlow.empty(),\r\n    start: { id: '<start>', version: 'v1' }\r\n  }"
        },
        "current": {
            "type": "string",
            "mutable": false,
            "complexType": {
                "original": "string",
                "resolved": "string",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": ""
            },
            "attribute": "current",
            "reflect": true,
            "defaultValue": "''"
        },
        "existing": {
            "type": "unknown",
            "mutable": false,
            "complexType": {
                "original": "string[]",
                "resolved": "string[] | undefined",
                "references": {}
            },
            "required": false,
            "optional": true,
            "docs": {
                "tags": [],
                "text": ""
            }
        },
        "editable": {
            "type": "boolean",
            "mutable": false,
            "complexType": {
                "original": "boolean",
                "resolved": "boolean",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": ""
            },
            "attribute": "editable",
            "reflect": true,
            "defaultValue": "false"
        },
        "createId": {
            "type": "unknown",
            "mutable": false,
            "complexType": {
                "original": "() => string",
                "resolved": "() => string",
                "references": {}
            },
            "required": false,
            "optional": false,
            "docs": {
                "tags": [],
                "text": ""
            },
            "defaultValue": "() => UUID.v4()"
        }
    }; }
    static get states() { return {
        "edgeHover": {},
        "linking": {}
    }; }
    static get events() { return [{
            "method": "onNode",
            "name": "node",
            "bubbles": true,
            "cancelable": true,
            "composed": true,
            "docs": {
                "tags": [],
                "text": ""
            },
            "complexType": {
                "original": "Reference",
                "resolved": "{ id: string; version: string; }",
                "references": {
                    "Reference": {
                        "location": "import",
                        "path": "../../models/reference"
                    }
                }
            }
        }, {
            "method": "onDelete",
            "name": "delete",
            "bubbles": true,
            "cancelable": true,
            "composed": true,
            "docs": {
                "tags": [],
                "text": ""
            },
            "complexType": {
                "original": "Reference",
                "resolved": "{ id: string; version: string; }",
                "references": {
                    "Reference": {
                        "location": "import",
                        "path": "../../models/reference"
                    }
                }
            }
        }]; }
    static get methods() { return {
        "addBlock": {
            "complexType": {
                "signature": "(target: Reference) => Promise<void>",
                "parameters": [{
                        "tags": [],
                        "text": ""
                    }],
                "references": {
                    "Promise": {
                        "location": "global"
                    },
                    "Reference": {
                        "location": "import",
                        "path": "../../models/reference"
                    }
                },
                "return": "Promise<void>"
            },
            "docs": {
                "text": "",
                "tags": []
            }
        },
        "deleteBlock": {
            "complexType": {
                "signature": "(target: Reference) => Promise<void>",
                "parameters": [{
                        "tags": [],
                        "text": ""
                    }],
                "references": {
                    "Promise": {
                        "location": "global"
                    },
                    "Reference": {
                        "location": "import",
                        "path": "../../models/reference"
                    }
                },
                "return": "Promise<void>"
            },
            "docs": {
                "text": "",
                "tags": []
            }
        },
        "unlink": {
            "complexType": {
                "signature": "(source: string, target: string) => Promise<void>",
                "parameters": [{
                        "tags": [],
                        "text": ""
                    }, {
                        "tags": [],
                        "text": ""
                    }],
                "references": {
                    "Promise": {
                        "location": "global"
                    }
                },
                "return": "Promise<void>"
            },
            "docs": {
                "text": "",
                "tags": []
            }
        }
    }; }
    static get elementRef() { return "el"; }
    static get watchers() { return [{
            "propName": "value",
            "methodName": "initialize"
        }]; }
}
