local addonName = ... ---@type string

---@class BetterBags: AceAddon
local addon = LibStub('AceAddon-3.0'):GetAddon(addonName)

---@class Constants: AceModule
local const = addon:GetModule('Constants')

---@class Localization: AceModule
local L = addon:GetModule('Localization')

---@class Debug: AceModule
local debug = addon:GetModule('Debug')

---@class Events: AceModule
local events = addon:GetModule('Events')

---@class Async: AceModule
local async = addon:GetModule('Async')

---@class Database: AceModule
local db = addon:GetModule('Database')

---@class Context: AceModule
local context = addon:GetModule('Context')

---@class BagButton: Button
---@field portrait Texture
---@field highlightTex Texture

---@class (exact) Theme
---@field key? string The key used to identify this theme. This will be set by the Themes module when registering the theme, you do not need to provide this.
---@field Name string The display name used by this theme in the theme selection window.
---@field Description string A description of the theme used by this theme in the theme selection window.
---@field Available boolean Whether or not this theme is available to the user.
---@field Portrait fun(frame: Frame) A function that applies the theme to a portrait frame.
---@field Simple fun(frame: Frame) A function that applies the theme to a simple frame.
---@field Flat fun(frame: Frame) A function that applies the theme to a flat frame.
---@field Opacity fun(frame: Frame, opacity: number) A callback that is called when the user changes the opacity of the frame. You should use this to change the alpha of your backdrops.
---@field SectionFont fun(font: FontString) A function that applies the theme to a section font.
---@field SetTitle fun(frame: Frame, title: string) A function that sets the title of the frame.
---@field ToggleSearch fun(frame: Frame, shown: boolean) A function that toggles the search box on the frame.
---@field PositionBagSlots? fun(frame: Frame, bagSlotWindow: Frame) A function that positions the bag slots on the frame.
---@field OffsetSidebar? fun(): number A function that offsets the sidebar by x pixels. 
---@field ItemButton? fun(button: Item): ItemButton A function that applies the theme to an item button.
---@field Tab? fun(tab: Button): PanelTabButtonTemplate A function that applies the theme to a tab.
---@field Reset fun() A function that resets the theme to its default state and removes any special styling.
---@field DisableMasque? boolean If set to true, Masque will not be used with this theme.

---@class Themes: AceModule
---@field themes table<string, Theme>
---@field windows table<WindowKind, Frame[]>
---@field sectionFonts table<string, FontString>
---@field titles table<string, string>
---@field itemButtons table<string, ItemButton>
---@field tabs table<string, PanelTabButtonTemplate>
local themes = addon:NewModule('Themes')

-- Initialize this bare as we will be adding themes from bare files.
themes.themes = {}

function themes:OnInitialize()
  self.windows = {
    [const.WINDOW_KIND.PORTRAIT] = {},
    [const.WINDOW_KIND.SIMPLE] = {},
    [const.WINDOW_KIND.FLAT] = {}
  }
  self.sectionFonts = {}
  self.titles = {}
  self.itemButtons = {}
  self.tabs = {}
end

function themes:OnEnable()
  local ctx = context:New('Themes_OnEnable')
  local theme = db:GetTheme()
  if self.themes[theme] and self.themes[theme].Available then
    self:ApplyTheme(ctx, theme)
  else
    db:SetTheme('Default')
    self:ApplyTheme(ctx, 'Default')
  end
end

-- GetCurrentTheme returns the current active theme, or the default theme if the current theme is not available.
---@return Theme
function themes:GetCurrentTheme()
  local theme = db:GetTheme()
  if self.themes[theme] and self.themes[theme].Available then
    return self.themes[theme]
  end
  return self.themes['Default']
end

---@param key string
---@param themeTemplate Theme
function themes:RegisterTheme(key, themeTemplate)
  themeTemplate.key = key
  self.themes[key] = themeTemplate
end

