package de.nowchess.bot

import de.nowchess.api.board.{Color, PieceType, Square}
import de.nowchess.api.game.GameContext

object Evaluation:

  val CHECKMATE_SCORE: Int = 10_000_000
  val DRAW_SCORE: Int = 0

  // Material values in centipawns (indexed by PieceType.ordinal: Pawn=0, Knight=1, Bishop=2, Rook=3, Queen=4, King=5)
  private val mgMaterial = Array(100, 325, 335, 500, 900, 20_000)
  private val egMaterial = Array(110, 310, 310, 530, 1_000, 20_000)

  private val TEMPO_BONUS: Int = 10

  // Piece-square tables (Simplified Evaluation Function, Michniewski)
  // Indexed by squareIndex = rank.ordinal * 8 + file.ordinal
  // White's perspective: rank 0 = home (r1), rank 7 = back rank (r8)
  // Black is vertically mirrored

  private val mgPawnTable: Array[Int] = Array(
    0, 0, 0, 0, 0, 0, 0, 0,
    50, 50, 50, 50, 50, 50, 50, 50,
    10, 10, 20, 30, 30, 20, 10, 10,
    5, 5, 10, 25, 25, 10, 5, 5,
    0, 0, 0, 20, 20, 0, 0, 0,
    5, -5, -10, 0, 0, -10, -5, 5,
    5, 10, 10, -20, -20, 10, 10, 5,
    0, 0, 0, 0, 0, 0, 0, 0
  )

  private val egPawnTable: Array[Int] = Array(
    0, 0, 0, 0, 0, 0, 0, 0,
    70, 70, 70, 70, 70, 70, 70, 70,
    40, 40, 40, 40, 40, 40, 40, 40,
    30, 30, 30, 30, 30, 30, 30, 30,
    20, 20, 20, 20, 20, 20, 20, 20,
    10, 10, 10, 10, 10, 10, 10, 10,
    5, 5, 5, 5, 5, 5, 5, 5,
    0, 0, 0, 0, 0, 0, 0, 0
  )

  private val mgKnightTable: Array[Int] = Array(
    -50, -40, -30, -30, -30, -30, -40, -50,
    -40, -20, 0, 0, 0, 0, -20, -40,
    -30, 0, 10, 15, 15, 10, 0, -30,
    -30, 5, 15, 20, 20, 15, 5, -30,
    -30, 0, 15, 20, 20, 15, 0, -30,
    -30, 5, 10, 15, 15, 10, 5, -30,
    -40, -20, 0, 5, 5, 0, -20, -40,
    -50, -40, -30, -30, -30, -30, -40, -50
  )

  private val egKnightTable: Array[Int] = Array(
    -30, -20, -10, -10, -10, -10, -20, -30,
    -20, 0, 5, 5, 5, 5, 0, -20,
    -10, 5, 15, 20, 20, 15, 5, -10,
    -10, 5, 20, 25, 25, 20, 5, -10,
    -10, 5, 20, 25, 25, 20, 5, -10,
    -10, 5, 15, 20, 20, 15, 5, -10,
    -20, 0, 5, 5, 5, 5, 0, -20,
    -30, -20, -10, -10, -10, -10, -20, -30
  )

  private val mgBishopTable: Array[Int] = Array(
    -20, -10, -10, -10, -10, -10, -10, -20,
    -10, 0, 0, 0, 0, 0, 0, -10,
    -10, 0, 5, 10, 10, 5, 0, -10,
    -10, 5, 5, 10, 10, 5, 5, -10,
    -10, 0, 10, 10, 10, 10, 0, -10,
    -10, 10, 10, 10, 10, 10, 10, -10,
    -10, 5, 0, 0, 0, 0, 5, -10,
    -20, -10, -10, -10, -10, -10, -10, -20
  )

  private val egBishopTable: Array[Int] = Array(
    -20, -10, -5, -5, -5, -5, -10, -20,
    -10, 0, 5, 5, 5, 5, 0, -10,
    -5, 5, 10, 10, 10, 10, 5, -5,
    -5, 5, 10, 15, 15, 10, 5, -5,
    -5, 5, 10, 15, 15, 10, 5, -5,
    -5, 5, 10, 10, 10, 10, 5, -5,
    -10, 0, 5, 5, 5, 5, 0, -10,
    -20, -10, -5, -5, -5, -5, -10, -20
  )

  private val mgRookTable: Array[Int] = Array(
    0, 0, 0, 0, 0, 0, 0, 0,
    5, 10, 10, 10, 10, 10, 10, 5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    0, 0, 0, 5, 5, 0, 0, 0
  )

  private val egRookTable: Array[Int] = Array(
    0, 0, 0, 0, 0, 0, 0, 0,
    5, 10, 10, 10, 10, 10, 10, 5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    -5, 0, 0, 0, 0, 0, 0, -5,
    0, 0, 0, 5, 5, 0, 0, 0
  )

  private val mgQueenTable: Array[Int] = Array(
    -20, -10, -10, -5, -5, -10, -10, -20,
    -10, 0, 0, 0, 0, 0, 0, -10,
    -10, 0, 5, 5, 5, 5, 0, -10,
    -5, 0, 5, 5, 5, 5, 0, -5,
    0, 0, 5, 5, 5, 5, 0, -5,
    -10, 5, 5, 5, 5, 5, 0, -10,
    -10, 0, 5, 0, 0, 0, 0, -10,
    -20, -10, -10, -5, -5, -10, -10, -20
  )

  private val egQueenTable: Array[Int] = Array(
    -15, -10, -8, -5, -5, -8, -10, -15,
    -10, 0, 3, 5, 5, 3, 0, -10,
    -8, 3, 10, 10, 10, 10, 3, -8,
    -5, 5, 10, 15, 15, 10, 5, -5,
    -5, 5, 10, 15, 15, 10, 5, -5,
    -8, 3, 10, 10, 10, 10, 3, -8,
    -10, 0, 3, 5, 5, 3, 0, -10,
    -15, -10, -8, -5, -5, -8, -10, -15
  )

  private val mgKingTable: Array[Int] = Array(
    -30, -40, -40, -50, -50, -40, -40, -30,
    -30, -40, -40, -50, -50, -40, -40, -30,
    -30, -40, -40, -50, -50, -40, -40, -30,
    -30, -40, -40, -50, -50, -40, -40, -30,
    -20, -30, -30, -40, -40, -30, -30, -20,
    -10, -20, -20, -20, -20, -20, -20, -10,
    20, 20, 0, 0, 0, 0, 20, 20,
    20, 30, 10, 0, 0, 10, 30, 20
  )

  private val egKingTable: Array[Int] = Array(
    -50, -40, -30, -20, -20, -30, -40, -50,
    -30, -20, -10, 0, 0, -10, -20, -30,
    -30, -10, 20, 30, 30, 20, -10, -30,
    -30, -10, 30, 40, 40, 30, -10, -30,
    -30, -10, 30, 40, 40, 30, -10, -30,
    -30, -10, 20, 30, 30, 20, -10, -30,
    -30, -30, 0, 0, 0, 0, -30, -30,
    -50, -30, -30, -30, -30, -30, -30, -50
  )

  private val phaseWeight: Map[PieceType, Int] = Map(
    PieceType.Knight -> 1,
    PieceType.Bishop -> 1,
    PieceType.Rook -> 2,
    PieceType.Queen -> 4
  )
  private val maxPhase = 24 // 4*4 + 4*2 + 4*1 + 4*1

  private val passedPawnBonus: Array[Int] = Array(0, 5, 10, 20, 35, 60, 100, 0)

  // Pawn structure penalties
  private val doubledMg = -10
  private val doubledEg = -25
  private val isolatedMg = -15
  private val isolatedEg = -20

  /** Evaluate the position from the perspective of context.turn.
   *  Positive = good for context.turn. */
  def evaluate(context: GameContext): Int =
    val phase = gamePhase(context.board)
    val material = materialAndPositional(context, phase)
    val structure = pawnStructure(context, phase)
    val bonuses = positionalBonuses(context, phase)
    material + structure + bonuses + TEMPO_BONUS

  private def gamePhase(board: de.nowchess.api.board.Board): Int =
    val phase = board.pieces.values.foldLeft(0) { (acc, piece) =>
      acc + phaseWeight.getOrElse(piece.pieceType, 0)
    }
    math.min(phase, maxPhase)

  private def taper(mg: Int, eg: Int, phase: Int): Int =
    (mg * phase + eg * (maxPhase - phase)) / maxPhase

  private def materialAndPositional(context: GameContext, phase: Int): Int =
    var mg = 0
    var eg = 0
    for (square, piece) <- context.board.pieces do
      val (psqMg, psqEg) = squareBonus(piece.pieceType, piece.color, square)
      val pieceMg = mgMaterial(piece.pieceType.ordinal) + psqMg
      val pieceEg = egMaterial(piece.pieceType.ordinal) + psqEg
      val sign = if piece.color == context.turn then 1 else -1
      mg += sign * pieceMg
      eg += sign * pieceEg
    taper(mg, eg, phase)

  private def squareBonus(pieceType: PieceType, color: Color, sq: Square): (Int, Int) =
    val rankIdx = if color == Color.White then sq.rank.ordinal else 7 - sq.rank.ordinal
    val fileIdx = sq.file.ordinal
    val squareIdx = rankIdx * 8 + fileIdx

    pieceType match
      case PieceType.Pawn => (mgPawnTable(squareIdx), egPawnTable(squareIdx))
      case PieceType.Knight => (mgKnightTable(squareIdx), egKnightTable(squareIdx))
      case PieceType.Bishop => (mgBishopTable(squareIdx), egBishopTable(squareIdx))
      case PieceType.Rook => (mgRookTable(squareIdx), egRookTable(squareIdx))
      case PieceType.Queen => (mgQueenTable(squareIdx), egQueenTable(squareIdx))
      case PieceType.King => (mgKingTable(squareIdx), egKingTable(squareIdx))

  private def pawnStructure(context: GameContext, phase: Int): Int =
    val friendlyPawns = context.board.pieces.filter((_, p) => p.color == context.turn && p.pieceType == PieceType.Pawn)
    val enemyPawns = context.board.pieces.filter((_, p) => p.color != context.turn && p.pieceType == PieceType.Pawn)

    var mg = 0
    var eg = 0

    // Group pawns by file
    val friendlyByFile = friendlyPawns.groupMap(s => s._1.file.ordinal)(s => s._1.rank.ordinal)
    val enemyByFile = enemyPawns.groupMap(s => s._1.file.ordinal)(s => s._1.rank.ordinal)

    // Doubled pawn penalty for friendly pawns
    for (file, ranks) <- friendlyByFile do
      if ranks.size > 1 then
        val doubledCount = ranks.size - 1
        mg += doubledCount * doubledMg
        eg += doubledCount * doubledEg

    // Isolated pawn penalty for friendly pawns
    for (file, _) <- friendlyByFile do
      val hasAdjacentFriendly = (file - 1 to file + 1).filter(f => f >= 0 && f < 8 && f != file).exists(friendlyByFile.contains)
      if !hasAdjacentFriendly then
        val pawnsOnFile = friendlyByFile(file).size
        mg += pawnsOnFile * isolatedMg
        eg += pawnsOnFile * isolatedEg

    // Same for enemy pawns (subtract from score)
    var enemyMg = 0
    var enemyEg = 0

    for (file, ranks) <- enemyByFile do
      if ranks.size > 1 then
        val doubledCount = ranks.size - 1
        enemyMg += doubledCount * doubledMg
        enemyEg += doubledCount * doubledEg

    for (file, _) <- enemyByFile do
      val hasAdjacentEnemy = (file - 1 to file + 1).filter(f => f >= 0 && f < 8 && f != file).exists(enemyByFile.contains)
      if !hasAdjacentEnemy then
        val pawnsOnFile = enemyByFile(file).size
        enemyMg += pawnsOnFile * isolatedMg
        enemyEg += pawnsOnFile * isolatedEg

    taper(mg - enemyMg, eg - enemyEg, phase)

  private def positionalBonuses(context: GameContext, phase: Int): Int =
    var score = 0
    for (sq, piece) <- context.board.pieces do
      val bonus = piece.pieceType match
        case PieceType.Pawn =>
          if isPassedPawn(context.board, sq, piece.color) then
            passedPawnBonus(sq.rank.ordinal)
          else 0
        case PieceType.Rook =>
          rookOpenFileBonus(context.board, sq, piece.color)
        case PieceType.King =>
          kingShieldBonus(context.board, sq, piece.color, phase)
        case _ => 0
      if piece.color == context.turn then score += bonus
      else score -= bonus
    score

  private def isPassedPawn(board: de.nowchess.api.board.Board, sq: Square, color: Color): Boolean =
    val enemyColor = color.opposite
    val pawnRank = sq.rank.ordinal
    val fileRange = (sq.file.ordinal - 1 to sq.file.ordinal + 1).filter(f => f >= 0 && f < 8)
    val rankCheck = if color == Color.White then (r: Int) => r > pawnRank else (r: Int) => r < pawnRank

    board.pieces.forall { (enemySq, enemyPiece) =>
      !(enemyPiece.color == enemyColor &&
        enemyPiece.pieceType == PieceType.Pawn &&
        fileRange.contains(enemySq.file.ordinal) &&
        rankCheck(enemySq.rank.ordinal))
    }

  private def rookOpenFileBonus(board: de.nowchess.api.board.Board, rookSq: Square, color: Color): Int =
    val hasFriendlyPawn = board.pieces.exists { (sq, piece) =>
      piece.color == color && piece.pieceType == PieceType.Pawn && sq.file == rookSq.file
    }
    val hasEnemyPawn = board.pieces.exists { (sq, piece) =>
      piece.color != color && piece.pieceType == PieceType.Pawn && sq.file == rookSq.file
    }
    if !hasFriendlyPawn && !hasEnemyPawn then 20  // open file
    else if !hasFriendlyPawn then 10              // semi-open file
    else 0

  private def kingShieldBonus(board: de.nowchess.api.board.Board, kingSq: Square, color: Color, phase: Int): Int =
    val shieldRankDelta = if color == Color.White then 1 else -1
    val shieldFiles = (kingSq.file.ordinal - 1 to kingSq.file.ordinal + 1).filter(f => f >= 0 && f < 8)
    val shieldRank = kingSq.rank.ordinal + shieldRankDelta

    if shieldRank < 0 || shieldRank > 7 then 0
    else
      val rawBonus = board.pieces.count { (sq, piece) =>
        piece.color == color &&
        piece.pieceType == PieceType.Pawn &&
        shieldFiles.contains(sq.file.ordinal) &&
        sq.rank.ordinal == shieldRank
      } * 10
      (rawBonus * phase) / maxPhase
