package sfs3.extensions.examples.tictactoe

import com.smartfoxserver.bitswarm.data.TransportType
import com.smartfoxserver.core.ISFSEvent
import com.smartfoxserver.core.SFSEventParam
import com.smartfoxserver.core.SFSEventType
import com.smartfoxserver.entities.Room
import com.smartfoxserver.entities.User
import com.smartfoxserver.entities.data.ISFSObject
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: SFSExtension()
{
    private var gameBoard: Board
    private var whoseTurn: User?
    private var gameStarted: Boolean
    private var moveCount: Int
    private var wins: Array<Int>

    init 
    {
        gameStarted = false
        whoseTurn = null
        moveCount = 0
        wins = arrayOf<Int>(0,0,0)
        gameBoard = Board()
    }

    override fun init()
    {
        // Register client request handlers
        addRequestHandler("ready", this::onReady)
        addRequestHandler("move", this::onMove)
        addRequestHandler("restart", this::onRestart)

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

    override fun destroy()
    {
        super.destroy()
        trace("TicTacToe Game Extension destroyed")
    }

    fun getLastWinnerId(): Int
    {
        return gameBoard.getWinner()
    }

    fun startGame()
    {
        if (gameStarted)
            throw 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
        val outParams = SFSObject()
        outParams.putInt("t", whoseTurn!!.getPlayerId())
        outParams.putInt("p1w", wins[1])
        outParams.putInt("p2w", wins[2])
        
        send("start", outParams, getParentRoom().getUserList())
    }

    fun stopGame(resetPlayers: Boolean)
    {
        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
        }
    }

    fun updateGameState()
    {
        // Increase moves counter               
        moveCount++
        
        // Switch turn
        whoseTurn = getParentRoom().getUserByPlayerId(if (whoseTurn!!.getPlayerId() == 1) 2 else 1)
        
        // Check if game is over
        checkGameOver()
    }

    fun checkGameOver()
    {
        val state: State = gameBoard.getState(moveCount)
        
        if (state != State.RUNNING)
        {
            trace("Game ended")
            
            // Stop game
            stopGame(false)
            
            // Get winner id
            val winnerId = getLastWinnerId()
            
            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")
            }
            
            val outParams = SFSObject()
            outParams.putInt("w", winnerId)
            
            // Send update to clients
            send("over", outParams, parentRoom.userList)
        }
    }

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

    private fun onReady(user: User, params: ISFSObject, txType: TransportType)
    {
        //val gameExt = getParentExtension() as TicTacToeExtension
        
        if (user.isPlayer)
        {
            // Check if two players are available and start game
            if (parentRoom.size.userCount == 2)
                startGame()
        }

        else
        {
            // Send game board state to spectator
            val outParams = SFSObject()
            outParams.putInt("t", if (whoseTurn == null) 0 else whoseTurn!!.playerId)
            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 && parentRoom.size.userCount == 2)
                outParams.putInt("w", getLastWinnerId())
            
            send("state", outParams, user)
        }
    }

    fun onMove(user: User, params: ISFSObject, txType: TransportType)
    {
        // Request must contain the 'r' and 'c' parameters, indicating which board slot was clicked by the player in turn
        if (!params.containsKey("r") || !params.containsKey("c"))
            throw SFSRuntimeException("Invalid request, one or more mandatory params are missing; required 'r' and 'c'")
        
        // Extract parameters
        val row:Int = params.getInt("r")
        val col:Int = 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, if (user.getPlayerId() == 1) Mark.CROSS else Mark.RING)
                    
                    // Send move to all users in the Room, including the player who made it
                    val outParams = SFSObject()
                    outParams.putInt("r", row)
                    outParams.putInt("c", col)
                    outParams.putInt("t", user.getPlayerId())
                    
                    send("move", outParams, getParentRoom().getUserList())
                    
                    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")

    }

    fun onRestart(sender: User, params: ISFSObject, txType: TransportType)
    {
        if (parentRoom.size.userCount == 2)
            startGame()
    }

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

    fun onUserGone(event:ISFSEvent)
    {
        val gameRoom = parentRoom
        
        // 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 
        val oldPlayerId:Int?
        
        // User disconnected
        if (event.getType() == SFSEventType.USER_DISCONNECT)
        {
            @Suppress("UNCHECKED_CAST")
            val playerIdsByRoom = event.getParameter(SFSEventParam.PLAYER_IDS_BY_ROOM) as Map<Room, Int>
            oldPlayerId = playerIdsByRoom.get(gameRoom)
        }
        
        // User left the room
        else
            oldPlayerId = event.getParameter(SFSEventParam.PLAYER_ID) as Int
        
        if (oldPlayerId != null && 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.size.userCount > 0)
                send("stop", null, gameRoom.userList)
        }
    }

    private fun onSpectatorToPlayer(evt:ISFSEvent)
    {
        // Check if two players are available and start game
        if (parentRoom.size.userCount == 2)
            startGame()
    }
}