-- 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

-- The tracker is in charge of displaying and managing frames.

-- Some functions I use a lot
local pairs, sqrt, cos, remove, GetTime = pairs, math.sqrt, math.cos, table.remove, GetTime

-- The tracker frame.
local tracker = CreateFrame("Frame", "QuestHelper2_Tracker", UIParent)

-- The minimum and maximum allowed dimensions of the tracker.
local min_tracker_width, min_tracker_height = 256, 80
local max_tracker_width, max_tracker_height = 384, 512

-- How the tracker is anchored to the screen.
local anchor_left, anchor_bottom = false, false

local target_w, target_h = min_tracker_width, min_tracker_height

-- If this is true, then the tracker won't fade out when the mouse isn't over it.
-- This is used when dragging and scrolling.
local tracker_in_use = false

-- Set to true when the player is in combat.
-- The tracker will remain hidden even when the cursor is over it, and not accept mouse input.
-- Don't edit this variable directly; invoke lockTracker or unlockTracker to change it.
local tracker_combat_locked = InCombatLockdown() and true or false

-- If true, the tracker will always ignore mouse input.
local tracker_locked = false

-- Set to true when the cursor has been over the tracker long enough for us to have decided
-- that the user wants to interact with the tracker.
-- If you change this variable, invoke updateAcceptingMouseInput().
local accepting_mouse_input = false

-- How visible is the tracker? This value is applied to everything in the alpha_mod set.
local alpha = 0.0

-- When accepting_mouse_input is set to false, all the frames in this set have their mouse input disabled,
-- or enabled if accepting_mouse_input is set to true.
local mouse_mod = {}

-- Similarly the uiobjects in this set will fade out when accepting_mouse_input is set to false, and
-- fade in when accepting_mouse_input is set to true.
local alpha_mod = {}

-- All the frames added to the tracker have objective_parent as their parent.
-- objective_parent is inside a scrollframe, so that they will be clipped if
-- they move too far outside of the tracker.
local objective_parent

-- How much the tracker is currently scrolled, first as a value between 0 and 1,
-- and another with that value converted to a y offset.
local scroll_pct = 0
local scroll_y_range = 0
local scrollbar

-- Maps frames to their priority.
local tracked_frames = {}

-- This is the frames in the order they appear in the tracker.
local tracked_frames_list = {}
local frame_list_dirty = false

-- This contains information about the motion of moving frames.
local frame_motion = {}

-- This contains the position of frames. Newly added frames may not have
-- a position, and if the frame is currently in motion, the data in frame_motion
-- takes precedence.
local frame_position = {}

-- This function is defined later.
local markFrameListDirty

-- Function defined later; sets the OnUpdate script for the tracker.
local updateTracker

local function hasScript(frame, script)
  return frame:HasScript(script) and frame:GetScript(script)
end

-- A test to see if a frame is clickable.
local function hasMouseHandler(frame)
  return hasScript(frame, "OnMouseDown") or hasScript(frame, "OnMouseUp") or hasScript(frame, "OnClick") or hasScript(frame, "OnEnter")
end

-- This adds a frame to the tracker, with some priority.
-- The frames with the lowest priority appear at the top.
-- You may re-add an already added frame in order to change that frames priority or
-- reposition it if its size was changed.
function shared.addFrameToTracker(frame, priority, ignore_locking)
  assert(type(priority) == "number")
  
  if tracker_combat_locked then
    -- I'll add your frame later, guy.
    shared.whenOutOfCombat(shared.addFrameToTracker, frame, priority)
    return
  end
  
  if not tracked_frames[frame] then
    table.insert(tracked_frames_list, frame)
    frame:ClearAllPoints()
    frame:SetParent(objective_parent)
    frame:Show()
    
    if not ignore_locking then
      if hasMouseHandler(frame) then
        frame:EnableMouse(accepting_mouse_input)
        mouse_mod[frame] = true
      else
        frame:EnableMouse(false)
      end
    end
  else
    assert(frame:GetParent() == objective_parent)
    assert(frame:IsShown())
  end
  
  tracked_frames[frame] = priority
  markFrameListDirty()
end

