/**
 * SVG edge rendering for the graph editor — bezier curves with arrowheads.
 */

const SVG_NS = 'http://www.w3.org/2000/svg';

/* ── Types ────────────────────────────────────────────────────── */

interface GraphNodeRect {
    x: number;
    y: number;
    width: number;
    height: number;
}

interface GraphEdge {
    from: string;
    to: string;
    type: string;
    field?: string;
    editable?: boolean;
    points?: { x: number; y: number }[] | null;
    fromNode?: GraphNodeRect;
    toNode?: GraphNodeRect;
    fromPortY?: number;
    toPortY?: number;
}

function svgEl(tag: string, attrs: Record<string, string | number> = {}): SVGElement {
    const el = document.createElementNS(SVG_NS, tag);
    for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v));
    return el;
}

/**
 * Render all edges into the given SVG group.
 * @param {SVGGElement} group
 * @param {Array} edges - [{from, to, type, points, fromNode, toNode}]
 */
export function renderEdges(group: SVGGElement, edges: GraphEdge[]): void {
    while (group.firstChild) group.firstChild.remove();

    // Defs for arrowheads
    const defs = svgEl('defs');
    const arrowTypes = new Set(edges.map(e => e.type));
    for (const type of arrowTypes) {
        defs.appendChild(_createArrowMarker(type));
    }
    group.appendChild(defs);

    for (const edge of edges) {
        const path = _renderEdge(edge);
        group.appendChild(path);
    }
}

function _createArrowMarker(type: string): SVGElement {
    const marker = svgEl('marker', {
        id: `arrow-${type}`,
        viewBox: '0 0 10 10',
        refX: 9,
        refY: 5,
        markerWidth: 8,
        markerHeight: 8,
        orient: 'auto-start-reverse',
    });
    const path = svgEl('path', {
        d: 'M 0 0 L 10 5 L 0 10 z',
        class: `graph-edge-arrow graph-edge-${type}`,
    });
    marker.appendChild(path);
    return marker;
}

function _renderEdge(edge: GraphEdge): SVGElement {
    const { from, to, type, fromNode, toNode, field, editable } = edge;
    const cssClass = `graph-edge graph-edge-${type}${editable === false ? ' graph-edge-nested' : ''}`;
    // Always use port-aware bezier — ELK routes without port knowledge so
    // its bend points don't align with actual port positions.
    const d = _defaultBezier(fromNode!, toNode!, edge.fromPortY, edge.toPortY);

    const path = svgEl('path', {
        class: cssClass,
        d,
        'marker-end': `url(#arrow-${type})`,
        'data-from': from,
        'data-to': to,
        'data-field': field || '',
    });

    // Tooltip
    const title = svgEl('title');
    title.textContent = field ? field.replace(/_/g, ' ') : `${from} → ${to}`;
    path.appendChild(title);

    return path;
}

/**
 * Convert ELK layout points to SVG path string.
 * Uses Catmull-Rom-to-Cubic conversion for smooth curves through all points.
 */
function _pointsToPath(points: { x: number; y: number }[]): string {
    if (points.length < 2) return '';
    if (points.length === 2) {
        return `M ${points[0].x} ${points[0].y} L ${points[1].x} ${points[1].y}`;
    }

    // For 3+ points, build a smooth cubic bezier through all of them
    let d = `M ${points[0].x} ${points[0].y}`;

    for (let i = 0; i < points.length - 1; i++) {
        const p0 = points[Math.max(0, i - 1)];
        const p1 = points[i];
        const p2 = points[i + 1];
        const p3 = points[Math.min(points.length - 1, i + 2)];

        // Catmull-Rom to cubic bezier control points
        const cp1x = p1.x + (p2.x - p0.x) / 6;
        const cp1y = p1.y + (p2.y - p0.y) / 6;
        const cp2x = p2.x - (p3.x - p1.x) / 6;
        const cp2y = p2.y - (p3.y - p1.y) / 6;

        d += ` C ${cp1x} ${cp1y} ${cp2x} ${cp2y} ${p2.x} ${p2.y}`;
    }

    return d;
}

/**
 * Adjust ELK-routed start/end points to match port Y positions.
 */
