package petrus

import scala.language.existentials

import argonaut.Json
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
import ch.qos.logback.classic.spi.{ ILoggingEvent }
import ch.qos.logback.core.{ ConsoleAppender, FileAppender }
import org.scalatest.BeforeAndAfterAll
import org.slf4j.Logger
import pl.edu.pw.mini.sg.game.boundedrationality.BoundedRationality
import pl.edu.pw.mini.sg.game.deterministic.{ BehavioralStrategy, PureStrategy }
import pl.edu.pw.mini.sg.game.graph.sud.{ DeterministicSingleUnitMoveGraphGameJson, DeterministicSingleUnitMoveGraphGameJsonDecoders }
import pl.edu.pw.mini.sg.game.results.TrainingResult
import pl.edu.pw.mini.sg.mcts.tree.simple.SimpleTreeFactory
import pl.edu.pw.mini.sg.mcts.tree.stat.MaxStatCalculator
import pl.edu.pw.mini.sg.mixed.FeasibleStrategy
import pl.edu.pw.mini.sg.mixed.expansion.MctsNodeExpansion
import pl.edu.pw.mini.sg.mixed.response.HistorySkipConfig
import pl.edu.pw.mini.sg.mixed.response.provider.ExactResponseProvider
import scala.io.Source

import argonaut.Parse
import ch.qos.logback.classic.{Level, LoggerContext}
import org.apache.commons.math3.random.{MersenneTwister, RandomGenerator}
import org.scalactic.TolerantNumerics
import org.scalatest.{Matchers, WordSpec}
import pl.edu.pw.mini.sg.game.deterministic.TreeExpander
import pl.edu.pw.mini.sg.game.deterministic.single.{DefenderGameView, GameWithPayoff, LinearizedAdvanceableGame, SinglePlayerGame}
import pl.edu.pw.mini.sg.game.graph.{DeterministicGraphGameConfig, DeterministicGraphGameConfigJson}
import pl.edu.pw.mini.sg.game.graph.sud.{DeterministicSingleUnitMoveGraphGame, SingleUnitAttackerState, SingleUnitDefenderState}
import pl.edu.pw.mini.sg.mixed.response.provider.ExactResponseProvider
import pl.edu.pw.mini.sg.game.util.MoveStateKey
import pl.edu.pw.mini.sg.mcts.{MctsParameters, UniformRandomMove}
import pl.edu.pw.mini.sg.mcts.propagation.SimplePropagation
import pl.edu.pw.mini.sg.mcts.selection.UctSelection
import pl.edu.pw.mini.sg.mcts.tree.stat.MeanStatCalculator
import pl.edu.pw.mini.sg.mixed.{MixedMethodConfig, UctAttackerSampler, IterationInfo}
import pl.edu.pw.mini.sg.mixed.adjuster.momentum.{MomentumAdjuster, MomentumDirectionLimitAdjuster, MomentumUpdateSignLimitAdjuster, SquaredNormalizerMomentumAdjuster}
import pl.edu.pw.mini.sg.mixed.adjuster.coorddamping.CoordDampingAdjuster
import pl.edu.pw.mini.sg.mixed.adjuster.signchange.{SignChangeCounterGlobalAdjuster, SignChangeCounterLocalAdjuster}
import pl.edu.pw.mini.sg.mixed.adjuster.globaldamping.GlobalDampingAdjuster
import pl.edu.pw.mini.sg.mixed.expander.{MctsExpander, NegativeAttacker, PositiveDefender}
import pl.edu.pw.mini.sg.mixed.decision.{AlwaysImproveDecision, ProportionalImproveDecision}
import pl.edu.pw.mini.sg.mixed.expansion.RandomNodeExpansion
import pl.edu.pw.mini.sg.mixed.stchooser.ValueSubtreeChooser
import pl.edu.pw.mini.sg.mixed.tree.{CalculateResults, InnerResultNode, MixedStrategyNode}
import pl.edu.pw.mini.sg.mixed.tree.MixedStrategyNode.unitST
import pl.edu.pw.mini.sg.mixed.prune.NoPrune
import scalaz.NonEmptyList
import pl.edu.pw.mini.sg.se.RNGImpl
import pl.edu.pw.mini.sg.game.util.attacker.OptimalAttacker

