-- SPDX-License-Identifier: GPL-3.0-or-later

---@class GameLogic: Object
---@field public room Room
---@field public skill_table table<Event, TriggerSkill[]>
---@field public skill_priority_table table<Event, number[]>
---@field public refresh_skill_table table<Event, TriggerSkill[]>
---@field public skills string[]
---@field public game_event_stack Stack
---@field public role_table string[][]
---@field public all_game_events GameEvent[]
---@field public event_recorder table<integer, GameEvent>
---@field public current_event_id integer
local GameLogic = class("GameLogic")

function GameLogic:initialize(room)
  self.room = room
  self.skill_table = {}   -- TriggerEvent --> TriggerSkill[]
  self.skill_priority_table = {}
  self.refresh_skill_table = {}
  self.skills = {}    -- skillName[]
  self.game_event_stack = Stack:new()
  self.all_game_events = {}
  self.event_recorder = {}
  self.current_event_id = 0

  self.role_table = {
    { "lord" },
    { "lord", "rebel" },
    { "lord", "rebel", "renegade" },
    { "lord", "loyalist", "rebel", "renegade" },
    { "lord", "loyalist", "rebel", "rebel", "renegade" },
    { "lord", "loyalist", "rebel", "rebel", "rebel", "renegade" },
    { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "renegade" },
    { "lord", "loyalist", "loyalist", "rebel", "rebel", "rebel", "rebel", "renegade" },
  }
end

function GameLogic:run()
  -- default logic
  local room = self.room
  table.shuffle(self.room.players)
  self:assignRoles()
  self.room.game_started = true
  room:doBroadcastNotify("StartGame", "")
  room:adjustSeats()

  self:chooseGenerals()

  self:buildPlayerCircle()
  self:broadcastGeneral()
  self:prepareDrawPile()
  self:attachSkillToPlayers()
  self:prepareForStart()

  self:action()
end

local function execGameEvent(type, ...)
  local event = GameEvent:new(type, ...)
  local _, ret = event:exec()
  return ret
end


function GameLogic:assignRoles()
  local room = self.room
  local n = #room.players
  local roles = self.role_table[n]
  table.shuffle(roles)

  for i = 1, n do
    local p = room.players[i]
    p.role = roles[i]
    if p.role == "lord" then
      p.role_shown = true
      room:broadcastProperty(p, "role")
    else
      room:notifyProperty(p, p, "role")
    end
  end
end

