-- 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, ...)

-- This file is in charge of creating frames for any enabled objectives; and 
-- adding or removing them from the tracker.

-- Frames without an objective go here.
local unused_frames = {}

-- Item buttons that aren't part of an objective frame go here.
local unused_item_buttons = {}

-- Maps objectives to their frames.
local objective_frames = {}

-- Sets how deeply nested an objective is.
local objective_depth = {}

local added_action_set = {}

local function addActionToFrame(frame, type, action)
  local action_key = type..":"..action
  
  if added_action_set[action_key] then
    -- We've already created a button for this action, let's not create another.
    return
  end
  
  local button = shared.createActionButton(type, action, frame, true)
  
  local buttons = frame.buttons
  local button_count = #buttons
  
  if button_count == 0 then
    -- First button, anchor to the frame.
    button:SetPoint("TOPRIGHT", frame, "TOPLEFT", -4, 0)
  else
    -- There are other buttons, anchor to the bottom of the last.
    button:SetPoint("TOPRIGHT", buttons[button_count], "BOTTOMLEFT", 0, -4)
  end
  
  buttons[button_count+1] = button
  added_action_set[action_key] = button
  return button
end

local objective_order = {}

-- Yes, this function creates a bit of garbage, but hey, that's what garbage collectors are for.
local function prioritizeFrames()
  table.wipe(objective_order)
  
  -- Assign the objectives a priority based on their position in the route.
  for i, obj in ipairs(shared.route) do
    if objective_frames[obj] then
      table.insert(objective_order, obj)
    end
  end
  
  if shared.settings.tracker_shows_routes then
    -- Don't sort the objectives, and don't indent any objectives.
    table.wipe(objective_depth)
    
    for i, obj in ipairs(objective_order) do
      objective_depth[obj] = 0
    end
  else
    shared.sortObjectivesByQuest(objective_order, objective_depth)
  end
  
  -- Remove any existing buttons from all the frames.
  for objective, frame in pairs(objective_frames) do
    for i, button in ipairs(frame.buttons) do
      shared.releaseActionButton(button)
    end
    
    table.wipe(frame.buttons)
  end
  
  table.wipe(added_action_set)
  
  -- Re-add buttons to objective frames, in order.
  for i, obj in ipairs(objective_order) do
    if obj.item_id then
      addActionToFrame(objective_frames[obj], "item", obj.item_id)
    end
    
    if obj.spell_id then
      addActionToFrame(objective_frames[obj], "spell", obj.spell_id)
    end
    
    if obj.macro_text then
      addActionToFrame(objective_frames[obj], "macro", obj.macro_text)
    end
  end
  
  -- Add all the frames to the tracker.
  for i, obj in ipairs(objective_order) do
    shared.addFrameToTracker(objective_frames[obj], i)
  end
end

local function objectiveFrameClicked(frame, button)
  if IsModifierKeyDown() then
    frame.objective(button)
  elseif button == "LeftButton" then
    frame.objective.selected = not frame.objective.selected
  elseif button == "RightButton" then
    shared.showObjectiveMenu(frame, frame.objective)
  else
    frame.objective(button)
  end
end

local priority_colour = {
  [0] = {.8,.8,.8}, -- Selected (Not necessarly selected, but forced to the same priority as the selected objective, due to dependencies)
  [1] = {0,1,0}, -- Highest
  [2] = {.5,.5,0}, -- High
  -- Default
  [4] = {.75,.25,0}, -- Low
  [5] = {1,0,0} -- Lowest
}