function _adjustEndpoints(points: { x: number; y: number }[], fromNode: GraphNodeRect, toNode: GraphNodeRect, fromPortY: number | undefined, toPortY: number | undefined): { x: number; y: number }[] {
    if (points.length < 2) return points;
    const result = points.map(p => ({ ...p }));
    if (fromPortY != null) {
        result[0].y = fromNode.y + fromPortY;
        result[0].x = fromNode.x + fromNode.width;
    }
    if (toPortY != null) {
        result[result.length - 1].y = toNode.y + toPortY;
        result[result.length - 1].x = toNode.x;
    }
    return result;
}

/**
 * Fallback bezier when no ELK routing is available.
 * Uses port Y offsets when provided, otherwise centers vertically.
 */
function _defaultBezier(fromNode: GraphNodeRect, toNode: GraphNodeRect, fromPortY: number | undefined, toPortY: number | undefined): string {
    const x1 = fromNode.x + fromNode.width;
    const y1 = fromNode.y + (fromPortY ?? fromNode.height / 2);
    const x2 = toNode.x;
    const y2 = toNode.y + (toPortY ?? toNode.height / 2);
    const dx = Math.abs(x2 - x1) * 0.4;
    return `M ${x1} ${y1} C ${x1 + dx} ${y1} ${x2 - dx} ${y2} ${x2} ${y2}`;
}

/**
 * Highlight edges that connect to a specific node (upstream chain).
 */
export function highlightChain(edgeGroup: SVGGElement, nodeId: string, edges: GraphEdge[]): Set<string> {
    // Build adjacency indexes for O(E) BFS instead of O(N*E)
    const toIndex = new Map();  // toId → [edge]
    const fromIndex = new Map(); // fromId → [edge]
    for (const e of edges) {
        if (!toIndex.has(e.to)) toIndex.set(e.to, []);
        toIndex.get(e.to).push(e);
        if (!fromIndex.has(e.from)) fromIndex.set(e.from, []);
        fromIndex.get(e.from).push(e);
    }

    // Find all ancestors recursively
    const upstream = new Set<string>();
    const stack: string[] = [nodeId];
    while (stack.length) {
        const current = stack.pop();
        for (const e of (toIndex.get(current) || [])) {
            if (!upstream.has(e.from)) {
                upstream.add(e.from);
                stack.push(e.from);
            }
        }
    }
    upstream.add(nodeId);

    // Downstream too
    const downstream = new Set<string>();
    const dStack = [nodeId];
    while (dStack.length) {
        const current = dStack.pop();
        for (const e of (fromIndex.get(current) || [])) {
            if (!downstream.has(e.to)) {
                downstream.add(e.to);
                dStack.push(e.to);
            }
        }
    }

    const chain = new Set([...upstream, ...downstream]);

    edgeGroup.querySelectorAll('.graph-edge').forEach(path => {
        const from = path.getAttribute('data-from') ?? '';
        const to = path.getAttribute('data-to') ?? '';
        const inChain = chain.has(from) && chain.has(to);
        path.classList.toggle('highlighted', inChain);
        path.classList.toggle('dimmed', !inChain);
    });

    // Dim flow-dot groups on non-chain edges
    edgeGroup.querySelectorAll('.graph-edge-flow').forEach(g => {
        const from = g.getAttribute('data-from') ?? '';
        const to = g.getAttribute('data-to') ?? '';
        (g as SVGElement).style.opacity = (chain.has(from) && chain.has(to)) ? '' : '0.12';
    });

    return chain;
}

/**
 * Clear all highlight/dim classes from edges.
 */
export function clearEdgeHighlights(edgeGroup: SVGGElement): void {
    edgeGroup.querySelectorAll('.graph-edge').forEach(path => {
        path.classList.remove('highlighted', 'dimmed');
    });
    edgeGroup.querySelectorAll('.graph-edge-flow').forEach(g => {
        (g as SVGElement).style.opacity = '';
    });
}

/* ── Edge colors (matching CSS) ── */

const EDGE_COLORS = {
    picture: '#42A5F5',
    colorstrip: '#66BB6A',
    value: '#FFA726',
    device: '#78909C',
    clock: '#26C6DA',
    audio: '#EF5350',
    template: '#AB47BC',
    scene: '#CE93D8',
    default: '#999',
};

export { EDGE_COLORS };

/**
 * Update edge paths connected to a specific node (e.g. after dragging).
 * Falls back to default bezier since ELK routing points are no longer valid.
 */
