local k=require("\105\110\118\111\107\097\116\105\111\110\046\100\111\116\097\050\046\065\098\105\108\105\116\121")local V=require("\112\108\046\076\105\115\116")local E=require("\112\108\046\099\108\097\115\115")local D=require("\109\111\115\101\115")local l=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\110\115\116\046\105\110\118\111\107\101\114")local x=E()D.extend(x,l)function x._init(V,k)V.hero=k end function x.swapAbilities(E,k,V)return E.hero:SwapAbilities(k.name,V.name,V.index<=l.MAX_VISIBLE_ABILITY_INDEX,k.index<=l.MAX_VISIBLE_ABILITY_INDEX)end function x.Invoke(E,V)local D=k(E.hero:FindAbilityByName(V))if D.index==l.INDEX_ABILITY_EMPTY1 then return end local x=k(E.hero:GetAbilityByIndex(l.INDEX_ABILITY_EMPTY1))if D.index==l.INDEX_ABILITY_EMPTY2 then E:swapAbilities(x,D)return end local h=k(E.hero:GetAbilityByIndex(l.INDEX_ABILITY_EMPTY2))E:swapAbilities(x,h)E:swapAbilities(h,D)end local function h(k,V)local E=V:GetAbilityName()if not V:IsHidden()then error(string.format("\084\114\105\101\100 \116\111 \114\101\109\111\118\101 \118\105\115\105\098\108\101 \097\098\105\108\105\116\121 \037\113\046",E))end local D=V:GetAbilityIndex()local l=V:GetLevel()k:RemoveAbility(E)V=k:AddAbility(E)V:SetAbilityIndex(D)V:SetLevel(l)end local function O(k,V,E)return V:CanAbilityBeUpgraded()==ABILITY_CAN_BE_UPGRADED and(k:GetAbilityPoints()>0 and V:GetLevel()<E)end function x.LevelUpAbilities(V,k)k=k or{}for E,D in ipairs(l.ORB_ABILITIES)do if k.maxLevel or k[D]then local E=V.hero:FindAbilityByName(D)local l=k[D]or{}local x if k.maxLevel or l.maxLevel then x=E:GetMaxLevel()elseif l.level then x=l.level else x=E:GetLevel()+1 end while O(V.hero,E,x)do V.hero:UpgradeAbility(E)end end end end function x.ResetAbilities(E,k)k=k or{}local D=0 local x=V.new(l.ORB_ABILITIES)..V.new(l.TALENT_ABILITIES)for k,V in ipairs(x)do local l=E.hero:FindAbilityByName(V)if l~=nil then D=D+l:GetLevel()l:SetLevel(0)end end if k.reinsertSpells then E:Invoke(l.ABILITY_EMPTY2)E:Invoke(l.ABILITY_EMPTY1)for k,V in ipairs(l.SPELL_ABILITIES)do local D=E.hero:FindAbilityByName(V)h(E.hero,D)end end return D end return x
--- Invoker class.
-- @classmod invokation.dota2.Invoker
local Ability = require("invokation.dota2.Ability")
local List = require("pl.List")
local class = require("pl.class")
local m = require("moses")

local INVOKER = require("invokation.const.invoker")

local M = class()

m.extend(M, INVOKER)

--- Constructor.
-- @tparam CDOTA_BaseNPC_Hero hero Hero entity
function M:_init(hero)
  self.hero = hero
end

function M:swapAbilities(ability1, ability2)
  return self.hero:SwapAbilities(
    ability1.name,
    ability2.name,
    ability2.index <= INVOKER.MAX_VISIBLE_ABILITY_INDEX,
    ability1.index <= INVOKER.MAX_VISIBLE_ABILITY_INDEX
  )
end

--- Invokes an ability by name.
-- @tparam string abilityName Ability name
function M:Invoke(abilityName)
  local invoked = Ability(self.hero:FindAbilityByName(abilityName))

  -- I(i) : [i, s2], [s3, s4, s5] -> [i, s2], [s3, s4, s5]
  if invoked.index == INVOKER.INDEX_ABILITY_EMPTY1 then
    return
  end

  local spell1 = Ability(self.hero:GetAbilityByIndex(INVOKER.INDEX_ABILITY_EMPTY1))

  -- I(i) : [s1, i], [s2, s3, s4] -> [i, s1], [s2, s3, s4]
  if invoked.index == INVOKER.INDEX_ABILITY_EMPTY2 then
    self:swapAbilities(spell1, invoked)
    return
  end

  local spell2 = Ability(self.hero:GetAbilityByIndex(INVOKER.INDEX_ABILITY_EMPTY2))

  -- I(i) : [s1, s2], [s3, s4, i] -> [s2, s1], [s3, s4, i] -> [i, s1], [s3, s4, s2]
  self:swapAbilities(spell1, spell2)
  self:swapAbilities(spell2, invoked)
