import { IWT2_CLASS_METADATA } from '../content/classes'
import { BULLDROME_BOSS_METADATA, IWT2_BOSS_METADATA } from '../content/bosses'
import type { Iwt2ArenaState, Iwt2HostileAddState, Iwt2PartyEntityState, Iwt2Vec2 } from './types'
import {
  addVec2,
  clampVec2ToArena,
  distanceVec2,
  dotVec2,
  lengthSqVec2,
  moveToward,
  normalizeVec2,
  scaleVec2,
  subtractVec2,
  withFallbackFacing,
} from './vector'

const CENTER_LEASH_BOSS_IDS = new Set(['great-jaggi', 'khezu'])
const CENTER_LEASH_START_DISTANCE = 230
const CENTER_LEASH_WALL_MARGIN = 96
const CENTER_LEASH_EXTRA_DISTANCE = 36
const PARTY_ARRIVAL_RADIUS = 3.5

export function tickPartyMember(
  member: Iwt2PartyEntityState,
  state: Iwt2ArenaState,
  inputMove: Iwt2Vec2,
  dt: number,
): Iwt2PartyEntityState {
  const metadata = IWT2_CLASS_METADATA[member.classId]
  const status = {
    stunnedSeconds: Math.max(0, member.status.stunnedSeconds - dt),
    knockedDownSeconds: Math.max(0, member.status.knockedDownSeconds - dt),
    invulnerableSeconds: Math.max(0, member.status.invulnerableSeconds - dt),
  }
  const attackCooldownRemaining = Math.max(0, member.attackCooldownRemaining - dt)
  const castSecondsRemaining = Math.max(0, member.castSecondsRemaining - dt)
  if (member.health <= 0) {
    return { ...member, velocity: { x: 0, y: 0 }, attackCooldownRemaining, castSecondsRemaining: 0, attackReady: false, status }
  }
  if (status.stunnedSeconds > 0 || status.knockedDownSeconds > 0) {
    return { ...member, velocity: { x: 0, y: 0 }, attackCooldownRemaining, castSecondsRemaining: 0, attackReady: false, status }
  }

  if (member.aiRole === 'player') {
    const normalizedInput = lengthSqVec2(inputMove) > 1 ? normalizeVec2(inputMove) : inputMove
    const velocity = scaleVec2(normalizedInput, metadata.moveSpeed)
    const position = clampVec2ToArena(
      {
        x: member.position.x + velocity.x * dt,
        y: member.position.y + velocity.y * dt,
      },
      member.radius,
      state.bounds,
    )
    return {
      ...member,
      position,
      velocity,
      facing: withFallbackFacing(velocity, member.facing),
      attackCooldownRemaining,
      castSecondsRemaining: 0,
      attackReady: false,
      status,
    }
  }

  const dangerDestination = getDangerAvoidancePosition(member, state)
  const attackTarget = getPriorityAttackTarget(member, state)
  const canCast = member.aiRole === 'ranged'
    && attackCooldownRemaining <= 0
    && !!attackTarget
    && canPartyMemberHitTarget({ ...member, attackCooldownRemaining, castSecondsRemaining }, attackTarget.position, attackTarget.radius)
  if (!dangerDestination && member.aiRole === 'ranged' && (member.castSecondsRemaining > 0 || canCast)) {
    const nextCastSecondsRemaining = member.castSecondsRemaining > 0
      ? castSecondsRemaining
      : metadata.castTime
    return {
      ...member,
      velocity: { x: 0, y: 0 },
      facing: withFallbackFacing(subtractVec2(attackTarget?.position ?? getPrimaryBoss(state).position, member.position), member.facing),
      attackCooldownRemaining,
      castSecondsRemaining: nextCastSecondsRemaining,
      attackReady: member.castSecondsRemaining > 0 && nextCastSecondsRemaining <= 0,
      status,
    }
  }

  const decisionSecondsRemaining = Math.max(0, member.decisionSecondsRemaining - dt)
  const desired = dangerDestination ?? getPartyDesiredPosition(member, state)
  const maxDistance = metadata.moveSpeed * dt
  const distanceToDesired = distanceVec2(member.position, desired)
  const movingToDesired = distanceToDesired > PARTY_ARRIVAL_RADIUS
  const position = movingToDesired
    ? clampVec2ToArena(moveToward(member.position, desired, maxDistance), member.radius, state.bounds)
    : member.position
  const velocity = movingToDesired
    ? scaleVec2(subtractVec2(position, member.position), dt > 0 ? 1 / dt : 0)
    : { x: 0, y: 0 }
  return {
    ...member,
    position,
    velocity,
    facing: withFallbackFacing(subtractVec2(attackTarget?.position ?? getPrimaryBoss(state).position, position), member.facing),
    attackCooldownRemaining,
    castSecondsRemaining: 0,
    attackReady: false,
    status,
    decisionSecondsRemaining: decisionSecondsRemaining <= 0
      ? nextDecisionInterval(member)
      : decisionSecondsRemaining,
  }
}

