local k=require("\109\111\115\101\115")local V=require("\112\108\046\115\116\114\105\110\103\120")local E=require("\105\110\118\111\107\097\116\105\111\110\046\099\111\110\115\116\046\101\110\117\109\115")local D={}local function l(k,...)if not IsInToolsMode()then return end print("\040\087\065\082\078\073\078\071\041 \091\107\118\093 "..k:format(...))end function D.Array(V)if not k.isTable(V)then return V end return k.map(V,function(k,V)return tonumber(V),k end)end function D.Bool(k)return k==1 end function D.Strings(E,D)if not k.isString(E)then return E end E=V.split(E,D)return k.map(E,k.unary(V.strip))end function D.Numbers(V,E)E=E or" "V=D.Strings(V,E)if not k.isTable(V)then return V end return k.map(V,k.unary(tonumber))end function D.EnumValue(V)if V==nil then return end if not k.isString(V)then l("\069\110\117\109\086\097\108\117\101\040\041\058 \105\110\118\097\108\105\100 \110\097\109\101 \037\115 \091\037\115\093",V,type(V))return 0 end local D=_G[V]or E[V]if D==nil then l("\069\110\117\109\086\097\108\117\101\040\041\058 \110\097\109\101 \037\113 \110\111\116 \102\111\117\110\100",V)return 0 end return D end function D.EnumValues(V)V=D.Strings(V,"\124")if not k.isTable(V)then return V end return k.map(V,D.EnumValue)end function D.Flags(V)V=D.EnumValues(V)if not k.isTable(V)then return V end return k.reduce(V,bit.bor)end local x={FIELD_FLOAT=D.Numbers;FIELD_INTEGER=D.Numbers}local h={CalculateSpellDamageTooltip=D.Bool;RequiresScepter=D.Bool}local function O(V,E,D)if V=="\118\097\114\095\116\121\112\101"then return E end local O=h[V]if O then return O(E)end if k.isString(E)and E:match("\037\115")then O=x[D]if O then return O(E)end l("\065\098\105\108\105\116\121\083\112\101\099\105\097\108\040\041\058 \117\110\107\110\111\119\110 \'\118\097\114\095\116\121\112\101\' \118\097\108\117\101\058 \037\113",D)end return E end function D.AbilitySpecial(V)if not k.isTable(V)then return V end if not k.isString(V.var_type)then return V end local E=(((k.chain(O)):partialRight(V.var_type)):rearg({2,1})):value()return k.map(V,E)end function D.AbilitySpecials(V)V=D.Array(V)if not k.isTable(V)then return V end return k.map(V,D.AbilitySpecial)end return D
--- KeyValue utils.
-- @module invokation.dota2.kv

local m = require("moses")
local stringx = require("pl.stringx")

local ENUMS = require("invokation.const.enums")

local M = {}

local function warn(fmt, ...)
  if not IsInToolsMode() then
    return
  end

  print("(WARNING) [kv] " .. fmt:format(...))
end

--- Transforms a KeyValue table value with numeric strings as keys into an array.
-- @tparam {[string]=any,...} value String-keyed table
-- @return Unmodified value if it's not a table
-- @treturn {any,...} Array of values
function M.Array(value)
  if not m.isTable(value) then
    return value
  end

  return m.map(value, function(v, k)
    return tonumber(k), v
  end)
end

--- Transforms a KeyValue value into a boolean.
-- @tparam any value Value
-- @treturn bool `true` if the value equals to the number `1`
-- @treturn bool `false` otherwise
function M.Bool(value)
  return value == 1
end

--- Splits a KeyValue string value into an array of strings.
-- @tparam string value String value
-- @tparam string sep Separator
-- @return Unmodified value if it's not a string
-- @treturn {string,...} String array
function M.Strings(value, sep)
  if not m.isString(value) then
    return value
  end

  value = stringx.split(value, sep)

  return m.map(value, m.unary(stringx.strip))
end

--- Splits a KeyValue string value into an array of numbers.
-- @tparam string value String value
-- @tparam[opt=" "] string sep Separator
-- @return Unmodified value if it's not a string
-- @treturn {number,...} Number array
function M.Numbers(value, sep)
  sep = sep or " "
  value = M.Strings(value, sep)

  if not m.isTable(value) then
    return value
  end

  return m.map(value, m.unary(tonumber))
end

