class GameMode {
    constructor(game) {
        this.game = game;
    }

    init() { }
    handleInput(row, col) { }
    checkMatches() { return []; }
    refill() { }
    hasPossibleMoves() { return true; }
}

class ClassicMode extends GameMode {
    constructor(game) {
        super(game);
        this.cols = 9;
        this.rows = 9;
        this.selectedGem = null;
        this.shuffles = 1;
        this.soloSwaps = 1;
        this.matchesMade = 0;
        this.score = 0;
        this.comboMultiplier = 1;
        this.nextShuffleThreshold = 5000;

        // Level System
        this.level = 1;
        this.matchesToNextLevel = 10;
        this.currentLevelProgress = 0;

        // Special Gems
        this.specialGemOnBoard = false;
        this.initialSpawnComplete = false;
    }

    init() {
        this.game.grid = Array(this.rows).fill().map(() => Array(this.cols).fill(null));
        this.specialGemOnBoard = false;
        this.initialSpawnComplete = false;

        // Try to load saved game
        if (this.loadGame()) {
            console.log("Game loaded from storage");
            // We still need to fill the board if it wasn't saved (we don't save the grid currently, just stats)
            // If we want to save the grid, that's more complex due to object references.
            // For now, let's just save the PROGRESS (Level, Score, etc) and generate a NEW board.
            // This prevents "stuck" boards from being permanent.
        }

        this.fillBoard();
        this.initialSpawnComplete = true;

        // Ensure no initial matches
        let safety = 0;
        while (this.findMatches().length > 0 && safety < 100) {
            this.game.grid = Array(this.rows).fill().map(() => Array(this.cols).fill(null));
            this.specialGemOnBoard = false;
            this.fillBoard();
            safety++;
        }
        this.updateUI();
    }

    saveGame() {
        const state = {
            level: this.level,
            score: this.score,
            matchesToNextLevel: this.matchesToNextLevel,
            currentLevelProgress: this.currentLevelProgress,
            shuffles: this.shuffles,
            soloSwaps: this.soloSwaps,
            nextShuffleThreshold: this.nextShuffleThreshold
        };
        localStorage.setItem('gemSwapSave', JSON.stringify(state));
    }

    loadGame() {
        const saved = localStorage.getItem('gemSwapSave');
        if (saved) {
            try {
                const state = JSON.parse(saved);
                this.level = state.level || 1;
                this.score = state.score || 0;
                this.matchesToNextLevel = state.matchesToNextLevel || 10;
                this.currentLevelProgress = state.currentLevelProgress || 0;
                this.shuffles = state.shuffles !== undefined ? state.shuffles : 1;
                this.soloSwaps = state.soloSwaps !== undefined ? state.soloSwaps : 1;
                this.nextShuffleThreshold = state.nextShuffleThreshold || 5000;
                return true;
            } catch (e) {
                console.error("Failed to load save", e);
                return false;
            }
        }
        return false;
    }

    fillBoard() {
        const allTypes = Object.values(GemType);
        const specialTypes = Object.values(SpecialType);

        for (let r = 0; r < this.rows; r++) {
            // Determine available types for this row (restrict bottom rows to prevent immediate stuck states)
            let availableTypes = allTypes;
            if (r === this.rows - 1) {
                availableTypes = allTypes.slice(0, 5);
            } else if (r === this.rows - 2) {
                availableTypes = allTypes.slice(0, 6);
            }

            for (let c = 0; c < this.cols; c++) {
                if (!this.game.grid[r][c]) {
                    let type;
                    let isSpecial = false;
                    let colorType = null;

                    // Chance to spawn special gem
                    // Only if: Initial spawn complete, Level >= 2, No special gem on board
                    if (this.initialSpawnComplete && this.level >= 2 && !this.specialGemOnBoard) {
                        // Base chance starts at 5% for Level 2
                        // Increase by 1% for each level above 2
                        let baseChance = 0.05 + (this.level - 2) * 0.01;

                        // Cap base chance at 15% to prevent it from becoming too common
                        baseChance = Math.min(baseChance, 0.15);

                        // Reduce by 50% for each combo level (multiplier > 1)
                        const chance = baseChance * Math.pow(0.5, this.comboMultiplier - 1);

                        if (Math.random() < chance) {
                            isSpecial = true;
                            type = specialTypes[Math.floor(Math.random() * specialTypes.length)];
                            // Assign a random color
                            colorType = allTypes[Math.floor(Math.random() * allTypes.length)];
                            this.specialGemOnBoard = true;
                        }
                    }

                    if (!isSpecial) {
                        let attempts = 0;
                        do {
                            type = availableTypes[Math.floor(Math.random() * availableTypes.length)];
                            attempts++;
                        } while (
                            attempts < 50 &&
                            ((r >= 2 && this.game.grid[r - 1][c]?.type === type && this.game.grid[r - 2][c]?.type === type) ||
                                (c >= 2 && this.game.grid[r][c - 1]?.type === type && this.game.grid[r][c - 2]?.type === type))
                        );
                    }

                    this.game.grid[r][c] = {
                        type: type,
                        colorType: colorType, // Null for standard gems
                        isSpecial: isSpecial,
                        row: r,
                        col: c,
                        id: Math.random().toString(36).substr(2, 9)
                    };
                }
            }
        }
    }