-- Removes a frame from the tracker.
function shared.removeFrameFromTracker(frame)
  if tracker_combat_locked then
    -- I'll remove your frame later, guy.
    shared.whenOutOfCombat(shared.removeFrameFromTracker, frame)
    return
  end
  
  if tracked_frames[frame] then
    frame:Hide()
    frame:ClearAllPoints()
    
    -- TODO: I have a strange problem with my frames becoming invisible if I don't change their parent.
    -- Maybe changes to hidden frames don't cause ScrollFrames to be updated?
    -- Strangely, the textures in FontString objects remain visible, even though the actual text
    -- and everything else is nowhere to be seen.
    -- Could be a bug; I should try to to write a test case someday.
    frame:SetParent(UIParent)
    
    if mouse_mod[frame] then
      mouse_mod[frame] = nil
      frame:EnableMouse(false)
    end
    
    tracked_frames[frame] = nil
    frame_position[frame] = nil
    frame_motion[frame] = nil
    
    for i, f in ipairs(tracked_frames_list) do
      if f == frame then
        table.remove(tracked_frames_list, i)
        markFrameListDirty()
        return
      end
    end
    
    -- Frame wasn't found in list?
    assert(false)
  end
end

local last_time
local function updateFrameMotion()
  local animating = false
  
  local now = GetTime()
  local delta = (now-last_time)*1.8
  last_time = now
  
  for frame, motion in pairs(frame_motion) do
    local x, y = motion[1], motion[2]
    local i, n = 3, #motion
    while i < n do
      local t, dx, dy = motion[i]+delta, motion[i+1], motion[i+2]
      if t >= 1 then
        -- This chunk of the motion is complete;
        -- Remove this chunk and all chunks before it,
        -- as they can no longer affect frame position.
        motion[1], motion[2], x, y = dx, dy, dx, dy
        
        for j=i+2,3,-1 do
          remove(motion,j)
        end
        
        i, n = 3, n-i
      else
        motion[i] = t
        
        -- Percentage of the distance across the circle.
        local pct = 0.5-cos(t*3.141592653589793)*0.5
        local ipct, displacement = 1-pct, sqrt(pct-pct*pct)
        x, y = x*ipct+dx*pct+(y-dy)*displacement, y*ipct+dy*pct+(x-dx)*displacement
        i = i+3
      end
      
      frame:ClearAllPoints()
      frame:SetPoint("TOPLEFT", tracker, "TOPLEFT", x, y+scroll_y_range*scroll_pct)
      
      if n == 2 then
        -- All motion chunks for this frame were removed;
        -- we're done moving it.
        frame_motion[frame] = nil
        
        local position = frame_position[frame]
        position.x, position.y = x, y
      else
        -- There's still movement left to do for this frame.
        animating = true
      end
    end
  end
  
  if animating then
    -- Ask to be called again next frame.
    return 0
  end
end

local function setFrameDestination(frame, x, y)
  local motion = frame_motion[frame]
  
  if not motion then
    -- The frame isn't currently being moved.
    
    local position = frame_position[frame]
    
    if not position then
      -- The frame doesn't have a position yet,
      -- so start the frame where we were
      -- told to move it to.
      frame_position[frame] = { x=x, y=y }
      frame:ClearAllPoints()
      frame:SetPoint("TOPLEFT", tracker, "TOPLEFT", x, y+scroll_y_range*scroll_pct)
      
      return
    end
    
    if position.x == x and
       position.y == y then
      -- Asked to move the frame to where it already is,
      -- so don't do anything. We'll update it's position in case it's affected by
      -- scrolling.
      frame:SetPoint("TOPLEFT", tracker, "TOPLEFT", x, y+scroll_y_range*scroll_pct)
      
      return
    end
    
    if not next(frame_motion) then
      -- There is currently nothing being animated;
      -- so lets arrange for updateFrameMotion to get called.
      last_time = GetTime()
      shared.onDelay(0, updateFrameMotion)
    end
    
    frame_motion[frame] = { position.x, position.y, 0, x, y }
    return
  end
  
  -- The frame is already in motion, so let's see if
  -- we need to change its travel plans...
  
  local len = #motion
  
  if motion[len-2] == 0 then
    -- The previous animation chunk hasn't started
    -- animating yet, so we'll simply replace it.
    motion[len-1], motion[len] = x, y
    return
  end
  
  -- The frame is already moving to the specified location,
  -- so don't do anything.
  if motion[len-1] == x and motion[len] == y then
    return
  end
  
  -- Add a new destination to the frame.
  motion[len+1], motion[len+2], motion[len+3] = 0, x, y
end

-- Compares frames by priority, for sorting tracked_frames_list
local function frameCmp(a, b)
  return tracked_frames[a] < tracked_frames[b]
end