local function updateFrame(frame)
  local objective = frame.objective
  
  if objective.selected then
    frame.priority_shadow:SetVertexColor(.3,.6,1)
    frame.priority_shadow:Show()
  elseif objective.priority == 3 then
    frame.priority_shadow:Hide()
  else
    frame.priority_shadow:SetVertexColor(unpack(priority_colour[objective.priority]))
    frame.priority_shadow:Show()
  end
  
  -- Fade out disabled objectives; normally they would be removed, but this might not happen if
  -- the player is in combat.
  frame:SetAlpha(objective.enabled and 1 or 0.5)
  
  if InCombatLockdown() then
    -- If in combat; the only thing we'll update is the progress text;
    -- and queue for a full update later.
    frame.progress_text:SetText(objective.formatted_progress)
    shared.whenOutOfCombat(updateFrame, frame)
    return
  end
  
  local text, text_overflow, progress_text = frame.text, frame.text_overflow, frame.progress_text
  
  local indent_width = (objective_depth[objective] or (objective.root and 0 or 1))*20
  local reserved_width = indent_width
  local reserved_height = 0
  
  local objective_progress = objective.formatted_progress
  
  if objective_progress == "" then
    progress_text:Hide()
    progress_text = nil
  else
    progress_text:SetText(objective.formatted_progress)
    progress_text:ClearAllPoints()
    progress_text:Show()
  end
  
  if progress_text then
    reserved_width = reserved_width + progress_text:GetStringWidth()+5
    reserved_height = reserved_height + progress_text:GetStringHeight()
    progress_text:SetPoint("RIGHT")
  end
  
  local available_width = frame:GetWidth()-reserved_width
  
  -- Root objectives get a different font from their objectives
  
  local font, r, g, b
  
  if objective.root and not shared.settings.tracker_shows_routes then
    font = GameFontNormalLargeLeft
    r, g, b = 1, .82, 0
  else
    font = GameTooltipHeaderText
    
    if objective.obtainable then
      r, g, b = 1, 1, 1
    else
      r, g, b = .7, .7, .7
    end
  end
  
  text:SetFontObject(font)
  text_overflow:SetFontObject(font)
  text:SetTextColor(r, g, b)
  text_overflow:SetTextColor(r, g, b)
  
  local objective_text = objective.text
  
  if not objective.owned then
    -- TODO: add a button to disable the defunct objective.
    objective_text = "|cffff0000(DEFUNCT)|r "..objective_text
  end
  
  text:ClearAllPoints()
  text:SetText(objective_text)
  
  frame.prefered_width = reserved_width + text:GetStringWidth()
  
  if text:GetStringWidth() <= available_width then
    text:SetPoint("LEFT", frame, "LEFT", indent_width, 0)
    -- We fit everything on one line, so hide the overflow text.
    text_overflow:Hide()
    frame:SetHeight(math.max(text:GetStringHeight(), reserved_height))
  else
    local last_good_index = 1
    
    while true do
      local index = objective_text:find("[%s%-]", last_good_index+1)
      
      if index then
        text:SetText(objective_text:sub(1,index-1))
        if text:GetStringWidth() > available_width then
          break
        else
          last_good_index = index
        end
      else
        break
      end
    end
    
    local broke_word = false
    
    if last_good_index == 1 then
      -- Failed to split on a word boundry. :(
      -- Let's try to add as many letters as possible, anyway.
      
      -- TODO: This may split the string in the middle of a formatting code.
      for i=2,objective_text:len() do
        text:SetText(objective_text:sub(1,i).."-")
        
        if text:GetStringWidth() > available_width then
          break
        else
          last_good_index = i+1
        end
      end
      
      broke_word = true
    end
    
    -- Append a dash if we had to split a word.
    local left = objective_text:sub(1,last_good_index-1)..(broke_word and "-" or "")
    
    -- Strip any leading whitespace.
    local right = objective_text:sub(last_good_index):match("^%s*(.*)$")
    
    -- Try to find the last color format code in the left part of the string.
    local ccode_end, ccode = select(2, left:find(".*(|c........)"))
    
    -- If its past the last colour reset code in the left string, copy it
    -- to the beginning of the right text string.
    if ccode and ccode_end > (left:find("|r") or 0) then
      right = ccode..right
    end
    
    text:SetText(left)
    text:SetPoint("TOPLEFT", frame, "TOPLEFT", indent_width, 0)
    
    text_overflow:ClearAllPoints()
    text_overflow:SetText(right)
    text_overflow:SetPoint("TOPLEFT", frame, "TOPLEFT", indent_width+10, -text:GetStringHeight())
    text_overflow:SetWidth(available_width-10)
    text_overflow:Show()
    
    frame:SetHeight(math.max(text:GetStringHeight()+text_overflow:GetStringHeight(), reserved_height))
  end
end

local old_setWidth
local function setWidth(frame, w)
  old_setWidth(frame, w)
  updateFrame(frame)
end

local function showTooltip(frame)
  if shared.settings.tracker_show_objective_tooltips then
    GameTooltip:SetOwner(_G.QuestHelper2_Tracker, "ANCHOR_NONE")
    GameTooltip:ClearAllPoints()
    
    if shared.settings.tracker_anchor_left then
      GameTooltip:SetPoint("LEFT", frame, "RIGHT", 3, 0)
    else
      GameTooltip:SetPoint("RIGHT", frame, "LEFT", -3, 0)
    end
    
    shared.setObjectiveTooltip(frame.objective, GameTooltip)
    
    GameTooltip:Show()
  end
end

local function hideTooltip()
  if shared.settings.tracker_show_objective_tooltips then
    GameTooltip:Hide()
  end
end

local route_objectives = {}
local visited_objectives = {}

local function visitReverseDependencies(objective)
  if not visited_objectives[objective] and route_objectives[objective] then
    visited_objectives[objective] = true
    
    if not objective.root then
      for dep in objective:revDependencies() do
        visitReverseDependencies(dep)
      end
    end
  end
end

local function collectRouteObjectives()
  if InCombatLockdown() then
    -- Wait until the player is out of combat before messing with anything.
    shared.whenOutOfCombat(shared.callOnce, collectRouteObjectives)
    
    return
  end
  
  -- Create a set of all the objectives in the route.
  for i, obj in ipairs(shared.route) do
    route_objectives[obj] = true
  end
  
  table.wipe(visited_objectives)
  
  -- Discover which objectives should actually be displayed in the route.
  for i, obj in ipairs(shared.route) do
    if obj.obtainable then
      visitReverseDependencies(obj)
    end
  end
  
  table.wipe(route_objectives)
  
  -- Remove the frames of any objectives that shouldn't be displayed first,
  -- so they can be re-assigned to new objectives rather than creating new frames.
  for obj, frame in pairs(objective_frames) do
    if not visited_objectives[obj] then
      for i, button in ipairs(frame.buttons) do
        shared.releaseActionButton(button)
      end
      
      table.wipe(frame.buttons)
      
      if frame.flash then
        -- Frame was flashing, cancel that so that it doesn't flash when recycled for another objective.
        frame:SetScript("OnUpdate", nil)
        frame.flash = nil
        frame.repeat_flash = nil
        frame.flash_texture:Hide()
      end
      
      objective_frames[obj] = nil
      unused_frames[frame] = true
      frame.objective = nil
      shared.removeFrameFromTracker(frame)
    end
  end
  
  -- Create or recycle frames for any objectives that aren't in the route.
  for obj in pairs(visited_objectives) do
    if not objective_frames[obj] then
      local frame = next(unused_frames)
      
      if frame then
        assert(frame.objective == nil)
        unused_frames[frame] = nil
      else
        frame = CreateFrame("Frame", nil, UIParent)
        
        -- Only making one backup copy of SetWidth to be shared by all the frames; they should
        -- all be using the same function.
        assert(old_setWidth == nil or old_setWidth == frame.SetWidth)
        
        old_setWidth = frame.SetWidth
        frame.SetWidth = setWidth
        
        frame.priority_shadow = frame:CreateTexture(nil, "BACKGROUND")
        frame.priority_shadow:SetTexture("Interface\\AddOns\\QuestHelper2\\MapOverlay\\Shadow.tga")
        frame.priority_shadow:SetBlendMode("ADD")
        frame.priority_shadow:SetAlpha(0.2)
        frame.priority_shadow:SetPoint("BOTTOMLEFT", 4, -1)
        frame.priority_shadow:SetPoint("TOPRIGHT", -4, 1)
        
        frame.flash_texture = frame:CreateTexture(nil, "BORDER")
        frame.flash_texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\Flash.tga")
        frame.flash_texture:SetBlendMode("ADD")
        frame.flash_texture:SetAllPoints()
        frame.flash_texture:Hide()
        
        frame.progress_text = frame:CreateFontString(nil, "OVERLAY", "NumberFontNormalLarge")
        frame.text = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLargeLeft")
        frame.text_overflow = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLeft")
        
        frame.buttons = {}
        
        frame:SetScript("OnMouseUp", objectiveFrameClicked)
        frame:SetScript("OnEnter", showTooltip)
        frame:SetScript("OnLeave", hideTooltip)
      end
      
      frame.objective = obj
      objective_frames[obj] = frame
      updateFrame(frame)
      -- We don't actually add the frames to the tracker yet; prioritizeFrames will do that.
    end
  end
  
  prioritizeFrames()
end

local function queueCollectRouteObjectives()
  shared.callOnce(collectRouteObjectives)
end

shared.onSetting("tracker_shows_routes", queueCollectRouteObjectives)

shared.onObjectiveModified(function (objective)
  -- TODO: Should create a frame if the modification was that it became obtainable;
  -- the route won't necessarly change in that case.
  
  -- Update what the frame is displaying, if it exists at all.
  local frame = objective_frames[objective]
  if frame then updateFrame(frame) end
  
  -- Call collectRouteObjectives because dependency changes to dependencies or obtainability may have
  -- changed what objectives should be displayed without actually altering the route.
  queueCollectRouteObjectives()
end)

shared.onObjectiveDisabled(function (objective)
  if InCombatLockdown() then
    -- The frame won't be removed due to the normal route updating due to us being in combat,
    -- and us delaying re-arranging everything due to the frames possibly being secure,
    -- so manually update the disabled objective.
    local frame = objective_frames[objective]
    if frame then updateFrame(frame) end
  end
end)

local function flashUpdate(frame, delta)
  local flash = frame.flash + delta
  if flash > 1 then
    if frame.repeat_flash then
      frame.repeat_flash = nil
      flash = 0
    else
      frame.flash = nil
      frame.flash_texture:Hide()
      frame:SetScript("OnUpdate", nil)
      return
    end
  end
  
  frame.flash = flash
  frame.flash_texture:SetAlpha(sqrt(flash-flash*flash))
  frame.flash_texture:SetTexCoord(1-flash*2,2-flash*2,0,1)
end

local function flashFrame(frame)
  if frame.flash then
    -- Frame is already flashing.
    frame.repeat_flash = true
  else
    frame.flash = 0
    frame:SetScript("OnUpdate", flashUpdate)
    frame.flash_texture:Show()
    flashUpdate(frame, 0)
  end
end

-- Flash objective frames when progress is made to their objective.
shared.onObjectiveProgress(function (objective)
  local frame = objective_frames[objective]
  if frame then flashFrame(frame) end
end)

shared.onRouteChanged(queueCollectRouteObjectives)