class PayoffValueSpec extends WordSpec with Matchers with BeforeAndAfterAll {
    override def beforeAll() = {
    ch.qos.logback.classic.BasicConfigurator.configureDefaultContext()
    val context: LoggerContext = org.slf4j.LoggerFactory.getILoggerFactory().asInstanceOf[LoggerContext];
    val rootLogger = context.getLogger(Logger.ROOT_LOGGER_NAME)
    rootLogger.detachAndStopAllAppenders()
    rootLogger.setLevel(Level.INFO)
    rootLogger.addAppender({
      val appender = new FileAppender[ILoggingEvent]()
      appender.setFile("graph-test.log")
      appender.setContext(context)
      appender.setEncoder({
        val enc = new PatternLayoutEncoder()
        enc.setContext(context)
        enc.setPattern("[%thread] %-20logger %level - %msg%n")
        enc.start()
        enc
      })
      appender.start()
      appender
    })
    val peaLogger = context.getLogger("pl.edu.pw.mini.sg.mixed")
    peaLogger.setLevel(Level.DEBUG)
//    context.getLogger("pl.edu.pw.mini.sg.mixed.MixedMethod$").setLevel(Level.DEBUG)
//    context.getLogger("pl.edu.pw.mini.sg.mixed.UctAttackerSampler$").setLevel(Level.DEBUG)
//    context.start();
  }

  implicit val doubleEquality = TolerantNumerics.tolerantDoubleEquality(0.003)

  val mctsConfig = MctsParameters(
    simulationsInRoot = 500,
    mctsSelection = UctSelection(1.4),
    mcPhaseMove = UniformRandomMove,
    propagation = SimplePropagation,
    treeFactory = SimpleTreeFactory(MeanStatCalculator)

  )

  val mixedMethodConfig = MixedMethodConfig(
    allRefinementIterations = 5000,
    feasibilityTryIterations = 10000,
    //strategyAdjuster = InhibitingStrategyAdjuster(1),
    //strategyAdjuster = LinearInhibitingStrategyAdjuster(1),
    strategyAdjuster = MomentumAdjuster,
//    strategyAdjuster = MomentumDirectionLimitAdjuster,
//    strategyAdjuster = SquaredNormalizerMomentumAdjuster(1.4),
//    strategyAdjuster = MomentumUpdateSignLimitAdjuster,
    //strategyAdjuster = CoordDampingAdjuster,
    //strategyAdjuster = GlobalDampingAdjuster,
    //strategyAdjuster = SignChangeCounterGlobalAdjuster,
    //   strategyAdjuster = SignChangeCounterLocalAdjuster,
    expansionDecision = MctsNodeExpansion(mctsConfig.copy(treeFactory = SimpleTreeFactory(MaxStatCalculator)), false, 0.05),

    // expansionDecision = RandomNodeExpansion(0.4,0.3,
    //   initialExpander = MctsExpander(mctsConfig, NegativeAttacker),
    //   positiveExpander = MctsExpander(mctsConfig, PositiveDefender),
    //   negativeExpander = MctsExpander(mctsConfig, NegativeAttacker),
    //   expandWorsePayoff = false
    // ),
    subtreeChooser = ValueSubtreeChooser(1),
    poorImprovementIterations = 500,
    poorImprovementEps = 1e-5,
    //improveDecision = ProportionalImproveDecision(-0.5,3)
    improveDecision = AlwaysImproveDecision,
    attackerOracleProvider = ExactResponseProvider(HistorySkipConfig.noSkip),
    treePruneDecision = NoPrune,
    boundedRationalityStrategy = BoundedRationality.fullyRational
  )