-- ApplyTheme is used to apply a theme to every window registered with RegisterWindow.
---@param ctx Context
---@param key string
function themes:ApplyTheme(ctx, key)
  assert(self.themes[key], 'Theme does not exist.')

  -- Reset the old theme.
  local oldTheme = db:GetTheme()
  if self.themes[oldTheme] then
    self.themes[oldTheme].Reset()
  end

  local theme = self.themes[key]
  db:SetTheme(key)

  -- Hide all tab decorations.
  --for _, tab in pairs(self.tabs) do
  --  --tab:Hide()
  --end

  -- Apply all portrait themes.
  for _, frame in pairs(self.windows[const.WINDOW_KIND.PORTRAIT]) do
    theme.Portrait(frame)
    local sizeInfo = db:GetBagSizeInfo(const.BAG_KIND.BACKPACK, db:GetBagView(const.BAG_KIND.BACKPACK))
    theme.Opacity(frame, sizeInfo.opacity)
    theme.ToggleSearch(frame, db:GetInBagSearch())
    -- Apply bagslots positioning if the theme has a custom function for it.
    if frame.Owner.slots then
      if theme.PositionBagSlots then
        theme.PositionBagSlots(frame, frame.Owner.slots.frame)
      else
        frame.Owner.slots.frame:ClearAllPoints()
        frame.Owner.slots.frame:SetPoint("BOTTOMLEFT", frame, "TOPLEFT", 0, 14)
      end
    end

    local offset = 0
    if theme.OffsetSidebar then
      offset = theme.OffsetSidebar()
    end
    frame.Owner.sideAnchor:ClearAllPoints()
    frame.Owner.sideAnchor:SetPoint("TOPRIGHT", frame, "TOPLEFT", offset, 0)
    frame.Owner.sideAnchor:SetPoint("BOTTOMRIGHT", frame, "BOTTOMLEFT", offset, 0)
    if frame.Owner.tabs then
      frame.Owner.tabs:Reload(ctx)
    end
  end

  -- Apply all simple frame themes.
  for _, frame in pairs(self.windows[const.WINDOW_KIND.SIMPLE]) do
    theme.Simple(frame)
    local sizeInfo = db:GetBagSizeInfo(const.BAG_KIND.BACKPACK, db:GetBagView(const.BAG_KIND.BACKPACK))
    theme.Opacity(frame, sizeInfo.opacity)
  end

  -- Apply all flat frame themes.
  for _, frame in pairs(self.windows[const.WINDOW_KIND.FLAT]) do
    theme.Flat(frame)
  end

  -- Apply all section fonts.
  for _, font in pairs(self.sectionFonts) do
    theme.SectionFont(font)
  end

  -- Hide all item button decorations.
  for _, button in pairs(self.itemButtons) do
    button:Hide()
  end


  --TODO(lobato): Create a new message just for redrawing items.
  events:SendMessage(ctx, 'bags/FullRefreshAll')
end

-- SetSearchState will show or hide the search bar for the given frame.
---@param ctx Context
---@param frame Frame
---@param shown boolean
function themes:SetSearchState(ctx, frame, shown)
  local theme = self.themes[db:GetTheme()]
  theme.ToggleSearch(frame, shown)
  events:SendMessage(ctx, 'bags/FullRefreshAll')
end

-- GetInBagSearchBox returns the in-bag search box for the given frame, if it exists.
-- This works by finding the themed decoration frame (a child of the bag frame) that contains the search box.
---@param frame Frame
---@return SearchFrame?
function themes:GetInBagSearchBox(frame)
  -- Iterate through children to find the decoration that has a search box
  local children = {frame:GetChildren()}
  for _, child in ipairs(children) do
    if child.search and child.search.textBox then
      return child.search
    end
  end
  return nil
end

-- RegisterPortraitWindow is used to register a protrait window frame to be themed by themes.
---@param frame Frame
---@param title string
function themes:RegisterPortraitWindow(frame, title)
  table.insert(self.windows[const.WINDOW_KIND.PORTRAIT], frame)
  self.titles[frame:GetName()] = title
end

-- RegisterSimpleWindow is used to register a protrait window frame to be themed by themes.
---@param frame Frame
---@param title string
function themes:RegisterSimpleWindow(frame, title)
  table.insert(self.windows[const.WINDOW_KIND.SIMPLE], frame)
  self.titles[frame:GetName()] = title
