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

/**
 * @fileoverview Base Tree Adapter - Basisklasse für Tree-Visualisierungs-Adapter
 * 
 * Enthält gemeinsame Infrastruktur für:
 * - IFrame-Kommunikation via IframeBridgeHost (bevorzugt) oder Legacy-postMessage (Fallback)
 * - State Management (NodeMap, Commands)
 * - Grundlegende Tree-Erstellung (createNode)
 * - Handshake & Reset
 * 
 * @class BaseTreeAdapter
 * @author Alexander Wolf
 * @version 2.0
 */
class BaseTreeAdapter {
    /**
     * @param {HTMLIFrameElement} iframeElement 
     */
    constructor(iframeElement) {
        this.iframe = iframeElement;
        this.nodeIdCounter = 0;
        this.nodeMap = new Map();
        this.ready = false;
        this.commands = [];
        
        // State
        this.currentGameState = null;
        this.currentConfig = null;
        this.nodeStates = new Map(); // id -> GameState
        this.treeStructure = new Map(); // id -> { parentId, childrenIds[], status, value, ... }
        this.rootPlayer = null;

        // Stats & Callbacks (gemeinsam für alle Adapter)
        /** @type {{nodesVisited: number, nodesPruned: number, evaluatedNodes: number}} */
        this.stats = {
            nodesVisited: 0,
            nodesPruned: 0,
            evaluatedNodes: 0,
        };
        /** @type {Function|null} Callback bei Statistik-Änderungen */
        this.onStatsChanged = null;
        /** @type {Function|null} Callback bei aktivem Knoten-Wechsel */
        this.onActiveNodeChanged = null;
        /** @type {Function|null} Callback bei Knoten-Klick aus TreeViz (nodeId, boardData) */
        this.onNodeClicked = null;
        /** @type {Function|null} Callback bei Expansion-Klick aus TreeViz (nodeId, boardData) */
        this.onNodeFocused = null;

        // Debug-Konfiguration (überschreibbar in Subklassen)
        /** @type {string} Debug-Domain-Key für DebugConfig */
        this._debugDomain = 'VIZ_TREE_ADAPTER';
        /** @type {string} Log-Prefix für diesen Adapter */
        this._debugPrefix = 'TreeAdapter';

        // ==================== BRIDGE INTEGRATION ====================
        /** @type {IframeBridgeHost|null} Bridge-Host-Instanz (wenn verfügbar) */
        this._bridge = null;
        /** @type {boolean} Bridge ist aktiv und hat Handshake abgeschlossen */
        this._bridgeActive = false;

        if (typeof IframeBridgeHost !== 'undefined' && this.iframe) {
            this._initBridge();
        } else {
            // Legacy-Fallback: Direktes postMessage
            this._debugLog('IframeBridgeHost not available — using legacy postMessage');
            this._initLegacyListener();
        }

        this.startHandshake();
    }

    /** @private Timeout für Handshake-Fallback (ms) */
    static HANDSHAKE_TIMEOUT_MS = 500;

    /**
     * Initialisiert die IframeBridgeHost-Instanz für die Kommunikation mit dem TreeViz-iframe.
     * @private
     */
    _initBridge() {
        try {
            this._bridge = new IframeBridgeHost(this.iframe, {
                sourceId: `tree-adapter-${this._debugPrefix.toLowerCase()}`,
                acceptLegacy: true,
                handshakeTimeout: 3000
            });

            // TREE:READY (Bridge-konform) oder SYSTEM:READY (auto-converted)
            this._bridge.on('SYSTEM:READY', () => {
                this._debugLog('Bridge: SYSTEM:READY received — adapter ready');
                this._bridgeActive = true;
                this.onTreeReady();
            });

            // TREE:NODE_CLICKED via Bridge
            this._bridge.on('TREE:NODE_CLICKED', (payload) => {
                this._debugLog('Bridge: NODE_CLICKED', { nodeId: payload?.nodeId });
                this.handleNodeClick(payload.nodeId);
                if (typeof this.onNodeClicked === 'function') {
                    this.onNodeClicked(payload.nodeId, payload.boardData);
                }
            });

            // NODE:CLICKED (Tree-Viz-Engine sendet mit NODE: Namespace)
            this._bridge.on('NODE:CLICKED', (payload) => {
                this._debugLog('Bridge: NODE:CLICKED', { nodeId: payload?.nodeId });
                this.handleNodeClick(payload.nodeId);
                if (typeof this.onNodeClicked === 'function') {
                    this.onNodeClicked(payload.nodeId, payload.boardData);
                }
            });

            // TREE:NODE_FOCUSED via Bridge
            this._bridge.on('TREE:NODE_FOCUSED', (payload) => {
                this._debugLog('Bridge: NODE_FOCUSED', { nodeId: payload?.nodeId });
                if (typeof this.onNodeFocused === 'function') {
                    this.onNodeFocused(payload.nodeId, payload.boardData);
                }
            });

            // NODE:FOCUSED (Tree-Viz-Engine sendet mit NODE: Namespace)
            this._bridge.on('NODE:FOCUSED', (payload) => {
                this._debugLog('Bridge: NODE:FOCUSED', { nodeId: payload?.nodeId });
                if (typeof this.onNodeFocused === 'function') {
                    this.onNodeFocused(payload.nodeId, payload.boardData);
                }
            });

            // TREE:EXPANSION_REQUEST via Bridge
            this._bridge.on('TREE:EXPANSION_REQUEST', (payload) => {
                this._debugLog('Bridge: EXPANSION_REQUEST', { nodeId: payload?.nodeId });
                this.handleExpansionRequest(payload.nodeId);
            });

            this._debugLog('IframeBridgeHost initialized', { sourceId: this._bridge.sourceId });
        } catch (err) {
            this._debugLog('Bridge initialization failed — falling back to legacy', { error: err.message }, 'warn');
            this._bridge = null;
            this._initLegacyListener();
        }
    }