end

--[[
  This is a hack. "AbilitySpecial" values for Invoker spells that depend on
  orb ability level (specials that have "levelkey" set to "quaslevel",
  "wexlevel" or "exortlevel") don't change when orb ability levels are
  manually changed (with `CDOTABaseAbility:SetLevel`). Since we're resetting
  orb levels to zero, this hack "resets" spell abilities by removing and
  reinserting them on the hero.

  WARNING: when removing an ability, make sure they aren't visible (invoked),
  otherwise the whole ability slot will disappear from the UI.
]]
local function reinsertSpellAbility(hero, ability)
  local name = ability:GetAbilityName()

  if not ability:IsHidden() then
    error(string.format("Tried to remove visible ability %q.", name))
  end

  local index = ability:GetAbilityIndex()
  local level = ability:GetLevel()

  hero:RemoveAbility(name)

  ability = hero:AddAbility(name)
  ability:SetAbilityIndex(index)
  ability:SetLevel(level)
end

local function canLevelUpAbility(hero, ability, targetLevel)
  return (ability:CanAbilityBeUpgraded() == ABILITY_CAN_BE_UPGRADED)
    and (hero:GetAbilityPoints() > 0)
    and (ability:GetLevel() < targetLevel)
end

--- Ability level up option.
--
-- If no option is given, level up the ability 1 level.
--
-- @table AbilityLevelUpOption
-- @tfield[opt] int level Specific level
-- @tfield[opt] bool maxLevel Level up to max ability level

--- Levels up orb abilities.
-- @tparam table options Options table
-- @tparam[opt=false] bool options.maxLevel Level up abilities to max level
-- @tparam[opt] AbilityLevelUpOption const.invoker.ABILITY_QUAS Quas level up option
-- @tparam[opt] AbilityLevelUpOption const.invoker.ABILITY_WEX Wex level up option
-- @tparam[opt] AbilityLevelUpOption const.invoker.ABILITY_EXORT Exort level up option
function M:LevelUpAbilities(options)
  options = options or {}

  for _, name in ipairs(INVOKER.ORB_ABILITIES) do
    if options.maxLevel or options[name] then
      local ability = self.hero:FindAbilityByName(name)
      local abilityOption = options[name] or {}
      local targetLevel

      if options.maxLevel or abilityOption.maxLevel then
        targetLevel = ability:GetMaxLevel()
      elseif abilityOption.level then
        targetLevel = abilityOption.level
      else
        targetLevel = ability:GetLevel() + 1
      end

      while canLevelUpAbility(self.hero, ability, targetLevel) do
        self.hero:UpgradeAbility(ability)
      end
    end
  end
end

--- Resets orb abilities levels to zero.
-- @tparam table options Options table
-- @tparam[opt=false] bool options.reinsertSpells Reinsert (reset) spell abilities
--   This is a workaround to fix spells not scaling back (special values that
--   depend on orb levels). Another way to fix this is to completely replace the
--   player's hero with `PlayerResource:ReplaceHeroWith`. If hero replacement is
--   being used, this option is not needed.
-- @treturn int The number of ability points gained by downgrading orb abilities
function M:ResetAbilities(options)
  options = options or {}

  local points = 0
  local resetAbilities = List.new(INVOKER.ORB_ABILITIES) .. List.new(INVOKER.TALENT_ABILITIES)

  for _, name in ipairs(resetAbilities) do
    local ability = self.hero:FindAbilityByName(name)

    if ability ~= nil then
      points = points + ability:GetLevel()
      ability:SetLevel(0)
    end
  end

  if options.reinsertSpells then
    self:Invoke(INVOKER.ABILITY_EMPTY2)
    self:Invoke(INVOKER.ABILITY_EMPTY1)

    for _, name in ipairs(INVOKER.SPELL_ABILITIES) do
      local ability = self.hero:FindAbilityByName(name)
      reinsertSpellAbility(self.hero, ability)
    end
  end

  return points
end

return M