end

-- RegisterFlatWindow is used to register a protrait window frame to be themed by themes.
---@param frame Frame
---@param title string
function themes:RegisterFlatWindow(frame, title)
  table.insert(self.windows[const.WINDOW_KIND.FLAT], frame)
  self.titles[frame:GetName()] = title
end

-- RegisterSectionFont is used to register a font to be used in the section headers.
---@param font FontString
function themes:RegisterSectionFont(font)
  table.insert(self.sectionFonts, font)
end

---@param button Item
function themes:UpdateItemButton(button)
  local theme = self.themes[db:GetTheme()]
  if theme.ItemButton then
    theme.ItemButton(button)
  end
end

---@param tab PanelTabButtonTemplate
function themes:UpdateTab(tab)
  local theme = self.themes[db:GetTheme()]
  if theme.Tab then
    theme.Tab(tab)
  end
end

---@return table<string, Theme>
function themes:GetAllThemes()
  ---@type table<string, Theme>
  local list = {}
  for key, theme in pairs(self.themes) do
    if theme.Available then
      list[key] = theme
    end
  end
  return list
end

function themes:UpdateOpacity()
  local theme = self.themes[db:GetTheme()]
  if not theme then return end
  for _, frame in pairs(self.windows[const.WINDOW_KIND.PORTRAIT]) do
    local sizeInfo = db:GetBagSizeInfo(const.BAG_KIND.BACKPACK, db:GetBagView(const.BAG_KIND.BACKPACK))
    theme.Opacity(frame, sizeInfo.opacity)
  end
  for _, frame in pairs(self.windows[const.WINDOW_KIND.SIMPLE]) do
    local sizeInfo = db:GetBagSizeInfo(const.BAG_KIND.BACKPACK, db:GetBagView(const.BAG_KIND.BACKPACK))
    theme.Opacity(frame, sizeInfo.opacity)
  end
end

---@param font FontString
function themes:UpdateSectionFont(font)
  local theme = self.themes[db:GetTheme()]
  theme.SectionFont(font)
end

---@param button Button
function themes:resetCloseButton(button)
  button:SetDisabledAtlas("RedButton-Exit-Disabled")
  button:SetNormalAtlas("RedButton-Exit")
  button:SetPushedAtlas("RedButton-exit-pressed")
  button:SetHighlightAtlas("RedButton-Highlight", "ADD")
end

function themes:SetTitle(frame, title)
  local theme = self.themes[db:GetTheme()]
  theme.SetTitle(frame, title)
  self.titles[frame:GetName()] = title
end

---@param ctx Context
---@param item Item
---@return ItemButton
function themes:GetItemButton(ctx, item)
  local theme = self:GetCurrentTheme()
  if theme.ItemButton then
    return theme.ItemButton(item)
  end
  local buttonName = item.button:GetName()
  local button = self.itemButtons[buttonName]
  if button then
    button:Show()
    events:SendMessage(ctx, 'item/NewButton', item, button)
    return button
  end
  button = themes.CreateBlankItemButtonDecoration(item.frame, "default", buttonName)
  events:SendMessage(ctx, 'item/NewButton', item, button)
  self.itemButtons[buttonName] = button
  return button
end

---@param ctx Context
---@param tab TabButton
---@return PanelTabButtonTemplate
function themes:GetTabButton(ctx, tab)
  local theme = self:GetCurrentTheme()
  if theme.Tab then
    return theme.Tab(tab)
  end
  local tabIndex = tab:GetName()
  local decoration = self.tabs[tabIndex]
  if decoration then
    async:AfterCombat(ctx, function(_)
      decoration:Show()
    end)
    return decoration
  end
  decoration = themes.CreateDefaultTabDecoration(tab)
  self.tabs[tabIndex] = decoration
  async:AfterCombat(ctx, function(_)
    decoration:Show()
  end)
  return decoration
end