function GameLogic:chooseGenerals()
  local room = self.room
  local generalNum = room.settings.generalNum
  local n = room.settings.enableDeputy and 2 or 1
  local lord = room:getLord()
  local lord_generals = {}

  if lord ~= nil then
    room.current = lord
    local generals = {}
    local lordlist = {}
    local lordpools = {}
    if room.settings.gameMode == "aaa_role_mode" then
      for _, general in pairs(Fk:getAllGenerals()) do
        if (not general.hidden and not general.total_hidden) and
        table.find(general.skills, function(s)
          return s.lordSkill
        end) and
        not table.find(lordlist, function(g)
          return g.trueName == general.trueName
        end) then
          table.insert(lordlist, general)
        end
      end
      lordlist = table.random(lordlist, 3) or {}
    end
    table.insertTable(generals, Fk:getGeneralsRandomly(generalNum, Fk:getAllGenerals(), nil, function(g)
      return table.contains(table.map(lordlist, function(g) return g.trueName end), g.trueName)
    end))
    for i = 1, #generals do
      generals[i] = generals[i].name
    end
    lordpools = table.simpleClone(generals)
    table.insertTable(lordpools, table.map(lordlist, function(g) return g.name end))
    lord_generals = room:askForGeneral(lord, lordpools, n)
    local lord_general, deputy
    if type(lord_generals) == "table" then
      deputy = lord_generals[2]
      lord_general = lord_generals[1]
    else
      lord_general = lord_generals
      lord_generals = {lord_general}
    end

    room:setPlayerGeneral(lord, lord_general, true)
    room:askForChooseKingdom({lord})
    room:broadcastProperty(lord, "general")
    room:broadcastProperty(lord, "kingdom")
    room:setDeputyGeneral(lord, deputy)
    room:broadcastProperty(lord, "deputyGeneral")
  end

  local nonlord = room:getOtherPlayers(lord, true)
  local generals = Fk:getGeneralsRandomly(#nonlord * generalNum, nil, lord_generals)
  table.shuffle(generals)
  for _, p in ipairs(nonlord) do
    local arg = {}
    for i = 1, generalNum do
      table.insert(arg, table.remove(generals, 1).name)
    end
    p.request_data = json.encode{ arg, n }
    p.default_reply = table.random(arg, n)
  end

  room:notifyMoveFocus(nonlord, "AskForGeneral")
  room:doBroadcastRequest("AskForGeneral", nonlord)

  for _, p in ipairs(nonlord) do
    if p.general == "" and p.reply_ready then
      local generals = json.decode(p.client_reply)
      local general = generals[1]
      local deputy = generals[2]
      room:setPlayerGeneral(p, general, true, true)
      room:setDeputyGeneral(p, deputy)
    else
      room:setPlayerGeneral(p, p.default_reply[1], true, true)
      room:setDeputyGeneral(p, p.default_reply[2])
    end
    p.default_reply = ""
  end

  room:askForChooseKingdom(nonlord)
end

function GameLogic:buildPlayerCircle()
  local room = self.room
  local players = room.players
  room.alive_players = {table.unpack(players)}
  for i = 1, #players - 1 do
    players[i].next = players[i + 1]
  end
  players[#players].next = players[1]
end

function GameLogic:broadcastGeneral()
  local room = self.room
  local players = room.players

  for _, p in ipairs(players) do
    assert(p.general ~= "")
    local general = Fk.generals[p.general]
    local deputy = Fk.generals[p.deputyGeneral]
    p.maxHp = p:getGeneralMaxHp()
    p.hp = deputy and math.floor((deputy.hp + general.hp) / 2) or general.hp
    p.shield = math.min(general.shield + (deputy and deputy.shield or 0), 5)
    -- TODO: setup AI here

    if p.role ~= "lord" then
      room:broadcastProperty(p, "general")
      room:broadcastProperty(p, "kingdom")
      room:broadcastProperty(p, "deputyGeneral")
    elseif #players >= 5 then
      p.maxHp = p.maxHp + 1
      p.hp = p.hp + 1
    end
    room:broadcastProperty(p, "maxHp")
    room:broadcastProperty(p, "hp")
    room:broadcastProperty(p, "shield")
  end
end

function GameLogic:prepareDrawPile()
  local room = self.room
  local allCardIds = Fk:getAllCardIds()

  for i = #allCardIds, 1, -1 do
    if Fk:getCardById(allCardIds[i]).is_derived then
      local id = allCardIds[i]
      table.removeOne(allCardIds, id)
      table.insert(room.void, id)
      room:setCardArea(id, Card.Void, nil)
    end
  end

  table.shuffle(allCardIds)
  room.draw_pile = allCardIds
  for _, id in ipairs(room.draw_pile) do
    self.room:setCardArea(id, Card.DrawPile, nil)
  end
end

function GameLogic:attachSkillToPlayers()
  local room = self.room
  local players = room.players

  local addRoleModSkills = function(player, skillName)
    local skill = Fk.skills[skillName]
    if skill.lordSkill and (player.role ~= "lord" or #room.players < 5) then
      return
    end

    if #skill.attachedKingdom > 0 and not table.contains(skill.attachedKingdom, player.kingdom) then
      return
    end

    room:handleAddLoseSkills(player, skillName, nil, false)
  end
  for _, p in ipairs(room.alive_players) do
    local skills = Fk.generals[p.general].skills
    for _, s in ipairs(skills) do
      addRoleModSkills(p, s.name)
    end
    for _, sname in ipairs(Fk.generals[p.general].other_skills) do
      addRoleModSkills(p, sname)
    end

    local deputy = Fk.generals[p.deputyGeneral]
    if deputy then
      skills = deputy.skills
      for _, s in ipairs(skills) do
        addRoleModSkills(p, s.name)
      end
      for _, sname in ipairs(deputy.other_skills) do
        addRoleModSkills(p, sname)
      end
    end
  end
end

function GameLogic:prepareForStart()
  local room = self.room
  local players = room.players

  self:addTriggerSkill(GameRule)
  for _, trig in ipairs(Fk.global_trigger) do
    self:addTriggerSkill(trig)
  end

  self.room:sendLog{ type = "$GameStart" }
end

function GameLogic:action()
  self:trigger(fk.GamePrepared)
  local room = self.room

  execGameEvent(GameEvent.DrawInitial)

  while true do
    execGameEvent(GameEvent.Round)
    if room.game_finished then break end
  end
end

---@param skill TriggerSkill
function GameLogic:addTriggerSkill(skill)
  if skill == nil or table.contains(self.skills, skill.name) then
    return
  end

  table.insert(self.skills, skill.name)

  for _, event in ipairs(skill.refresh_events) do
    if self.refresh_skill_table[event] == nil then
      self.refresh_skill_table[event] = {}
    end
    table.insert(self.refresh_skill_table[event], skill)
  end

  for _, event in ipairs(skill.events) do
    if self.skill_table[event] == nil then
      self.skill_table[event] = {}
    end
    table.insert(self.skill_table[event], skill)

    if self.skill_priority_table[event] == nil then
      self.skill_priority_table[event] = {}
    end

    local priority_tab = self.skill_priority_table[event]
    local prio = skill.priority_table[event]
    if not table.contains(priority_tab, prio) then
      for i, v in ipairs(priority_tab) do
        if v < prio then
          table.insert(priority_tab, i, prio)
          break
        end
      end

      if not table.contains(priority_tab, prio) then
        table.insert(priority_tab, prio)
      end
    end

    if not table.contains(self.skill_priority_table[event],
      skill.priority_table[event]) then

      table.insert(self.skill_priority_table[event],
        skill.priority_table[event])
    end
  end

  if skill.visible then
    if (Fk.related_skills[skill.name] == nil) then return end
    for _, s in ipairs(Fk.related_skills[skill.name]) do
      if (s.class == TriggerSkill) then
        self:addTriggerSkill(s)
      end
    end
  end
end

---@param event Event
---@param target ServerPlayer|nil
---@param data any|nil
function GameLogic:trigger(event, target, data, refresh_only)
  local room = self.room
  local broken = false
  local skills = self.skill_table[event] or {}
  local skills_to_refresh = self.refresh_skill_table[event] or Util.DummyTable
  local _target = room.current -- for iteration
  local player = _target

  if #skills_to_refresh > 0 then repeat do
    -- refresh skills. This should not be broken
    for _, skill in ipairs(skills_to_refresh) do
      if skill:canRefresh(event, target, player, data) then
        skill:refresh(event, target, player, data)
      end
    end
    player = player.next
  end until player == _target end

  if #skills == 0 or refresh_only then return end

  local prio_tab = self.skill_priority_table[event]
  local prev_prio = math.huge

  for _, prio in ipairs(prio_tab) do
    if broken then break end
    if prio >= prev_prio then
      -- continue
      goto trigger_loop_continue
    end

    repeat do
      local triggerables = table.filter(skills, function(skill)
        return skill.priority_table[event] == prio and
          skill:triggerable(event, target, player, data)
      end)

      local skill_names = table.map(triggerables, function(skill)
        return skill.name
      end)

      while #skill_names > 0 do
        local skill_name = prio <= 0 and table.random(skill_names) or
          room:askForChoice(player, skill_names, "trigger", "#choose-trigger")

        local skill = skill_name == "game_rule" and GameRule
          or Fk.skills[skill_name]

        local len = #skills
        broken = skill:trigger(event, target, player, data)

        table.insertTable(
          skill_names,
          table.map(table.filter(table.slice(skills, len - #skills), function(s)
            return
              s.priority_table[event] == prio and
              s:triggerable(event, target, player, data)
          end), function(s) return s.name end)
        )

        broken = broken or (event == fk.AskForPeaches
          and room:getPlayerById(data.who).hp > 0)

        if broken then break end
        table.removeOne(skill_names, skill_name)
      end

      if broken then break end

      player = player.next
    end until player == _target

    prev_prio = prio
    ::trigger_loop_continue::
  end

  return broken
end

---@return GameEvent
function GameLogic:getCurrentEvent()
  return self.game_event_stack.t[self.game_event_stack.p]
end

-- 在指定历史范围中找至多n个符合条件的事件
---@param eventType integer @ 要查找的事件类型
---@param n integer @ 最多找多少个
---@param func fun(e: GameEvent): boolean @ 过滤用的函数
---@param scope integer @ 查询历史范围，只能是当前阶段/回合/轮次
---@return GameEvent[] @ 找到的符合条件的所有事件，最多n个但不保证有n个
function GameLogic:getEventsOfScope(eventType, n, func, scope)
  scope = scope or Player.HistoryTurn
  local event = self:getCurrentEvent()
  local start_event ---@type GameEvent
  if scope == Player.HistoryGame then
    start_event = self.all_game_events[1]
  elseif scope == Player.HistoryRound then
    start_event = event:findParent(GameEvent.Round, true)
  elseif scope == Player.HistoryTurn then
    start_event = event:findParent(GameEvent.Turn, true)
  elseif scope == Player.HistoryPhase then
    start_event = event:findParent(GameEvent.Phase, true)
  end

  return start_event:searchEvents(eventType, n, func)
end

function GameLogic:dumpEventStack(detailed)
  local top = self:getCurrentEvent()
  local i = self.game_event_stack.p
  local inspect = p
  if not top then return end

  print("===== Start of event stack dump =====")
  if not detailed then print("") end

  repeat
    local printable_data
    if type(top.data) ~= "table" then
      printable_data = top.data
    else
      printable_data = table.cloneWithoutClass(top.data)
    end

    if not detailed then
      print("Stack level #" .. i .. ": " .. tostring(top))
    else
      print("\nStack level #" .. i .. ":")
      inspect{
        eventId = GameEvent:translate(top.event),
        data = printable_data or "nil",
      }
    end

    top = top.parent
    i = i - 1
  until not top

  print("\n===== End of event stack dump =====")
end

function GameLogic:dumpAllEvents(from, to)
  from = from or 1
  to = to or #self.all_game_events
  assert(from <= to)

  local indent = 0
  local tab = "  "
  for i = from, to, 1 do
    local v = self.all_game_events[i]
    if type(v) ~= "table" then
      indent = math.max(indent - 1, 0)
      -- v = "End"
      -- print(tab:rep(indent) .. string.format("#%d: %s", i, v))
    else
      print(tab:rep(indent) .. string.format("%s", tostring(v)))
      if v.id ~= v.end_id then
        indent = indent + 1
      end
    end
  end
end

function GameLogic:breakEvent(ret)
  coroutine.yield("__breakEvent", ret)
end

function GameLogic:breakTurn()
  local event = self:getCurrentEvent():findParent(GameEvent.Turn)
  event:shutdown()
end

return GameLogic