-- Recalculates the positions of all the frames, and possibly
-- alters the size of the tracker.
local frame_y = {}
local function recomputeFrameList()
  if tracker_combat_locked then
    -- We'll position the frames when we leave combat.
    shared.whenOutOfCombat(recomputeFrameList)
    return
  end
  
  if frame_list_dirty then
    table.sort(tracked_frames_list, frameCmp)
    
    target_w = min_tracker_width
    
    local objective_width = tracker:GetWidth()-8
    
    for i, frame in ipairs(tracked_frames_list) do
      if frame.prefered_width then
        target_w = math.max(target_w, frame.prefered_width)
      end
    end
    
    -- Remove 8 for the borders, and an extra 16 in case we need space for the scrollbar.
    target_w = math.min(target_w, max_tracker_width-8-16)
    
    table.wipe(frame_y)
    
    local y = -28
    for i, frame in ipairs(tracked_frames_list) do
      -- objective_frame replaces the SetWidth method so that it automatically sets the frame to the correct height.
      frame:SetWidth(target_w)
      
      local objective_height = frame:GetHeight()
      
      frame_y[frame] = y
      
      y = y - objective_height
    end
    
    -- Add the border to the with.
    target_w = target_w+8
    target_h = math.max(-y+4, min_tracker_height)
    
    if target_h > max_tracker_height then
      
      -- There are more objectives than we can display, make room for the scroll bar.
      
      local available_space = max_tracker_height-32
      local required_space = target_h-32
      
      scroll_y_range = required_space-available_space
      
      local height = math.max(32, available_space/required_space*available_space)
      
      scrollbar:SetPoint("TOP", tracker, 0, scroll_pct*(height-available_space)-28)
      scrollbar:SetHeight(height)
      scrollbar:Show()
      
      -- Make room for the scroll bar.
      target_w = target_w + 16
      
      target_h = max_tracker_height
    else
      scroll_y_range = 0
      -- TODO: Stop scrolling.
      scrollbar:Hide()
    end
    
    -- Delay moving the frames until now, because we weren't confident about the
    -- scroll offset.
    for i, frame in ipairs(tracked_frames_list) do
      setFrameDestination(frame, 4, frame_y[frame])
    end
    
    updateTracker()
    
    frame_list_dirty = false
  end
end

markFrameListDirty = function()
  if frame_list_dirty == false then
    frame_list_dirty = true
    shared.onDelay(0, recomputeFrameList)
  end
end

-- Well then, let's make the tracker frame pretty.

local function hideTooltip()
  GameTooltip:Hide()
end

tracker:SetPoint("TOPRIGHT", MinimapCluster, "BOTTOMRIGHT", -36, 0)
tracker:SetMovable(true)
tracker:SetResizable(true)
tracker:SetClampedToScreen(true)
tracker:SetUserPlaced(true)
tracker:SetWidth(min_tracker_width)
tracker:SetHeight(min_tracker_height)
tracker:SetFrameStrata("BACKGROUND")
tracker:Show()

local tracker_size_preview = tracker:CreateTexture(nil, "OVERLAY")
tracker_size_preview:SetPoint("TOPRIGHT")
tracker_size_preview:SetTexture(1,0,1,.4)

local drag_handle = CreateFrame("Frame", nil, tracker)
drag_handle:SetWidth(48)
drag_handle:SetHeight(20)
drag_handle:SetMovable(true)
drag_handle:RegisterForDrag("LeftButton")

mouse_mod[drag_handle] = true

local function reanchorTracker()
  local anchor_frame = UIParent
  
  local tracker_scale, anchor_scale = tracker:GetEffectiveScale(), anchor_frame:GetEffectiveScale()
  
  local anchor
  
  if anchor_left then
    if anchor_bottom then
      anchor = "BOTTOMLEFT"
    else
      anchor = "TOPLEFT"
    end
  else
    if anchor_bottom then
      anchor = "BOTTOMRIGHT"
    else
      anchor = "TOPRIGHT"
    end
  end
  
  local x, y
  
  if anchor_left then
    x = (tracker:GetLeft()*tracker_scale-anchor_frame:GetLeft()*anchor_scale)/tracker_scale
  else
    x = (tracker:GetRight()*tracker_scale-anchor_frame:GetRight()*anchor_scale)/tracker_scale
  end
  
  if anchor_bottom then
    y = (tracker:GetBottom()*tracker_scale-anchor_frame:GetBottom()*anchor_scale)/tracker_scale
  else
    y = (tracker:GetTop()*tracker_scale-anchor_frame:GetTop()*anchor_scale)/tracker_scale
  end
  
  tracker_size_preview:ClearAllPoints()
  tracker_size_preview:SetPoint(anchor)
  
  tracker:ClearAllPoints()
  tracker:SetPoint(anchor, anchor_frame, anchor, x, y)
end

drag_handle:SetScript("OnEnter", function ()
  GameTooltip:SetOwner(drag_handle, "ANCHOR_CURSOR")
  GameTooltip:AddLine(L.TRACKER_DRAG_HANDLE_TOOLTIP)
  GameTooltip:Show()
end)

