-- Copyright (C) 2010 Tyson Brown
-- 
-- This file is part QuestHelper2, an AddOn for World of Warcraft.
-- 
-- QuestHelper2 is free software: you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
-- 
-- QuestHelper2 is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU General Public License for more details.
-- 
-- You should have received a copy of the GNU General Public License
-- along with QuestHelper2. If not, see <http://www.gnu.org/licenses/>.

if LibDebug then LibDebug() end
local shared = select(2, ...)
local L = shared.L

-- Blizzard's tracker has a template called WatchFrameItemButtonTemplate for this,
-- but it doesn't inherit from SecureActionButtonTemplate, which I suspect means
-- I'm screwed if I wanted to use it.
-- 
-- Just as well, it is works using quests rather than items, so it probably
-- wouldn't have worked for me in any case.

-- Released button frames get stored here for recycling.
local unused_buttons = {}

local primary_button = nil

-- Maps an item id to a set of button frames that require that item.
local required_items = {}

-- Maps a spell id to a set of button frames that require that spell.
local required_spells = {}

local item_count = {}

local function updateButtonRange(self)
  local in_range = false
  
  if self:GetAttribute("type") == "item" then
    in_range = IsItemInRange(self.item_id, "target") == 1
  elseif self:GetAttrubute("type") == "spell" then
    -- TODO: IsSpellInRange might require a spell name rather than the ID.
    in_range = IsSpellInRange(GetSpellInfo(self.spell_id), BOOKTYPE_SPELL, "target") == 1
  end
  
  if in_range then
    self.range_indicator:SetVertexColor(0.6, 0.6, 0.6)
  else
    self.range_indicator:SetVertexColor(1.0, 0.0, 0.0)
  end
end

local function setupButtonDefault(self)
  if self:GetAttribute("type") == "item" then
    self:SetAlpha(0.5)
    self:GetNormalTexture():SetVertexColor(0.4, 0.4, 0.4)
  else
    -- Spells and macros are always opaque.
    self:SetAlpha(1)
    self:GetNormalTexture():SetVertexColor(1,1,1)
  end
  
  self:SetScript("OnUpdate", nil)
  self.count_text:Hide()
  self.range_indicator:Hide()
end

local function showButtonTooltip(self)
  if shared.settings.tracker_show_item_tooltips then
    GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
    
    local button_type = self:GetAttribute("type")
    if button_type == "item" then
      GameTooltip:SetHyperlink((select(2, GetItemInfo(self.item_id))))
    elseif button_type == "spell" then
      GameTooltip:SetHyperlink(GetSpellLink(self.spell_id))
    else
      GameTooltip:SetText(MACRO)
      GameTooltip:AddLine(self:GetAttribute("macrotext"), 1, 1, 1, true)
    end
    
    if self == primary_button then
      local key = GetBindingKey("CLICK QuestHelper2_PrimaryActionButton:LeftButton")
      
      if key then
        local title = _G[GameTooltip:GetName().."TextLeft1"]
        
        -- Inject the name of the bound hotkey into the tooltip title.
        title:SetText(("%s %s(%s)%s"):format(title:GetText(), NORMAL_FONT_COLOR_CODE, GetBindingText(key, "KEY_") or key, FONT_COLOR_CODE_CLOSE))
      else
        -- Append a notice saying that you can bind a hotkey to this action.
        GameTooltip:AddLine(L.HOTKEY_ACTION_NOT_BOUND:format(("%s%s%s"):format(NORMAL_FONT_COLOR_CODE, KEY_BINDINGS, FONT_COLOR_CODE_CLOSE)), 1, .5, 0, true)
      end
    end
    
    GameTooltip:Show()
  end
end

local function hideButtonTooltip()
  GameTooltip:Hide()
end