  implicit val rng = new RNGImpl(new org.apache.commons.math3.random.MersenneTwister())

  def readGame(file: String) : DeterministicSingleUnitMoveGraphGame = {
    import DeterministicGraphGameConfigJson.deterministicGraphGameConfigCodec
    val gameConfig: DeterministicGraphGameConfig = Parse.decode[DeterministicGraphGameConfig](
      Source.fromInputStream(getClass.getResourceAsStream(file)).mkString
    ) match {
      case Right(v) => v
      case Left(err) => throw new RuntimeException(err.toString)
    }

    DeterministicSingleUnitMoveGraphGame(gameConfig)
  }

  def checkStrategies(
    s1: BehavioralStrategy[Int, SingleUnitDefenderState],
    s2: BehavioralStrategy[Int, SingleUnitDefenderState]
  ) : Unit = {
    s1.state should equal(s2.state)
    s1.probabilities.map(_._1) should contain theSameElementsAs(s2.probabilities.map(_._1))

    s1.probabilities.foreach{case (m, prob) => s2.probabilities.find(_._1 == m).get._2 should equal(prob)}
    s1.successorsInTree should contain theSameElementsAs(s2.successorsInTree)

    s1.successorsInTree.foreach{case (m, s) =>
      checkStrategies(s1.nextState(m, s), s2.nextState(m, s))
    }
  }

  def checkAgainstAttacker(
    game: DeterministicSingleUnitMoveGraphGame,
    s1: BehavioralStrategy[Int, SingleUnitDefenderState],
    s2: BehavioralStrategy[Int, SingleUnitDefenderState],
    attacker: PureStrategy[Int, SingleUnitAttackerState],
    json: Json
  ) : Unit = {
    s1.state shouldEqual (game.defenderState)
    s2.state shouldEqual (game.defenderState)
    s1.probabilities.map(_._1) should contain theSameElementsAs(s2.probabilities.map(_._1))

    s1.probabilities.foreach{case (dm, prob) => (withClue(s"${s1}\n${s2}\n${json}"){s2.probabilities.find(_._1==dm).get._2 should equal (prob)})}

    if(!game.isLeaf) {
      s1.probabilities.foreach{case (dm, _) => {
        val nextGame = game.makeMove(dm, attacker.move)
        checkAgainstAttacker(nextGame, s1.nextState(dm, nextGame.defenderState),
          s2.nextState(dm, nextGame.defenderState),
          attacker.nextState(nextGame.attackerState), json)
      }}
    }
  }


