if not WeakAuras.IsLibsOK() then return end
if not WeakAuras.IsCataOrMistsOrRetail() then return end
---@type string
local AddonName = ...
---@class Private
local Private = select(2, ...)

--- @class LibSpecialization
--- @field Register fun(self: LibSpecialization, name: string, callback: function)
--- @field MySpecialization fun(): number, string, string
local LibSpec = LibStub("LibSpecialization")

--- @alias specData {[1]: number, [2]: string, [3]: string, [4]: string}

--- @type table<string, specData>
local nameToSpecMap = {}

local nameToTalents = {}

--- @type table<string, string>
local nameToUnitMap = {
  [GetUnitName("player", true)] = "player"
}

--- @type function[]
local subscribers = {}

--- @class LibSpecWrapper
--- @field Register fun(callback: fun(unit: string))
--- @field SpecForUnit fun(unit: string): number?
--- @field SpecRolePositionForUnit fun(unit: string): number?, string?, string?
--- @field CheckTalentForUnit fun(unit: string, talentId: number): boolean?

Private.LibSpecWrapper = {
  Register = function(callback) end,
  SpecForUnit = function(unit) end,
  SpecRolePositionForUnit = function(unit) end,
  CheckTalentForUnit = function(unit) end,
}
if LibSpec then
  local frame = CreateFrame("Frame")
  frame:RegisterEvent("PLAYER_LOGIN")
  frame:RegisterEvent("GROUP_ROSTER_UPDATE")
  frame:SetScript("OnEvent", function()
    --- @type string
    local ownName = GetUnitName("player", true)

    nameToUnitMap = {}
    nameToUnitMap[ownName] = "player"

    if IsInRaid() then
      local max = GetNumGroupMembers()
      for i = 1, max do
        local name = GetUnitName(WeakAuras.raidUnits[i], true)
        nameToUnitMap[name] = WeakAuras.raidUnits[i]
      end
    else
      local max = GetNumSubgroupMembers()
      for i = 1, max do
        local name = GetUnitName(WeakAuras.partyUnits[i], true)
        nameToUnitMap[name] = WeakAuras.partyUnits[i]
      end
    end

    for name in pairs(nameToSpecMap) do
      if not nameToUnitMap[name] then
        nameToSpecMap[name] = nil
      end
    end
  end)

  --- LibSpecialization callback
  ---@param specId number
  ---@param role string
  ---@param position string
  ---@param sender string
  ---@param talentString string
  local function LibSpecCallback(specId, role, position, sender, talentString)
    if nameToSpecMap[sender]
       and nameToSpecMap[sender][1] == specId
       and nameToSpecMap[sender][2] == role
       and nameToSpecMap[sender][3] == position
       and nameToSpecMap[sender][4] == talentString
    then
      return
    end

    if not nameToUnitMap[sender] then
      return
    end

    nameToSpecMap[sender] = {specId, role, position, talentString}
    nameToTalents[sender] = nil
    for _, f in ipairs(subscribers) do
      f(nameToUnitMap[sender])
    end
  end

  local libSpecCallBackTable = {}
  LibSpec.RegisterGroup(libSpecCallBackTable, LibSpecCallback)

  function Private.LibSpecWrapper.Register(f)
    tinsert(subscribers, f)
  end

  function Private.LibSpecWrapper.SpecForUnit(unit)
    if UnitIsUnit(unit, "player") then
      return (LibSpec.MySpecialization())
    end

    if nameToSpecMap[GetUnitName(unit, true)] then
      return nameToSpecMap[GetUnitName(unit, true)][1]
    end
  end

  function Private.LibSpecWrapper.SpecRolePositionForUnit(unit)
    if UnitIsUnit(unit, "player") then
      return LibSpec.MySpecialization()
    end
    local data = nameToSpecMap[GetUnitName(unit, true)]
    if data then
      return unpack(data)
    else
      return nil
    end
  end

  local function ReadLoadoutHeader(importStream)
    local bitWidthHeaderVersion = 8
    local bitWidthSpecID = 16
    local headerBitWidth = bitWidthHeaderVersion + bitWidthSpecID + 128;

    local importStreamTotalBits = importStream:GetNumberOfBits();
    if( importStreamTotalBits < headerBitWidth) then
      return false, 0, 0, 0;
    end
    local serializationVersion = importStream:ExtractValue(bitWidthHeaderVersion);
    local specID = importStream:ExtractValue(bitWidthSpecID);

    -- treeHash is a 128bit hash, passed as an array of 16, 8-bit values
    local treeHash = {};
    for i=1,16,1 do
      treeHash[i] = importStream:ExtractValue(8);
    end
    return true, serializationVersion, specID, treeHash;
  end

  local validSerializationVersions = {
    [1] = true,
    [2] = true
  }

  if WeakAuras.IsRetail() then
    function Private.LibSpecWrapper.CheckTalentForUnit(unit, talentId)
      if UnitIsUnit(unit, "player") then
        return select(4, WeakAuras.GetTalentById(talentId))
      end
      local unitName = GetUnitName(unit, true)
      if not nameToTalents[unitName] then
        -- Parse Talent String once and store which talents are selected
        if not nameToSpecMap[unitName] then return nil end
        local talentString = nameToSpecMap[unitName][4]
        if not talentString then return nil end

        local importStream = CreateAndInitFromMixin(ImportDataStreamMixin, talentString)
        local headerValid, serializationVersion, specID, treeHash = ReadLoadoutHeader(importStream);
        local currentSerializationVersion = C_Traits.GetLoadoutSerializationVersion();
        if(not headerValid) then
          return nil
        end
        if(serializationVersion ~= currentSerializationVersion or not validSerializationVersions[serializationVersion]) then
          return nil
        end

        local treeID = C_ClassTalents.GetTraitTreeForSpec(specID)

        local results = {};
        local bitWidthRanksPurchased = 6

        local _, _, talentsData = Private.GetTalentData(specID)
        local treeNodes = C_Traits.GetTreeNodes(treeID);
        for _, nodeId in ipairs(treeNodes) do
          local nodeSelectedValue = importStream:ExtractValue(1)
          local isNodeSelected = nodeSelectedValue == 1
          local isPartiallyRanked = false
          local partialRanksPurchased = 0
          local isChoiceNode = false
          local choiceNodeSelection = 1

          if(isNodeSelected) then
            if serializationVersion == 2 then
              local nodePurchasedValue = importStream:ExtractValue(1)
              local isNodePurchased = nodePurchasedValue == 1
              if(isNodePurchased) then
                local isPartiallyRankedValue = importStream:ExtractValue(1)
                isPartiallyRanked = isPartiallyRankedValue == 1
                if(isPartiallyRanked) then
                  partialRanksPurchased = importStream:ExtractValue(bitWidthRanksPurchased)
                end
                local isChoiceNodeValue = importStream:ExtractValue(1)
                isChoiceNode = isChoiceNodeValue == 1
                if(isChoiceNode) then
                  choiceNodeSelection = importStream:ExtractValue(2) + 1
                end
              end
            else
              local isPartiallyRankedValue = importStream:ExtractValue(1)
              isPartiallyRanked = isPartiallyRankedValue == 1
              if(isPartiallyRanked) then
                partialRanksPurchased = importStream:ExtractValue(bitWidthRanksPurchased)
              end
              local isChoiceNodeValue = importStream:ExtractValue(1)
              isChoiceNode = isChoiceNodeValue == 1
              if(isChoiceNode) then
                choiceNodeSelection = importStream:ExtractValue(2) + 1
              end
            end
          end

          local talentData = talentsData and talentsData[nodeId] and talentsData[nodeId][choiceNodeSelection]
          if talentData then
            if isPartiallyRanked then
              results[talentData[1]] = partialRanksPurchased
            else
              results[talentData[1]] = nodeSelectedValue == 1 and talentData[5] or 0
            end
          end
          if isChoiceNode then
            local unselectedChoiceNodeIdx = choiceNodeSelection == 1 and 2 or 1
            local unselectedTalentData = talentsData and talentsData[nodeId] and talentsData[nodeId][unselectedChoiceNodeIdx]
            if unselectedTalentData then
              results[unselectedTalentData[1]] = 0
            end
          end
        end
        nameToTalents[unitName] = results
      end

      if nameToTalents[unitName] then
        return nameToTalents[unitName][talentId]
      end
    end
  elseif WeakAuras.IsMists() then
    local function cacheData(unitName)
      -- Parse Talent String once and store which talents are selected
      if not nameToSpecMap[unitName] then return nil end
      local JSONtalentString = nameToSpecMap[unitName][4]
      if not JSONtalentString then return nil end

      local talentsAndGlyphs = C_EncodingUtil.DeserializeJSON(JSONtalentString)
      if not talentsAndGlyphs then return nil end

      local results = {
        talents = {},
        glyphs = {}
      }

      for _, id in ipairs(talentsAndGlyphs.talents) do
        if id > 0 then
          results.talents[id] = true
        end
      end
      for _, id in ipairs(talentsAndGlyphs.glyphs) do
        if id > 0 then
          results.glyphs[id] = true
        end
      end
      nameToTalents[unitName] = results
    end

    function Private.LibSpecWrapper.CheckTalentForUnit(unit, talentId)
      if UnitIsUnit(unit, "player") then
        return select(4, WeakAuras.GetTalentById(talentId))
      end
      local unitName = GetUnitName(unit, true)
      if not nameToTalents[unitName] then
        cacheData(unitName)
      end

      if nameToTalents[unitName] then
        return nameToTalents[unitName].talents[talentId]
      end
    end

    function Private.LibSpecWrapper.CheckGlyphForUnit(unit, glyphId)
      local unitName = GetUnitName(unit, true)
      if not nameToTalents[unitName] then
        cacheData(unitName)
      end

      if nameToTalents[unitName] then
        return nameToTalents[unitName].glyphs[glyphId]
      end
    end
  end

else -- non retail
  function Private.LibSpecWrapper.Register(f)

  end

  function Private.LibSpecWrapper.SpecForUnit(unit)
    return nil
  end

  function Private.LibSpecWrapper.SpecRolePositionForUnit(unit)
    return nil
  end

  function Private.LibSpecWrapper.CheckTalentForUnit(unit)
    return nil
  end
end

-- Export for GenericTrigger
WeakAuras.SpecForUnit = Private.LibSpecWrapper.SpecForUnit
WeakAuras.SpecRolePositionForUnit = Private.LibSpecWrapper.SpecRolePositionForUnit
WeakAuras.CheckTalentForUnit = Private.LibSpecWrapper.CheckTalentForUnit

if Private.LibSpecWrapper.CheckGlyphForUnit then
  WeakAuras.CheckGlyphForUnit = Private.LibSpecWrapper.CheckGlyphForUnit
else
  WeakAuras.CheckGlyphForUnit = function() return nil end
end