drag_handle:SetScript("OnLeave", hideTooltip)

drag_handle:SetScript("OnDragStart", function ()
  hideTooltip()
  tracker_in_use = true
  tracker:StartMoving()
end)

drag_handle:SetScript("OnDragStop", function ()
  tracker_in_use = false
  tracker:StopMovingOrSizing()
  reanchorTracker()
end)

scrollbar = CreateFrame("Frame", nil, tracker)
scrollbar:SetWidth(16)
scrollbar:SetHeight(128)
scrollbar:SetPoint("RIGHT", tracker, -4, 0)
-- Alter frame level to ensure this appears over objective_parent, the scroll frame its contained within, and the objectives themselves.
scrollbar:SetFrameLevel(tracker:GetFrameLevel()+4)
scrollbar:Hide()

function scrollbar:UpdateScroll()
  local offset = (self.y_base - select(2, GetCursorPosition()))/self:GetEffectiveScale()
  
  local gap = tracker:GetHeight()-32-self:GetHeight()
  
  local pct = self.pct_base + offset/gap
  
  if pct < 0 or pct ~= pct then
    pct = 0
  elseif pct > 1 then
    pct = 1
  end
  
  self:SetPoint("TOP", tracker, 0, -28-pct*gap)
  
  scroll_pct = pct
  
  for i, frame in ipairs(tracked_frames_list) do
    setFrameDestination(frame, 4, frame_y[frame])
  end
end

function scrollbar:StartDrag()
  self.pct_base = scroll_pct
  self.y_base = select(2, GetCursorPosition())
  self:SetScript("OnUpdate", self.UpdateScroll)
end

function scrollbar:StopDrag()
  self:SetScript("OnUpdate", nil)
end

scrollbar:SetScript("OnMouseDown", scrollbar.StartDrag)
scrollbar:SetScript("OnMouseUp", scrollbar.StopDrag)
mouse_mod[scrollbar] = true
alpha_mod[scrollbar] = true

local hide_button = CreateFrame("Button", nil, tracker)
hide_button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
hide_button:SetNormalTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\HideButtonUp.tga")
hide_button:SetPushedTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\HideButtonDown.tga")
hide_button:SetHighlightTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\ButtonHighlight.tga", "ADD")
hide_button:SetWidth(20)
hide_button:SetHeight(20)

hide_button:SetScript("OnEnter", function ()
  GameTooltip:SetOwner(hide_button, "ANCHOR_CURSOR")
  GameTooltip:AddLine(L.TRACKER_MINIMIZE_TOOLTIP)
  GameTooltip:Show()
end)

hide_button:SetScript("OnLeave", hideTooltip)

local status_button = CreateFrame("Button", nil, tracker)
status_button:SetNormalTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\RouteButtonUp.tga")
status_button:SetPushedTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\RouteButtonDown.tga")
status_button:SetHighlightTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\ButtonHighlight.tga", "ADD")
status_button:SetWidth(20)
status_button:SetHeight(20)
mouse_mod[status_button] = true
alpha_mod[status_button] = true

status_button:SetScript("OnEnter", function ()
  GameTooltip:SetOwner(status_button, "ANCHOR_CURSOR")
  GameTooltip:AddLine(L.TRACKER_SHOW_ROUTE_TOOLTIP)
  GameTooltip:Show()
end)

status_button:SetScript("OnLeave", hideTooltip)

local autosort_button = CreateFrame("Button", nil, tracker)
autosort_button:SetHighlightTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\SortButtonHighlight.tga", "ADD")
autosort_button:SetWidth(136)
autosort_button:SetHeight(20)
autosort_button:SetNormalFontObject("GameFontNormalLarge")
autosort_button:SetPushedTextOffset(2, -2)
mouse_mod[autosort_button] = true
alpha_mod[autosort_button] = true

autosort_button:SetScript("OnEnter", function ()
  GameTooltip:SetOwner(autosort_button, "ANCHOR_CURSOR")
  GameTooltip:AddLine(L.TRACKER_AUTO_ROUTING_TOOLTIP)
  GameTooltip:Show()
end)

autosort_button:SetScript("OnLeave", hideTooltip)

-----
-- Textures that aren't imporant; I can just create them and then forget they exist.
-----

-- Header
local header_texture = tracker:CreateTexture(nil, "ARTWORK")
header_texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\TrackerHeader.tga")
header_texture:SetWidth(256)
header_texture:SetHeight(32)
alpha_mod[header_texture] = true