  def resultCheck(game: DeterministicSingleUnitMoveGraphGame, optimalAttackerStrategy: PureStrategy[Int, SingleUnitAttackerState], expectedResult: Double) = {
    beforeAll()
    val (optimalDefender, optimalDefenderPayoff) = UctAttackerSampler.defenderForGivenAttacker[
      Int, Int,
      SingleUnitDefenderState,
      SingleUnitAttackerState,
      DeterministicSingleUnitMoveGraphGame
    ](mixedMethodConfig)(game, optimalAttackerStrategy)() match {
      case FeasibleStrategy(optimalDefender, optimalDefenderPayoff) => (optimalDefender, optimalDefenderPayoff)
      case _ => fail("No feasible strategy found")
    }

    val (attacker, payoff) = OptimalAttacker.calculate[
      Int, Int,
      SingleUnitDefenderState,
      SingleUnitAttackerState,
      DeterministicSingleUnitMoveGraphGame
    ](game, optimalDefender)(BoundedRationality.fullyRational)


    val json : Json = BehavioralStrategy.strategyEncode(DeterministicSingleUnitMoveGraphGameJson.defenderMoveEncode, DeterministicSingleUnitMoveGraphGameJson.defenderStateEncode).encode(optimalDefender)
    val dec = new DeterministicSingleUnitMoveGraphGameJsonDecoders(game.defenderState.graphGameConfig)
    val str1 = TrainingResult.behavioralStrategyDecode(dec.defenderMoveDecode, dec.defenderStateDecode).decodeJson(json).getOr(throw new RuntimeException("cannot decode"))

    val (attacker2, payoff2) = OptimalAttacker.calculate[
      Int, Int,
      SingleUnitDefenderState,
      SingleUnitAttackerState,
      DeterministicSingleUnitMoveGraphGame
    ](game, str1)(BoundedRationality.fullyRational)

    val payoff3 = OptimalAttacker.payoff[
      Int, Int,
      SingleUnitDefenderState,
      SingleUnitAttackerState,
      DeterministicSingleUnitMoveGraphGame
    ](game, str1, attacker)(BoundedRationality.fullyRational)
      

    "serialized strategy should be equivalent to original" in {
      checkStrategies(optimalDefender, str1)
      checkStrategies(str1, optimalDefender)
    }

    "serialized strategy give the same probabilities as original" in {
      checkAgainstAttacker(game, optimalDefender, str1, attacker, json)
    }

    "payoff agains previous attacker should be the same" in {
      payoff3.real.attacker shouldEqual(payoff.real.attacker)
    }

    "serialized payoff against recaulculated optimal shoudl be equal" in {
      payoff.real.attacker should equal(payoff2.real.attacker)
      payoff2.real.defender should equal (expectedResult)

    }

    s"defender payoff should be ${expectedResult}" in {
      optimalDefenderPayoff.real.defender should equal(expectedResult)
      payoff.real.defender should equal (expectedResult)
    }
  }


  "graph game1" when {
    val game = readGame("game1-5.json")

    val linearizedGame = LinearizedAdvanceableGame[Int, SingleUnitAttackerState](NonEmptyList(game.attackerState), Nil, Nil)

    val optimalAttackerStrategy = {
      val gg = linearizedGame.makeMove(5).makeMove(5).makeMove(5).makeMove(5).makeMove(6)
      require(gg.possibleMoves.isEmpty)
      gg.pureStrategy
    }

    implicit val ii = IterationInfo.dummyIterationInfo
    val optimalDefenderStrategy = {
      val dgv = DefenderGameView[Int, Int, SingleUnitDefenderState, SingleUnitAttackerState, DeterministicSingleUnitMoveGraphGame](game, optimalAttackerStrategy)
      val strategy = new MixedStrategyNode[Int, SingleUnitDefenderState, Unit](game.defenderState)(
      MixedStrategyNode.unitST);
      {
        val dd = dgv.makeMove(6)
        strategy.addMove(6, 0.5)
        strategy.addSuccessor(new MoveStateKey(6, dd.playerState), new MixedStrategyNode[Int, SingleUnitDefenderState, Unit](dd.playerState))
      };
      {
        strategy.addMove(6, 0.5)
        strategy.addSuccessor(new MoveStateKey(6, dgv.makeMove(6).playerState), {
          val s2 = dgv.makeMove(6)
          val n2 = new MixedStrategyNode[Int, SingleUnitDefenderState, Unit](s2.playerState)
          n2.addMove(6, 1.0)
          n2.addSuccessor(new MoveStateKey(6, s2.makeMove(6).playerState), {
            val s3 = s2.makeMove(6)
            val n3 = new MixedStrategyNode[Int, SingleUnitDefenderState, Unit](s3.playerState)
            n3.addMove(6, 1.0)
            n3.addSuccessor(new MoveStateKey(6, s3.makeMove(6).playerState), {
              val s4 = s3.makeMove(6)
              val n4 = new MixedStrategyNode[Int, SingleUnitDefenderState, Unit](s4.playerState)
              n4.addMove(6, 1.0)
              n4.addSuccessor(new MoveStateKey(6, s4.makeMove(6).playerState), {
                val s5 = s4.makeMove(6)
                val n5 = new MixedStrategyNode[Int, SingleUnitDefenderState, Unit](s5.playerState)
                n5.addMove(6, 1.0)
                n5.addSuccessor(new MoveStateKey(6, s5.makeMove(6).playerState), {
                  val s6 = s5.makeMove(6)
                  require(s6.possibleMoves.isEmpty)
                  new MixedStrategyNode[Int, SingleUnitDefenderState, Unit](s6.playerState)
                })
                n5
              })
              n4
            })
            n3
          })
          n2
        })
      }
      strategy
    }
    implicit val br = BoundedRationality.fullyRational
    "calculate (optimalAttackerStrategy, optimalDefenderStrategy)" should {
      "give payoff of" in {
        val rt = new InnerResultNode[Int, SingleUnitDefenderState](optimalDefenderStrategy,
        game.defenderState)(BoundedRationality.fullyRational)
        CalculateResults.calculate(game, optimalDefenderStrategy, optimalAttackerStrategy, rt, new TreeExpander[Int, SingleUnitDefenderState, GameWithPayoff]{
          override type SubtreeData[MO,ST] = Unit
          def zeroSubtreeData[MM <: Int, SS <: SingleUnitDefenderState]: SubtreeData[MM,SS] = ???
          def expand[MM <: Int, SS <: SingleUnitDefenderState, GG <: SinglePlayerGame[MM,SS,GG] with GameWithPayoff](
            state: GG, subtreeData: SubtreeData[MM,SS]
          )(implicit rng: pl.edu.pw.mini.sg.rng.RandomGenerator) : (MM, SubtreeData[MM, SS]) = ???

        }, false)

        rt.payoff.real.defender should equal(0.032)
        rt.payoff.real.attacker should equal(-.0334)
      }
    }

    
    "calculate optimal defender strategy" should {
      resultCheck(game, optimalAttackerStrategy, 0.018)
    }
  }

