/*
** Copyright (C) 2021-2026 initMAX s.r.o.
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/

/**
 * Class representing the Tetris game board.
 * Manages the logical state and DOM representation of the playfield.
 */
class GameBoard {
    /**
     * 2D array holding the logical state of the playfield (0 = empty, >0 = filled).
     * @type {number[][]}
     */
    #playfieldState = null;

    /**
     * 2D array holding references to DOM elements (cells).
     * @type {HTMLElement[][]}
     */
    #playfieldCells = null;

    /**
     * The DOM element representing the playfield.
     * @type {HTMLElement}
     */
    #playfieldElement = null;

    /**
     * Width of the playfield in cells.
     * @type {number}
     */
    #width = null;

    /**
     * Height of the playfield in cells.
     * @type {number}
     */
    #height = null;

    /**
     * Creates an instance of the game board.
     * @param {*} playfieldElement - The DOM element representing the playfield.
     * @param {*} width - The width of the playfield.
     * @param {*} height - The height of the playfield.
     */
    constructor(playfieldElement, width, height) {
        this.#playfieldElement = playfieldElement;

        this.#width = width;
        this.#height = height;

        this.initPlayfield();
    }

    /**
     * Initializes the playfield state and DOM structure.
     */
    initPlayfield() {
        // 2D array holding the logical state of the playfield (0 = empty, >0 = filled)
        this.#playfieldState = Array.from(
            { length: this.#height },
            () => new Array(this.#width).fill(0)
        );

        // 2D array holding references to DOM elements (cells)
        this.#playfieldCells = Array.from(
            { length: this.#height },
            () => new Array(this.#width)
        );

        // Set CSS custom properties for playfield dimensions
        this.#playfieldElement.style.setProperty('--board-size-x', this.#width);
        this.#playfieldElement.style.setProperty('--board-size-y', this.#height);

        // Generate DOM cells and store references in playfieldCells
        for (let y = 0; y < this.#height; y++) {
            for (let x = 0; x < this.#width; x++) {
                const cell = document.createElement('div');

                // Store initial data as dataset attributes for debugging/visualization
                cell.dataset.x = x;
                cell.dataset.y = y;
                cell.dataset.value = this.#playfieldState[y][x];

                // Save reference to the cell in the grid
                this.#playfieldCells[y][x] = cell;

                // Append the cell to the playfield container
                this.#playfieldElement.appendChild(cell);
            }
        }
    }

    /**
     * Resets the playfield to an empty state.
     */
    resetPlayfield() {
        for (let y = 0; y < this.#height; y++) {
            for (let x = 0; x < this.#width; x++) {
                // Reset logic state
                this.#playfieldState[y][x] = 0;

                // Reset DOM cell dataset
                const cell = this.#playfieldCells[y][x];
                cell.dataset.value = 0;

                // Optionally clear CSS classes if used for coloring tetrominoes
                cell.className = '';
            }
        }
    }

    /**
     * Returns true if the coordinates are inside the playfield bounds.
     */
    isInside(x, y) {
        return (
            Number.isInteger(x) &&
            Number.isInteger(y) &&
            x >= 0 && x < this.#width &&
            y >= 0 && y < this.#height
        );
    }

    /**
     * Reads the logical value at (x, y).
     * Returns null if outside of bounds.
     */
    getCell(x, y) {
        if (!this.isInside(x, y)) return null;
        return this.#playfieldState[y][x];
    }

    /**
     * Updates both logic and DOM at (x, y).
     * Returns true on success, false if out of bounds.
     *
     * value: number (0 = empty, >0 = filled/type id)
     */
    updateCell(x, y, value, state = 'falling') {
        if (!this.isInside(x, y)) return false;

        // Update logic state
        this.#playfieldState[y][x] = value;

        // Update DOM cell
        const cell = this.#playfieldCells[y][x];
        cell.dataset.value = value;

        if (value === 0) {
            cell.className = '';
        } else {
            // reset before applying
            cell.className = '';
            cell.classList.add(state === 'falling' ? 'filled' : 'dropped');
        }

        return true;
    }
/**
 * Checks for full lines, removes them, and shifts everything above down with animation.
 * @returns {Promise<number>} - Promise resolved with number of cleared lines
 */
clearFullLines() {
    return new Promise(resolve => {
        const FALL_DURATION = 200; 
        let clearedLines = 0;
        let rowsToClear = [];

        // 1️⃣ Najdi plné řádky
        for (let y = this.#height - 1; y >= 0; y--) {
            if (this.#playfieldState[y].every(v => v !== 0)) {
                clearedLines++;
                rowsToClear.push(y);
                for (let x = 0; x < this.#width; x++) {
                    this.#playfieldCells[y][x].classList.add('line-clearing');
                }
            }
        }

        if (rowsToClear.length === 0) return resolve(0);

        // 2️⃣ Čekej na dokončení animace vyčištění řádků
        let animationPromises = [];
        for (let y of rowsToClear) {
            for (let x = 0; x < this.#width; x++) {
                const cell = this.#playfieldCells[y][x];
                animationPromises.push(new Promise(r => {
                    cell.addEventListener('animationend', function handler() {
                        cell.removeEventListener('animationend', handler);
                        cell.classList.remove('line-clearing');
                        cell.className = ''; // Clear DOM immediately
                        cell.dataset.value = 0; // Reset data
                        r();
                    });
                }));
            }
        }

        Promise.all(animationPromises).then(() => {
            const fallDistances = Array.from({ length: this.#height }, () => new Array(this.#width).fill(0));
            for (let x = 0; x < this.#width; x++) {
                let emptyBelow = 0;
                for (let y = this.#height - 1; y >= 0; y--) {
                    if (rowsToClear.includes(y)) emptyBelow++;
                    else if (this.#playfieldState[y][x] !== 0) fallDistances[y][x] = emptyBelow;
                }
            }

            const newPlayfieldState = Array.from({ length: this.#height }, () => new Array(this.#width).fill(0));
            let targetY = this.#height - 1;
            for (let sourceY = this.#height - 1; sourceY >= 0; sourceY--) {
                if (!rowsToClear.includes(sourceY)) {
                    newPlayfieldState[targetY] = [...this.#playfieldState[sourceY]];
                    targetY--;
                }
            }

            let fallPromises = [];
            for (let y = 0; y < this.#height; y++) {
                for (let x = 0; x < this.#width; x++) {
                    const fall = fallDistances[y][x];
                    if (fall > 0) {
                        const cell = this.#playfieldCells[y][x];
                        const newY = y + fall; 
                        if (newY < this.#height) {
                            fallPromises.push(new Promise(r => {
                                cell.style.transition = `transform ${fall * FALL_DURATION}ms ease-in`;
                                cell.style.transform = `translateY(${fall * 32}px)`; // 32 default cell height
                                cell.addEventListener('transitionend', function handler() {
                                    cell.removeEventListener('transitionend', handler);
                                    // Move cell to final position in DOM
                                    this.#playfieldCells[newY][x].dataset.value = cell.dataset.value;
                                    this.#playfieldCells[newY][x].className = 'dropped';
                                    cell.className = ''; // Clear original cell
                                    cell.dataset.value = 0;
                                    cell.style.transition = '';
                                    cell.style.transform = '';
                                    r();
                                }.bind(this));
                            }));
                        }
                    }
                }
            }

            Promise.all(fallPromises).then(() => {
                this.#playfieldState = newPlayfieldState;
                this.#updateDOM(); // Final DOM update
                resolve(clearedLines);
            });
        });
    });
}

/**
 * Updates the DOM to match the playfield state
 */
#updateDOM() {
    for (let y = 0; y < this.#height; y++) {
        for (let x = 0; x < this.#width; x++) {
            const value = this.#playfieldState[y][x];
            const cell = this.#playfieldCells[y][x];
            cell.dataset.value = value;
            cell.className = value === 0 ? '' : 'dropped';
        }
    }
}
    /**
     * Method for determining bar height trigger
     * @returns {Number} - Number of row occupied by "dropped" tetronimio
     */
    getHighestOccupiedRow() {
        for (let y = 0; y < this.#height; y++) {
            if (this.#playfieldState[y].some(value => value !== 0)) {
                return y;
            }
        }
        return -1; 
    }
}
