package pl.edu.pw.mini.sg.movingtargets

import canes.game.{ Target, MovingTargetsGame, DefenderPosition => DDefenderPosition}
import org.apache.commons.math3.random.RandomGenerator
import scala.annotation.tailrec
import pl.edu.pw.mini.jk.sg.game.immutable.Payoffs
import scala.collection.immutable.SortedSet
import scala.runtime.ScalaRunTime


final case class AttackStart(timeStep: Int, targetId: Int)

final case class MovingTargetsGameConfig(
  game: MovingTargetsGame,
  spaceDiscretization: Double
) {

  import game._

  override val hashCode = ScalaRunTime._hashCode(this)

  lazy val leftDepotBound = depots.map(_.x).min
  lazy val rightDepotBound = depots.map(_.x).max
  lazy val topDepotBound = depots.map(_.y).max
  lazy val bottomDepotBound = depots.map(_.y).min
  lazy val leftGridLimit : Int = - ((origin.x - leftDepotBound)/spaceDiscretization).ceil.toInt
  lazy val rightGridLimit : Int = ((rightDepotBound - origin.x)/spaceDiscretization).ceil.toInt
  lazy val topGridLimit : Int = ((topDepotBound - origin.y)/spaceDiscretization).ceil.toInt
  lazy val bottomGridLimit : Int = - ((origin.y - bottomDepotBound)/spaceDiscretization).ceil.toInt

  final def realPositionToGrid(realPosition: DDefenderPosition) : (Double, Double) =
    (
      ((realPosition.x - origin.x)/spaceDiscretization),
      ((realPosition.y - origin.y)/spaceDiscretization)
    )

  final def depotPositionToGrid(depotId: Int) = {
    val d = getDepotById(depotId)
    realPositionToGrid(DDefenderPosition(d.x, d.y))
  }

  lazy val possibleStepsInTime : Int = (defenderSpeed*timeInterval/spaceDiscretization).toInt

  lazy val defenderStepInTime = defenderSpeed * timeInterval
  lazy val defenderMoveRangeSquared = defenderSpeed*timeInterval*defenderSpeed*timeInterval

  lazy val roundLimit = Math.ceil(gameTime/timeInterval).toInt

  final def getTargetById(id: Int) = targets.filter(_.id == id)(0)
  final def getDepotById(id: Int) = depots.filter(_.id == id)(0)
  final def isInGrid(position: DefenderPosition) : Boolean = position match {
    case DefenderPosition(x,y) =>
      x >= leftGridLimit && x <= rightGridLimit && y >= bottomGridLimit && y <= topGridLimit
  }

  val origin = depots(0)

  lazy val defenderStartingPositions: Vector[DefenderPosition] = defenderStartDepots.map(id => {
    (DefenderPosition.cached(_,_)).tupled(depotPositionToGrid(id) match {case (x,y) => (x.round.toInt, y.round.toInt)})
  }).toVector

  final def randomPossiblePosition(pos: DefenderPosition, rng: RandomGenerator) : DefenderPosition = {
    @tailrec
    def randomPositionInSquare : DefenderPosition = {
      val xJump = (rng.nextInt(2*possibleStepsInTime+1) - possibleStepsInTime)
      val yJump = (rng.nextInt(2*possibleStepsInTime+1) - possibleStepsInTime)
      val newPos = pos.translate(xJump, yJump)
      if(isProperMove(pos, newPos))
        newPos
      else
        randomPositionInSquare
    }

    randomPositionInSquare
  }

  final def possibleXValues(currentX: Int) : Range = Range.inclusive(
    Math.max(currentX-possibleStepsInTime, leftGridLimit),
    Math.min(currentX+possibleStepsInTime, rightGridLimit)
  )

  final def randomPossibleX(rng: RandomGenerator)(currentX: Int) : Int = {
    val lb = Math.max(currentX-possibleStepsInTime, leftGridLimit)
    val ub = Math.min(currentX+possibleStepsInTime, rightGridLimit)
    rng.nextInt(ub-lb+1)+lb
  }


  final val possibleYStepsForDx : Vector[Int] =
    Vector.tabulate(possibleStepsInTime+1)( dx =>
      (Math.sqrt((defenderMoveRangeSquared - spaceDiscretization*spaceDiscretization*(dx)*(dx)))/spaceDiscretization).toInt
    )

  final def possibleYSteps(currX : Int, newX: Int) : Int = possibleYStepsForDx(Math.abs(currX-newX))

  final def possibleYValues(currentPosition: DefenderPosition, newX: Int) : Range = {
    val steps : Int = possibleYSteps(currentPosition.x, newX)
    Range.inclusive(
      Math.max(bottomGridLimit, currentPosition.y-steps),
      Math.min(currentPosition.y+steps, topGridLimit)
    )
  }

  final def getCoveredTargetIds(position: DefenderPosition, timeStep: Int) : SortedSet[Int] = 
    SortedSet(targets.view.filter(targetInRange(toDoublePositions(position), _, timeStep)).map(_.id) :_*)

  final def getCoveredTargetIds(positions: Vector[DefenderPosition], timeStep: Int) : SortedSet[Int] =
    positions.view.map(getCoveredTargetIds(_, timeStep)).foldLeft(SortedSet[Int]()){ _ ++ _ }

  final def targetInRange(dp: DDefenderPosition, target: Target, timeStep: Int) : Boolean = {
    val (tx, ty) = getTargetPosition(target, timeStep)
    val dx = tx-dp.x
    val dy = ty-dp.y
    dx*dx+dy*dy <= defenderRangeSq
  }

  final def randomPossibleY(rng: RandomGenerator)(currentPosition: DefenderPosition, newX: Int) : Int = {
    val steps : Int = possibleYSteps(currentPosition.x, newX)
    val lb = Math.max(bottomGridLimit, currentPosition.y-steps)
    val ub = Math.min(currentPosition.y+steps, topGridLimit)
    rng.nextInt(ub-lb+1)+lb
  }

  final def verifyYValue(currentPosition: DefenderPosition, newX: Int, newY: Int) : Boolean = {
    val dx = Math.abs(currentPosition.x-newX)
    val dy = Math.abs(currentPosition.y-newY)
    dy <= possibleYStepsForDx(dx)
  }

  final def checkXValue(currentX: Int, newX: Int) : Boolean =
    Math.abs(currentX - newX) <= possibleStepsInTime

  final def isProperMove(pos: DefenderPosition, newPos: DefenderPosition) : Boolean =
    GeometryHelpers.distanceSq(newPos.asPositionTuple(this), pos.asPositionTuple(this)) <= defenderMoveRangeSquared && isInGrid(newPos)

  final def possiblePositions(pos: DefenderPosition) : Seq[DefenderPosition] = {
    val xPossibilities = possibleXValues(pos.x)

    xPossibilities.view.flatMap(x => possibleYValues(pos, x).map(DefenderPosition.cached(x,_)))
  }

  final def getTargetPosition(ta: Target, timeStep: Int) : (Double, Double) = {
    val currentTime = timeInterval*timeStep

    lazy val depot1 = getDepotById(ta.depot1Id)

    ta.schedule match {
      case Nil => (depot1.x, depot1.y)
      case first :: _ if first >= currentTime => (depot1.x, depot1.y)
      case _ => {
        val (lastStartTime, lastStartIdx) = ta.schedule.zipWithIndex.takeWhile{case (e, i) => e < currentTime}.toList.last
        val startDepot = getDepotById(if (lastStartIdx % 2 == 0)  ta.depot1Id else ta.depot2Id)
        val endDepot = getDepotById(if (lastStartIdx % 2 == 1)  ta.depot1Id else ta.depot2Id)

        val depotDistance = GeometryHelpers.distance((startDepot.x, startDepot.y), (endDepot.x, endDepot.y))
        val currentPosition = Math.min(depotDistance, (currentTime-lastStartTime)*targetSpeed)
        val relativePosition = currentPosition/depotDistance

        (startDepot.x*(1-relativePosition)+endDepot.x*relativePosition,
          startDepot.y*(1-relativePosition)+endDepot.y*relativePosition)
      }
    }
  }

  final def getTargetPosition(target: Int, timeStep: Int) : (Double, Double) = {
    val ta = getTargetById(target)
    getTargetPosition(ta, timeStep)
  }

  final def toDoublePositions(dp: DefenderPosition) : DDefenderPosition =
    DDefenderPosition(origin.x+dp.x*spaceDiscretization, origin.y+dp.y*spaceDiscretization)

  final def calculateTargetAttack(as: AttackStart, newPositions: Seq[DefenderPosition], timeStep:Int) : (Payoffs, Option[AttackStart]) = {
    val pos = getTargetPosition(as.targetId, timeStep)
    val targ = getTargetById(as.targetId)

    if(newPositions.exists(x => GeometryHelpers.distance(x.asPositionTuple(this), pos) <= defenderRange))
      (Payoffs(targ.defenderUtilityUnuccessfulAttack, targ.attackerUtilityUnuccessfulAttack, 0, 1), None)
    else if(timeStep == as.timeStep + game.attackLength)
      (Payoffs(targ.defenderUtilitySuccessfulAttack, targ.attackerUtilitySuccessfulAttack, 1, 0), None)
    else
      (Payoffs(0,0,0,0), Some(as))
  }

  final def getRewardForAS(as: Option[AttackStart], positions: Seq[DefenderPosition], timeStep: Int) : (Payoffs, Option[AttackStart]) =
    as match {
      case Some(as) => calculateTargetAttack(as, positions, timeStep)
      case _ => (Payoffs(0,0,0,0), as)
    }

}


object MovingTargetsGameConfig {
  def inferSpaceDiscretization(game: MovingTargetsGame) = MovingTargetsGameConfig(game = game,
    spaceDiscretization = Math.min(game.defenderRange, game.defenderSpeed*game.timeInterval/2))

  import scala.language.implicitConversions
  implicit def toGame(mtgc: MovingTargetsGameConfig) : MovingTargetsGame = mtgc.game
}
