viz/adapters/tree-adapters/generic-tree-adapter.js

/**
 * @fileoverview GenericTreeAdapter - Spielunabhängiger Tree-Adapter für numerische Bäume
 *
 * Erbt von BaseTreeAdapter und arbeitet ausschließlich mit numerischen Werten.
 * Kein Wissen über TicTacToe, Knights-Tour oder andere Spiele.
 *
 * Features:
 * - boardType: 'numeric' für alle Knoten (Kreisdarstellung im Diagramm-Modus)
 * - Interaktive Klick-Evaluation (handleNodeClick → evaluateNode)
 * - Minimax-Logik (MAX/MIN-Ebenen, Wert-Propagation, Best-Edge-Highlighting)
 * - Optional: Alpha-Beta-Pruning (enableAlphaBeta-Flag)
 * - Statistik-Callbacks (onStatsChanged, onActiveNodeChanged)
 *
 * Separation of Concerns:
 *   - Adapter orchestriert Baum-Aufbau und Evaluation
 *   - Rendering delegiert an NumericNodeRenderer (via boardType: 'numeric')
 *   - Farben/Status aus zentraler StatusConfig
 *
 * @class GenericTreeAdapter
 * @extends BaseTreeAdapter
 * @author Alexander Wolf
 * @version 1.0
 * @see ENGINEERING_CONVENTIONS.md
 */
class GenericTreeAdapter extends BaseTreeAdapter {

    /**
     * @param {HTMLIFrameElement} iframeElement - Ziel-iFrame für Tree-Visualisierung
     */
    constructor(iframeElement) {
        super(iframeElement);

        // Debug-Konfiguration für diesen Adapter
        this._debugDomain = 'VIZ_TREE_ADAPTER_GENERIC';
        this._debugPrefix = 'GenericTreeAdapter';

        /** @type {boolean} Steuert ob Alpha-Beta-Pruning aktiv ist */
        this.enableAlphaBeta = false;

        this.constants = this._resolveConstants();
    }

    /**
     * Lädt zentrale Konstanten mit robusten Fallbacks.
     * @returns {Object} Aufgelöste Konstanten
     * @private
     */
    _resolveConstants() {
        const numViz = typeof NUMERIC_VIZ_CONSTANTS !== 'undefined' ? NUMERIC_VIZ_CONSTANTS : {};
        const edgeColors = numViz.EDGE_COLORS || {};
        const edgeWidths = numViz.EDGE_WIDTHS || {};

        return {
            edgeBestColor: edgeColors.BEST || '#27ae60',
            edgePrunedColor: edgeColors.PRUNED || '#bdc3c7',
            edgeCutoffAlpha: edgeColors.ALPHA_CUTOFF || '#e74c3c',
            edgeCutoffBeta: edgeColors.BETA_CUTOFF || '#3498db',
            edgeBestWidth: edgeWidths.BEST || 3,
            edgePrunedWidth: edgeWidths.PRUNED || 1,
            edgeCutoffWidth: edgeWidths.CUTOFF || 2.5,
        };
    }

    /**
     * Liefert Farbe und Breite für Best-Edge-Highlighting.
     * @param {number} bestValue
     * @returns {{color: string, width: number}}
     * @protected
     */
    _getHighlightEdgeStyle(bestValue) {
        return { color: this.constants.edgeBestColor, width: this.constants.edgeBestWidth };
    }

    /**
     * Liefert initiale Konfiguration für die Tree-Visualisierung.
     * @returns {Object} Config-Objekt für TreeEngine
     */
    getInitialConfig() {
        return {
            showLevelIndicators: true,
            levelIndicatorType: 'minimax',
            rootPlayerColor: '#e74c3c',
            opponentColor: '#3498db',
            enableTreeExpansion: true,
        };
    }

    // ========================================================================
    // BAUM-AUFBAU (wird von TreeFactory aufgerufen)
    // ========================================================================