local function updateButtonState(button, start_time, duration, cooldown_enabled, range)
  if range then
    button:SetScript("OnUpdate", updateButtonRange)
    updateButtonRange(button)
    button.range_indicator:Show()
  else
    button:SetScript("OnUpdate", nil)
    button.range_indicator:Hide()
  end
  
  CooldownFrame_SetTimer(button.cooldown, start_time, duration, cooldown_enabled)
  
  -- What does it even mean to have an item with
  -- no cooldown and a cooldown duration?
  -- Well, this is that they do in WatchFrameItem_UpdateCooldown, so meh.
  if duration > 0 and cooldown_enabled == 0 then
    button:GetNormalTexture():SetVertexColor(0.4, 0.4, 0.4)
  else
    button:GetNormalTexture():SetVertexColor(1, 1, 1)
  end
end

local function recheckBags()
  local lockdown = InCombatLockdown()
  local need_unsafe_update = false
  
  table.wipe(item_count)
  
  for bag_id=0,NUM_BAG_SLOTS do
    for slot=1,GetContainerNumSlots(bag_id) do
      local link = GetContainerItemLink(bag_id, slot)
      if link then
        local item_id = tonumber(link:match("|Hitem:(%d+):"))
        local button_set = required_items[item_id]
        
        if button_set then
          local texture, count = GetContainerItemInfo(bag_id, slot)
          
          if not item_count[item_id] then
            -- This is the first time we've encountered this item during this pass.
            item_count[item_id] = count
            
            local item_name = link:match("|h%[(.-)%]|h")
            local start_time, duration, cooldown_enabled = GetContainerItemCooldown(bag_id, slot)
            local range = IsItemInRange(item_id)
            
            for button in pairs(button_set) do
              if button:GetAttribute("item") ~= item_name then
                if lockdown then
                  -- Can't change the item attribute of the button while in combat lockdown. :(
                  need_unsafe_update = true
                else
                  button:SetAttribute("item", item_name)
                end
              end
              
              updateButtonState(button, start_time, duration, cooldown_enabled, range)
              button:SetAlpha(1)
              button:SetNormalTexture(texture)
            end
          else
            -- Already setup the buttons, just increase the item count.
            item_count[item_id] = item_count[item_id] + count
          end
        end
      end
    end
  end
  
  -- These items are actually in the player's inventory and have been setup;
  -- all thats left to do is set the item counts on the button now that they've
  -- been tallied.
  for item_id, count in pairs(item_count) do
    for button in pairs(required_items[item_id]) do
      if count > 1 then
        -- If there are multiple items, display how many there are.
        button.count_text:SetText(tostring(count))
        button.count_text:Show()
      else
        button.count_text:Hide()
      end
    end
  end
  
  -- These items however aren't in the players inventory and haven't
  -- been setup yet.
  for item_id, button_set in pairs(required_items) do
    if not item_count[item_id] then
      for button in pairs(button_set) do
        setupButtonDefault(button)
      end
    end
  end
  
  if need_unsafe_update then
    -- Need to make changes that can't be done while in combat.
    shared.whenOutOfCombat(shared.callOnce, recheckBags)
  end
end

local function checkSpellCooldowns()
  for spell_id, button_set in pairs(required_spells) do
    local start_time, duration, cooldown_enabled = GetSpellCooldown(spell_id)
    local range = IsSpellInRange(GetSpellInfo(spell_id), BOOKTYPE_SPELL, "target")
    
    for button in pairs(button_set) do
      updateButtonState(button, start_time, duration, cooldown_enabled, range)
    end
  end
end

local function queueBagRecheck()
  shared.callOnce(recheckBags)
end

local function queueSpellCooldownCheck()
  shared.callOnce(checkSpellCooldowns)
end

-- TODO: Also check spell buttons for range and what-not.

shared.onEvent("PLAYER_TARGET_CHANGED", queueBagRecheck)
shared.onEvent("BAG_UPDATE", queueBagRecheck)
shared.onEvent("BAG_UPDATE_COOLDOWN", queueBagRecheck)

shared.onEvent("UNIT_SPELLCAST_STOP", queueSpellCooldownCheck)
shared.onEvent("UNIT_SPELLCAST_CHANNEL_STOP", queueSpellCooldownCheck)

