package sfs3.extensions.examples.tictactoe

import com.smartfoxserver.core.SFSEventParam
import com.smartfoxserver.core.SFSEventType
import com.smartfoxserver.entities.User
import com.smartfoxserver.entities.data.SFSObject
import com.smartfoxserver.exceptions.SFSRuntimeException
import com.smartfoxserver.extensions.ExtensionLogLevel
import com.smartfoxserver.extensions.SFSExtension
import sfs3.extensions.examples.tictactoe.game.Board
import sfs3.extensions.examples.tictactoe.game.Mark
import sfs3.extensions.examples.tictactoe.game.State

class TicTacToeExtension extends SFSExtension
{
    private Board gameBoard
    private User whoseTurn
    private boolean gameStarted
    private int moveCount
    private int[] wins

    @Override
    void init()
    {
        // Initialize properties
        moveCount = 0
        wins = new int[] {0,0,0}
        gameBoard = new Board()
        gameStarted = false
        whoseTurn = null

        addRequestHandler("ready", this::onReady)
        addRequestHandler("move", this::onMove)
        addRequestHandler("restart", this::onRestart)

        // Register server event handlers
        addEventListener(SFSEventType.USER_DISCONNECT, this::onUserGone)
        addEventListener(SFSEventType.USER_LEAVE_ROOM, this::onUserGone)
        addEventListener(SFSEventType.SPECTATOR_TO_PLAYER, this::onSpectatorToPlayer)
    }

    @Override
    void destroy()
    {
        super.destroy()
    }

    def startGame()
    {
        if (gameStarted)
            throw new IllegalStateException("Game is already started")

        // Reset state
        gameStarted = true
        gameBoard.reset()

        // No turn assigned? let's start with player 1
        if (whoseTurn == null)
            whoseTurn = getParentRoom().getUserByPlayerId(1)

        // Send START event to clients with:
        // - the active player
        // - the total wins of player 1 and 2
        def outParams = new SFSObject()
        outParams.putInt("t", whoseTurn.getPlayerId())
        outParams.putInt("p1w", wins[1])
        outParams.putInt("p2w", wins[2])

        send("start", outParams, getParentRoom().getUserList())
    }

    def stopGame(resetPlayers)
    {
        gameStarted = false
        moveCount = 0

        // In case a player left the game (see OnUserGoneHandler handler),
        // the turn and total wins are reset
        if (resetPlayers)
        {
            whoseTurn = null
            wins[1] = 0
            wins[2] = 0
        }
    }

    def updateGameState()
    {
        moveCount++

        // Switch turn
        whoseTurn = getParentRoom().getUserByPlayerId(whoseTurn.getPlayerId() == 1 ? 2 : 1)

        // Check if game is over
        checkGameOver()
    }

/**
 * Check if the game is over based on the board state.
 * If game is over, send outcome to all users in the Room (players and spectators).
 */
    def checkGameOver()
    {
        def state = gameBoard.getState(moveCount)

        if (state != State.RUNNING)
        {
            trace("Game ended")
            stopGame(false)

            def winnerId = gameBoard.winner

            if (state == State.END_WITH_WINNER)
            {
                // Update wins counter
                wins[winnerId]++;
                trace("Winner is player ", winnerId)
            }

            else if (state == State.END_WITH_TIE)
            {
                trace("It's a tie");
            }

            def outParams = new SFSObject()
            outParams.putInt("w", winnerId)

            // Send update to clients
            send("over", outParams, getParentRoom().getUserList());
        }
    }


    // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
    // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---

    def onReady(user, params, txType)
    {
        if (user.isPlayer())
        {
            if (getParentRoom().getSize().getUserCount() == 2)
                startGame()
        }

        else
        {
            def outParams = new SFSObject()
            outParams.putInt("t", whoseTurn == null ? 0 : whoseTurn.getPlayerId())
            outParams.putBool("run", gameStarted)
            outParams.putSFSArray("board", gameBoard.toSFSArray())
            outParams.putInt("p1w", wins[1])
            outParams.putInt("p2w", wins[2])

            // If the game ended right before the spectator joined, also send its outcome
            if (gameStarted && getParentRoom().getSize().getUserCount() == 2)
                outParams.putInt("w", gameBoard.winner)

            send("state", outParams, user);
        }
    }

    def onMove(user, params, txType)
    {
        if (!params.containsKey("r") || !params.containsKey("c"))
            throw new SFSRuntimeException("Invalid request, one or more mandatory params are missing; required 'r' and 'c'")

        // Extract parameters
        int	row = params.getInt("r")
        int col = params.getInt("c")

        trace(String.format("Handling move from player %s (%s, %s)", user.getPlayerId(), row, col))

        // Game must be started
        if (gameStarted)
        {
            // The player sending the move must be the active one
            if (whoseTurn == user)
            {
                // The board slot corresponding to the passed coordinates must be empty
                if (gameBoard.getMarkAt(row, col) == Mark.EMPTY)
                {
                    // Set game board mark
                    gameBoard.setMarkAt(row, col, user.getPlayerId() == 1 ? Mark.CROSS : Mark.RING)

                    // Send move to all users in the Room, including the player who made it
                    def outParams = new SFSObject()
                    outParams.putInt("r", row)
                    outParams.putInt("c", col)
                    outParams.putInt("t", user.getPlayerId())

                    send("move", outParams, getParentRoom().getUserList())

                    // Update game state
                    updateGameState()
                }

                // Wrong move
                else
                    trace(ExtensionLogLevel.WARN, "Wrong move error; slot $row, $col is not empty")
            }

            // Wrong turn
            else
                trace(ExtensionLogLevel.WARN, "Wrong move error; expected turn of player $whoseTurn, received move from: $user")
        }

        // Wrong game state
        else
            trace(ExtensionLogLevel.WARN, "Wrong move error; game not yet started")
    }

    def onRestart(user, params, txType)
    {
        if (getParentRoom().getSize().getUserCount() == 2)
            startGame()
    }

    def onUserGone(evt)
    {
        def gameRoom = getParentRoom()

        // Get id of the player who left the game
        // This can't be extracted from the User object available in event parameters, so we have to rely on other parameters
        def oldPlayerId = null

        // User disconnected
        if (evt.getType() == SFSEventType.USER_DISCONNECT)
        {
            def playerIdsByRoom = evt.getParameter(SFSEventParam.PLAYER_IDS_BY_ROOM)
            oldPlayerId = playerIdsByRoom.get(gameRoom)
        }

        // User left the room
        else
            oldPlayerId = evt.getParameter(SFSEventParam.PLAYER_ID)

        if (oldPlayerId != null)
        {
            // Check if gone user was a player
            if (oldPlayerId > 0)
            {
                // Stop game resetting turn and players wins
                stopGame(true)

                // If a player is still inside the game room, notify them that the game is now stopped
                if (gameRoom.getSize().getUserCount() > 0)
                    send("stop", null, gameRoom.getUserList())
            }
        }
    }

    def onSpectatorToPlayer(evt)
    {
        if (getParentRoom().getSize().getUserCount() == 2)
            startGame()
    }
}