    /**
     * Erstellt einen numerischen Knoten im Baum.
     *
     * @param {number|null} value - Knotenwert (null für innere Knoten vor Evaluation)
     * @param {number|null} parentId - Elternknoten-ID (null für Root)
     * @param {Object} opts - Zusätzliche Optionen
     * @param {number}  opts.depth - Tiefe im Baum
     * @param {boolean} opts.isMaximizing - MAX- oder MIN-Knoten
     * @param {boolean} [opts.isTerminal=false] - Blattknoten?
     * @param {boolean} [opts.isPruned=false] - Geprunter Knoten?
     * @param {number}  [opts.alpha] - Alpha-Wert (nur bei AB)
     * @param {number}  [opts.beta] - Beta-Wert (nur bei AB)
     * @returns {number} Neue Knoten-ID
     */
    addNumericNode(value, parentId, opts) {
        const metadata = {
            value: value,
            depth: opts.depth,
            isMaximizing: opts.isMaximizing,
            isTerminal: opts.isTerminal || false,
            isPruned: opts.isPruned || false,
        };

        // Alpha/Beta nur setzen wenn AB aktiv
        if (this.enableAlphaBeta) {
            metadata.alpha = opts.alpha !== undefined ? opts.alpha : -Infinity;
            metadata.beta = opts.beta !== undefined ? opts.beta : Infinity;
        }

        const status = opts.isPruned ? 'PRUNED' : 'WAIT';

        // Synthetisches State-Objekt als Key für nodeMap
        const syntheticState = { id: this.nodeIdCounter };

        const nodeId = this.createNode(syntheticState, parentId, metadata, status, 'numeric');

        // nodeStates speichert null (kein Spielzustand)
        this.nodeStates.set(nodeId, null);

        // treeStructure eintragen
        const structData = {
            parentId: parentId,
            children: [],
            status: status,
            value: value,
            depth: opts.depth,
            isMaximizing: opts.isMaximizing,
            isTerminal: opts.isTerminal || false,
        };

        if (this.enableAlphaBeta) {
            structData.alpha = metadata.alpha;
            structData.beta = metadata.beta;
            structData.inheritedAlpha = metadata.alpha;
            structData.inheritedBeta = metadata.beta;
        }

        this.treeStructure.set(nodeId, structData);

        // Parent-Referenz aktualisieren
        if (parentId !== null) {
            const parentData = this.treeStructure.get(parentId);
            if (parentData) {
                parentData.children.push(nodeId);
            }
        }

        return nodeId;
    }

    // ========================================================================
    // BAUM-GENERIERUNG (Einstiegspunkte)
    // ========================================================================

    /**
     * Baut einen vordefinierten Baum auf und startet die Visualisierung.
     *
     * @param {Object} treeDefinition - Baumdefinition aus TreeFactory
     * @param {Object} treeDefinition.root - Wurzelknoten-Definition
     * @param {Object} config - Konfiguration
     * @param {boolean} [config.enableAlphaBeta=false] - AB-Pruning aktivieren
     * @returns {Object} Ergebnis mit Statistiken
     */
    visualizeTree(treeDefinition, config = {}) {
        this._debugLog('visualizeTree:start', { enableAlphaBeta: config.enableAlphaBeta });

        this.enableAlphaBeta = config.enableAlphaBeta === true;
        this.stats = { nodesVisited: 0, nodesPruned: 0, evaluatedNodes: 0 };
        this._notifyStatsChanged();

        this.reset();

        // Baum rekursiv aufbauen
        this._buildSubtree(treeDefinition.root, null, 0, true);

        // Status aller Knoten initial setzen
        this._initializeAllStatuses();

        this._debugLog('visualizeTree:complete', {
            totalNodes: this.nodeIdCounter,
            commandQueueSize: this.commands.length,
        });

        this.flushCommands();

        return {
            nodesVisited: this.stats.nodesVisited,
            nodesPruned: this.stats.nodesPruned,
            evaluatedNodes: this.stats.evaluatedNodes,
            bestValue: null,
        };
    }

    /**
     * Baut einen Teilbaum rekursiv auf.
     *
     * @param {Object} nodeDef - Knoten-Definition {value, children[]}
     * @param {number|null} parentId - Eltern-ID
     * @param {number} depth - Aktuelle Tiefe
     * @param {boolean} isMaximizing - MAX oder MIN Ebene
     * @returns {number} Erstellte Knoten-ID
     * @private
     */
    _buildSubtree(nodeDef, parentId, depth, isMaximizing) {
        const isLeaf = !nodeDef.children || nodeDef.children.length === 0;
        const value = isLeaf ? nodeDef.value : null;

        const nodeId = this.addNumericNode(value, parentId, {
            depth: depth,
            isMaximizing: isMaximizing,
            isTerminal: isLeaf,
        });

        if (!isLeaf) {
            for (const childDef of nodeDef.children) {
                this._buildSubtree(childDef, nodeId, depth + 1, !isMaximizing);
            }
        }

        return nodeId;
    }