function shared.createActionButton(button_type, data, parent, primary)
  -- I mean, seriously, don't do that.
  assert(not InCombatLockdown())
  
  assert(button_type == "item" or button_type == "spell" or button_type == "macro")
  
  local button
  
  if primary then
    if not primary_button then
      -- Needs to be created.
    elseif unused_buttons[primary_button] then
      -- You've got it.
      button = primary_button
    else
      -- The primary button isn't available.
      primary = false
      button = next(unused_buttons)
    end
  else
    button = next(unused_buttons)
    if button == primary_button then button = next(unused_buttons, button) end
  end
  
  if button then
    unused_buttons[button] = nil
    button:Show()
  else
    if primary then
      button = CreateFrame("Button", "QuestHelper2_PrimaryActionButton", UIParent, "SecureActionButtonTemplate")
      primary_button = button
    else
      button = CreateFrame("Button", nil, UIParent, "SecureActionButtonTemplate")
    end
    
    button:SetHighlightTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\ButtonHighlight.tga")
    
    button.cooldown = CreateFrame("Cooldown", nil, button, "CooldownFrameTemplate")
    button.cooldown:SetAllPoints()
    
    button.range_indicator = button:CreateFontString(nil, "OVERLAY", "NumberFontNormalSmallGray")
    button.range_indicator:SetPoint("TOPRIGHT", button, 3, -2)
    button.range_indicator:SetText(GetText("RANGE_INDICATOR"))
    
    button.count_text = button:CreateFontString(nil, "OVERLAY", "NumberFontNormal")
    button.count_text:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -1, 2)
    
    button.texture = button:CreateTexture(nil, "ARTWORK")
    
    button:SetWidth(26)
    button:SetHeight(26)
    
    button:SetScript("OnEnter", showButtonTooltip)
    button:SetScript("OnLeave", hideButtonTooltip)
  end
  
  if button_type == "item" then
    button:SetAttribute("type", "item")
    button.item_id = assert(tonumber(data))
    button:SetAttribute("item", GetItemInfo(button.item_id) or "")
    button:SetNormalTexture(select(10, GetItemInfo(button.item_id)) or "Interface\\ICONS\\INV_Crate_05‎.blp")
    
    local button_set = required_items[button.item_id]
    
    if button_set then
      button_set[button] = true
    else
      required_items[button.item_id] = {[button]=true}
    end
    
    queueBagRecheck()
  elseif button_type == "spell" then
    button:SetAttribute("type", "spell")
    button.spell_id = assert(tonumber(data))
    button:SetAttribute("spell", GetSpellInfo(button.spell_id) or "")
    button:SetNormalTexture(select(3, GetSpellInfo(button.spell_id)) or "Interface\\ICONS\\Spell_Fire_Blueflamering.blp")
    
    local button_set = required_spells[button.spell_id]
    
    if button_set then
      button_set[button] = true
    else
      required_spells[button.spell_id] = {[button]=true}
    end
    
    queueSpellCooldownCheck()
  else  -- macrotext
    button:SetAttribute("type", "macro")
    button:SetAttribute("macrotext", data)
    button:SetNormalTexture("Interface\\ICONS\\INV_Misc_Gear_01.blp")
  end
  
  button:SetParent(parent)
  
  setupButtonDefault(button)
  
  button.valid = true
  
  return button
end

function shared.releaseActionButton(button)
  assert(not InCombatLockdown())
  
  assert(button.valid)
  
  -- Be gone little button; you're free!
  unused_buttons[button] = true
  button:SetParent(UIParent)
  button:ClearAllPoints()
  button:Hide()
  button.valid = nil
  
  if button:GetAttribute("type") == "item" then
    -- This button no longer needs to be updated for this item.
    local button_set = assert(required_items[button.item_id])
    button_set[button] = nil
    
    -- No buttons use this item any more, so get rid of the button set for that item.
    if not next(button_set) then
      required_items[button.item_id] = nil
    end
  elseif button:GetAttribute("type") == "spell" then
    local button_set = assert(required_spells[button.spell_id])
    button_set[button] = nil
    
    -- No buttons use this spell any more, so get rid of the button set for that spell.
    if not next(button_set) then
      required_spells[button.spell_id] = nil
    end
  end
end
