viz/tree-viz/engines/renderer.js

/**
 * @fileoverview TreeRenderer - Canvas-Rendering Modul für Bäume
 * 
 * Zentrale Rendering-Engine für alle visuellen Baum-Elemente:
 * - renderEdges(): Verbindungslinien zwischen Knoten
 * - renderNode(): Einzelne Knoten mit Status-Styling
 * - renderLabel(): Node-Labels mit Font-Metriken
 * - renderOverlay(): Debug-Overlay mit Viewport-Informationen
 * 
 * Unterstützt spezialisierte Board-Renderer:
 * - KnightsTourNodeRenderer (für Knights-Tour Boards)
 * - RotateBoxNodeRenderer (für RotateBox Boards)
 * 
 * @author Alexander Wolf
 * @version 2.0
 */
var TreeRenderer = {
    /**
     * Referenzen zu spezialisierten Renderern (optional)
     * Können externe Dateien laden oder null sein
     */
    knightsTourRenderer: typeof KnightsTourNodeRenderer !== 'undefined' ? KnightsTourNodeRenderer : null,
    rotateBoxRenderer: typeof RotateBoxNodeRenderer !== 'undefined' ? RotateBoxNodeRenderer : null,
    minimaxRenderer: typeof MinimaxNodeRenderer !== 'undefined' ? MinimaxNodeRenderer : null,
    numericRenderer: typeof NumericNodeRenderer !== 'undefined' ? NumericNodeRenderer : null,
    
    // Safe getters for global STYLE_CONFIG and STYLE_CONFIG_GLOBAL
    // These are defined in status-config.js and must be loaded first
    getStyleConfig() {
        if (typeof window !== 'undefined' && window.STYLE_CONFIG) {
            return window.STYLE_CONFIG;
        }
        if (typeof StatusConfig !== 'undefined' && StatusConfig.getStyleConfig) {
            return StatusConfig.getStyleConfig();
        }
        return {};
    },

    getStyleConfigGlobal() {
        if (typeof window !== 'undefined' && window.STYLE_CONFIG_GLOBAL) {
            return window.STYLE_CONFIG_GLOBAL;
        }
        if (typeof StatusConfig !== 'undefined' && StatusConfig.STYLE_CONFIG_GLOBAL) {
            return StatusConfig.STYLE_CONFIG_GLOBAL;
        }
        return {};
    },

    /**
     * Rendert alle Kanten des Baums
     * WICHTIG: Viewport-Transformationen müssen bereits vom Aufrufer angewendet sein!
     * @param {CanvasRenderingContext2D} ctx - bereits mit Viewport-Transform
     * @param {Array} edges - Array von { from, to, label }
     * @param {Map} nodes - Map von nodeId → node
     */
    renderEdges(ctx, edges, nodes) {
        for (const edge of edges) {
            const fromNode = nodes.get(edge.from);
            const toNode = nodes.get(edge.to);
            
            if (!fromNode || !toNode) continue;

            const dx = toNode.x - fromNode.x;
            const dy = toNode.y - fromNode.y;
            const len = Math.sqrt((dx * dx) + (dy * dy));
            if (len === 0) continue;

            const ux = dx / len;
            const uy = dy / len;
            const nodeGap = Math.max((fromNode.radius || 15), (toNode.radius || 15)) + 6;

            const startX = fromNode.x + (ux * nodeGap);
            const startY = fromNode.y + (uy * nodeGap);
            const endX = toNode.x - (ux * nodeGap);
            const endY = toNode.y - (uy * nodeGap);

            const isHighlighted = edge.highlighted === true;
            ctx.strokeStyle = edge.color || (isHighlighted ? '#888' : 'rgba(136, 136, 136, 0.55)');
            ctx.lineWidth = isHighlighted ? (edge.width || 3) : 1.25;
            ctx.beginPath();
            ctx.moveTo(startX, startY);
            ctx.lineTo(endX, endY);
            ctx.stroke();

            // Pfeilspitze bei hervorgehobenen Kanten
            if (isHighlighted && edge.showArrow) {
                const arrowLen = 10;
                let ax, ay;
                if (edge.arrowDirection === 'from') {
                    // Pfeil zeigt zum Elternknoten (Kind → Eltern)
                    ax = startX; ay = startY;
                    const aAngle = Math.atan2(startY - endY, startX - endX);
                    ctx.fillStyle = ctx.strokeStyle;
                    ctx.beginPath();
                    ctx.moveTo(ax, ay);
                    ctx.lineTo(ax - arrowLen * Math.cos(aAngle - Math.PI / 6), ay - arrowLen * Math.sin(aAngle - Math.PI / 6));
                    ctx.lineTo(ax - arrowLen * Math.cos(aAngle + Math.PI / 6), ay - arrowLen * Math.sin(aAngle + Math.PI / 6));
                    ctx.closePath();
                    ctx.fill();
                } else {
                    // Pfeil zeigt zum Kindknoten (Eltern → Kind)
                    ax = endX; ay = endY;
                    const aAngle = Math.atan2(endY - startY, endX - startX);
                    ctx.fillStyle = ctx.strokeStyle;
                    ctx.beginPath();
                    ctx.moveTo(ax, ay);
                    ctx.lineTo(ax - arrowLen * Math.cos(aAngle - Math.PI / 6), ay - arrowLen * Math.sin(aAngle - Math.PI / 6));
                    ctx.lineTo(ax - arrowLen * Math.cos(aAngle + Math.PI / 6), ay - arrowLen * Math.sin(aAngle + Math.PI / 6));
                    ctx.closePath();
                    ctx.fill();
                }
            }

            // Edge label
            if (edge.label) {
                const midX = (fromNode.x + toNode.x) / 2;
                const midY = (fromNode.y + toNode.y) / 2;
                
                ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
                ctx.fillRect(midX - 15, midY - 10, 30, 20);
                
                ctx.fillStyle = '#333';
                ctx.font = '12px Arial';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(edge.label, midX, midY);
            }
        }
    },

    /**
     * Rendert alle Knoten des Baums (mit Board-Support!)
     * WICHTIG: Viewport-Transformationen müssen bereits vom Aufrufer angewendet sein!
     * @param {CanvasRenderingContext2D} ctx - bereits mit Viewport-Transform
     * @param {Map} nodes - Map von nodeId → node
     * @param {Object} statusTypes - Status-Definitionen
     * @param {Function} getNodeStyle - Funktion zum Abrufen des Node-Stils
     * @param {number} scale - Viewport scale für Line-Width Berechnung
     * @param {Object} config - Config mit Board-Render-Optionen (optional)
     */
    renderNodes(ctx, nodes, statusTypes, getNodeStyle, scale = 1, config = {}) {
        // Sortiere Knoten nach Priorität (höher = zuletzt = oben)
        const sortedNodes = Array.from(nodes.values()).sort((a, b) => {
            const getPriority = (node) => {
                if (node.status) {
                    const priorities = Array.from(node.status).map(s => statusTypes[s]?.priority || 0);
                    return Math.max(...priorities, 0);
                }
                return 0;
            };
            return getPriority(a) - getPriority(b);
        });

        for (const node of sortedNodes) {
            const style = getNodeStyle(node);
            const radius = node.radius || 15;

            // FIX: Check if node has boardData (Knights-Tour or RotateBox board)
            if (node.boardData) {
                this.renderBoardNode(ctx, node, style, radius, scale, config);
            } else {
                // Regular circle node
                // Glow effect - nutze zentrale STYLE_CONFIG_GLOBAL Einstellung
                const STYLE_CONFIG_GLOBAL = this.getStyleConfigGlobal();
                if (style.glowColor) {
                    ctx.shadowBlur = (typeof STYLE_CONFIG_GLOBAL !== 'undefined' ? STYLE_CONFIG_GLOBAL.shadowBlur : 15) / scale;
                    ctx.shadowColor = style.glowColor;
                } else {
                    ctx.shadowBlur = 0;
                }

                // Knoten zeichnen
                ctx.fillStyle = style.fillColor || '#ddd';
                ctx.beginPath();
                ctx.arc(node.x, node.y, radius, 0, Math.PI * 2);
                ctx.fill();

                // Rand zeichnen
                if (style.borderDash && style.borderDash.length > 0) {
                    ctx.setLineDash(style.borderDash);
                } else {
                    ctx.setLineDash([]);
                }
                
                ctx.strokeStyle = style.borderColor || '#888';
                ctx.lineWidth = (style.borderWidth || 2) / scale;
                ctx.stroke();
                ctx.setLineDash([]);

                // Label
                ctx.shadowBlur = 0;
                ctx.fillStyle = '#000';
                ctx.font = `${12 / scale}px Arial`;
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(node.label || node.id, node.x, node.y);
            }
            
            // Draw expansion indicator if node is expandable (disabled only if enableTreeExpansion === false)
            if (typeof NodeExpansionEngine !== 'undefined' && NodeExpansionEngine.isExpandable(node)) {
                // Skip only if explicitly disabled
                if (!config || config.enableTreeExpansion !== false) {
                    NodeExpansionEngine.drawExpansionIndicator(ctx, node, scale);
                }
            }
        }
    },

    /**
     * Rendert einen Knoten mit Board-Daten (Knights-Tour oder RotateBox)
     * Nutzt spezialisierte Renderer wenn verfügbar
     * WICHTIG: Nutzt zentrale STYLE_CONFIG für Board-Zustände (BOARD_WON, BOARD_NORMAL)
     * 
     * @param {CanvasRenderingContext2D} ctx
     * @param {Object} node - Node mit boardData
     * @param {Object} style - Node style (bereits aus STYLE_CONFIG)
     * @param {number} radius - Node radius
     * @param {number} scale - Viewport scale
     * @param {Object} config - Config mit boardRenderStyle Einstellungen (optional)
     */
    renderBoardNode(ctx, node, style, radius, scale, config = {}) {
        const board = node.boardData;
        const size = radius * 2;
        const halfSize = size / 2;

        // Numerische Knoten: Der NumericNodeRenderer zeichnet komplett eigenständig
        // (Kreis, Füllung, Rand, Wert, Rolle, Alpha/Beta).
        // Kein rechteckiger Hintergrund oder Rand nötig.
        if (node.boardType === 'numeric') {
            if (typeof NumericNodeRenderer !== 'undefined') {
                NumericNodeRenderer.render(ctx, board, node.x, node.y, size, scale, node.metadata, style);
            }
            return;
        }

        // Wähle Board-Style aus STYLE_CONFIG basierend auf board.won
        const STYLE_CONFIG = this.getStyleConfig();
        let boardStyle = style;
        if (board.won) {
            // Nutze zentrale BOARD_WON Definition statt lokale Hardcodes
            boardStyle = TreeFeaturesEngine._convertStyleToRenderFormat(STYLE_CONFIG.BOARD_WON, config);
        } else if (!style.fillColor) {
            // Falls kein Style vom Node selbst, nutze BOARD_NORMAL
            boardStyle = TreeFeaturesEngine._convertStyleToRenderFormat(STYLE_CONFIG.BOARD_NORMAL, config);
        }

        // Glow effect - nutze zentrale STYLE_CONFIG_GLOBAL Einstellung
        const STYLE_CONFIG_GLOBAL = this.getStyleConfigGlobal();
        if (boardStyle.glowColor) {
            ctx.shadowBlur = (typeof STYLE_CONFIG_GLOBAL !== 'undefined' ? STYLE_CONFIG_GLOBAL.glowBlur : 20) / scale;
            ctx.shadowColor = boardStyle.glowColor;
        } else {
            ctx.shadowBlur = 0;
        }

        // White background
        ctx.fillStyle = '#ffffff';
        ctx.fillRect(node.x - halfSize, node.y - halfSize, size, size);

        // Detect board type and render using appropriate renderer
        // WICHTIG: Reihenfolge der Checks ist kritisch!
        // 1. Explicit boardType check (am spezifischsten)
        // 2. Knights-Tour (hat board.size)
        // 3. RotateBox (hat board.rows/cols)
        if (node.boardType === 'minimax') {
            // Minimax/TicTacToe board (explizit markiert)
            if (typeof MinimaxNodeRenderer !== 'undefined') {
                MinimaxNodeRenderer.render(ctx, board, node.x, node.y, size, scale, node.metadata);
            }
        } else if (board.size !== undefined && board.grid && Array.isArray(board.grid[0])) {
            // Knights-Tour board (hat size + 2D grid array)
            if (typeof KnightsTourNodeRenderer !== 'undefined') {
                KnightsTourNodeRenderer.render(ctx, board, node.x, node.y, size, scale, 'classic');
            }
        } else if (board.rows !== undefined && board.cols !== undefined) {
            // RotateBox board (has rows/cols)
            if (typeof RotateBoxNodeRenderer !== 'undefined') {
                RotateBoxNodeRenderer.render(ctx, board, node.x, node.y, size, scale);
            }
        }

        // Apply styling (border, glow) - AUS ZENTRALER STYLE_CONFIG
        ctx.strokeStyle = boardStyle.borderColor || '#333';
        ctx.lineWidth = (boardStyle.borderWidth || 2) / scale;
        ctx.setLineDash(boardStyle.borderDash || []);
        ctx.strokeRect(node.x - halfSize, node.y - halfSize, size, size);
        ctx.setLineDash([]);

        ctx.shadowBlur = 0;
        
        // Draw Label below the board (if present)
        // Für Minimax-Boards werden Label nicht gezeichnet, da Metadaten (V, α, β)
        // bereits unterhalb gerendert werden und sonst überlagern.
        const shouldShowLabel = !node.boardData;

        if (node.label && node.label !== node.id && shouldShowLabel) {
             // Check if this is a placeholder label that should only show when READY
             const isPlaceholderLabel = node.label.includes('f = ???') || node.label.includes('?');
             const hasReadyStatus = node.status && node.status.has('READY');
             
             // Only draw placeholder labels if node is READY, but always draw evaluation results
             const shouldDrawLabel = !isPlaceholderLabel || hasReadyStatus;
             
             if (shouldDrawLabel) {
                ctx.fillStyle = '#000';
                 // Fixed font size in world space so it shrinks when zooming out (nodes get small)
                 ctx.font = '10px Arial';
                 ctx.textAlign = 'center';
                 ctx.textBaseline = 'top';
                 
                 // Position relative to bottom of board + padding
                 const labelY = node.y + halfSize + 5;
                 
                 // Split by newline to handle potential multi-line labels
                 const lines = String(node.label).split('\n');
                 lines.forEach((line, i) => {
                     ctx.fillText(line, node.x, labelY + (i * 12));
                 });
             }
        }
    },

    /**
     * Rendert Overlay-Informationen (Zoom-Level, Debug-Info, etc.)
     * WICHTIG: Muss NACH ctx.restore() aufgerufen werden, da es im Screen-Space zeichnet!
     * @param {CanvasRenderingContext2D} ctx
     * @param {number} scale - Zoom-Level vom Viewport
     * @param {Object} config - { showOverlay, maxDepth, levelLabels }
     * @param {Map} nodes - Map für zusätzliche Stats
     */
    renderOverlay(ctx, scale, config, nodes) {
        // Draw Level Indicators (Max/Min) if enabled
        // These are drawn "behind" typical UI overlay but "above" background
        // However, since we are in screen space here, we need to be careful.
        // Actually, dotted lines should be in world space (move with tree), 
        // labels on the left should be in screen space (fixed X) but World Y?
        
        // This function is called in SCREEN SPACE (after restore).
        // To draw level lines that move with the tree, we should have done it in render().
        // BUT, the requirements say "An der Seite links vom Baum sollen Max und Min geschrieben sein (overlay?)".
        // "Overlay" suggests fixed position on screen.
        
        // Let's draw standard Overlay UI first
        if (config.showOverlay) {
            // Zoom indicator
            ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
            ctx.fillRect(10, 10, 120, 30);
            ctx.fillStyle = '#fff';
            ctx.font = '14px Arial';
            ctx.textAlign = 'left';
            ctx.textBaseline = 'top';
            ctx.fillText(`Zoom: ${(scale * 100).toFixed(0)}%`, 20, 18);

            // Node count
            const nodeCount = nodes ? nodes.size : 0;
            if (nodeCount > 0) {
                ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
                ctx.fillRect(10, 50, 120, 30);
                ctx.fillStyle = '#fff';
                ctx.fillText(`Nodes: ${nodeCount}`, 20, 58);
            }
        }
    },

    /**
     * Draws level indicators (Min/Max labels and dotted lines).
     * THIS MUST BE CALLED WITHIN THE TRANSFORMED CONTEXT (World Space)
     * @param {CanvasRenderingContext2D} ctx 
     * @param {Object} config - { levelHeight, maxDepth, rootPlayerColor, opponentColor }
     * @param {number} treeTopY - Y position of root node (usually 0)
     * @param {Object} viewport - For calculating screen-fixed labels
     */
    renderLevelIndicators(ctx, config, treeTopY = 0, viewport) {
        if (!config.showLevelIndicators) return;

        const maxDepth = config.currentMaxDepth || 0;
        const levelHeight = config.levelHeight || 120;
        
        // Colors
        const maxColor = config.rootPlayerColor || '#e74c3c'; // Red (Max/Root)
        const minColor = config.opponentColor || '#3498db';   // Blue (Min/Opponent)
        
        // Define visible area in world space to draw lines
        // We want lines to span the whole visible width
        const visibleLeft = -viewport.offsetX / viewport.scale;
        const visibleRight = (ctx.canvas.width - viewport.offsetX) / viewport.scale;
        
        // Draw lines and labels
        ctx.save();
        
        for (let d = 0; d <= maxDepth; d++) {
            const y = treeTopY + ((d+0.7) * levelHeight);
            
            // Draw dotted line between levels (except before root)
            if (d > 0) {
                const lineY = y - (levelHeight / 2); // Line in the middle of the level
                
                ctx.beginPath();
                ctx.strokeStyle = '#ccc';
                ctx.lineWidth = 2;
                ctx.setLineDash([5, 5]);
                ctx.moveTo(visibleLeft, lineY);
                ctx.lineTo(visibleRight, lineY);
                ctx.stroke();
                ctx.setLineDash([]); // Reset dash
                
                // Draw Labels (Max / Min) - only for lines between levels
                // d=1 -> MAX, d=2 -> MIN, d=3 -> MAX, etc.
                const isMax = (d % 2 === 1);
                const label = isMax ? 'MAX' : 'MIN';
                const color = isMax ? maxColor : minColor;
                
                // Position label above the line
                const labelX = (10 - viewport.offsetX) / viewport.scale; // 10px from edge
                const labelY = lineY - 5; // Position closer to root, just above the line

                ctx.fillStyle = color;
                // Use scale-invariant font size
                ctx.font = `bold ${16 / viewport.scale}px Arial`;
                ctx.textAlign = 'left';
                ctx.textBaseline = 'bottom';
                ctx.fillText(label, labelX, labelY);
            }
        }
        
        ctx.restore();
    },

    /**
     * Clear canvas
     * @param {CanvasRenderingContext2D} ctx
     * @param {number} width
     * @param {number} height
     */
    clear(ctx, width, height) {
        ctx.clearRect(0, 0, width, height);
    }
};

// Make globally available
if (typeof window !== 'undefined') {
    window.TreeRenderer = TreeRenderer;
}

// Export for Node.js/CommonJS
if (typeof module !== 'undefined' && module.exports) {
    module.exports = TreeRenderer;
}