--- Converts an enum name into an enum value (a global value defined by the engine).
-- @tparam string name Enum name
-- @treturn nil If name is `nil`
-- @treturn int `0` if the enum name is invalid (not a string) or is not defined
-- @treturn int The enum value if it exists
function M.EnumValue(name)
  if name == nil then
    return
  end

  if not m.isString(name) then
    warn("EnumValue(): invalid name %s [%s]", name, type(name))
    return 0
  end

  -- selene: allow(global_usage)
  local value = _G[name] or ENUMS[name]

  if value == nil then
    warn("EnumValue(): name %q not found", name)
    return 0
  end

  return value
end

--- Splits a KeyValue string value into an array of enum values.
--
-- It uses the separator `"|"` to split the string, then converts the values
-- using @{EnumValue}.
--
-- @tparam string value String value
-- @return Unmodified value if it's not a string
-- @treturn {int,...} Array of enum values (integers)
function M.EnumValues(value)
  value = M.Strings(value, "|")

  if not m.isTable(value) then
    return value
  end

  return m.map(value, M.EnumValue)
end

--- Converts a KeyValue string value into a numeric bitmap flag.
--
-- It uses the separator `"|"` to split the string, then converts the values
-- using @{EnumValue} and reduces the values with bitwise OR.
--
-- @tparam string value String value
-- @return Unmodified value if it's not a string
-- @treturn int OR'ed enum values
function M.Flags(value)
  value = M.EnumValues(value)

  if not m.isTable(value) then
    return value
  end

  return m.reduce(value, bit.bor)
end

local ABILITY_SPECIAL_CAST = { FIELD_FLOAT = M.Numbers, FIELD_INTEGER = M.Numbers }

--- Table of known extra fields to be converted in @{AbilitySpecial}.
-- @table ABILITY_SPECIAL_EXTRA
local ABILITY_SPECIAL_EXTRA = {
  CalculateSpellDamageTooltip = M.Bool, -- bool
  RequiresScepter = M.Bool, -- bool
}

local function castAbilitySpecialField(key, value, varType)
  if key == "var_type" then
    return value
  end

  local castFn = ABILITY_SPECIAL_EXTRA[key]

  if castFn then
    return castFn(value)
  end

  if m.isString(value) and value:match("%s") then
    castFn = ABILITY_SPECIAL_CAST[varType]

    if castFn then
      return castFn(value)
    end

    warn("AbilitySpecial(): unknown 'var_type' value: %q", varType)
  end

  return value
end

--- AbilitySpecialEntry spec
-- @table AbilitySpecialEntry
-- @tfield string var_type
-- @field[opt] ... variable number of special fields

--- Parses a single ability special value (a single entry in an `AbilitySpecial` value).
--
-- It converts the "main" special key value using the type provided in the
-- `var_type` field and converts known extra fields to known types.
--
-- For example, if `var_type` is `"FIELD_FLOAT"` and the main special key
-- `cooldown` has the value `"10.5 7.3 5.1"`, it will set `cooldown` in the
-- result with the value `{10.5, 7.3, 5.1}` (an array of actual floats).
--
-- See @{ABILITY_SPECIAL_EXTRA} for the table of known extra fields.
--
-- @tparam AbilitySpecialEntry value An `AbilitySpecial` entry
-- @return Unmodified value if it's not a string
-- @return Unmodified value if it does not contain a `var_type` field
-- @treturn AbilitySpecialEntry Clone of original value with the "main" field
--   converted to the appropriate type
function M.AbilitySpecial(value)
  if not m.isTable(value) then
    return value
  end

  if not m.isString(value.var_type) then
    return value
  end

  local castField =
    m.chain(castAbilitySpecialField):partialRight(value.var_type):rearg({ 2, 1 }):value()

  return m.map(value, castField)
end

--- Parses a whole `AbilitySpecial` field.
--
-- Converts the field into an array with @{Array}, then uses @{AbilitySpecial}
-- to parse each entry in the field.
--
-- @tparam {[string]=AbilitySpecialEntry,...} value `AbilitySpecial` field value
-- @return Unmodified value if it's not a table
-- @treturn {AbilitySpecialEntry,...} Array of parsed entries
function M.AbilitySpecials(value)
  value = M.Array(value)

  if not m.isTable(value) then
    return value
  end

  return m.map(value, M.AbilitySpecial)
end

return M