export function updateEdgesForNode(group: SVGGElement, nodeId: string, nodeMap: Map<string, GraphNodeRect>, edges: GraphEdge[]): void {
    for (const edge of edges) {
        if (edge.from !== nodeId && edge.to !== nodeId) continue;
        const fromNode = nodeMap.get(edge.from);
        const toNode = nodeMap.get(edge.to);
        if (!fromNode || !toNode) continue;

        const d = _defaultBezier(fromNode!, toNode!, edge.fromPortY, edge.toPortY);
        group.querySelectorAll(`.graph-edge[data-from="${edge.from}"][data-to="${edge.to}"]`).forEach(pathEl => {
            if ((pathEl.getAttribute('data-field') || '') === (edge.field || '')) {
                pathEl.setAttribute('d', d);
            }
        });
    }
}

/**
 * Render animated flow dots on edges leading to running nodes.
 * @param {SVGGElement} group - the edges group
 * @param {Array} edges
 * @param {Set<string>} runningIds - IDs of currently running nodes
 */
export function renderFlowDots(group: SVGGElement, edges: GraphEdge[], runningIds: Set<string>): void {
    // Clear previous flow state
    group.querySelectorAll('.graph-edge-flow').forEach(el => el.remove());
    group.querySelectorAll('.graph-edge.graph-edge-active').forEach(el => el.classList.remove('graph-edge-active'));

    if (!runningIds || runningIds.size === 0) return;

    // Build adjacency index for O(E) BFS instead of O(N*E)
    const toIndex = new Map(); // toId → [{edge, idx}]
    for (let i = 0; i < edges.length; i++) {
        const e = edges[i];
        if (!toIndex.has(e.to)) toIndex.set(e.to, []);
        toIndex.get(e.to).push({ edge: e, idx: i });
    }

    // Build a Map from edge key to path element for O(1) lookup instead of querySelector
    const pathIndex = new Map();
    group.querySelectorAll('.graph-edge').forEach(pathEl => {
        const key = `${pathEl.getAttribute('data-from')}|${pathEl.getAttribute('data-to')}|${pathEl.getAttribute('data-field') || ''}`;
        pathIndex.set(key, pathEl);
    });

    // Collect all upstream edges that feed into running nodes (full chain)
    const activeEdges = new Set<number>();
    const visited = new Set();
    const stack = [...runningIds];
    while (stack.length) {
        const cur = stack.pop();
        if (visited.has(cur)) continue;
        visited.add(cur);
        for (const { edge, idx } of (toIndex.get(cur) || [])) {
            activeEdges.add(idx);
            stack.push(edge.from);
        }
    }

    for (const idx of activeEdges) {
        const edge = edges[idx];
        const pathEl = pathIndex.get(`${edge.from}|${edge.to}|${edge.field || ''}`);
        if (!pathEl) continue;

        // Hybrid: static styling (thicker + glow)
        pathEl.classList.add('graph-edge-active');

        // Animated dots flowing along the path
        const d = pathEl.getAttribute('d');
        if (!d) continue;
        const color = EDGE_COLORS[edge.type] || EDGE_COLORS.default;
        const flowG = svgEl('g', { class: 'graph-edge-flow', 'data-from': edge.from, 'data-to': edge.to });
        for (const beginFrac of ['0s', '1s']) {
            const circle = svgEl('circle', { fill: color, opacity: '0.9', r: '2.5' });
            const anim = document.createElementNS(SVG_NS, 'animateMotion');
            anim.setAttribute('dur', '2s');
            anim.setAttribute('repeatCount', 'indefinite');
            anim.setAttribute('begin', beginFrac);
            anim.setAttribute('path', d);
            circle.appendChild(anim);
            flowG.appendChild(circle);
        }
        group.appendChild(flowG);
    }
}

/**
 * Update flow dot paths for edges connected to a node (after drag).
 */
export function updateFlowDotsForNode(group: SVGGElement, nodeId: string, nodeMap: Map<string, GraphNodeRect>, edges: GraphEdge[]): void {
    // Just remove and let caller re-render if needed; or update paths
    // For simplicity, remove all flow dots — they'll be re-added on next render cycle
    group.querySelectorAll('.graph-edge-flow').forEach(el => el.remove());
}
