/*
** 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 single Tetromino object.
 * Handles rotation, movement and rendering.
 */
class Tetromino {
    constructor(type, rotations, color, board, spawnX, spawnY) {
        this.type = type;
        this.rotations = rotations;
        this.color = color;
        this.board = board;
        this.x = spawnX;
        this.y = spawnY;
        this.rotation = 0;
        this.cells = rotations[0];
        this.cellsOnBoard = [];
    }

    render() {
        if (this.cellsOnBoard.length) {
            this.cellsOnBoard.forEach(([x, y]) => this.board.updateCell(x, y, 0));
        }

        const cellsOnBoard = [];
        this.cells.forEach(([x, y]) => {
            const bx = this.x + x;
            const by = this.y + y;
            if (this.board.isInside(bx, by)) {
                this.board.updateCell(bx, by, this.color, 'falling');
                cellsOnBoard.push([bx, by]);
            }
        });
        this.cellsOnBoard = cellsOnBoard;
    }

    rotate() {
        if (this.type === 'O') {
            return;
        }

        const newRotation = (this.rotation + 1) % this.rotations.length;
        const newCells = this.rotations[newRotation];

        if (this.canPlace(newCells, this.x, this.y)) {
            this.rotation = newRotation;
            this.cells = newCells;
            this.render();
            return;
        }

        const kicks = [-1, 1];
        for (const dx of kicks) {
            if (this.canPlace(newCells, this.x + dx, this.y)) {
                this.x += dx;
                this.rotation = newRotation;
                this.cells = newCells;
                this.render();
                return;
            }
        }
    }


    /**
     * @param {number} dx horizontal move (-1 = left, 1 = right, 0 = no movement)
     * @param {number} dy horizontal move (1 = down, 0 = no movement)
     * @returns {boolean} True if move was successfull, false if it wasn't
     */
    move(dx, dy) {
        const canMove = this.cells.every(([x, y]) => {
            const bx = this.x + x + dx;
            const by = this.y + y + dy;
            if (!this.board.isInside(bx, by)) return false;
            const cellValue = this.board.getCell(bx, by);
            const isOnCurrent = this.cellsOnBoard.some(([cx, cy]) => cx === bx && cy === by);
            return cellValue === 0 || isOnCurrent;
        });

        if (canMove) {
            this.x += dx;
            this.y += dy;
            this.render();
            return true;
        }
        return false;
    }

    hardDrop() {
        while (this.move(0, 1)) {}
    }

    canPlace(cells = this.cells, offsetX = this.x, offsetY = this.y) {
        const currentOccupiedSet = new Set(
            (this.cellsOnBoard || this.cells.map(([cx, cy]) => [this.x + cx, this.y + cy]))
                .map(([px, py]) => `${px},${py}`)
        );

        return cells.every(([x, y]) => {
            const boardX = offsetX + x;
            const boardY = offsetY + y;

            if (!this.board.isInside(boardX, boardY)) return false;

            const cellVal = this.board.getCell(boardX, boardY);

            if (cellVal !== 0) {
                if (!currentOccupiedSet.has(`${boardX},${boardY}`)) return false;
            }

            return true;
        });
    }
}