    handleSelection(row, col) {
        if (this.game.isLocked) return;
        const clickedGem = this.game.grid[row][col];

        // Debug Log for Special Gems
        if (clickedGem) {
            console.log(`Gem Clicked at [${row},${col}]:`, {
                type: clickedGem.type,
                colorType: clickedGem.colorType,
                isSpecial: clickedGem.isSpecial
            });
        }

        this.selectedGem = clickedGem;
        this.game.render();
    }

    attemptSwap(r1, c1, r2, c2) {
        if (this.game.isLocked) return;

        const gem1 = this.game.grid[r1][c1];
        const gem2 = this.game.grid[r2][c2];

        console.log('Attempting swap. Solo swaps before:', this.soloSwaps);
        this.game.swapGems(gem1, gem2);
        this.selectedGem = null;
    }

    handleInput(row, col) {
        if (this.game.isLocked) return;

        const clickedGem = this.game.grid[row][col];

        if (!this.selectedGem) {
            this.handleSelection(row, col);
        } else {
            const dRow = Math.abs(this.selectedGem.row - row);
            const dCol = Math.abs(this.selectedGem.col - col);

            if (dRow + dCol === 1) {
                this.game.swapGems(this.selectedGem, clickedGem);
                this.selectedGem = null;
            } else {
                if (this.selectedGem === clickedGem) {
                    this.selectedGem = null;
                } else {
                    this.handleSelection(row, col);
                }
                this.game.render();
            }
        }
    }

    findMatches() {
        let matches = [];
        const grid = this.game.grid;

        // Helper to check if 3 gems match (handling wildcards)
        const isMatch = (g1, g2, g3) => {
            if (!g1 || !g2 || !g3) return false;

            const gems = [g1, g2, g3];

            // If any gem is special, it acts as a wildcard.
            // We need to check if the NON-special gems share the same color.
            // Note: Standard gems have colorType === type (or just use type if colorType is missing/same)
            // Special gems have a colorType assigned, but they match ANY color in a line?
            // User said: "they can be matched with any of the 7 color gems"
            // This implies they are WILDCARDS, not just their assigned color.
            // But they DO have a color.
            // If they are wildcards, they should match [Red, Star(Orange), Red].
            // Should they match [Red, Star(Orange), Blue]? No, that's not a match.
            // So the rule is: All non-special gems in the set must match each other.

            const nonSpecialGems = gems.filter(g => !g.isSpecial);

            if (nonSpecialGems.length === 0) return true; // 3 Specials (rare but match)
            if (nonSpecialGems.length === 1) return true; // 2 Specials + 1 Normal (match)

            // Check if all non-special gems have the same effective color
            // For standard gems, effective color is their type (or colorType if we normalized it)
            // In fillBoard, standard gems have colorType: null. So we use type.
            const firstColor = nonSpecialGems[0].colorType || nonSpecialGems[0].type;

            for (let i = 1; i < nonSpecialGems.length; i++) {
                const color = nonSpecialGems[i].colorType || nonSpecialGems[i].type;
                if (color !== firstColor) return false;
            }

            return true;
        };

        // Horizontal
        for (let r = 0; r < this.rows; r++) {
            for (let c = 0; c < this.cols - 2; c++) {
                if (isMatch(grid[r][c], grid[r][c + 1], grid[r][c + 2])) {
                    matches.push(grid[r][c]);
                    matches.push(grid[r][c + 1]);
                    matches.push(grid[r][c + 2]);
                }
            }
        }

        // Vertical
        for (let c = 0; c < this.cols; c++) {
            for (let r = 0; r < this.rows - 2; r++) {
                if (isMatch(grid[r][c], grid[r + 1][c], grid[r + 2][c])) {
                    matches.push(grid[r][c]);
                    matches.push(grid[r + 1][c]);
                    matches.push(grid[r + 2][c]);
                }
            }
        }

        return [...new Set(matches)];
    }

    checkPotentialMatch(r1, c1, r2, c2) {
        const grid = this.game.grid;
        if (!grid[r1][c1] || !grid[r2][c2]) return false;

        // Temporarily swap
        const temp = grid[r1][c1];
        grid[r1][c1] = grid[r2][c2];
        grid[r2][c2] = temp;

        // Check for matches
        const matches = this.findMatches();

        // Swap back
        grid[r2][c2] = grid[r1][c1];
        grid[r1][c1] = temp;

        return matches.length > 0;
    }