---@param tab TabButton
---@return PanelTabButtonTemplate
function themes.CreateDefaultTabDecoration(tab)
  local decoration = CreateFrame("button", tab:GetName() .. "default", tab, "BetterBagsSecureBagTabTemplate") --[[@as PanelTabButtonTemplate]]
  if tab.sabtClick then
    decoration:SetAttribute("type", "click")
    decoration:SetAttribute("clickbutton", tab.sabtClick)
    decoration:SetAttribute("overrideBankType", Enum.BankType.Account)
  end
  decoration:SetPoint("TOPLEFT", tab, "TOPLEFT", 0, 0)
  decoration:RegisterForClicks("LeftButtonDown", "RightButtonDown")
  return decoration
end

---@param parent Button|ItemButton|Frame
---@param theme string
---@param buttonName string
---@return ItemButton
function themes.CreateBlankItemButtonDecoration(parent, theme, buttonName)
  ---@type ItemButton
  local button
  if addon.isRetail then
    button = CreateFrame("ItemButton", buttonName.."Decoration"..theme, parent, "ContainerFrameItemButtonTemplate") --[[@as ItemButton]]
  else
    button = CreateFrame("Button", buttonName.."Decoration"..theme, parent, "ContainerFrameItemButtonTemplate") --[[@as ItemButton]]
    ---@diagnostic disable-next-line: duplicate-set-field
    button.SetMatchesSearch = function(me, match)
      if match then
        me.searchOverlay:Hide()
      else
        me.searchOverlay:Show()
      end
    end
  end
  button:SetAllPoints()

  if addon.isRetail then
    button.ItemSlotBackground = button:CreateTexture(nil, "BACKGROUND", "ItemSlotBackgroundCombinedBagsTemplate", -6)
    button.ItemSlotBackground:SetAllPoints(button)
    button.ItemSlotBackground:Hide()
  end

  button:SetFrameLevel(parent:GetFrameLevel() > 0 and parent:GetFrameLevel() - 1 or 0)
  button.IconTexture = _G[buttonName.."Decoration"..theme.."IconTexture"]
  if not button.IconQuestTexture then
    button.IconQuestTexture = _G[buttonName.."Decoration"..theme.."IconQuestTexture"]
  end
  button:Show()
  return button
end