    /**
     * Initialisiert den Legacy-Message-Listener (Fallback ohne Bridge).
     * @private
     */
    _initLegacyListener() {
        this._messageListener = (event) => {
            if (!event.data) return;
            // Bridge-Messages ignorieren
            if (event.data._bridge) return;
            this.handleMessage(event.data);
        };
        window.addEventListener('message', this._messageListener);
    }

    /**
     * Entfernt Event-Listener und räumt Ressourcen auf.
     */
    destroy() {
        if (this._bridge) {
            this._bridge.destroy();
            this._bridge = null;
        }
        if (this._messageListener) {
            window.removeEventListener('message', this._messageListener);
            this._messageListener = null;
        }
    }

    /**
     * Verarbeitet eingehende Nachrichten vom Visualizer
     * @param {Object} data 
     */
    handleMessage(data) {
        this._debugLog('Message from iframe received', { type: data?.type });
        if (data.type === 'TREE_READY') {
            this._debugLog('TREE_READY received - adapter ready for communication');
            this.onTreeReady();
        }
        else if (data.type === 'NODE_EXPANSION_REQUEST') {
            this._debugLog('NODE_EXPANSION_REQUEST from iframe', { nodeId: data?.nodeId });
            this.handleExpansionRequest(data.nodeId);
        }
        else if (data.type === 'NODE_CLICKED') {
            this._debugLog('NODE_CLICKED from iframe', { nodeId: data?.nodeId });
            this.handleNodeClick(data.nodeId);
            if (typeof this.onNodeClicked === 'function') {
                this.onNodeClicked(data.nodeId, data.boardData);
            }
        }
        else if (data.type === 'NODE_FOCUSED') {
            this._debugLog('NODE_FOCUSED from iframe', { nodeId: data?.nodeId });
            if (typeof this.onNodeFocused === 'function') {
                this.onNodeFocused(data.nodeId, data.boardData);
            }
        }
    }

    /**
     * Startet den Handshake mit dem IFrame.
     * Setzt ready-Flag als Fallback nach Timeout.
     */
    startHandshake() {
        setTimeout(() => { this.ready = true; }, BaseTreeAdapter.HANDSHAKE_TIMEOUT_MS);
    }
    
    /**
     * Wird aufgerufen wenn das IFrame TREE_READY meldet.
     * @protected
     */
    onTreeReady() {
        this.ready = true;
        // Default impl: do nothing or send config
    }

    /**
     * Sendet einen einzelnen Command an das IFrame.
     * Nutzt IframeBridgeHost wenn verfügbar, sonst Legacy-postMessage.
     * @param {Object} command - Command-Objekt mit action-Property
     */
    sendCommand(command) {
        if (!this.iframe || !this.iframe.contentWindow) {
            this._debugLog('sendCommand: Iframe not available', { action: command?.action }, 'error');
            return;
        }

        // Bridge bevorzugen
        if (this._bridge) {
            this._debugLog('sendCommand via Bridge', { action: command?.action });
            this._bridge.send('TREE:COMMAND', command);
            return;
        }

        // Legacy-Fallback
        this._debugLog('sendCommand via postMessage (legacy)',
            { action: command?.action, hasCommands: !!command?.commands?.length });
        this.iframe.contentWindow.postMessage({ type: 'TREE_COMMAND', command }, '*');
    }

    /**
     * Sendet alle gepufferten Commands als Batch und leert den Puffer.
     */
    flushCommands() {
        if (this.commands.length > 0) {
            this._debugLog('flushCommands: Sending batch',
                { commandCount: this.commands.length, batch: true });
            this.sendCommand({ action: 'BATCH', commands: this.commands });
            this.commands = [];
        }
    }