    async processMatches() {
        let matches = this.findMatches();

        // Check for Special Gems at Bottom Row
        const bottomRow = this.rows - 1;
        for (let c = 0; c < this.cols; c++) {
            const gem = this.game.grid[bottomRow][c];
            if (gem && gem.isSpecial) {
                matches.push(gem);
            }
        }
        // Deduplicate matches
        matches = [...new Set(matches)];

        let totalMatches = 0;

        if (matches.length > 3) {
            this.comboMultiplier = 2;
        } else {
            this.comboMultiplier = 1;
        }

        while (matches.length > 0) {
            this.game.isLocked = true;
            totalMatches += matches.length;

            // Check if we are removing any special gems
            matches.forEach(gem => {
                if (gem.isSpecial) {
                    this.specialGemOnBoard = false;
                }
            });

            // Calculate Score
            const points = matches.length * 100 * this.comboMultiplier;
            this.addScore(points);

            if (this.comboMultiplier > 1) {
                this.showComboText(this.comboMultiplier);
            }

            this.comboMultiplier++;

            // Remove Gems
            await this.game.removeGems(matches);

            // Gravity & Refill
            this.game.applyGravity();
            this.fillBoard();

            // Render
            this.game.render();
            await new Promise(r => setTimeout(r, 400));

            // Update Level Progress
            this.currentLevelProgress += Math.floor(matches.length / 3);
            this.updateUI();

            if (this.currentLevelProgress >= this.matchesToNextLevel) {
                await this.handleLevelComplete();
                return;
            }

            matches = this.findMatches();

            // Check Bottom Row Specials again after gravity
            for (let c = 0; c < this.cols; c++) {
                const gem = this.game.grid[bottomRow][c];
                if (gem && gem.isSpecial) {
                    matches.push(gem);
                }
            }
            matches = [...new Set(matches)];
        }

        this.game.isLocked = false;
        this.checkGameState();
    }

    async handleLevelComplete() {
        this.game.isLocked = true;

        // Show Level Cleared Modal
        const modal = document.getElementById('level-cleared-modal');
        const text = document.getElementById('level-cleared-text');
        text.innerText = `LEVEL ${this.level} CLEARED!`;

        // Re-trigger animation
        text.style.animation = 'none';
        text.offsetHeight; /* trigger reflow */
        text.style.animation = null;

        modal.classList.remove('hidden');

        // Wait halfway through text animation (0.75s)
        await new Promise(r => setTimeout(r, 750));

        // Clear Board Animation (Explode/Fall off)
        // We can reuse removeGems on the whole grid
        const allGems = this.game.grid.flat().filter(g => g !== null);
        await this.game.removeGems(allGems, 'explode');

        // Update Level Stats
        this.level++;
        this.currentLevelProgress = 0;
        this.matchesToNextLevel += 1; // Increase difficulty by 1 match per level

        // Awards
        if (this.shuffles < 3) this.shuffles++;
        if (this.soloSwaps < 3) this.soloSwaps++;

        // Refill Board immediately (overlapping with end of removeGems animation)
        this.fillBoard();
        this.game.render(); // New gems appear and fall

        // Wait for fall animation (400ms) + a little extra for the text to finish reading
        await new Promise(r => setTimeout(r, 1000));

        // Hide Modal
        modal.classList.add('hidden');

        // Clear Undo History on new level
        this.lastMove = null;
        this.updateUndoUI();

        this.updateUI();
        this.game.isLocked = false;
        this.checkGameState();
    }

    addScore(points) {
        this.score += points;
        // Check for earned shuffle
        if (this.score >= this.nextShuffleThreshold) {
            this.shuffles++;
            this.nextShuffleThreshold += 5000;
            // Trigger UI notification (optional)
        }
        this.updateUI();
    }

    useShuffle() {
        if (this.shuffles > 0) {
            this.shuffles--;
            this.shuffleBoard();
            this.updateUI();
            this.checkGameState();
        }
    }

    shuffleBoard() {
        // Flatten, shuffle, reassign
        let gems = this.game.grid.flat().filter(g => g !== null);
        for (let i = gems.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            // Swap types AND IDs to ensure DOM elements move
            const tempType = gems[i].type;
            const tempId = gems[i].id;

            gems[i].type = gems[j].type;
            gems[i].id = gems[j].id;

            gems[j].type = tempType;
            gems[j].id = tempId;
        }

        this.game.render();
        setTimeout(() => this.processMatches(), 500); // Wait for shuffle anim
    }