export function canPartyMemberHitTarget(member: Iwt2PartyEntityState, targetPosition: Iwt2Vec2, targetRadius = 0): boolean {
  if (member.health <= 0) return false
  if (member.status.stunnedSeconds > 0 || member.status.knockedDownSeconds > 0) return false
  const metadata = IWT2_CLASS_METADATA[member.classId]
  if (metadata.attackDamage <= 0) return false
  return distanceVec2(member.position, targetPosition) <= metadata.attackRange + member.radius + targetRadius
}

function getPartyDesiredPosition(
  member: Iwt2PartyEntityState,
  state: Iwt2ArenaState,
): Iwt2Vec2 {
  const drift = decisionDrift(member, state.time)
  const attackTarget = getPriorityAttackTarget(member, state)
  const anchor = attackTarget?.position ?? getPrimaryBoss(state).position
  const centerLeash = getTankCenterLeashPosition(member, state)
  if (centerLeash) return centerLeash
  return {
    x: anchor.x + member.preferredOffset.x + drift.x,
    y: anchor.y + member.preferredOffset.y + drift.y,
  }
}

function getTankCenterLeashPosition(
  member: Iwt2PartyEntityState,
  state: Iwt2ArenaState,
): Iwt2Vec2 | null {
  const boss = getPrimaryBoss(state)
  if (member.aiRole !== 'tank' || !CENTER_LEASH_BOSS_IDS.has(boss.bossId)) return null

  const center = arenaCenter(state)
  const bossCenterDistance = distanceVec2(boss.position, center)
  const nearWall = isBossNearWall(boss, state)
  if (!nearWall && bossCenterDistance < CENTER_LEASH_START_DISTANCE) return null

  const bossMetadata = IWT2_BOSS_METADATA[boss.bossId]
  const towardCenter = withFallbackFacing(subtractVec2(center, boss.position), {
    x: boss.position.x < center.x ? 1 : -1,
    y: boss.position.y < center.y ? 0.35 : -0.35,
  })
  const leashDistance = bossMetadata.meleeRange + boss.radius + member.radius + CENTER_LEASH_EXTRA_DISTANCE
  return clampVec2ToArena(
    addVec2(boss.position, scaleVec2(towardCenter, leashDistance)),
    member.radius,
    state.bounds,
  )
}

function arenaCenter(state: Iwt2ArenaState): Iwt2Vec2 {
  return {
    x: state.bounds.width * 0.5,
    y: state.bounds.height * 0.5,
  }
}

function isBossNearWall(boss: Iwt2ArenaState['boss'], state: Iwt2ArenaState): boolean {
  const margin = boss.radius + CENTER_LEASH_WALL_MARGIN
  return (
    boss.position.x <= margin
    || boss.position.x >= state.bounds.width - margin
    || boss.position.y <= margin
    || boss.position.y >= state.bounds.height - margin
  )
}

function getDangerAvoidancePosition(member: Iwt2PartyEntityState, state: Iwt2ArenaState): Iwt2Vec2 | null {
  const hazardEscape = getHazardAvoidancePosition(member, state)
  if (hazardEscape) {
    return hazardEscape
  }

  for (const boss of state.bosses) {
    if (boss.health <= 0) continue
    if (boss.bossId === 'bulldrome' && (boss.attackPhase === 'slamWindup' || boss.attackPhase === 'slamRecover')) {
      const distance = distanceVec2(member.position, boss.position)
      const dangerRadius = BULLDROME_BOSS_METADATA.slamRadius + member.radius + 34
      if (distance < dangerRadius) {
        const away = normalizeVec2(subtractVec2(member.position, boss.position))
        return clampVec2ToArena(addVec2(member.position, scaleVec2(away, dangerRadius - distance + 40)), member.radius, state.bounds)
      }
    }

    if (boss.bossId === 'bulldrome' && (boss.attackPhase === 'chargeWindup' || boss.attackPhase === 'charging')) {
      const danger = chargeDanger(member.position, boss)
      if (danger.inside || danger.ahead) {
        const perpendicular = { x: -danger.direction.y, y: danger.direction.x }
        const side = dotVec2(subtractVec2(member.position, boss.chargeStart), perpendicular) >= 0 ? 1 : -1
        const escape = addVec2(member.position, scaleVec2(perpendicular, side * (boss.radius * 2.8 + member.radius)))
        return clampVec2ToArena(escape, member.radius, state.bounds)
      }
    }
  }

  return null
}