    /**
     * Erzeugt einen eindeutigen Schlüssel für ein Board/State-Objekt.
     * @param {Object} board - Board- oder State-Objekt
     * @returns {string} Eindeutiger Schlüssel
     */
    getBoardKey(board) {
        return board.getStateKey ? board.getStateKey() : JSON.stringify(board.grid || board);
    }
    
    /**
     * Setzt den Adapter-Zustand zurück und sendet CLEAR an das IFrame.
     */
    reset() {
        this._debugLog('Reset: clearing state and sending CLEAR command');
        this.nodeIdCounter = 0;
        this.nodeMap.clear();
        this.treeStructure.clear();
        this.nodeStates.clear();
        this.commands = [];
        this.sendCommand({ action: 'CLEAR' });
        
        // Config senden via Template Method
        const config = this.getInitialConfig();
        if (config) {
            this._debugLog('Sending initial config', { config });
            this.sendCommand({ 
                action: 'UPDATE_CONFIG', 
                config: config 
            });
        }
    }

    /**
     * Liefert Initiale Config (überschreibbar)
     */
    getInitialConfig() {
        return {};
    }

    /**
     * Erstellt einen Basis-Knoten und registriert ihn.
     *
     * Unterstützte boardTypes:
     * - 'minimax': TicTacToe-Grid Darstellung (default für Rückwärtskompatibilität)
     * - 'numeric': Kreis mit Zahlenwert (Diagramm-Modus, Lehrbuch-Darstellung)
     * - undefined:  Auto-Detection anhand boardData-Struktur (Knights-Tour, RotateBox)
     *
     * @param {GameState|Object} state - Spielzustand oder generisches Datenobjekt
     * @param {number|null} parentId
     * @param {Object} metadata - Metadaten (value, isMaximizing, alpha, beta, etc.)
     * @param {string} status
     * @param {string} [boardType='minimax'] - Expliziter boardType für Renderer-Routing
     */
    createNode(state, parentId, metadata, status = 'WAIT', boardType = 'minimax') {
        const nodeId = this.nodeIdCounter++;
        const stateKey = this.getBoardKey(state);
        this.nodeMap.set(stateKey, nodeId);

        // boardData-Erstellung abhängig vom boardType
        let boardData;
        if (boardType === 'numeric') {
            // Numerischer Modus: Minimalistische boardData mit Wert
            boardData = {
                value: metadata?.value ?? null,
            };
        } else {
            // Standard-Modus (minimax/andere): Grid-basierte boardData
            boardData = {
                grid: [...(state.grid || state)],
                currentPlayer: state.currentPlayer,
                size: state.size || 3,
                winner: state.winner
            };
        }

        const command = {
            action: 'ADD_NODE',
            id: nodeId,
            label: "",
            boardData: boardData,
            boardType: boardType,
            metadata: { ...metadata },
            status: status
        };
        
        if (parentId !== null) command.parentId = parentId;
        
        this.commands.push(command);
        return nodeId;
    }

    handleExpansionRequest(nodeId) {
        if (this.nodeStates.has(nodeId)) {
             this.expandNodeChildren(nodeId, this.nodeStates.get(nodeId));
        }
        this.flushCommands();
    }

    // ========================================================================
    // GEMEINSAME METHODEN (geerbt von allen Subklassen)
    // ========================================================================

    /**
     * Einheitlicher Debug-Logger für alle Tree-Adapter.
     * Subklassen setzen this._debugDomain und this._debugPrefix im Konstruktor.
     *
     * @param {string} message - Log-Nachricht
     * @param {Object} [payload] - Optionaler Payload
     * @param {string} [level='debug'] - Log-Level ('debug', 'warn', 'error')
     * @protected
     */
    _debugLog(message, payload, level = 'debug') {
        const domain = typeof DEBUG_DOMAINS !== 'undefined' && DEBUG_DOMAINS[this._debugDomain]
            ? DEBUG_DOMAINS[this._debugDomain]
            : this._debugDomain;
        if (payload !== undefined) {
            DebugConfig.log(domain, level, `[${this._debugPrefix}] ${message}`, payload);
        } else {
            DebugConfig.log(domain, level, `[${this._debugPrefix}] ${message}`);
        }
    }

    /**
     * Benachrichtigt UI über Statistik-Änderungen.
     * @protected
     */
    _notifyStatsChanged() {
        this._debugLog('stats-update', {
            visited: this.stats.nodesVisited,
            pruned: this.stats.nodesPruned,
            evaluated: this.stats.evaluatedNodes,
        });
        if (typeof this.onStatsChanged !== 'function') return;
        this.onStatsChanged({ ...this.stats });
    }

