package pl.edu.pw.mini.game.search

import pl.edu.pw.mini.sg.game.deterministic.DeterministicGame
import pl.edu.pw.mini.sg.game.graph.{Edge, GraphConfig}

import scala.collection.immutable.HashSet
import scala.collection.mutable
import scala.collection.mutable.Queue

/**
  * Class representing the game state of a SearchGame.
  * @param defenderState  current defender state (unit positions, timestep and visible tracks)
  * @param attackerState  current attacker state (unit position and timestep)
  * @param tracks         all tracks that the attacker left
  */
final case class SearchGame(
                             defenderState: SearchGameDefenderState,
                             attackerState: SearchGameAttackerState,
                             tracks: Set[Int],
                             wasAttackerCaught: Boolean = false
 ) extends DeterministicGame[List[Int], SearchGameAttackerMove, SearchGameDefenderState, SearchGameAttackerState, SearchGame] {
  override def makeMove(defenderMove: List[Int], attackerMove: SearchGameAttackerMove): SearchGame = {

    val nextAttacker = attackerState.continuation(attackerMove)
    val nextTracks = if(!(attackerState.gameConfig.coveringUpTracksPossible &&
      attackerState.attackerPosition == nextAttacker.attackerPosition))
      tracks + nextAttacker.attackerPosition
    else
      tracks - attackerState.attackerPosition

    //vertices where tracks can be observed
    val observableVertices = defenderState.gameConfig.trackVertices
    val nextVisibleTracks = defenderMove.filter(nextTracks.contains).filter(observableVertices.contains).toSet

    val nextDefender = defenderState.continuation(defenderMove, nextVisibleTracks)
    val timesteps = nextDefender.gameConfig.rounds

    val attackerCaught = defenderState.unitPositions.zip(defenderMove).exists{
      case (oldPos, newPos) => oldPos == attackerMove.newPosition && newPos == attackerState.attackerPosition
    }

    if(isAttackerCaught(nextAttacker, nextDefender) || isAttackerOnTarget(nextAttacker) || attackerCaught) {
      SearchGame(nextDefender.copy(timeStep = timesteps), nextAttacker.copy(timeStep = timesteps), nextTracks, attackerCaught)
    }
    else
      SearchGame(nextDefender, nextAttacker, nextTracks)
  }

  override val (defenderPayoff, attackerPayoff): (Double, Double) = {
    if(isAttackerCaught(attackerState, defenderState))
      (1, -1)
    else {
      attackerState.gameConfig.targets.find(_.position == attackerState.attackerPosition) match {
        case Some(SearchGameTarget(_, payoff)) => (-1, payoff)
        case None => (0, 0)
      }
    }
  }

  private def isAttackerCaught(attackerState: AttackerState, defenderState: DefenderState) = {
    defenderState.unitPositions.contains(attackerState.attackerPosition) || wasAttackerCaught
  }

  private def isAttackerOnTarget(attackerState: AttackerState): Boolean = {
    attackerState.gameConfig.targets.map(_.position).contains(attackerState.attackerPosition)
  }

  private def canAttackerReachTarget(attackerState: AttackerState):Boolean = {
    val game = attackerState.gameConfig
    game.targets.exists(
      t => distance(game.graphConfig, attackerState.attackerPosition, t.position) <= game.rounds - attackerState.timeStep
    )
  }

  private def distance(graph: GraphConfig, from: Int, to: Int): Int = {
    val queue = new mutable.Queue[Int]
    val visited = new Array[Boolean](graph.vertexCount)
    val distances = new Array[Int](graph.vertexCount)

    distances(from) = 0
    visited(from) = true
    queue += from

    while(queue.nonEmpty) {
      val current = queue.dequeue()

      if(current == to)
        return distances(current)

      graph.edges.filter(_.from == current).foreach{case Edge(_, next) => {
          if(!visited(next)) {
            distances(next) = distances(current) + 1
            queue.enqueue(next)
            visited(next) = true
          }
        }
      }
    }

    distances(to)
  }


}

object SearchGame {
  def apply(config: SearchGameConfig): SearchGame = {
    apply(config.defenderInitialState, config.attackerInitialState, new HashSet[Int])
  }
}