  "graph game 3a" when {
    val game = readGame("game3a-5.ggame")
    val linearizedGame = LinearizedAdvanceableGame[Int, SingleUnitAttackerState](NonEmptyList(game.attackerState), Nil, Nil)

    val optimalAttackerStrategy = {
        val gg = linearizedGame.makeMove(8).makeMove(9).makeMove(10).makeMove(3).makeMove(0)
        require(gg.possibleMoves.isEmpty)
        gg.pureStrategy
    }




    "calculate optimal defender strategy" should {
      resultCheck(game, optimalAttackerStrategy, -.150)
    }
  }

    "graph game sb-1" should {
    val game = readGame("smallbuilding-1-5.ggame")
    val linearizedGame = LinearizedAdvanceableGame[Int, SingleUnitAttackerState](NonEmptyList(game.attackerState), Nil, Nil)

    val optimalAttackerStrategy = {
        val gg = linearizedGame.makeMove(9).makeMove(10).makeMove(6).makeMove(2).makeMove(2)
        require(gg.possibleMoves.isEmpty)
        gg.pureStrategy
    }




      "calculate optimal defender strategy" should {
        resultCheck(game, optimalAttackerStrategy, 0)
      }
  }


  "graph game 3d" should {
    val game = readGame("game3d-5.ggame")
    val linearizedGame = LinearizedAdvanceableGame[Int, SingleUnitAttackerState](NonEmptyList(game.attackerState), Nil, Nil)

    val optimalAttackerStrategy = {
        val gg = linearizedGame.makeMove(8).makeMove(7).makeMove(6).makeMove(5).makeMove(2)
        require(gg.possibleMoves.isEmpty)
        gg.pureStrategy
    }

    "calculate optimal defender strategy" should {
      resultCheck(game, optimalAttackerStrategy, .030)
    }
  }

  "graph game sb-96" should {
    val game = readGame("smallbuilding-96-5.ggame")
    val linearizedGame = LinearizedAdvanceableGame[Int, SingleUnitAttackerState](NonEmptyList(game.attackerState), Nil, Nil)

    val optimalAttackerStrategy = {
        val gg = linearizedGame.makeMove(13).makeMove(14).makeMove(10).makeMove(9).makeMove(2)
        require(gg.possibleMoves.isEmpty)
        gg.pureStrategy
    }

    "calculate optimal defender strategy" should {
      resultCheck(game, optimalAttackerStrategy, .006)
    }
  }