-- Top Left Corner
local corner_tl_texture = tracker:CreateTexture(nil, "BORDER")
corner_tl_texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\TrackerTopLeftCorner.tga")
corner_tl_texture:SetWidth(32)
corner_tl_texture:SetHeight(32)
corner_tl_texture:SetPoint("TOPLEFT", -4, -20)
alpha_mod[corner_tl_texture] = true

-- Top Right Corner
local corner_tr_texture = tracker:CreateTexture(nil, "BORDER")
corner_tr_texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\TrackerTopRightCorner.tga")
corner_tr_texture:SetWidth(32)
corner_tr_texture:SetHeight(32)
alpha_mod[corner_tr_texture] = true

-- Bottom Right Corner
local corner_br_texture = tracker:CreateTexture(nil, "BORDER")
corner_br_texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\TrackerBottomRightCorner.tga")
corner_br_texture:SetWidth(32)
corner_br_texture:SetHeight(32)
alpha_mod[corner_br_texture] = true
  
  -- Bottom Left Corner
local corner_bl_texture = tracker:CreateTexture(nil, "BORDER")
corner_bl_texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\TrackerBottomLeftCorner.tga")
corner_bl_texture:SetWidth(32)
corner_bl_texture:SetHeight(32)
alpha_mod[corner_bl_texture] = true

-- Left Edge
local edge_l_texture = tracker:CreateTexture(nil, "BORDER")
edge_l_texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\TrackerLeftEdge.tga", true)
edge_l_texture:SetWidth(32)
edge_l_texture:SetHeight(32)
edge_l_texture:SetVertTile(true)
alpha_mod[edge_l_texture] = true

-- Right Edge
local edge_r_texture = tracker:CreateTexture(nil, "BORDER")
edge_r_texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\TrackerRightEdge.tga", true)
edge_r_texture:SetWidth(32)
edge_r_texture:SetHeight(32)
edge_r_texture:SetVertTile(true)
alpha_mod[edge_r_texture] = true

local function mirrorTexture(texture, mirrored)
  if mirrored then
    texture:SetTexCoord(1,0,0,1)
  else
    texture:SetTexCoord(0,1,0,1)
  end
end

local function redrawTracker(mirrored)
  mirrorTexture(header_texture, mirrored)
  mirrorTexture(corner_tl_texture, mirrored)
  mirrorTexture(corner_tr_texture, mirrored)
  mirrorTexture(corner_bl_texture, mirrored)
  mirrorTexture(corner_br_texture, mirrored)
  mirrorTexture(edge_l_texture, mirrored)
  mirrorTexture(edge_r_texture, mirrored)
  
  drag_handle:ClearAllPoints()
  autosort_button:ClearAllPoints()
  status_button:ClearAllPoints()
  hide_button:ClearAllPoints()
  
  header_texture:ClearAllPoints()
  corner_tl_texture:ClearAllPoints()
  corner_tr_texture:ClearAllPoints()
  corner_bl_texture:ClearAllPoints()
  corner_br_texture:ClearAllPoints()
  edge_l_texture:ClearAllPoints()
  edge_r_texture:ClearAllPoints()
  
  if mirrored then
    drag_handle:SetPoint("TOPLEFT", 192, -4)
    autosort_button:SetPoint("TOPLEFT", 52, -4)
    status_button:SetPoint("TOPLEFT", 28, -4)
    hide_button:SetPoint("TOPLEFT", 4, -4)
    
    header_texture:SetPoint("TOPLEFT", -4, 4)
    corner_tl_texture:SetPoint("TOPRIGHT", 4, -20)
    corner_tr_texture:SetPoint("TOPLEFT", -4, -20)
    corner_br_texture:SetPoint("BOTTOMLEFT", -4, -4)
    corner_bl_texture:SetPoint("BOTTOMRIGHT", 4, -4)
    edge_l_texture:SetPoint("TOPRIGHT", 4, -52)
    edge_l_texture:SetPoint("BOTTOMLEFT", tracker, "BOTTOMRIGHT", -28, 28)
    edge_r_texture:SetPoint("TOPLEFT", -4, -52)
    edge_r_texture:SetPoint("BOTTOMRIGHT", tracker, "BOTTOMLEFT", 28, 28)
  else
    drag_handle:SetPoint("TOPRIGHT", -192, -4)
    autosort_button:SetPoint("TOPRIGHT", -52, -4)
    status_button:SetPoint("TOPRIGHT", -28, -4)
    hide_button:SetPoint("TOPRIGHT", -4, -4)
    
    header_texture:SetPoint("TOPRIGHT", 4, 4)
    corner_tl_texture:SetPoint("TOPLEFT", -4, -20)
    corner_tr_texture:SetPoint("TOPRIGHT", 4, -20)
    corner_br_texture:SetPoint("BOTTOMRIGHT", 4, -4)
    corner_bl_texture:SetPoint("BOTTOMLEFT", -4, -4)
    edge_l_texture:SetPoint("TOPLEFT", -4, -52)
    edge_l_texture:SetPoint("BOTTOMRIGHT", tracker, "BOTTOMLEFT", 28, 28)
    edge_r_texture:SetPoint("TOPRIGHT", 4, -52)
    edge_r_texture:SetPoint("BOTTOMLEFT", tracker, "BOTTOMRIGHT", -28, 28)
  end