    hasPossibleMoves() {
        // Clone grid to test swaps
        const grid = this.game.grid;
        const rows = this.rows;
        const cols = this.cols;

        // Helper to check if a swap results in a match
        const trySwap = (r1, c1, r2, c2) => {
            // Swap types
            let temp = grid[r1][c1].type;
            grid[r1][c1].type = grid[r2][c2].type;
            grid[r2][c2].type = temp;

            const hasMatch = this.findMatches().length > 0;

            // Swap back
            temp = grid[r1][c1].type;
            grid[r1][c1].type = grid[r2][c2].type;
            grid[r2][c2].type = temp;

            return hasMatch;
        };

        // Check all horizontal swaps
        for (let r = 0; r < rows; r++) {
            for (let c = 0; c < cols - 1; c++) {
                if (trySwap(r, c, r, c + 1)) return true;
            }
        }

        // Check all vertical swaps
        for (let r = 0; r < rows - 1; r++) {
            for (let c = 0; c < cols; c++) {
                if (trySwap(r, c, r + 1, c)) return true;
            }
        }

        return false;
    }

    checkGameState() {
        if (!this.hasPossibleMoves()) {
            if (this.shuffles > 0) {
                // Auto-prompt or just wait for user?
                // For now, let's just wait, but maybe highlight the shuffle button
                console.log("No moves! Shuffle required.");
            } else {
                this.game.gameOver(this.score);
            }
        }
    }

    updateUI() {
        document.getElementById('score').textContent = this.score;
        document.getElementById('level').textContent = this.level;

        // Update Level Progress Bar
        const progressPct = Math.min(100, (this.currentLevelProgress / this.matchesToNextLevel) * 100);
        const progressFill = document.getElementById('level-progress-bar');
        if (progressFill) {
            progressFill.style.width = `${progressPct}%`;
        }

        const progressBadge = document.getElementById('progress-badge');
        if (progressBadge) {
            progressBadge.textContent = this.currentLevelProgress;
        }

        // Update Counts (Buttons)
        const shuffleBtn = document.getElementById('shuffle-btn');
        if (shuffleBtn) {
            shuffleBtn.textContent = `SHUFFLE (${this.shuffles})`;
            if (this.shuffles === 0) {
                shuffleBtn.classList.add('disabled');
            } else {
                shuffleBtn.classList.remove('disabled');
            }
        }

        const soloBtn = document.getElementById('solo-swap-btn');
        if (soloBtn) {
            soloBtn.textContent = `SOLO SWAPS (${this.soloSwaps})`;
            if (this.soloSwaps === 0) {
                soloBtn.classList.add('disabled');
            } else {
                soloBtn.classList.remove('disabled');
            }
        }

        this.updateUndoUI();
        this.saveGame();
    }

    updateUndoUI() {
        const btn = document.getElementById('undo-btn');
        if (!btn) return; // Safety check

        if (this.lastMove) {
            btn.classList.remove('disabled');
        } else {
            btn.classList.add('disabled');
        }
    }

    highlightSoloSwap() {
        const btn = document.getElementById('solo-swap-btn');
        // Reset animation
        btn.classList.remove('solo-swap-anim');
        void btn.offsetWidth; // Force reflow
        btn.classList.add('solo-swap-anim');

        // Cleanup class after animation
        setTimeout(() => {
            btn.classList.remove('solo-swap-anim');
        }, 300);
    }

    async undoLastMove() {
        if (!this.lastMove || this.game.isLocked) return;

        if (this.lastMove.type === 'solo_swap') {
            const { gem1, gem2 } = this.lastMove;

            // Refund Solo Swap
            this.soloSwaps++;
            console.log('Undo performed. Solo swaps restored to:', this.soloSwaps);

            // Update UI immediately to reflect the restored count
            this.updateUI();

            // Perform reverse swap (no drag)
            await this.game.swapGems(gem1, gem2, true);

            // Clear undo
            this.lastMove = null;
            this.updateUI(); // Update again to clear undo button state
            this.highlightSoloSwap(); // Highlight refund
        }
    }

    showComboText(multiplier) {
        const comboEl = document.createElement('div');
        comboEl.className = 'combo-text';

        if (multiplier === 2) {
            comboEl.innerText = "Double Match!!";
            comboEl.style.color = "#fbbf24"; // Gold/Amber
            comboEl.style.fontSize = "2.5rem";
        } else {
            comboEl.innerText = `${multiplier}x Combo!`;
            // Scale color/size with combo
            if (multiplier >= 4) {
                comboEl.style.color = "#ef4444"; // Red
                comboEl.style.fontSize = "3rem";
            }
        }

        this.game.boardEl.appendChild(comboEl);

        // Animate
        requestAnimationFrame(() => {
            comboEl.style.opacity = '1';
            comboEl.style.transform = 'translate(-50%, -50%) scale(1.2)';
        });

        setTimeout(() => {
            comboEl.style.opacity = '0';
            comboEl.style.transform = 'translate(-50%, -100%) scale(1)';
            setTimeout(() => comboEl.remove(), 500);
        }, 1000);
    }
}

class GemDropGame {
    constructor() {
        this.grid = [];
        this.mode = new ClassicMode(this);
        this.isLocked = false;
        this.boardEl = document.getElementById('game-board');
        this.gemElements = new Map(); // Map ID -> DOM Element

        this.init();
    }

