package sfs3.extensions.examples.tictactoe

import com.smartfoxserver.bitswarm.data.TransportType
import com.smartfoxserver.core.{ISFSEvent, SFSEventParam, SFSEventType}
import com.smartfoxserver.entities.{Room, User}
import com.smartfoxserver.entities.data.*
import com.smartfoxserver.exceptions.SFSRuntimeException
import com.smartfoxserver.extensions.{ExtensionLogLevel, SFSExtension}
import game.Board
import game.Mark
import game.State

import java.util

class TicTacToeExtension extends SFSExtension
{
  private var gameBoard: Board = null
  private var whoseTurn: User = null
  private var gameStarted = false
  private var moveCount = 0
  private var wins: Array[Int] = null

  override def init(): Unit = {
    moveCount = 0
    wins = Array[Int](0, 0, 0)
    gameBoard = new Board()

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

    addEventHandler(SFSEventType.USER_DISCONNECT, this.onUserGone)
    addEventHandler(SFSEventType.USER_LEAVE_ROOM, this.onUserGone)
    addEventHandler(SFSEventType.SPECTATOR_TO_PLAYER, this.onSpectatorToPlayer)
  }

  override def destroy(): Unit = {
    super.destroy()
    trace("TicTacToe Extension destroyed");
  }

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

  private def checkGameOver(): Unit = {
    val state = gameBoard.getState(moveCount)
    if (state ne State.RUNNING)
    {
      trace("Game ended")

      // Stop game
      stopGame(false)

      // Get winner id
      val winnerId = gameBoard.getWinner

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

      else if (state eq State.END_WITH_TIE) trace("It's a tie")

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

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

  def startGame(): Unit =
  {
    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
    val 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: Boolean): Unit = {
    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 onReady(user: User, params: ISFSObject, txType: TransportType): Unit = {
    if (user.isPlayer) 
    {
      // Check if two players are available and start game
      if (getParentRoom.getSize.getUserCount == 2) 
        startGame()
    }
    
    else 
    {
      // Send game board state to spectator
      val outParams = new SFSObject()
      outParams.putInt("t", if (whoseTurn == null) 0
      else 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.getWinner)

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

  def onMove(user: User, params: ISFSObject, txType: TransportType): Unit = {
    // 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 new SFSRuntimeException("Invalid request, one or more mandatory params are missing; required 'r' and 'c'")
    }

    // Extract parameters
    val row = params.getInt("r")
    val 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, 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 = 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(sender: User, params: ISFSObject, txType: TransportType): Unit = {
    if (getParentRoom.getSize.getUserCount == 2)
      startGame();
  }

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

  def onUserGone(event: ISFSEvent): Unit = {
    val 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
    var oldPlayerId: Integer = null

    // User disconnected
    if (event.getType eq SFSEventType.USER_DISCONNECT) {

      @SuppressWarnings(Array("unchecked"))
      val playerIdsByRoom = event.getParameter(SFSEventParam.PLAYER_IDS_BY_ROOM).asInstanceOf[util.Map[Room, Int]]
      oldPlayerId = playerIdsByRoom.get(gameRoom)

    }
    else oldPlayerId = event.getParameter(SFSEventParam.PLAYER_ID).asInstanceOf[Integer]

    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(event: ISFSEvent): Unit = {
    // Check if two players are available and start game

    if (getParentRoom().getSize().getUserCount() == 2)
      startGame();
  }

}