    /**
     * Setzt initiale Status für alle Knoten nach dem Aufbau.
     * Ohne AB: Blattknoten → automatisch EVALUATED (Wert steht fest).
     * Mit AB: Blattknoten → READY (Nutzer muss klicken, damit AB-Werte propagiert werden).
     * @private
     */
    _initializeAllStatuses() {
        let autoEvaluated = 0;

        // Schritt 1: Blattknoten-Initialisierung
        for (const [nodeId, data] of this.treeStructure) {
            if (data.isTerminal) {
                const value = data.value !== null && data.value !== undefined ? data.value : 0;

                if (this.enableAlphaBeta) {
                    // Mit AB: Blattknoten als READY markieren, damit Nutzer sie anklickt
                    // und AB-Werte korrekt propagiert werden
                    NodeStatusManager.setNodeStatus(nodeId, 'READY', [], this.treeStructure, this.commands);
                    this.commands.push({
                        action: 'UPDATE_NODE',
                        id: nodeId,
                        data: {
                            label: String(value),
                            boardData: { value: value },
                            metadata: {
                                depth: data.depth,
                                isMaximizing: data.isMaximizing,
                                value: value,
                                alpha: data.alpha,
                                beta: data.beta,
                            },
                        }
                    });
                } else {
                    // Ohne AB: Blattknoten direkt als EVALUATED setzen
                    NodeStatusManager.setNodeStatus(nodeId, 'EVALUATED', [], this.treeStructure, this.commands);
                    this.commands.push({
                        action: 'UPDATE_NODE',
                        id: nodeId,
                        data: {
                            label: String(value),
                            boardData: { value: value },
                            metadata: {
                                depth: data.depth,
                                isMaximizing: data.isMaximizing,
                                value: value,
                            },
                        }
                    });
                    autoEvaluated++;
                }
            }
        }

        // Schritt 2: Innere Knoten prüfen
        // Elternknoten von Blättern werden READY (alle Kinder EVALUATED)
        for (const [nodeId, data] of this.treeStructure) {
            if (!data.isTerminal) {
                this.checkNodeStatus(nodeId);
            }
        }

        // Statistiken für auto-evaluierte Blätter
        this.stats.nodesVisited += autoEvaluated;
        this.stats.evaluatedNodes += autoEvaluated;
        this._notifyStatsChanged();
    }

    // ========================================================================
    // INTERAKTIVE EVALUATION (handleNodeClick + checkNodeStatus geerbt von BaseTreeAdapter)
    // ========================================================================

    /**
     * Bewertet einen Knoten (Blatt oder innerer Knoten).
     *
     * - Blatt: Übernimmt den gespeicherten Wert
     * - Innerer Knoten: MAX/MIN über evaluierte Kinder
     * - Bei AB: Prüft Pruning nach Evaluation
     *
     * @param {number} nodeId - Zu bewertende Knoten-ID
     */
    evaluateNode(nodeId) {
        const data = this.treeStructure.get(nodeId);
        if (!data) {
            this._debugLog('evaluateNode:missing-data', { nodeId });
            return;
        }

        this.commands = [];
        this.stats.nodesVisited += 1;
        this.stats.evaluatedNodes += 1;
        this._notifyStatsChanged();

        let value = 0;
        let labelText = '';
        let statusesToAdd = ['EVALUATED'];

        // CASE 1: Blattknoten
        if (data.children.length === 0) {
            value = data.value !== null && data.value !== undefined ? data.value : 0;
            labelText = String(value);
            // Blattknoten erhalten neutralen EVALUATED-Status
        }
        // CASE 2: Innerer Knoten
        else {
            const evaluatedChildren = data.children
                .map(cid => this.treeStructure.get(cid))
                .filter(c => c && c.status === 'EVALUATED');

            if (evaluatedChildren.length === 0) {
                this._debugLog('evaluateNode:no-evaluated-children', { nodeId });
                return;
            }

            const values = evaluatedChildren.map(c => c.value);

            if (data.isMaximizing) {
                value = Math.max(...values);
                labelText = `Max = ${value}`;
            } else {
                value = Math.min(...values);
                labelText = `Min = ${value}`;
            }

            // Best-Edge hervorheben
            this._highlightBestEdges(nodeId, value);
            statusesToAdd = ['EVALUATED'];
        }

        // Wert speichern
        data.value = value;

        // Metadata-Update für Renderer (boardData.value aktualisieren)
        const metadataUpdate = {
            depth: data.depth,
            isMaximizing: data.isMaximizing,
            value: value,
        };

        if (this.enableAlphaBeta) {
            metadataUpdate.alpha = data.alpha;
            metadataUpdate.beta = data.beta;
        }

        // Status setzen
        NodeStatusManager.setNodeStatus(nodeId, 'EVALUATED', statusesToAdd, this.treeStructure, this.commands);

        // Node-Update senden (Label + Metadata + boardData.value)
        this.commands.push({
            action: 'UPDATE_NODE',
            id: nodeId,
            data: {
                label: labelText,
                boardData: { value: value },
                metadata: metadataUpdate,
            }
        });

        // Parent-Status und ggf. AB-Pruning prüfen
        if (data.parentId !== null) {
            if (this.enableAlphaBeta) {
                this._checkPruning(data.parentId, nodeId);
            }
            this.checkNodeStatus(data.parentId);
        }

        this._debugLog('evaluateNode:done', {
            nodeId, value, parentId: data.parentId,
            enableAlphaBeta: this.enableAlphaBeta,
        });

        if (typeof this.onActiveNodeChanged === 'function') {
            this.onActiveNodeChanged(nodeId, data);
        }

        this._notifyStatsChanged();
        this.flushCommands();
    }