    init() {
        // Don't start game immediately. Show Main Menu.
        this.showMainMenu();
        this.setupInput();

        // Handle Resizing
        this.resizeGame();
        window.addEventListener('resize', () => this.resizeGame());

        // Prevent default touch actions (scrolling/zooming) globally
        document.addEventListener('touchmove', (e) => {
            e.preventDefault();
        }, { passive: false });

        // Prevent double-tap zoom
        document.addEventListener('dblclick', (e) => {
            e.preventDefault();
        });
    }

    resizeGame() {
        const container = document.getElementById('game-container');
        const baseWidth = 600; // Our design width
        const padding = 10; // Minimal padding for edge-to-edge feel on small screens

        // Reset transform to measure natural height
        container.style.transform = 'none';
        container.style.marginTop = '0px'; // Reset margin too

        const naturalHeight = container.scrollHeight;

        // Use clientWidth/Height for reliable viewport size excluding scrollbars
        const availableWidth = document.documentElement.clientWidth;
        const availableHeight = document.documentElement.clientHeight;

        const scaleX = (availableWidth - (padding * 2)) / baseWidth;
        const scaleY = (availableHeight - (padding * 2)) / naturalHeight;

        // Use the smaller scale to fit both dimensions
        const scale = Math.min(scaleX, scaleY, 1.2); // Cap max scale at 1.2x

        container.style.transform = `scale(${scale})`;

        // Center vertically if we have space
        const scaledHeight = naturalHeight * scale;
        if (scaledHeight < availableHeight) {
            const topOffset = (availableHeight - scaledHeight) / 2;
            // We use marginTop because transform-origin is top center
            container.style.marginTop = `${topOffset}px`;
        } else {
            container.style.marginTop = '0px';
        }
    }

    showMainMenu() {
        const menu = document.getElementById('main-menu');
        const continueBtn = document.getElementById('menu-continue-btn');

        // Check for save
        const hasSave = localStorage.getItem('gemSwapSave');
        if (hasSave) {
            continueBtn.classList.remove('disabled');
        } else {
            continueBtn.classList.add('disabled');
        }

        menu.classList.remove('hidden');
    }

    startNewGame() {
        // Clear save
        localStorage.removeItem('gemSwapSave');

        // Re-init mode (clears board, resets stats)
        this.mode = new ClassicMode(this);
        this.mode.init();
        this.render();

        // Hide menu
        document.getElementById('main-menu').classList.add('hidden');
    }

    continueGame() {
        if (!localStorage.getItem('gemSwapSave')) return;

        // If we haven't initialized the board yet (first load), do it now
        if (this.grid.length === 0) {
            this.mode.init();
            this.render();
        }

        document.getElementById('main-menu').classList.add('hidden');
    }