end

redrawTracker(anchor_left)

;(function ()
  local frame = CreateFrame("ScrollFrame", nil, tracker)
  frame:SetPoint("BOTTOMLEFT", tracker, "BOTTOMLEFT", -100, 4)
  frame:SetPoint("TOPRIGHT", tracker, "TOPRIGHT", 100, -28)
  frame:Show()
  
  -- The top left corner of objective_parent
  objective_parent = CreateFrame("Frame", nil, frame)
  objective_parent:SetWidth(1)
  objective_parent:SetHeight(1)
  objective_parent:Show()
  frame:SetScrollChild(objective_parent)
  
  -- Top Edge
  local texture = tracker:CreateTexture(nil, "BORDER")
  texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\TrackerTopEdge.tga", true)
  texture:SetWidth(32)
  texture:SetHeight(32)
  texture:SetPoint("TOPLEFT", 28, -20)
  texture:SetPoint("BOTTOMRIGHT", tracker, "TOPRIGHT", -28, -52)
  texture:SetHorizTile(true)
  alpha_mod[texture] = true
  
  -- Bottom Edge
  texture = tracker:CreateTexture(nil, "BORDER")
  texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\TrackerBottomEdge.tga", true)
  texture:SetWidth(32)
  texture:SetHeight(32)
  texture:SetPoint("BOTTOMLEFT", 28, -4)
  texture:SetPoint("TOPRIGHT", tracker, "BOTTOMRIGHT", -28, 28)
  texture:SetHorizTile(true)
  alpha_mod[texture] = true
  
  -- Background
  texture = tracker:CreateTexture(nil, "BACKGROUND")
  texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\TrackerBody.tga", true)
  texture:SetWidth(256)
  texture:SetHeight(256)
  texture:SetPoint("BOTTOMLEFT", 4, 4)
  texture:SetPoint("TOPRIGHT", -4, -28)
  texture:SetVertTile(true)
  texture:SetHorizTile(true)
  alpha_mod[texture] = true
  
  -- Scrollbar
  texture = scrollbar:CreateTexture(nil, "ARTWORK")
  texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\ScrollBarTop.tga")
  texture:SetWidth(16)
  texture:SetHeight(16)
  texture:SetPoint("TOPLEFT")
  
  texture = scrollbar:CreateTexture(nil, "BACKGROUND")
  texture:SetTexture("Interface\\AddOns\\QuestHelper2\\MapOverlay\\Shadow.tga")
  texture:SetVertexColor(0,0,0,.25)
  texture:SetWidth(16)
  texture:SetHeight(16)
  texture:SetPoint("TOPRIGHT", tracker, "TOPRIGHT", -4, -28)
  texture:SetPoint("BOTTOMLEFT", tracker, "BOTTOMRIGHT", -20, 4)
  
  texture = scrollbar:CreateTexture(nil, "BORDER")
  texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\ScrollBarBottom.tga")
  texture:SetWidth(16)
  texture:SetHeight(16)
  texture:SetPoint("BOTTOMLEFT")
  
  texture = scrollbar:CreateTexture(nil, "ARTWORK")
  texture:SetTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\ScrollBarBody.tga", true)
  texture:SetWidth(16)
  texture:SetHeight(32)
  texture:SetPoint("TOPRIGHT", scrollbar, "TOPRIGHT", 0, -8)
  texture:SetPoint("BOTTOMLEFT", scrollbar, "BOTTOMLEFT", 0, 8)
  texture:SetVertTile(true)
end)()

local tracker_menu_settings = {
  "tracker_shown",
  "tracker_hide_combat",
  "tracker_locked",
  "tracker_show_objective_tooltips",
  "digsite_create_objectives"
}

hide_button:SetScript("OnClick", function (self, button)
  if button == "RightButton" then
    shared.showMainMenu(self, 16, 0, tracker_menu_settings, "tracker")
  else
    shared.settings.tracker_minimized = not shared.settings.tracker_minimized
  end
end)

