import { State } from "./State.js";

/**
 * Represents a game.
 */
export class Game
{
    /**
     * The number of chips required for a win.
     */
    static #count = 4;

    /**
     * The width of the board.
     */
    static #width = 7;

    /**
     * The height of the board.
     */
    static #height = 6;

    /**
     * The previous states of the game.
     *
     * @type {IState[]}
     */
    #previousStates = [];

    /**
     * The state of the game.
     *
     * @type {IState}
     */
    #state;

    /**
     * Initializes a new instance of the {@link Game `Game`} class.
     */
    constructor()
    {
        this.#state = State.create(Game.#width, Game.#height);
    }

    /**
     * Gets the state of the game.
     */
    get state()
    {
        return this.#state;
    }

    /**
     * Gets the board of the game.
     */
    get board()
    {
        return this.state.board;
    }

    /**
     * Gets the current player.
     *
     * @type {CellOwner}
     */
    get currentPlayer()
    {
        return this.state.turnCount % 2 === 0 ? "r" : "b";
    }

    /**
     * Gets the id of the player that is winning.
     *
     * @type {CellOwner}
     */
    get winner()
    {
        for (let yOffset = 0; yOffset <= 1; yOffset++)
        {
            for (let xOffset = (yOffset === 1) ? -1 : 1; xOffset <= 1; xOffset++)
            {
                let lowerBound = Math.max(0, xOffset * (Game.#count - 1) * -1);
                let upperBound = Math.min(Game.#width, Game.#width - (xOffset * (Game.#count - 1)));

                for (let y = 0; y < (Game.#height - yOffset * (Game.#count - 1)); y++)
                {
                    for (let x = lowerBound; x < upperBound; x++)
                    {
                        /**
                         * @type {CellOwner[]}
                         */
                        let tokens = [];

                        for (let i = 0; i < Game.#count; i++)
                        {
                            tokens.push(this.board[y + i * yOffset][x + i * xOffset]);
                        }

                        let player = tokens[0];

                        if (
                            player !== "" &&
                            tokens.every((token) => token === player))
                        {
                            return player;
                        }
                    }
                }
            }
        }

        return null;
    }

    /**
     * Dumps the state of the game.
     *
     * @returns {IState}
     * The JSON string representing the state.
     */
    dump()
    {
        return {
            turnCount: this.state.turnCount,
            board: this.state.board
        };
    }

    /**
     * Loads the game from the specified {@link data `data`}.
     *
     * @param {IState} data
     * The data to load.
     */
    load(data)
    {
        this.setState([], data);

        if (!this.#state)
        {
            this.reset();
        }
    }

    /**
     * Resets the game.
     */
    reset()
    {
        this.setState([], State.create(Game.#width, Game.#height));
    }

    /**
     * Reverts the last move.
     */
    undo()
    {
        this.#state = this.#previousStates.pop();

        if (!this.#state)
        {
            this.reset();
        }
    }

    /**
     * Sets the value located at the specified {@link path `path`} to the specified {@link value `value`}.
     *
     * @param  {(keyof any)[]} path
     * The path of the value to set.
     *
     * @param {any} value
     * The value to set.
     */
    setState(path, value)
    {
        this.#previousStates.push(this.#state);
        this.#state = { ...this.#state };
        this.#state.turnCount++;

        if (path.length === 0)
        {
            this.#state = value;
        }
        else if (path.length === 1)
        {
            this.#state = {
                ...this.#state,
                [path[0]]: value
            };
        }
        else
        {
            let target = /** @type {any} */ (this.#state);

            while (path.length > 1)
            {
                if (Array.isArray(target[path[0]]))
                {
                    target[path[0]] = [...target[path[0]]];
                }
                else
                {
                    target[path[0]] = { ...target[path[0]] };
                }

                target = target[path[0]];
                path = path.slice(1);
            }

            target[path[0]] = value;
        }
    }

    /**
     * Adds a chip to the board indicated by the {@link x `x`} and the {@link y `y`} coordinate.
     *
     * @param {number} x
     * The x coordinate to add the chip to.
     *
     * @param {number} y
     * The y coordinate to add the chip to.
     *
     * @returns {boolean}
     * A value indicating whether the chip could be added.
     */
    addChip(x, y)
    {
        if (!this.winner)
        {
            for (let i = Game.#height - 1; i >= 0; i--)
            {
                if (this.board[i][x] === "")
                {
                    this.setState(["board", i, x], this.currentPlayer);
                    return true;
                }
            }
        }

        return false;
    }
}