---@param bag Bag
---@param decoration Frame
---@return BagButton
function themes.SetupBagButton(bag, decoration)
  -- JIT include the context menu, due to load order.

  ---@class ContextMenu: AceModule
  local contextMenu = addon:GetModule('ContextMenu')

  local bagButton = CreateFrame("Button", nil, decoration) --[[@as BagButton]]
  bagButton:SetFrameStrata("HIGH")
  bagButton:EnableMouse(true)
  bagButton:SetWidth(40)
  bagButton:SetHeight(40)
  bagButton:SetPoint("TOPLEFT", decoration, "TOPLEFT", -10, 7)
  bagButton:SetFrameLevel(1001)

  local portraitSize = 48
  bagButton.portrait = bagButton:CreateTexture()
  bagButton.portrait:SetTexture([[Interface\Containerframe\Bagslots2x]])
  bagButton.portrait:SetTexCoord(0, 0.2, 0, 1)
  bagButton.portrait:SetSize(portraitSize, portraitSize * 1.25)
  bagButton.portrait:SetPoint("CENTER", bagButton, "CENTER", 0, 0)
  bagButton.portrait:SetDrawLayer("OVERLAY", 7)

  bagButton.highlightTex = bagButton:CreateTexture("BetterBagsBagButtonTextureHighlight", "BACKGROUND")
  bagButton.highlightTex:SetTexture([[Interface\Containerframe\Bagslots2x]])
  bagButton.highlightTex:SetSize(portraitSize, portraitSize * 1.25)
  bagButton.highlightTex:SetTexCoord(0.2, 0.3992, 0, 1)
  bagButton.highlightTex:SetPoint("CENTER", bagButton, "CENTER", 2, 0)
  bagButton.highlightTex:SetAlpha(0)
  bagButton.highlightTex:SetDrawLayer("OVERLAY", 7)

  local anig = bagButton.highlightTex:CreateAnimationGroup("BetterBagsBagButtonTextureHighlightAnim")
  local ani = anig:CreateAnimation("Alpha")
  ani:SetFromAlpha(0)
  ani:SetToAlpha(1)
  ani:SetDuration(0.2)
  ani:SetSmoothing("IN")
  if db:GetFirstTimeMenu() then
    ani:SetDuration(0.4)
    anig:SetLooping("BOUNCE")
    anig:Play()
  end
  bagButton:SetScript("OnEnter", function()
    if not db:GetFirstTimeMenu() then
      anig:Stop()
      bagButton.highlightTex:SetAlpha(1)
      anig:Play()
    end
    GameTooltip:SetOwner(bagButton, "ANCHOR_LEFT")
    if bag.kind == const.BAG_KIND.BACKPACK then
      GameTooltip:AddDoubleLine(L:G("Left Click"), L:G("Open Menu"), 1, 0.81, 0, 1, 1, 1)
      GameTooltip:AddDoubleLine(L:G("Shift Left Click"), L:G("Search Bags"), 1, 0.81, 0, 1, 1, 1)
      GameTooltip:AddDoubleLine(L:G("Right Click"), L:G("Sort Bags"), 1, 0.81, 0, 1, 1, 1)
    else
      GameTooltip:AddDoubleLine(L:G("Left Click"), L:G("Open Menu"), 1, 0.81, 0, 1, 1, 1)
      GameTooltip:AddDoubleLine(L:G("Shift Left Click"), L:G("Search Bags"), 1, 0.81, 0, 1, 1, 1)
      if addon.isRetail then
        if bag.bankTab == const.BANK_TAB.REAGENT then
          GameTooltip:AddDoubleLine(L:G("Right Click"), L:G("Deposit Reagent Items"), 1, 0.81, 0, 1, 1, 1)
        else
          GameTooltip:AddDoubleLine(L:G("Right Click"), L:G("Deposit Warbank Items"), 1, 0.81, 0, 1, 1, 1)
        end
      end
    end

    if CursorHasItem() then
      local cursorType, _, itemLink = GetCursorInfo()
      if cursorType == "item" then
        GameTooltip:AddLine(" ", 1, 1, 1)
        GameTooltip:AddLine(format(L:G("Drop %s here to create a new category for it."), itemLink), 1, 1, 1)
      end
    end
    GameTooltip:Show()
  end)
  bagButton:SetScript("OnLeave", function()
    GameTooltip:Hide()
    if not db:GetFirstTimeMenu() then
      anig:Stop()
      bagButton.highlightTex:SetAlpha(0)
      anig:Restart(true)
    end
  end)
  bagButton:RegisterForClicks("LeftButtonUp", "RightButtonUp")
  addon.SetScript(bagButton, "OnReceiveDrag", function(ctx)
    bag:CreateCategoryForItemInCursor(ctx)
  end)
  addon.SetScript(bagButton, "OnClick", function(ctx, _, e)
    if e == "LeftButton" then
      if db:GetFirstTimeMenu() then
        db:SetFirstTimeMenu(false)
        bagButton.highlightTex:SetAlpha(1)
        anig:SetLooping("NONE")
        anig:Restart()
      end
      if IsShiftKeyDown() then
        BetterBags_ToggleSearch()
      elseif CursorHasItem() and GetCursorInfo() == "item" then
        bag:CreateCategoryForItemInCursor(ctx)
      else
        contextMenu:Show(ctx, bag.menuList)
      end
    elseif e == "RightButton" then
      if bag.kind == const.BAG_KIND.BANK and addon.isRetail then
        if bag.bankTab <= Enum.BagIndex.CharacterBankTab_6 then
          C_Bank.AutoDepositItemsIntoBank(Enum.BankType.Character)
          C_Container.SortBankBags()
        else
          C_Bank.AutoDepositItemsIntoBank(Enum.BankType.Account)
          C_Container.SortAccountBankBags()
        end
      else
        bag:Sort(ctx)
      end
    end
  end)
  return bagButton
end