    // ========================================================================
    // ALPHA-BETA PRUNING
    // ========================================================================

    /**
     * Prüft ob am Elternknoten ein Alpha-Beta-Cutoff auftritt.
     * Aktualisiert Alpha/Beta-Bounds und pruned ggf. verbleibende Geschwister.
     *
     * @param {number} parentId - Elternknoten-ID
     * @param {number} [evaluatedChildId=null] - Gerade evaluiertes Kind (für Edge-Highlighting)
     * @private
     */
    _checkPruning(parentId, evaluatedChildId = null) {
        const parent = this.treeStructure.get(parentId);
        if (!parent || parent.status === 'EVALUATED') return;

        // Bounds neu berechnen
        this._recomputeBounds(parentId, evaluatedChildId);

        if (parent.alpha >= parent.beta) {
            this._debugLog('pruning:cutoff', {
                parentId, alpha: parent.alpha, beta: parent.beta,
            });

            let newlyPruned = 0;
            parent.children.forEach(childId => {
                const child = this.treeStructure.get(childId);
                if (!child || child.status === 'EVALUATED' || child.status === 'PRUNED') return;

                newlyPruned += this._pruneSubtree(childId, parentId);
            });

            if (newlyPruned > 0) {
                this.stats.nodesPruned += newlyPruned;
                this._notifyStatsChanged();
            }
        }
    }