function getHazardAvoidancePosition(member: Iwt2PartyEntityState, state: Iwt2ArenaState): Iwt2Vec2 | null {
  let escape = { x: 0, y: 0 }
  let maxNeededDistance = 0

  for (const hazard of state.hazards) {
    const offset = subtractVec2(member.position, hazard.position)
    const distance = distanceVec2(member.position, hazard.position)
    const dangerRadius = hazard.radius + member.radius + 46
    if (distance >= dangerRadius) continue

    const fallback = withFallbackFacing(subtractVec2(member.position, getPrimaryBoss(state).position), {
      x: member.position.x < state.bounds.width * 0.5 ? -1 : 1,
      y: member.position.y < state.bounds.height * 0.5 ? -0.35 : 0.35,
    })
    const away = distance > 0.001 ? normalizeVec2(offset) : fallback
    const urgency = dangerRadius - distance
    escape = addVec2(escape, scaleVec2(away, urgency))
    maxNeededDistance = Math.max(maxNeededDistance, urgency)
  }

  if (maxNeededDistance <= 0) return null

  const direction = withFallbackFacing(escape, {
    x: member.position.x < state.bounds.width * 0.5 ? -1 : 1,
    y: 0,
  })
  return clampVec2ToArena(
    addVec2(member.position, scaleVec2(direction, maxNeededDistance + 72)),
    member.radius,
    state.bounds,
  )
}

function getPriorityAttackTarget(
  member: Iwt2PartyEntityState,
  state: Iwt2ArenaState,
): (Iwt2ArenaState['boss'] | Iwt2HostileAddState) | undefined {
  if (member.health <= 0) return undefined
  const livingAdds = state.hostileAdds.filter((add) => add.health > 0)
  if (livingAdds.length > 0) {
    return livingAdds.reduce((best, add) => (
      distanceVec2(member.position, add.position) < distanceVec2(member.position, best.position) ? add : best
    ), livingAdds[0])
  }
  const livingBosses = state.bosses.filter((boss) => boss.health > 0)
  if (livingBosses.length === 0) return undefined
  return livingBosses.reduce((best, boss) => (
    distanceVec2(member.position, boss.position) < distanceVec2(member.position, best.position) ? boss : best
  ), livingBosses[0])
}

function chargeDanger(position: Iwt2Vec2, boss: Iwt2ArenaState['boss']) {
  const segment = subtractVec2(boss.chargeEnd, boss.chargeStart)
  const lengthSq = Math.max(1, lengthSqVec2(segment))
  const direction = normalizeVec2(segment)
  const offset = subtractVec2(position, boss.chargeStart)
  const along = dotVec2(offset, segment) / lengthSq
  const closest = addVec2(boss.chargeStart, scaleVec2(segment, Math.min(1, Math.max(0, along))))
  const distance = distanceVec2(position, closest)
  const width = boss.radius + 22
  return {
    direction,
    inside: along >= -0.08 && along <= 1.08 && distance <= width,
    ahead: boss.attackPhase === 'chargeWindup' && along > -0.12 && along < 1.05 && distance <= width + 38,
  }
}

function getPrimaryBoss(state: Iwt2ArenaState) {
  return state.bosses.find((boss) => boss.health > 0) ?? state.bosses[0] ?? state.boss
}

function decisionDrift(member: Iwt2PartyEntityState, time: number): Iwt2Vec2 {
  const seed = member.id.length * 17
  return {
    x: Math.sin(time * 0.7 + seed) * 16,
    y: Math.cos(time * 0.9 + seed) * 14,
  }
}

function nextDecisionInterval(member: Iwt2PartyEntityState): number {
  if (member.aiRole === 'tank') return 0.34
  if (member.aiRole === 'ranged') return 0.48
  if (member.aiRole === 'flanker') return 0.28
  return 0.38
}
