games/rotatebox/controller.js

/**
 * Controller RotateBox.
 * @fileoverview 
 */
const RotateController = {
    currentBoard: null,
    optimalPath: [],
    isOffPath: false,
    isAnimating: false,
    canvas: null,
    ctx: null,
    /** @type {IframeBridgeClient|null} Bridge-Client für iframe-Kommunikation */
    bridge: null,
    /** @type {boolean} true wenn Bridge-Handshake (CONFIG:INIT) erfolgreich war */
    _bridgeActive: false,

    checkUrlParams() {
        const params = new URLSearchParams(window.location.search);
        
        // Level parameter
        const level = params.get('level');
        if (level !== null) {
            const selectInfo = document.getElementById('boardSelect');
            if (selectInfo && selectInfo.querySelector(`option[value="${level}"]`)) {
                selectInfo.value = level;
            }
        }

        // hide_ai: Versteckt KI-Lösen Button
        if (params.has('hide_ai') || params.get('hide_ai') === 'true') {
            const solveBtn = document.getElementById('solveBtn');
            if (solveBtn) solveBtn.style.display = 'none';
            const hr = document.querySelector('.stats-panel hr');
            if (hr) hr.style.display = 'none';
        }
        
        // hideControls: Versteckt gesamtes Menü
        if (params.has('hideControls') || params.get('hideControls') === 'true') {
            const menu = document.getElementById('menu');
            if (menu) menu.style.display = 'none';
        }
        
        // hideLevelSelect: Versteckt nur die Level-Auswahl
        if (params.has('hideLevelSelect') || params.get('hideLevelSelect') === 'true') {
            const boardSelect = document.getElementById('boardSelect');
            if (boardSelect) {
                const group = boardSelect.closest('.control-group');
                if (group) group.style.display = 'none';
            }
        }
        
        // hideReset: Versteckt Reset Button
        if (params.has('hideReset') || params.get('hideReset') === 'true') {
            const resetBtn = document.getElementById('resetBtn');
            if (resetBtn) resetBtn.style.display = 'none';
        }
        
        // hideAnimation: Versteckt Animation Checkbox
        if (params.has('hideAnimation') || params.get('hideAnimation') === 'true') {
            const animToggle = document.getElementById('animateToggle');
            if (animToggle) {
                const group = animToggle.closest('.control-group');
                if (group) group.style.display = 'none';
            }
        }
        
        // hideInstructions: Versteckt die Spielbeschreibung
        if (params.has('hideInstructions') || params.get('hideInstructions') === 'true') {
            const instructions = document.querySelector('.instructions');
            if (instructions) instructions.style.display = 'none';
        }

        // hideBackBtn: Versteckt den Zurück Button
        if (params.has('hideBackBtn') || params.get('hideBackBtn') === 'true') {
            const backBtn = document.getElementById('backToMenu');
            if (backBtn) backBtn.style.display = 'none';
        }
    },

    init() {
        this.canvas = document.getElementById('gameCanvas');
        this.ctx = this.canvas.getContext('2d');

        this.checkUrlParams();

        document.getElementById('boardSelect').onchange = () => this.reset();
        document.getElementById('resetBtn').onclick = () => this.reset();
        document.getElementById('solveBtn').onclick = () => this.runAISolver();
        
        const animBtn = document.getElementById('animateBtn');
        if (animBtn) animBtn.onclick = () => this.playSolution();

        window.addEventListener('keydown', (e) => {
            if (["ArrowLeft", "ArrowRight"].includes(e.key)) e.preventDefault();
            if (e.key === 'ArrowLeft') this.handleMove(false);
            if (e.key === 'ArrowRight') this.handleMove(true);
        });

        // Bridge-Integration: Im iframe auf CONFIG:INIT warten statt sofort zu rendern
        if (window.parent !== window && typeof IframeBridgeClient !== 'undefined') {
            this._initBridge();
        } else {
            this.reset();
        }
    },

    /**
     * Initialisiert den IframeBridgeClient für die Kommunikation mit dem Host.
     * Wartet auf CONFIG:INIT und fällt bei Timeout auf URL-Parameter zurück.
     * @private
     */
    _initBridge() {
        this.bridge = new IframeBridgeClient({
            sourceId: 'rotatebox-game',
            clientType: 'game',
            acceptLegacy: true
        });

        // CONFIG:INIT — komplette Konfiguration vom Host
        this.bridge.on('CONFIG:INIT', (config) => {
            this._bridgeActive = true;
            clearTimeout(this._bridgeTimeout);
            this._applyConfig(config);
            this.reset();
        });

        // CONFIG:UPDATE — partielle Änderung (z.B. KI-Button einblenden)
        this.bridge.on('CONFIG:UPDATE', (config) => {
            this._applyConfig(config);
        });

        // INPUT:KEYBOARD — Tastatureingaben vom Host (Bridge + Legacy-Konvertierung)
        this.bridge.on('INPUT:KEYBOARD', (payload) => {
            if (payload.key === 'ArrowLeft') this.handleMove(false);
            else if (payload.key === 'ArrowRight') this.handleMove(true);
        });

        // Fallback: Wenn kein CONFIG:INIT kommt (Legacy-Host), URL-Parameter verwenden
        this._bridgeTimeout = setTimeout(() => {
            if (!this._bridgeActive) {
                this.reset();
            }
        }, 1000);
    },

    /**
     * Wendet eine Konfiguration aus CONFIG:INIT oder CONFIG:UPDATE an.
     * Steuert UI-Sichtbarkeit und Runtime-Parameter.
     *
     * @private
     * @param {Object} config - Konfigurationsobjekt
     * @param {Object} [config.ui] - UI-Sichtbarkeit (hideAi, hideLevelSelect, etc.)
     * @param {Object} [config.runtime] - Runtime-Parameter (level, etc.)
     */
    _applyConfig(config) {
        if (!config) return;

        // Runtime-Parameter
        if (config.runtime && config.runtime.level !== undefined) {
            const select = document.getElementById('boardSelect');
            if (select && select.querySelector(`option[value="${config.runtime.level}"]`)) {
                select.value = String(config.runtime.level);
            }
        }

        // UI-Sichtbarkeit
        if (config.ui) {
            if (config.ui.hideAi !== undefined) {
                const solveBtn = document.getElementById('solveBtn');
                const hr = document.querySelector('.stats-panel hr');
                if (solveBtn) solveBtn.style.display = config.ui.hideAi ? 'none' : '';
                if (hr) hr.style.display = config.ui.hideAi ? 'none' : '';
            }
            if (config.ui.hideLevelSelect !== undefined) {
                const boardSelect = document.getElementById('boardSelect');
                if (boardSelect) {
                    const group = boardSelect.closest('.control-group');
                    if (group) group.style.display = config.ui.hideLevelSelect ? 'none' : '';
                }
            }
            if (config.ui.hideControls !== undefined) {
                const menu = document.getElementById('menu');
                if (menu) menu.style.display = config.ui.hideControls ? 'none' : '';
            }
            if (config.ui.hideReset !== undefined) {
                const resetBtn = document.getElementById('resetBtn');
                if (resetBtn) resetBtn.style.display = config.ui.hideReset ? 'none' : '';
            }
            if (config.ui.hideAnimation !== undefined) {
                const animToggle = document.getElementById('animateToggle');
                if (animToggle) {
                    const group = animToggle.closest('.control-group');
                    if (group) group.style.display = config.ui.hideAnimation ? 'none' : '';
                }
            }
            if (config.ui.hideInstructions !== undefined) {
                const instructions = document.querySelector('.instructions');
                if (instructions) instructions.style.display = config.ui.hideInstructions ? 'none' : '';
            }
            if (config.ui.hideBackBtn !== undefined) {
                const backBtn = document.getElementById('backToMenu');
                if (backBtn) backBtn.style.display = config.ui.hideBackBtn ? 'none' : '';
            }
        }
    },

    reset() {
        const selector = document.getElementById('boardSelect');
        this.currentBoard = new RotateBoard(selector.value);
        this.optimalPath = []; 
        this.isOffPath = false;
        this.isAnimating = false;
      
        document.getElementById('winMessage').classList.add('hidden');
        document.getElementById('aiOutput').classList.add('hidden');
        document.getElementById('pathWarning').classList.add('hidden');
        document.getElementById('solutionPath').innerHTML = '';
        
        document.getElementById('solveBtn').disabled = false;
        document.getElementById('solveBtn').innerText = "KI Lösung suchen 🧠";
        
        this.updateStats();
        this.render();
    },

    async handleMove(isRight) {
        if (!this.currentBoard || this.currentBoard.won || this.isAnimating) return;
        
        const moveChar = isRight ? 'R' : 'L';
        this.currentBoard.rotate(isRight);

        // Pfad-Check
        if (this.optimalPath.length > 0 && !this.isOffPath) {
            // moves ist 1-basiert, Array ist 0-basiert
            if (moveChar !== this.optimalPath[this.currentBoard.moves - 1]) {
                this.isOffPath = true;
                document.getElementById('pathWarning').classList.remove('hidden');
            }
        }

        // --- HIGHLIGHTING LOGIK ---
        this.updatePathHighlighting();

        const useAnimation = document.getElementById('animateToggle').checked;
        if (useAnimation) {
            this.isAnimating = true;
            await animateRelax(this.currentBoard, this.canvas, this.ctx, 0.15, () => this.render());
            this.isAnimating = false;
        } else {
            this.currentBoard.relaxBoardSync(); 
            this.render();
        }
        
        // Spielstatus an Parent senden (Bridge oder Legacy)
        if (window.parent && window.parent !== window) {
            if (this._bridgeActive && this.bridge) {
                this.bridge.send('GAME:STATE', { rotationCount: this.currentBoard.moves });
            } else {
                window.parent.postMessage({ 
                    type: 'gameState',
                    rotationCount: this.currentBoard.moves
                }, '*');
            }
        }
        
        if (this.currentBoard.won) {
            document.getElementById('winMessage').classList.remove('hidden');
            if (window.parent && window.parent !== window) {
                if (this._bridgeActive && this.bridge) {
                    this.bridge.send('GAME:WON', { moves: this.currentBoard.moves });
                } else {
                    window.parent.postMessage({ type: 'gameWon', moves: this.currentBoard.moves }, '*');
                }
            }
        }
        this.updateStats();
        this.render();
    },

    updatePathHighlighting() {
        // 1. Alle entfernen
        document.querySelectorAll('.step-badge').forEach(el => el.classList.remove('active'));
        
        // 2. Aktuellen finden
        // Wir suchen das Badge mit der ID "step-X", wobei X der aktuelle Move-Count ist.
        // Bsp: Nach 1. Zug (moves=1) soll Badge "step-1" leuchten.
        const currentMove = this.currentBoard.moves;
        const activeBadge = document.getElementById(`step-${currentMove}`);
        
        if (activeBadge) {
            activeBadge.classList.add('active');
            // Auto-Scroll, damit das Badge sichtbar bleibt
            activeBadge.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
        }
    },

    render() {
        if (!this.currentBoard) return;
        drawRotateBoard(this.currentBoard, this.canvas, this.ctx);
    },
    
    updateStats() {
        document.getElementById('moveCount').innerText = this.currentBoard.moves;
    },

    async runAISolver() {
        this.reset(); // Board immer zurücksetzen, bevor KI-Lösung startet
        const btn = document.getElementById('solveBtn');
        btn.disabled = true;
        btn.innerText = "Rechne...";
        
        const simBoard = this.currentBoard.clone(); 
        const result = await solveBFS(simBoard);
        
        if (result) {
            this.optimalPath = result.path;
            
            document.getElementById('aiOutput').classList.remove('hidden');
            document.getElementById('stat-depth').innerText = result.path.length;
            document.getElementById('stat-nodes').innerText = result.nodes;
            document.getElementById('stat-duplicates').innerText = result.duplicates || 0;
            
            const pathDiv = document.getElementById('solutionPath');
            pathDiv.innerHTML = '';
            
            // Badges rendern
            // Start-Offset: Falls User schon 5 Züge gemacht hat, beginnt der Pfad bei Schritt 6.
            const startMove = this.currentBoard.moves;
            
            result.path.forEach((dir, i) => {
                const span = document.createElement('span');
                span.className = 'step-badge';
                // ID generieren: step-(Start + Index + 1)
                span.id = `step-${startMove + i + 1}`;
                span.innerText = dir;
                pathDiv.appendChild(span);
            });
            
            this.isOffPath = false;
            document.getElementById('pathWarning').classList.add('hidden');
            btn.innerText = "Lösung anzeigen";
        } else {
            alert("Keine Lösung gefunden!");
            btn.innerText = "Nichts gefunden";
        }
        
        btn.disabled = false;
    },

    async playSolution() {
        if (this.optimalPath.length === 0 || this.isAnimating) return;
        
        // Speichere die Lösung und UI-State, weil reset() sie löscht
        const savedPath = [...this.optimalPath];
        const aiOutputEl = document.getElementById('aiOutput');
        const solutionPathEl = document.getElementById('solutionPath');
        const savedSolutionHTML = solutionPathEl ? solutionPathEl.innerHTML : '';
        
        this.reset();
        await new Promise(r => setTimeout(r, 200));
        
        // Stelle die Lösung und UI wieder her
        this.optimalPath = savedPath;
        if (aiOutputEl) aiOutputEl.classList.remove('hidden');
        if (solutionPathEl) solutionPathEl.innerHTML = savedSolutionHTML;
        
        for (const move of this.optimalPath) {
            if(this.currentBoard.won) break;
            await this.handleMove(move === 'R');
            await new Promise(r => setTimeout(r, 250));
        }
    }
};

window.RotateController = RotateController;
window.onload = () => RotateController.init();