    /**
     * Berechnet Alpha/Beta-Bounds eines Knotens aus seinen evaluierten Kindern.
     * Visualisiert den AB-Informationsfluss über farbkodierte Kanten:
     * - Rot: α-Update (Wert geht nach oben, MAX-Knoten)
     * - Blau: β-Update (Wert geht nach oben, MIN-Knoten)
     * - Propagation nach unten an offene Kinder in gleicher Farbe
     *
     * @param {number} nodeId - Knoten-ID
     * @param {number} [evaluatedChildId=null] - Kind das gerade evaluiert wurde
     * @private
     */
    _recomputeBounds(nodeId, evaluatedChildId = null) {
        const node = this.treeStructure.get(nodeId);
        if (!node) return;

        // Alte Bounds sichern für Änderungserkennung
        const oldAlpha = node.alpha;
        const oldBeta = node.beta;

        let alpha = node.inheritedAlpha !== undefined ? node.inheritedAlpha : -Infinity;
        let beta = node.inheritedBeta !== undefined ? node.inheritedBeta : Infinity;

        const evaluatedChildren = node.children
            .map(cid => this.treeStructure.get(cid))
            .filter(c => c && c.status === 'EVALUATED');

        if (node.isMaximizing && evaluatedChildren.length > 0) {
            const maxVal = Math.max(...evaluatedChildren.map(c => c.value));
            alpha = Math.max(alpha, maxVal);
        }
        if (!node.isMaximizing && evaluatedChildren.length > 0) {
            const minVal = Math.min(...evaluatedChildren.map(c => c.value));
            beta = Math.min(beta, minVal);
        }

        node.alpha = alpha;
        node.beta = beta;

        // ---- AB-Propagation Edge-Highlighting ----
        const alphaChanged = node.alpha > oldAlpha;
        const betaChanged = node.beta < oldBeta;

        if (evaluatedChildId !== null && (alphaChanged || betaChanged)) {
            // Edge vom evaluierten Kind zum Elternknoten (Wert geht nach oben)
            const propColor = alphaChanged
                ? this.constants.edgeCutoffAlpha
                : this.constants.edgeCutoffBeta;
            this.commands.push({
                action: 'HIGHLIGHT_EDGE',
                from: nodeId,
                to: evaluatedChildId,
                color: propColor,
                width: this.constants.edgeCutoffWidth,
                showArrow: true,
                arrowDirection: 'from',  // Kind → Eltern (Wert fließt nach oben)
            });

            // Propagation nach unten: Bounds rekursiv an alle offenen Nachkommen
            node.children.forEach(childId => {
                if (childId === evaluatedChildId) return;
                const child = this.treeStructure.get(childId);
                if (!child || child.status === 'EVALUATED' || child.status === 'PRUNED') return;
                this.commands.push({
                    action: 'HIGHLIGHT_EDGE',
                    from: nodeId,
                    to: childId,
                    color: propColor,
                    width: this.constants.edgeCutoffWidth,
                    showArrow: true,
                    arrowDirection: 'to',
                });
            });
        }

        // Metadata-Update an Visualisierung senden
        this.commands.push({
            action: 'UPDATE_NODE',
            id: nodeId,
            data: {
                metadata: {
                    depth: node.depth,
                    isMaximizing: node.isMaximizing,
                    value: node.value,
                    alpha: node.alpha,
                    beta: node.beta,
                },
            },
        });

        // Bounds an offene Kinder propagieren
        node.children.forEach(childId => {
            const child = this.treeStructure.get(childId);
            if (!child || child.status === 'EVALUATED' || child.status === 'PRUNED') return;

            child.inheritedAlpha = node.alpha;
            child.inheritedBeta = node.beta;
            child.alpha = node.alpha;
            child.beta = node.beta;

            this.commands.push({
                action: 'UPDATE_NODE',
                id: childId,
                data: {
                    metadata: {
                        depth: child.depth,
                        isMaximizing: child.isMaximizing,
                        value: child.value,
                        alpha: child.alpha,
                        beta: child.beta,
                    },
                },
            });
        });
    }

    /**
     * Markiert einen Teilbaum rekursiv als PRUNED.
     *
     * @param {number} nodeId - Start-Knoten
     * @param {number|null} parentId - Elternknoten (für Kanten-Highlighting)
     * @returns {number} Anzahl neu geprunter Knoten
     * @private
     */
    _pruneSubtree(nodeId, parentId) {
        const node = this.treeStructure.get(nodeId);
        if (!node) return 0;
        if (node.status === 'EVALUATED') return 0;

        let count = 0;
        if (node.status !== 'PRUNED') {
            // Wert beibehalten für Anzeige im Renderer (nicht auf null setzen)
            NodeStatusManager.setNodeStatus(nodeId, 'PRUNED', [], this.treeStructure, this.commands);

            // Pruned-Metadata aktualisieren für Renderer
            this.commands.push({
                action: 'UPDATE_NODE',
                id: nodeId,
                data: {
                    metadata: {
                        depth: node.depth,
                        isMaximizing: node.isMaximizing,
                        value: node.value,
                        isPruned: true,
                    },
                },
            });

            // Kante als gepruned markieren
            if (parentId !== null) {
                this.commands.push({
                    action: 'HIGHLIGHT_EDGE',
                    from: parentId,
                    to: nodeId,
                    color: this.constants.edgePrunedColor,
                    width: this.constants.edgePrunedWidth,
                });
            }

            count = 1;
        }

        node.children.forEach(childId => {
            count += this._pruneSubtree(childId, nodeId);
        });

        return count;
    }

    // ========================================================================
    // EXPANSION (not used in generic mode, but required by BaseTreeAdapter)
    // ========================================================================

    /**
     * Nicht verwendet im generischen Modus (Baum wird vollständig aufgebaut).
     * @param {number} nodeId
     * @param {*} state
     */
    expandNodeChildren(nodeId, state) {
        // No-op: Generic trees are fully built before visualization
    }
}