status_button:SetScript("OnClick", function ()
  shared.settings.tracker_shows_routes = not shared.settings.tracker_shows_routes
end)

shared.onSetting("locale", function ()
  autosort_button:SetText(L.AUTO_SORT)
end)

shared.onSetting("routing_enabled", function (enabled)
  if enabled then
    autosort_button:SetNormalTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\SortButtonUp.tga")
    autosort_button:SetPushedTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\SortButtonDown.tga")
  else
    autosort_button:SetNormalTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\SortButtonDisabledUp.tga")
    autosort_button:SetPushedTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\SortButtonDisabledDown.tga")
  end
end)

autosort_button:SetScript("OnClick", function ()
  shared.settings.routing_enabled = not shared.settings.routing_enabled
end)

shared.onSetting("tracker_anchor_left", function (left)
  anchor_left = left
  redrawTracker(left)
  reanchorTracker()
end)

shared.onSetting("tracker_anchor_bottom", function (bottom)
  anchor_bottom = bottom
  reanchorTracker()
end)

shared.onSetting("tracker_minimized", function (minimized)
  if minimized then
    hide_button:SetNormalTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\ShowButtonUp.tga")
    hide_button:SetPushedTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\ShowButtonDown.tga")
    hide_button:SetParent(UIParent)
    hide_button:SetScale(tracker:GetScale())
    shared.whenOutOfCombat(tracker.Hide, tracker)
  else
    hide_button:SetNormalTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\HideButtonUp.tga")
    hide_button:SetPushedTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\HideButtonDown.tga")
    if shared.settings.tracker_shown then shared.whenOutOfCombat(tracker.Show, tracker) end
    hide_button:SetScale(1.0)
    hide_button:SetParent(tracker)
  end
end)

shared.onSetting("tracker_shows_routes", function (routes)
  if routes then
    status_button:SetNormalTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\ObjectiveButtonUp.tga")
    status_button:SetPushedTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\ObjectiveButtonDown.tga")
  else
    status_button:SetNormalTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\RouteButtonUp.tga")
    status_button:SetPushedTexture("Interface\\AddOns\\QuestHelper2\\Tracker\\RouteButtonDown.tga")
  end
end)

shared.onSetting("tracker_shown", function (shown)
  if shown then
    WatchFrame:Hide()
    hide_button:Show()
    if not shared.settings.tracker_minimized then shared.whenOutOfCombat(tracker.Show, tracker) end
  else
    WatchFrame:Show()
    shared.whenOutOfCombat(tracker.Hide, tracker)
    hide_button:Hide()
  end
end)

-- This function updates the alpha value of everything in alpha_mod;
-- call it when you change the value of alpha.
local function updateAlpha()
  for ui_object in pairs(alpha_mod) do
    ui_object:SetAlpha(alpha)
  end
end

updateAlpha()

local function trackerUpdater(tracker, delta)
  local done = true
  
  local w, h = tracker:GetWidth(), tracker:GetHeight()
  
  if w ~= target_w or h ~= target_h then
    local dx, dy = target_w-w, target_h-h
    local dist = math.sqrt(dx*dx+dy*dy)
    
    if dist > 0.1 then
      local scale = 350*delta/dist
      w = (dx > 0 and math.min or math.max)(target_w, w + dx * scale)
      h = (dy > 0 and math.min or math.max)(target_h, h + dy * scale)
      done = false
    else
      w = target_w
      h = target_h
    end
    
    reanchorTracker()
    tracker:SetWidth(w)
    tracker:SetHeight(h)
  end
  
  if accepting_mouse_input and alpha < 1 then
    alpha = alpha + 2.5 * delta
    if alpha >= 1 then alpha = 1 else done = false end
    updateAlpha()
  end
  
  if not accepting_mouse_input then
    if scroll_pct > 0 then
      -- Automatically scroll back to the top of the objective list.
      scroll_pct = scroll_pct - delta/scroll_y_range*200
      
      if scroll_pct > 0 then
        done = false
      else
        scroll_pct = 0
      end
      
      scrollbar:SetPoint("TOP", tracker, 0, scroll_pct*(32-h+scrollbar:GetHeight())-28)
      
      for i, frame in ipairs(tracked_frames_list) do
        setFrameDestination(frame, 4, frame_y[frame])
      end
    end
    
    if alpha > 0 then
      alpha = alpha - 1.5 * delta
      if alpha <= 0 then alpha = 0 else done = false end
      updateAlpha()
    end
  end
  
  if done then
    tracker:SetScript("OnUpdate", nil)
  end
end

updateTracker = function ()
  tracker:SetScript("OnUpdate", trackerUpdater)