    setupInput() {
        // Menu Buttons
        document.getElementById('menu-new-game-btn').addEventListener('click', () => {
            this.startNewGame();
        });

        document.getElementById('menu-continue-btn').addEventListener('click', () => {
            this.continueGame();
        });

        document.getElementById('main-menu-btn').addEventListener('click', () => {
            this.showMainMenu();
        });

        document.getElementById('reload-app-btn').addEventListener('click', async () => {
            if ('serviceWorker' in navigator) {
                const registrations = await navigator.serviceWorker.getRegistrations();
                for (const registration of registrations) {
                    await registration.unregister();
                }
            }
            // Force reload ignoring cache
            window.location.reload(true);
        });

        let startX, startY, startRow, startCol;
        let isDragging = false;
        let activeGemEl = null;
        let currentTargetEl = null;
        let dragAxis = null; // 'x' or 'y'

        // Swap Helper State
        let lastDiffX = 0;
        let lastDiffY = 0;
        let potentialTargetRow = null;
        let potentialTargetCol = null;

        const handleStart = (e) => {
            const gemEl = e.target.closest('.gem');
            if (!gemEl || this.isLocked) return;

            isDragging = true;
            activeGemEl = gemEl;
            startRow = parseInt(gemEl.dataset.row);
            startCol = parseInt(gemEl.dataset.col);
            dragAxis = null;

            // Reset Helper State
            lastDiffX = 0;
            lastDiffY = 0;
            potentialTargetRow = null;
            potentialTargetCol = null;

            // Touch or Mouse
            const point = e.touches ? e.touches[0] : e;
            startX = point.clientX;
            startY = point.clientY;

            // Select the gem visually
            this.mode.handleSelection(startRow, startCol);

            // Disable transition during drag for instant follow
            activeGemEl.style.transition = 'none';
            activeGemEl.style.zIndex = 100;
        };

        const handleMove = (e) => {
            if (!isDragging || !activeGemEl || this.isLocked) return;
            e.preventDefault();

            const point = e.touches ? e.touches[0] : e;
            let diffX = point.clientX - startX;
            let diffY = point.clientY - startY;

            // Direction Locking
            if (!dragAxis) {
                if (Math.abs(diffX) > 10) {
                    dragAxis = 'x';
                } else if (Math.abs(diffY) > 10) {
                    dragAxis = 'y';
                } else {
                    return; // Wait for clear direction
                }
            }

            if (dragAxis === 'x') diffY = 0;
            if (dragAxis === 'y') diffX = 0;

            // Update Helper State
            lastDiffX = diffX;
            lastDiffY = diffY;

            // Visual Drag
            activeGemEl.style.transform = `translate(${diffX}px, ${diffY}px)`;

            // Determine potential target
            let targetRow = startRow;
            let targetCol = startCol;

            if (dragAxis === 'x') {
                targetCol += diffX > 0 ? 1 : -1;
            } else {
                targetRow += diffY > 0 ? 1 : -1;
            }

            // Check bounds
            if (targetRow >= 0 && targetRow < this.mode.rows &&
                targetCol >= 0 && targetCol < this.mode.cols &&
                (targetRow !== startRow || targetCol !== startCol)) {

                potentialTargetRow = targetRow;
                potentialTargetCol = targetCol;

                const targetGem = this.grid[targetRow][targetCol];
                if (targetGem) {
                    const targetEl = this.gemElements.get(targetGem.id);

                    // If target changed, reset old one
                    if (currentTargetEl && currentTargetEl !== targetEl) {
                        currentTargetEl.style.transform = '';
                        currentTargetEl.style.transition = '';
                    }

                    currentTargetEl = targetEl;

                    // Move target in opposite direction (1:1)
                    let moveX = 0;
                    let moveY = 0;

                    if (dragAxis === 'x') moveX = -diffX;
                    else moveY = -diffY;

                    if (currentTargetEl) {
                        currentTargetEl.style.transition = 'none';
                        currentTargetEl.style.transform = `translate(${moveX}px, ${moveY}px)`;
                    }
                }
            } else {
                potentialTargetRow = null;
                potentialTargetCol = null;
                if (currentTargetEl) {
                    currentTargetEl.style.transform = '';
                    currentTargetEl.style.transition = '';
                    currentTargetEl = null;
                }
            }

            const threshold = 50; // px to trigger swap

            if (Math.abs(diffX) > threshold || Math.abs(diffY) > threshold) {
                if (targetRow >= 0 && targetRow < this.mode.rows &&
                    targetCol >= 0 && targetCol < this.mode.cols) {

                    // Reset drag state but KEEP transforms for seamless swap
                    isDragging = false;

                    // We do NOT clear activeGemEl.style.transform here.
                    // We let swapGems handle the transition from transform -> new position.

                    this.mode.attemptSwap(startRow, startCol, targetRow, targetCol);

                    // Cleanup references (swapGems will handle the DOM elements)
                    activeGemEl = null;
                    currentTargetEl = null;
                }
            }
        };

        const handleEnd = () => {
            if (isDragging && activeGemEl) {
                // Swap Helper Logic:
                // If user dragged a bit (intent) and it results in a match, force the swap.
                const dragThreshold = 15; // Lower threshold for "intent"
                if ((Math.abs(lastDiffX) > dragThreshold || Math.abs(lastDiffY) > dragThreshold) &&
                    potentialTargetRow !== null && potentialTargetCol !== null) {

                    // Check if this move would create a match
                    if (this.mode.checkPotentialMatch(startRow, startCol, potentialTargetRow, potentialTargetCol)) {
                        // It's a match! Force the swap.
                        this.mode.attemptSwap(startRow, startCol, potentialTargetRow, potentialTargetCol);

                        // Cleanup
                        activeGemEl = null;
                        currentTargetEl = null;
                        isDragging = false;
                        this.mode.selectedGem = null;
                        return;
                    }
                }

                // Snap back (Standard behavior)
                activeGemEl.style.transition = 'transform 0.3s ease-out';
                activeGemEl.style.transform = 'translate(0, 0)';

                if (currentTargetEl) {
                    currentTargetEl.style.transition = 'transform 0.3s ease-out';
                    currentTargetEl.style.transform = 'translate(0, 0)';
                }

                setTimeout(() => {
                    if (activeGemEl) {
                        activeGemEl.style.transition = '';
                        activeGemEl.style.transform = '';
                        activeGemEl.style.zIndex = '';
                    }
                    if (currentTargetEl) {
                        currentTargetEl.style.transition = '';
                        currentTargetEl.style.transform = '';
                    }
                    activeGemEl = null;
                    currentTargetEl = null;
                }, 300);
            }
            isDragging = false;
            if (this.mode.selectedGem) {
                this.mode.selectedGem = null;
                this.render();
            }
        };

        // Mouse Events
        this.boardEl.addEventListener('mousedown', handleStart);
        document.addEventListener('mousemove', handleMove);
        document.addEventListener('mouseup', handleEnd);

        // Touch Events
        this.boardEl.addEventListener('touchstart', handleStart, { passive: false });
        document.addEventListener('touchmove', handleMove, { passive: false });
        document.addEventListener('touchend', handleEnd);

        document.getElementById('shuffle-btn').addEventListener('click', () => {
            if (!this.isLocked) this.mode.useShuffle();
        });

        document.getElementById('undo-btn').addEventListener('click', () => {
            if (!this.isLocked) this.mode.undoLastMove();
        });

        document.getElementById('restart-btn').addEventListener('click', () => {
            location.reload();
        });
    }