    /**
     * Prüft ob ein Blattknoten terminal ist.
     * Überschreibbar in Subklassen für spielspezifische Terminal-Erkennung.
     *
     * @param {number} nodeId - Knoten-ID
     * @param {Object} data - treeStructure-Eintrag
     * @returns {boolean}
     * @protected
     */
    _isLeafTerminal(nodeId, data) {
        return data.isTerminal === true;
    }

    /**
     * Prüft und aktualisiert den Status eines Knotens.
     * - Blattknoten (terminal) → READY
     * - Innerer Knoten (alle Kinder EVALUATED/PRUNED) → READY
     * - Sonst → WAIT
     *
     * @param {number} nodeId - Zu prüfende Knoten-ID
     */
    checkNodeStatus(nodeId) {
        const data = this.treeStructure.get(nodeId);
        if (!data) return;
        if (data.status === 'EVALUATED' || data.status === 'PRUNED') return;

        let newStatus = 'WAIT';

        if (data.children.length === 0) {
            newStatus = this._isLeafTerminal(nodeId, data) ? 'READY' : 'WAIT';
        } else {
            const allDone = data.children.every(childId => {
                const child = this.treeStructure.get(childId);
                return child && (child.status === 'EVALUATED' || child.status === 'PRUNED');
            });
            newStatus = allDone ? 'READY' : 'WAIT';
        }

        if (newStatus !== data.status) {
            NodeStatusManager.setNodeStatus(nodeId, newStatus, [], this.treeStructure, this.commands);
        }
    }

    /**
     * Verarbeitet Klick auf einen Knoten.
     * Nur READY-Knoten können evaluiert werden.
     *
     * @param {number} nodeId - Geklickte Knoten-ID
     */
    handleNodeClick(nodeId) {
        const data = this.treeStructure.get(nodeId);
        if (!data || data.status !== 'READY') return;

        this._debugLog('handleNodeClick', { nodeId, status: data.status });

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

        // Vorherige Kanten-Highlights zurücksetzen (AB-Propagation etc.)
        this.sendCommand({ action: 'RESET_EDGE_HIGHLIGHTS' });

        this.commands = [];
        NodeStatusManager.setNodeStatus(nodeId, 'ACTIVE', [], this.treeStructure, this.commands);
        this.flushCommands();
        this.evaluateNode(nodeId);
    }

    /**
     * Markiert die beste(n) Kante(n) eines inneren Knotens.
     * Nutzt _getHighlightEdgeStyle() als Hook für adaptersspezifische Farben/Breiten.
     *
     * @param {number} nodeId - Elternknoten
     * @param {number} bestValue - Bester Wert (MAX oder MIN)
     * @protected
     */
    _highlightBestEdges(nodeId, bestValue) {
        const data = this.treeStructure.get(nodeId);
        if (!data) return;

        const style = this._getHighlightEdgeStyle(bestValue);
        data.children.forEach(childId => {
            const child = this.treeStructure.get(childId);
            if (!child || child.value !== bestValue) return;
            this.commands.push({
                action: 'HIGHLIGHT_EDGE',
                from: nodeId,
                to: childId,
                color: style.color,
                width: style.width,
                showArrow: true,
                arrowDirection: 'from',  // Kind → Eltern (bester Wert wurde gewählt)
            });
        });
    }

    /**
     * Liefert Farbe und Breite für Best-Edge-Highlighting.
     * Überschreibbar in Subklassen für spezifische Farblogik.
     *
     * @param {number} bestValue - Bester Wert des Knotens
     * @returns {{color: string, width: number}}
     * @protected
     */
    _getHighlightEdgeStyle(bestValue) {
        return { color: '#27ae60', width: 3 };
    }

    /**
     * Markiert rekursiv alle Kanten eines Teilbaums ab parentId nach unten.
     * Folgt allen offenen (nicht EVALUATED/PRUNED) Nachkommen.
     * Wird für AB-Propagation-Pfeile verwendet.
     *
     * @param {number} parentId - Startknoten
     * @param {string} color - Kantenfarbe
     * @param {number} width - Kantenbreite
     * @protected
     */
    _highlightPropagationSubtree(parentId, color, width) {
        const parent = this.treeStructure.get(parentId);
        if (!parent || !parent.children) return;

        parent.children.forEach(childId => {
            const child = this.treeStructure.get(childId);
            if (!child || child.status === 'EVALUATED' || child.status === 'PRUNED') return;
            this.commands.push({
                action: 'HIGHLIGHT_EDGE',
                from: parentId,
                to: childId,
                color: color,
                width: width,
                showArrow: true,
                arrowDirection: 'to',
            });
            // Rekursiv in Kinder absteigen
            this._highlightPropagationSubtree(childId, color, width);
        });
    }

    // Abstract/Empty methods to be implemented by subclasses
    expandNodeChildren(nodeId, state) {}
    evaluateNode(nodeId) {}
}