import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
 * Copyright (c) 2020-present, Goldman Sachs
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { Background, BackgroundVariant, Controls, getNodesBounds, getViewportForBounds, MiniMap, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { observer } from 'mobx-react-lite';
import { useCallback, useEffect, useMemo } from 'react';
import { noop } from '@finos/legend-shared';
import { toPng, toSvg } from 'html-to-image';
import { DownloadIcon, ExpandIcon, RefreshIcon, ResizeIcon, } from '@finos/legend-art';
import { DatabaseTableNode, DatabaseForeignRelationStubNode, } from './DatabaseTableNode.js';
import { buildJoinEdges, collectForeignKeyColumns, estimateNodeHeight, getRelationId, isView, layoutDatabaseDiagram, getOrderedRelations, resolveJoinFormula, } from './DatabaseDiagramHelper.js';
const NODE_TYPES = {
    table: DatabaseTableNode,
    foreignStub: DatabaseForeignRelationStubNode,
};
/**
 * Resolve a Table or View by its on-canvas node id (`<schema>.<name>`).
 * Used by the click handler to translate React Flow's string id back into a
 * metamodel reference for selection.
 */
const findRelationById = (editorState, id) => {
    for (const schema of editorState.database.schemas) {
        for (const table of schema.tables) {
            if (getRelationId(table) === id) {
                return table;
            }
        }
        for (const view of schema.views) {
            if (getRelationId(view) === id) {
                return view;
            }
        }
    }
    return undefined;
};
/**
 * Inner canvas — must be wrapped in `<ReactFlowProvider>` so that the
 * `useReactFlow()` hook can fit-view after layout.
 */
const DatabaseDiagramCanvasInner = observer((props) => {
    const { editorState } = props;
    const { database, selectedRelation, selectedColumn, selectedJoin, selectedFilter, filterFormulas, joinFormulas, viewColumnFormulas, viewGroupByFormulas, selectedViewColumnName, panToSelectedRequestCounter, fitAllRequestCounter, resetLayoutRequestCounter, } = editorState;
    // Compute nodes + edges from the metamodel. We rebuild on metamodel changes
    // (driven by `database` identity), but the layout itself (positions) is
    // memoized so dagre runs only once per metamodel snapshot.
    const { laidOutNodes, laidOutEdges } = useMemo(() => {
        const fkColumns = collectForeignKeyColumns(database);
        // Build canvas nodes for both tables AND views. The kind tag drives the
        // table-node component's icon choice and column-row content.
        const relationNodes = getOrderedRelations(database).map(({ schema, relation }) => ({
            id: getRelationId(relation),
            relation,
            schemaName: schema.name,
            estimatedHeight: estimateNodeHeight(relation),
        }));
        const { edges: joinEdges, foreignStubs } = buildJoinEdges(database);
        // Foreign stubs participate in dagre layout the same way real nodes do,
        // so cross-database join edges get a sensible position. They render as
        // a smaller placeholder node (see `DatabaseForeignRelationStubNode`).
        const FOREIGN_STUB_HEIGHT = 60;
        const positions = layoutDatabaseDiagram([
            ...relationNodes.map((n) => ({
                id: n.id,
                relation: n.relation,
                estimatedHeight: n.estimatedHeight,
            })),
            ...foreignStubs.map((s) => ({
                id: s.id,
                // The layout helper only reads `id` and `estimatedHeight`; the
                // `relation` field is unused for stubs but required by the
                // shared type. Coerce safely — the helper never dereferences
                // it.
                relation: undefined,
                estimatedHeight: FOREIGN_STUB_HEIGHT,
            })),
        ], joinEdges);
        const reactFlowNodes = relationNodes.map((node) => {
            const pos = positions.get(node.id) ?? { x: 0, y: 0 };
            return {
                id: node.id,
                type: 'table',
                position: { x: pos.x, y: pos.y },
                data: {
                    relation: node.relation,
                    kind: isView(node.relation) ? 'view' : 'table',
                    schemaName: node.schemaName,
                    isSelected: false,
                    isJoinEndpoint: false,
                    fkColumns,
                    selectedColumn: undefined,
                    // Filled in by `selectionAwareNodes` from observable state.
                    viewColumnFormulas: new Map(),
                    viewGroupByFormulas: new Map(),
                    selectedViewColumnName: undefined,
                },
            };
        });
        // Stub nodes for foreign endpoints of cross-database joins. Rendered
        // smaller and visually distinct (dashed border) so users can tell at a
        // glance that the actual relation lives in another store.
        foreignStubs.forEach((stub) => {
            const pos = positions.get(stub.id) ?? { x: 0, y: 0 };
            reactFlowNodes.push({
                id: stub.id,
                type: 'foreignStub',
                position: { x: pos.x, y: pos.y },
                data: {
                    schemaName: stub.schemaName,
                    relationName: stub.relationName,
                    ownerPath: stub.ownerPath,
                    isJoinEndpoint: false,
                },
            });
        });
        const reactFlowEdges = joinEdges.map((edge) => ({
            id: edge.id,
            source: edge.source,
            target: edge.target,
            label: edge.name,
            // Loop self-joins use the React Flow `step` edge type so the path
            // bows out cleanly into a loop instead of overlapping the node.
            // Cross-database edges keep the standard smoothstep — same shape
            // but the dashed style below conveys the foreign endpoint.
            type: edge.isSelfJoin ? 'smoothstep' : 'smoothstep',
            animated: false,
            labelBgPadding: [4, 2],
            labelBgBorderRadius: 2,
            data: {
                join: edge.join,
                endpoints: { sourceId: edge.source, targetId: edge.target },
                isSelfJoin: edge.isSelfJoin,
                isCrossDatabase: edge.isCrossDatabase,
            },
            // Cross-database edges use a dashed stroke; self-joins keep the
            // solid line but the loop shape itself signals self-join. Both
            // get overridden again by the selection-aware pass below when the
            // user picks one.
            ...(edge.isCrossDatabase
                ? { style: { strokeDasharray: '4 3' } }
                : {}),
        }));
        return { laidOutNodes: reactFlowNodes, laidOutEdges: reactFlowEdges };
    }, [database]);
    const [nodes, setNodes, onNodesChange] = useNodesState(laidOutNodes);
    const [edges, , onEdgesChange] = useEdgesState(laidOutEdges);
    const { fitView } = useReactFlow();
    // Reset whenever the database identity changes (e.g. after graph rebuild).
    useEffect(() => {
        setNodes(laidOutNodes);
        const timer = window.setTimeout(() => {
            fitView({ padding: 0.15, duration: 200 }).catch(noop());
        }, 0);
        return () => window.clearTimeout(timer);
    }, [laidOutNodes, setNodes, fitView]);
    // Resolve the two endpoint table ids of the selected join (if any). Used
    // both for the `--join-endpoint` highlight on the two nodes and for the
    // pan-to-fit-both effect below. We compute it here once per selection
    // change rather than walking edges in multiple places.
    const selectedJoinEndpointIds = useMemo(() => {
        if (!selectedJoin) {
            return null;
        }
        const match = laidOutEdges.find((e) => e.data?.join === selectedJoin);
        return match?.data?.endpoints ?? null;
    }, [selectedJoin, laidOutEdges]);
    // Mirror selection from MobX into node data so each table-node renders
    // its current visual state. Three independent flags:
    //   - isSelected: blue ring (single-table focus)
    //   - isJoinEndpoint: yellow ring (one of the two endpoints of the
    //     selected join — distinct color so the user can tell the two modes
    //     apart)
    //   - selectedColumn: forwarded only to the matching table
    const selectionAwareNodes = useMemo(() => nodes.map((n) => {
        const isSelected = n.type === 'table' && selectedRelation
            ? n.id === getRelationId(selectedRelation)
            : false;
        const isJoinEndpoint = selectedJoinEndpointIds !== null &&
            (n.id === selectedJoinEndpointIds.sourceId ||
                n.id === selectedJoinEndpointIds.targetId);
        if (n.type === 'foreignStub') {
            return {
                ...n,
                data: {
                    ...n.data,
                    isJoinEndpoint,
                },
            };
        }
        return {
            ...n,
            data: {
                ...n.data,
                isSelected,
                isJoinEndpoint,
                selectedColumn: isSelected ? selectedColumn : undefined,
                // Forward the live formula maps. Only view-kind nodes use
                // them, but it's cheap to pass to all and keeps the data
                // shape uniform.
                viewColumnFormulas,
                viewGroupByFormulas,
                // Same story for the view column-mapping selection — only
                // the focused view's node ends up with a non-undefined
                // value, but every node receives the prop for shape stability.
                selectedViewColumnName: isSelected
                    ? selectedViewColumnName
                    : undefined,
            },
        };
    }), [
        nodes,
        selectedRelation,
        selectedColumn,
        selectedJoinEndpointIds,
        viewColumnFormulas,
        viewGroupByFormulas,
        selectedViewColumnName,
    ]);
    // Highlight the selected edge with a yellow stroke that matches the
    // endpoint-table ring color, and lift it visually. Other edges keep their
    // default style (driven by SCSS in `_database-editor.scss`).
    const selectionAwareEdges = useMemo(() => edges.map((e) => {
        const isSelected = Boolean(selectedJoin && e.data?.join === selectedJoin);
        // Build with `exactOptionalPropertyTypes` in mind — only attach
        // `style` when actually overriding it, so TS doesn't see `undefined`
        // assigned to an optional-but-not-undefined-allowed prop.
        const styled = {
            ...e,
            labelStyle: {
                fontSize: 10,
                fill: isSelected
                    ? 'var(--color-yellow-200)'
                    : 'var(--color-light-grey-200)',
            },
            labelBgStyle: {
                fill: 'var(--color-dark-grey-100)',
            },
            zIndex: isSelected ? 10 : 0,
        };
        if (isSelected) {
            styled.style = {
                stroke: 'var(--color-yellow-200)',
                strokeWidth: 2,
            };
        }
        return styled;
    }), [edges, selectedJoin]);
    // Pan to whatever's selected when the panel asks us to. Two modes:
    //   - Table selected → fit on that one node.
    //   - Join selected → fit to encompass both endpoint nodes.
    // The counter (rather than the selection itself) drives this so canvas
    // clicks don't pan — the user is already looking at what they clicked.
    useEffect(() => {
        if (panToSelectedRequestCounter === 0) {
            return;
        }
        if (selectedJoinEndpointIds) {
            fitView({
                nodes: [
                    { id: selectedJoinEndpointIds.sourceId },
                    { id: selectedJoinEndpointIds.targetId },
                ],
                duration: 400,
                padding: 0.3,
                maxZoom: 1.2,
            }).catch(noop());
            return;
        }
        if (selectedRelation) {
            fitView({
                nodes: [{ id: getRelationId(selectedRelation) }],
                duration: 400,
                padding: 0.4,
                maxZoom: 1.2,
            }).catch(noop());
        }
        // Intentional: only the counter triggers re-pan. Selection identities
        // are resolved at fire time from the closure.
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [panToSelectedRequestCounter]);
    // Toolbar: fit-all. Increments via `editorState.requestFitAll()`. Skips
    // the initial render (counter starts at 0) so we don't double-fit on
    // mount \u2014 React Flow already runs `fitView={true}` on first layout.
    useEffect(() => {
        if (fitAllRequestCounter === 0) {
            return;
        }
        fitView({ duration: 400, padding: 0.15 }).catch(noop());
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [fitAllRequestCounter]);
    // Toolbar: reset layout. Re-runs dagre over the ORIGINAL `laidOutNodes`
    // positions and replaces the live `nodes` state with them, undoing any
    // user-initiated drags. Edges don't carry positions so they pass
    // through untouched.
    useEffect(() => {
        if (resetLayoutRequestCounter === 0) {
            return undefined;
        }
        setNodes(laidOutNodes);
        // Defer the fit so React Flow has a frame to apply the new positions.
        const timer = window.setTimeout(() => {
            fitView({ padding: 0.15, duration: 300 }).catch(noop());
        }, 0);
        return () => window.clearTimeout(timer);
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [resetLayoutRequestCounter]);
    /**
     * Toolbar: export the diagram as a PNG. The strategy is the standard
     * React Flow recipe \u2014 compute the bounding box of every current
     * node, ask React Flow for the matching viewport (so the rendered
     * image fits the entire graph at a sensible zoom), then rasterize the
     * `.react-flow__viewport` element with `html-to-image`'s `toPng`.
     *
     * We pass the viewport transform via `style` so html-to-image clones
     * the viewport with the right CSS transform applied; that yields a
     * complete picture even when the user has panned/zoomed off-screen.
     * We pass the viewport transform via `style` so html-to-image clones
     * the viewport with the right CSS transform applied; that yields a
     * complete picture even when the user has panned/zoomed off-screen.
     *
     * Image width/height are clamped: the natural viewport size would
     * either be too small (no nodes selected, default zoom) or huge
     * (large databases). 1920x1080 is the upper bound; the helper
     * scales down if needed while preserving aspect.
     */
    const exportDiagramAsPng = useCallback(async (format = 'png') => {
        const viewportEl = document.querySelector('.database-diagram__canvas-shell .react-flow__viewport');
        if (!viewportEl) {
            return;
        }
        const bounds = getNodesBounds(selectionAwareNodes);
        const padding = 40;
        // Cap output size so the export stays usable as an image asset.
        // The cap matters for PNG (hard pixel ceiling); for SVG it just
        // sets the root viewBox — vector graphics scale infinitely.
        const maxWidth = 1920;
        const maxHeight = 1080;
        const naturalWidth = bounds.width + padding * 2;
        const naturalHeight = bounds.height + padding * 2;
        const scale = Math.min(1, maxWidth / naturalWidth, maxHeight / naturalHeight);
        const imageWidth = naturalWidth * scale;
        const imageHeight = naturalHeight * scale;
        const transform = getViewportForBounds(bounds, imageWidth, imageHeight, 0.1, 4, 0.1);
        const options = {
            backgroundColor: 'transparent',
            width: imageWidth,
            height: imageHeight,
            style: {
                width: `${imageWidth}px`,
                height: `${imageHeight}px`,
                transform: `translate(${transform.x}px, ${transform.y}px) scale(${transform.zoom})`,
            },
            // Bust caches so re-exports after edits pick up new content.
            cacheBust: true,
        };
        // `toPng` returns a base64 data URL; `toSvg` returns a
        // `data:image/svg+xml;...` URL with the inlined SVG markup.
        // Either works as the `href` of a download anchor.
        const dataUrl = format === 'svg'
            ? await toSvg(viewportEl, options)
            : await toPng(viewportEl, options);
        const link = document.createElement('a');
        // Path-based filename so the user can correlate the file with the
        // database it came from when they have many such exports.
        const safeName = editorState.database.path.replace(/[^a-z0-9_.-]/gi, '_');
        link.download = `${safeName}.${format}`;
        link.href = dataUrl;
        link.click();
    }, [selectionAwareNodes, editorState]);
    return (_jsxs("div", { className: "database-diagram__canvas-shell", children: [_jsxs(ReactFlow, { className: "database-diagram__canvas", nodes: selectionAwareNodes, edges: selectionAwareEdges, nodeTypes: NODE_TYPES, onNodesChange: onNodesChange, onEdgesChange: onEdgesChange, onNodeClick: (_, node) => {
                    // Foreign-stub nodes don't correspond to a real relation in this
                    // database, so a click on them is just an inert acknowledgement
                    // — we skip the relation selection rather than clearing it.
                    if (node.type === 'foreignStub') {
                        return;
                    }
                    const matching = findRelationById(editorState, node.id);
                    editorState.setSelectedRelation(matching);
                }, onEdgeClick: (_, edge) => {
                    if (edge.data?.join) {
                        editorState.focusOnJoin(edge.data.join);
                    }
                }, onPaneClick: () => editorState.clearSelection(), nodesDraggable: true, nodesConnectable: false, elementsSelectable: true, proOptions: { hideAttribution: true }, minZoom: 0.2, maxZoom: 1.5, fitView: true, children: [_jsx(Background, { variant: BackgroundVariant.Dots, gap: 18, size: 1 }), _jsx(Controls, { showInteractive: false }), _jsx(MiniMap, { pannable: true, zoomable: true, className: "database-diagram__minimap", 
                        // Highlight the currently focused node(s) on the minimap so the
                        // user can see at a glance where their selection sits relative
                        // to the rest of the graph (especially useful for large
                        // databases where the selection can scroll off-screen).
                        nodeColor: (node) => {
                            const data = node.data;
                            if (data?.isJoinEndpoint) {
                                return 'var(--color-yellow-200)';
                            }
                            if (data?.isSelected) {
                                return 'var(--color-blue-200)';
                            }
                            return 'var(--color-dark-grey-200)';
                        }, maskColor: "rgba(0, 0, 0, 0.6)" })] }), _jsxs("div", { className: "database-diagram__toolbar", children: [_jsx("button", { type: "button", className: "database-diagram__toolbar__btn", title: "Fit all to view", onClick: () => editorState.requestFitAll(), children: _jsx(ExpandIcon, {}) }), _jsx("button", { type: "button", className: "database-diagram__toolbar__btn", title: "Fit selection to view", disabled: !selectedRelation && !selectedJoin, onClick: () => {
                            // Reuse the existing pan-to-selected pipeline by re-issuing
                            // the current focus. We bump the counter via the matching
                            // focus action so the canvas effect runs the same fit logic
                            // it does for side-panel clicks.
                            if (selectedJoin) {
                                editorState.focusOnJoin(selectedJoin);
                            }
                            else if (selectedRelation) {
                                editorState.focusOnRelation(selectedRelation);
                            }
                        }, children: _jsx(ResizeIcon, {}) }), _jsx("button", { type: "button", className: "database-diagram__toolbar__btn", title: "Reset layout (re-run auto-layout)", onClick: () => editorState.requestResetLayout(), children: _jsx(RefreshIcon, {}) }), _jsxs("button", { type: "button", className: "database-diagram__toolbar__btn", title: "Export diagram as PNG", onClick: () => {
                            exportDiagramAsPng('png').catch(noop());
                        }, children: [_jsx(DownloadIcon, {}), _jsx("span", { className: "database-diagram__toolbar__btn__label", children: "PNG" })] }), _jsxs("button", { type: "button", className: "database-diagram__toolbar__btn", title: "Export diagram as SVG (vector, editable)", onClick: () => {
                            exportDiagramAsPng('svg').catch(noop());
                        }, children: [_jsx(DownloadIcon, {}), _jsx("span", { className: "database-diagram__toolbar__btn__label", children: "SVG" })] })] }), selectedJoin && (_jsxs("div", { className: "database-diagram__floating-card database-diagram__floating-card--join", children: [_jsxs("div", { className: "database-diagram__floating-card__title", children: [_jsx("span", { className: "database-diagram__floating-card__kind", children: "JOIN" }), _jsx("span", { className: "database-diagram__floating-card__name", children: selectedJoin.name })] }), _jsx("div", { className: "database-diagram__floating-card__formula", children: resolveJoinFormula(joinFormulas, selectedJoin.name) })] })), selectedFilter && (_jsxs("div", { 
                // Filters don't have an on-canvas anchor (they live at the
                // database level, not on a specific table/edge). Showing them
                // as a floating card is the canvas-level affordance: clicking
                // a filter in the side panel still gives users an immediate
                // visual response on the canvas surface they're staring at.
                className: "database-diagram__floating-card database-diagram__floating-card--filter", children: [_jsxs("div", { className: "database-diagram__floating-card__title", children: [_jsx("span", { className: "database-diagram__floating-card__kind", children: "FILTER" }), _jsx("span", { className: "database-diagram__floating-card__name", children: selectedFilter.name })] }), _jsx("div", { className: "database-diagram__floating-card__formula", children: filterFormulas.get(selectedFilter.name) ?? 'filter [...]' })] }))] }));
});
export const DatabaseDiagramCanvas = observer((props) => (_jsx(ReactFlowProvider, { children: _jsx(DatabaseDiagramCanvasInner, { editorState: props.editorState }) })));
//# sourceMappingURL=DatabaseDiagramCanvas.js.map