    render() {
        const activeIds = new Set();
        const gemSize = 60; // Must match CSS var --gem-size
        const gap = 4;      // Must match CSS var --gap

        // Count new gems per column for animation offset
        const newGemCounts = new Array(this.mode.cols).fill(0);
        for (let c = 0; c < this.mode.cols; c++) {
            for (let r = 0; r < this.mode.rows; r++) {
                const gem = this.grid[r][c];
                if (gem && !this.gemElements.has(gem.id)) {
                    newGemCounts[c]++;
                }
            }
        }

        for (let r = 0; r < this.mode.rows; r++) {
            for (let c = 0; c < this.mode.cols; c++) {
                const gem = this.grid[r][c];
                if (gem) {
                    activeIds.add(gem.id);
                    let gemEl = this.gemElements.get(gem.id);

                    if (!gemEl) {
                        // Create new
                        gemEl = document.createElement('div');
                        gemEl.className = 'gem';
                        gemEl.innerHTML = GemFactory.createGemSVG(gem.type, gem.colorType);
                        this.boardEl.appendChild(gemEl);
                        this.gemElements.set(gem.id, gemEl);

                        // Initial position for NEW gems (fall from above)
                        // Start Row = r - totalNewInCol
                        const startRow = r - newGemCounts[c];
                        gemEl.style.left = `${c * (gemSize + gap)}px`;
                        gemEl.style.top = `${startRow * (gemSize + gap)}px`;

                        // Force reflow to ensure start position is registered
                        gemEl.offsetHeight;

                        // Set target position (transition will handle the slide)
                        gemEl.style.top = `${r * (gemSize + gap)}px`;

                        // Stagger animation: Bottom rows land first
                        // Total time budget ~1s. 
                        // We have 8 rows. Let's stagger by 50ms per row from bottom up.
                        // Row 7 (bottom) -> delay 0
                        // Row 0 (top) -> delay 7 * 50 = 350ms
                        // But we want them to LAND at different times. 
                        // Since they all fall at the same speed (0.4s), we just delay the START.
                        // Actually, user wants them to HIT bottom first.
                        // So bottom row should start falling first? Or just land first?
                        // If they start at same time, they land at same time (relative to distance).
                        // Wait, if they are falling into empty space, the bottom one falls furthest?
                        // No, in 'render', we are just moving them from A to B.
                        // If we want bottom to land first, we give TOP rows a delay.
                        // Delay = (Rows - 1 - r) * 0.05s
                        const delay = (this.mode.rows - 1 - r) * 0.05;
                        gemEl.style.transitionDelay = `${delay}s`;

                        // Clear delay after transition so it doesn't affect future moves
                        setTimeout(() => {
                            if (gemEl) gemEl.style.transitionDelay = '';
                        }, 1000);
                    } else {
                        // Update position
                        gemEl.style.left = `${c * (gemSize + gap)}px`;
                        gemEl.style.top = `${r * (gemSize + gap)}px`;
                    }

                    // Update state classes
                    gemEl.dataset.row = r;
                    gemEl.dataset.col = c;

                    if (this.mode.selectedGem === gem) {
                        gemEl.classList.add('selected');
                    } else {
                        gemEl.classList.remove('selected');
                    }
                }
            }
        }

        // Remove old gems
        for (const [id, el] of this.gemElements) {
            if (!activeIds.has(id)) {
                el.remove();
                this.gemElements.delete(id);
            }
        }
    }