  "graph game sb-56" should {
    val game = readGame("smallbuilding-56-5.ggame")
    val linearizedGame = LinearizedAdvanceableGame[Int, SingleUnitAttackerState](NonEmptyList(game.attackerState), Nil, Nil)

    val optimalAttackerStrategy = {
        val gg = linearizedGame.makeMove(13).makeMove(9).makeMove(5).makeMove(6).makeMove(2)
        require(gg.possibleMoves.isEmpty)
        gg.pureStrategy
    }


    "calculate optimal defender strategy" should {
      resultCheck(game, optimalAttackerStrategy, .052)
    }
  }

  "graph game sb-93" should {
    val game = readGame("smallbuilding-93-5.ggame")
    val linearizedGame = LinearizedAdvanceableGame[Int, SingleUnitAttackerState](NonEmptyList(game.attackerState), Nil, Nil)

    val optimalAttackerStrategy = {
        val gg = linearizedGame.makeMove(5).makeMove(6).makeMove(7).makeMove(11).makeMove(1)
        require(gg.possibleMoves.isEmpty)
        gg.pureStrategy
    }

    "calculate optimal defender strategy" should {
      resultCheck(game, optimalAttackerStrategy, -.233334)
    }
  }

  "graph game sb-91" should {
    val game = readGame("smallbuilding-91-5.ggame")
    val linearizedGame = LinearizedAdvanceableGame[Int, SingleUnitAttackerState](NonEmptyList(game.attackerState), Nil, Nil)

    val optimalAttackerStrategy = {
        val gg = linearizedGame.makeMove(9).makeMove(5).makeMove(15).makeMove(6).makeMove(2)
        require(gg.possibleMoves.isEmpty)
        gg.pureStrategy
    }

    "calculate optimal defender strategy" should {
      resultCheck(game, optimalAttackerStrategy, -.1872)
    }
  }

  "graph game sb-74" should {
    val game = readGame("smallbuilding-74-5.ggame")
    val linearizedGame = LinearizedAdvanceableGame[Int, SingleUnitAttackerState](NonEmptyList(game.attackerState), Nil, Nil)

    val optimalAttackerStrategy = {
        val gg = linearizedGame.makeMove(13).makeMove(14).makeMove(10).makeMove(6).makeMove(2)
        require(gg.possibleMoves.isEmpty)
        gg.pureStrategy
    }

    "calculate optimal defender strategy" should {
      resultCheck(game, optimalAttackerStrategy, .050)
    }
  }

  "graph game sb-24" should {
    val game = readGame("smallbuilding-24-5.ggame")
    val linearizedGame = LinearizedAdvanceableGame[Int, SingleUnitAttackerState](NonEmptyList(game.attackerState), Nil, Nil)

    val optimalAttackerStrategy = {
        val gg = linearizedGame.makeMove(9).makeMove(10).makeMove(6).makeMove(7).makeMove(1)
        require(gg.possibleMoves.isEmpty)
        gg.pureStrategy
    }

    "calculate optimal defender strategy" should {
      resultCheck(game, optimalAttackerStrategy, .052)
    }
  }

  "graph game superdiv-88" should {
    val game = readGame("smallbuilding-superdiv-88.ggame")
    val linearizedGame = LinearizedAdvanceableGame[Int, SingleUnitAttackerState](NonEmptyList(game.attackerState), Nil, Nil)

    val optimalAttackerStrategy = {
        val gg = linearizedGame.makeMove(8).makeMove(8).makeMove(4).makeMove(0).makeMove(1)
        require(gg.possibleMoves.isEmpty)
        gg.pureStrategy
    }

    "calculate optimal defender strategy" should {
      resultCheck(game, optimalAttackerStrategy, .1999)
    }
  }
}