end

-- This function updates the whether the mouse is enabled for everything in mouse_mod;
-- call it when you change the value of accepting_mouse_input.
local function updateAcceptingMouseInput()
  for frame in pairs(mouse_mod) do
    frame:EnableMouse(accepting_mouse_input)
  end
  
  if accepting_mouse_input then
    if alpha < 1 then
      updateTracker()
    end
  else
    if alpha > 0 then
      updateTracker()
    end
    
    tracker_in_use = false
    tracker:StopMovingOrSizing()
    scrollbar:StopDrag()
    reanchorTracker()
  end
end

updateAcceptingMouseInput()

-- This checks the tracker every so often and decides whether to enable or disable mouse input to the tracker.
local ticks = 0
local function recheckMouseOver()
  assert(not tracker_locked)
  
  if tracker_combat_locked then
    ticks = 0
  else
    local is_over = tracker_in_use or tracker:IsMouseOver()
    
    if is_over and ticks < 4 then
      ticks = ticks + 1
      if ticks == 4 then
        accepting_mouse_input = true
        updateAcceptingMouseInput()
      end
    elseif not is_over and ticks > 0 then
      ticks = ticks - 1
      if ticks == 0 then
        accepting_mouse_input = false
        updateAcceptingMouseInput()
      end
    end
  end
  
  -- Call us again.
  return 0.2
end

local function lockTracker()
  tracker_combat_locked = true
  accepting_mouse_input = false
  updateAcceptingMouseInput()
  
  alpha = 0
  updateAlpha()
  
  tracker:SetScript("OnUpdate", nil)
  tracker:SetWidth(target_w)
  tracker:SetHeight(target_h)
  
  -- Teleport any frames that are in motion to where they're supposed to be.
  shared.cancelDelayed(updateFrameMotion)
  
  for frame, motion in pairs(frame_motion) do
    local motion_len = #motion
    local x, y = motion[motion_len-1], motion[motion_len]
    
    local position = frame_position[frame]
    position.x, position.y = x, y
    
    scroll_pct, scroll_y_range = 0, 0
    
    frame:ClearAllPoints()
    frame:SetPoint("TOPLEFT", tracker, "TOPLEFT", x, y)
    frame_motion[frame] = nil
  end
  
  -- Stop checking if the mouse is over the tracker.
  if tracker:IsVisible() and not tracker_locked then
    shared.cancelDelayed(recheckMouseOver)
  end
end

local function unlockTracker()
  tracker_combat_locked = false
  updateTracker()
  
  if tracker:IsVisible() and not tracker_locked then
    shared.onDelay(0.2, recheckMouseOver)
  end
end

tracker:SetScript("OnShow", function ()
  if not tracker_combat_locked and not tracker_locked then
    shared.onDelay(0.2, recheckMouseOver)
  end
end)

tracker:SetScript("OnHide", function ()
  if not tracker_combat_locked and not tracker_locked then
    shared.cancelDelayed(recheckMouseOver)
  end
end)

local function hideSizePreview()
  tracker_size_preview:Hide()
end

local function showSizePreview()
  tracker_size_preview:SetWidth(max_tracker_width)
  tracker_size_preview:SetHeight(max_tracker_height)
  tracker_size_preview:Show()
  shared.cancelDelayed(hideSizePreview)
  shared.onDelay(1, hideSizePreview)
end

shared.onSetting("tracker_max_height", function (height)
  max_tracker_height = height
  markFrameListDirty()
  showSizePreview()
end)

shared.onSetting("tracker_max_width", function (width)
  max_tracker_width = width
  markFrameListDirty()
  showSizePreview()
end)

shared.onSetting("tracker_scale", function (scale)
  tracker:SetScale(scale)
  showSizePreview()
end)

shared.onSetting("tracker_locked", function (locked)
  if tracker_locked ~= locked then
    tracker_locked = locked
    
    if not tracker_combat_locked then
      if locked then
        accepting_mouse_input = false
        updateAcceptingMouseInput()
        shared.cancelDelayed(recheckMouseOver)
      else
        shared.onDelay(0.2, recheckMouseOver)
      end
    end
  end
end)

shared.onCombatEnter(function ()
  if shared.settings.tracker_hide_combat and shared.settings.tracker_shown then
    hide_button:Hide()
    tracker:Hide()
  end
  
  lockTracker()
end)

shared.onCombatLeave(function ()
  if shared.settings.tracker_hide_combat and shared.settings.tracker_shown then
    hide_button:Show()
    
    if not shared.settings.tracker_minimized then
      tracker:Show()
    end
  end
  
  unlockTracker()
end)