    async swapGems(gem1, gem2, isUndo = false) {
        this.isLocked = true;

        const el1 = this.gemElements.get(gem1.id);
        const el2 = this.gemElements.get(gem2.id);

        // 1. Record First positions (Visual)
        // This captures the element exactly where it is (dragged or static)
        const rect1 = el1.getBoundingClientRect();
        const rect2 = el2.getBoundingClientRect();

        // 2. Clear Transforms
        // We must clear the drag transforms now so they don't pollute the 'Last' position calculation.
        // Visually this would cause a jump, but we're about to FLIP it anyway.
        el1.style.transform = '';
        el2.style.transform = '';

        // 3. Apply Swap (Data)
        const tempType = gem1.type;
        const tempId = gem1.id;

        gem1.type = gem2.type;
        gem1.id = gem2.id;

        gem2.type = tempType;
        gem2.id = tempId;

        // 4. Render (updates left/top to Last position)
        // We temporarily disable transitions
        el1.style.transition = 'none';
        el2.style.transition = 'none';

        this.render();

        // 5. Calculate Invert (Delta)
        // Now el1/el2 are at their destination grid positions with NO transform.
        const newRect1 = el1.getBoundingClientRect();
        const newRect2 = el2.getBoundingClientRect();

        const dx1 = rect1.left - newRect1.left;
        const dy1 = rect1.top - newRect1.top;

        const dx2 = rect2.left - newRect2.left;
        const dy2 = rect2.top - newRect2.top;

        // 6. Apply Transform (Invert)
        // Put them back where they were visually (rect1/rect2)
        el1.style.transform = `translate(${dx1}px, ${dy1}px)`;
        el2.style.transform = `translate(${dx2}px, ${dy2}px)`;

        // Force reflow
        el1.offsetHeight;
        el2.offsetHeight;

        // 7. Play (Animate transform to 0)
        const easing = 'transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1.2)'; // Slightly tighter plop
        el1.style.transition = easing;
        el2.style.transition = easing;

        el1.style.transform = '';
        el2.style.transform = '';

        // Schedule cleanup of transition property exactly when animation ends
        // This ensures that subsequent gravity/render calls use the CSS transitions
        setTimeout(() => {
            if (el1) el1.style.transition = '';
            if (el2) el2.style.transition = '';
        }, 300);

        // Check matches immediately
        const matches = this.mode.findMatches();
        if (matches.length > 0) {
            // Regular move resulted in a match - Clear Undo History
            this.mode.lastMove = null;
            this.mode.updateUndoUI();

            // Overlap: Wait only 150ms (halfway through swap) then trigger match processing
            await new Promise(r => setTimeout(r, 150));
            await this.mode.processMatches();
        } else {
            // No Match - Revert unless it's a Solo Swap or an Undo

            // If it's an UNDO, we just wanted to swap them back. We are done.
            if (isUndo) {
                this.isLocked = false;
                return;
            }

            // Check for Solo Swap
            console.log('No match found. Checking solo swaps. isUndo:', isUndo, 'soloSwaps:', this.mode.soloSwaps);
            if (this.mode.soloSwaps > 0) {
                console.log('Consuming solo swap');
                // Consume Solo Swap
                this.mode.soloSwaps--;
                this.mode.updateUI();
                this.mode.highlightSoloSwap();

                // Store Undo State
                this.mode.lastMove = {
                    type: 'solo_swap',
                    gem1: gem1,
                    gem2: gem2
                };
                this.mode.updateUndoUI();

                // Allow the swap (do not revert)
                this.isLocked = false;

                // We should check if the board has moves now, just in case
                this.mode.checkGameState();
            } else {
                // No Solo Swaps available - Animate badge to show it's empty
                this.mode.highlightSoloSwap();

                // Revert
                const tempType = gem1.type;
                const tempId = gem1.id;

                gem1.type = gem2.type;
                gem1.id = gem2.id;

                gem2.type = tempType;
                gem2.id = tempId;

                this.render();
                // Render triggers CSS transition on left/top (defined in CSS)
                // We just need to wait for it.
                await new Promise(r => setTimeout(r, 300));
                this.isLocked = false;
            }
        }
    }

    async removeGems(gems, animationType = 'match') {
        // Add visual class and detach from tracking
        gems.forEach(g => {
            const el = this.gemElements.get(g.id);
            if (el) {
                el.classList.add(animationType); // 'match' or 'explode'
                // Detach from map so render() ignores it
                this.gemElements.delete(g.id);

                // Schedule actual DOM removal after animation finishes
                const duration = animationType === 'explode' ? 400 : 500;
                setTimeout(() => el.remove(), duration);
            }
            // Remove from data grid immediately
            this.grid[g.row][g.col] = null;
        });

        // Wait for animation overlap
        // Match: 250ms (half of 500)
        // Explode: 0ms (immediate refill)
        const waitTime = animationType === 'explode' ? 0 : 250;
        if (waitTime > 0) {
            await new Promise(r => setTimeout(r, waitTime));
        }
    }

    applyGravity() {
        // Move gems down
        for (let c = 0; c < this.mode.cols; c++) {
            let emptySlots = 0;
            for (let r = this.mode.rows - 1; r >= 0; r--) {
                if (this.grid[r][c] === null) {
                    emptySlots++;
                } else if (emptySlots > 0) {
                    // Move gem down
                    this.grid[r + emptySlots][c] = this.grid[r][c];
                    this.grid[r + emptySlots][c].row = r + emptySlots;
                    this.grid[r][c] = null;
                }
            }
        }
    }

    async renderNewGems() {
        // New gems have been added to grid by fillBoard()
        // render() will create them.
        // Ideally, we'd spawn them at y < 0 and animate in.
        // For now, let's just render them.
        this.render();
        await new Promise(r => setTimeout(r, 100));
    }

    gameOver(score) {
        document.getElementById('final-score').innerText = score;
        document.getElementById('game-over-modal').classList.remove('hidden');
    }
}
