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

function shared.normalizeLocation(area_id, level, x, y, ex, ey)
  if type(ex) == "number" then
    assert(type(ey) == "number")
    return area_id, level, x, y, ex, ey
  elseif type(ex) == "boolean" then
    if ex then
      return area_id, level, x, y, shared.getEntrance(area_id, level, x, y)
    else
      return area_id, level, x, y, nil, nil
    end
  else
    assert(ex == nil and ey == nil)
    return area_id, level, x, y, nil, nil
  end
end

local inside = nil
function shared.isInside()
  if inside == nil then
    -- This trick was borrowed from Astrolabe for determining if we are indoors.
    -- Warcraft's IsIndoors function only tells us if we aren't allowed to mount.
    
    if(GetCVar("minimapZoom") == GetCVar("minimapInsideZoom")) then
      local zoom = Minimap:GetZoom()
      Minimap:SetZoom(zoom + (zoom < 2 and 1 or -1))
      inside = GetCVar("minimapZoom")+0 ~= Minimap:GetZoom()
      Minimap:SetZoom(zoom)
    else
      inside = GetCVar("minimapZoom")+0 ~= Minimap:GetZoom()
    end
  end
  
  return inside
end

-- This whole thing probably doesn't work for instances. I need to fix that.

-- If we're indoors, this is where we entered.
local entrance_id, entrance_level, entrance_x, entrance_y = nil, nil, nil, nil

-- If we're indoors, but don't know how we got in here, we add callbacks to this
-- table and call then when the player moves outside and we discover the entrance.
local pending_location_callbacks = {}

-- Gets the players coordinates location, not accounting for entances.
-- If agressive is true, will try changing maps in order to get a position even if WorldMapFrame is visible.
-- Returns nil if we don't know.
local last_id, last_level, last_x, last_y = nil, nil, nil, nil
local function playerPosition(aggressive)
  local passive = true
  
  if not WorldMapFrame:IsVisible() then
    -- If the map isn't being shown, we can be as agressive as we want,
    -- nobody will notice us messing around with it.
    aggressive = true
    passive = false
  end
  
  local area_id, level, x, y
  
  if passive then
    area_id, level, x, y = GetCurrentMapAreaID(), GetCurrentMapDungeonLevel(), GetPlayerMapPosition("player")
    
    if x == 0 and y == 0 then
      x, y = nil, nil
    end
  end
  
  if not x and aggressive then
    shared.pushMap()
    
    SetMapToCurrentZone()
    area_id, level, x, y = GetCurrentMapAreaID(), GetCurrentMapDungeonLevel(), GetPlayerMapPosition("player")
    
    shared.popMap()
    
    if x == 0 and y == 0 then
      x, y = nil, nil
    else
      last_id, last_level, last_x, last_y = area_id, level, x, y
    end
  end
  
  if x then
    if last_id and (last_id ~= area_id or last_level ~= level) then
      -- Convert location to be relative to the last known area the player was in.
      x, y = shared.convertCoords(area_id, level, x, y, last_id, last_level)
      
      if x then
        last_x, last_y = x, y
        return last_id, last_level, x, y
      end
    else
      last_id, last_level, last_x, last_y = area_id, level, x, y
      return area_id, level, x, y
    end
  end
  
  return last_id, last_level, last_x, last_y
end

-- Get the players location right away.
-- Returns what onLocation would have passed to the callback if
-- the answer is available, otherwise it returns all nils.
function shared.whereAmI()
  local area_id, level, x, y = playerPosition(false)
  
  if area_id then
    assert(level, x, y)
    
    if shared.isInside() and not entrance_id then
      -- Don't know where the entrance is, make a guess.
      return area_id, level, x, y, shared.getEntrance(area_id, level, x, y)
    end
    
    if entrance_id then
      x, y = shared.convertCoords(area_id, level, x, y, entrance_id, entrance_level)
      
      if x then
        return entrance_id, entrance_level, x, y, entrance_x, entrance_y
      end
    else
      return area_id, level, x, y, nil, nil
    end
  end
  
  return nil, nil, nil, nil, nil, nil
end

-- This calls cb with 6 parameters when the players current location is known.
--   area_id, area_level, x, y, entrance_x, entrance_y
-- 
-- entrance_x and entrance_y will be nill if the player is outside.
-- The call will be delayed if the player is inside, and we don't know
-- where the entrance is, and possibly never if the player is teleported
-- without moving without through the entrance.
function shared.onLocation(cb, ...)
  cb = shared.callback(cb, ...)
  
  local area_id, level, x, y = playerPosition(true)
  
  if not area_id then return end
  
  if shared.isInside() and not entrance_id then
    -- The player is inside and we don't know where the entrance is.
    -- Store this callback and invoke it when we find out the answer.
    
    pending_location_callbacks[{area_id=area_id, level=level, x=x, y=y}] = cb
  else
    if entrance_id then
      assert(shared.isInside())
      x, y = shared.convertCoords(area_id, level, x, y, entrance_id, entrance_level)
      
      if x then
        cb(entrance_id, entrance_level, x, y, entrance_x, entrance_y)
      end
    else
      cb(area_id, level, x, y, nil, nil)
    end
  end
end

local building_entry_cb = {}

function shared.onBuildingEntry(cb)
  table.insert(building_entry_cb, shared.callback(cb))
end

-- This is a list of points defining the limits of a building.
-- We simply add all the points the player has been seen in, and
-- create a convex polygon around them; then remove any points
-- that don't make up that polygon's perimeter.
local building_perimeter = setmetatable({}, shared.meta.ConvexPoly)

local function buildingBoundryCheck()
  local area_id, level, x, y = playerPosition(false)
  
  if area_id then
    x, y = shared.convertCoords(area_id, level, x, y, entrance_id, entrance_level)
    if x then building_perimeter:addVertex(x, y) end
  end
  
  return 0.5
end

-- Set to true when it is safe to assume the player moved from outside to inside.
-- If false, the player may have left the world or started inside, in which case,
-- they're obviously not standing at the entrance for whatever they entered.
local safe_transition = false

shared.onEvent("MINIMAP_UPDATE_ZOOM", shared.callOnce, function ()
  inside = nil -- May have moved through an entrance; get rid of the cached value.
  
  if shared.isInside() then
    if safe_transition and not entrance_id then
      -- The player was outside, and now they're inside. They must have just walked through the entrance.
      
      entrance_id, entrance_level, entrance_x, entrance_y = playerPosition(true)
      
      if entrance_id then
        shared.onDelay(0.5, buildingBoundryCheck)
      end
    end
  else
    shared.cancelDelayed(buildingBoundryCheck)
    
    local area_id, level, ex, ey
    
    if safe_transition and entrance_id then
      area_id, level, ex, ey = playerPosition(true)
      -- Report where a building was entered and exited,
      -- as well as the perimeter we detected for it.
      
      if area_id then
        -- Convert everything we collected to be relative to where the player is now.
        local convertCoords = shared.convertCoords
        
        local entrance_x, entrance_y = convertCoords(entrance_id, entrance_level, entrance_x, entrance_y, area_id, level)
        
        if entrance_x then
          local good = true
          for i = 1,#building_perimeter,2 do
            building_perimeter[i], building_perimeter[i+1] = convertCoords(entrance_id, entrance_level, building_perimeter[i], building_perimeter[i+1], area_id, level)
            if not building_perimeter[i] then
              good = false
              break
            end
          end
          
          if good then
            shared.invokeCallbackList(building_entry_cb, area_id, level, entrance_x, entrance_y, ex, ey, building_perimeter)
          end
        end
      end
    end
    
    table.wipe(building_perimeter)
    
    -- The player is outside, so it's now safe to detect entrance transitions.
    safe_transition, entrance_id, entrance_level, entrance_x, entrance_y = true, nil, nil, nil, nil
    
    if next(pending_location_callbacks) then
      -- There were callbacks passed to onLocation that couldn't be
      -- invoked because we didn't know where the entrance was.
      -- Since we know now, we'll call them.
      
      if not area_id then
        area_id, level, ex, ey = playerPosition(true)
      end
      
      if area_id then
        for loc, cb in pairs(pending_location_callbacks) do
          local x, y = shared.convertCoords(loc.area_id, loc.level, loc.x, loc.y, area_id, level)
          if x then
            xpcall(shared.callback(cb, area_id, level, x, y, ex, ey), geterrorhandler())
          end
        end
      end
      
      table.wipe(pending_location_callbacks)
    end
  end
end)

local function playerTeleported()
  -- Called when the player hearths or teleports. Since that will
  -- mess up our entrance detection, we need to mark it as unsafe.
  entrance_id, last_id, last_level, last_x, last_y = nil, nil, nil, nil, nil
  safe_transition = false
  table.wipe(pending_location_callbacks)
  table.wipe(building_perimeter)
  shared.cancelDelayed(buildingBoundryCheck)
end

hooksecurefunc("PortGraveyard", playerTeleported)
hooksecurefunc("RepopMe", playerTeleported)

shared.onEvent("PLAYER_ENTERING_WORLD", playerTeleported)
shared.onEvent("CINEMATIC_START", playerTeleported)
shared.onEvent("CINEMATIC_STOP", playerTeleported)

shared.onEvent("UNIT_AURA", function (unit)
  if unit == "player" and UnitAura("player", "Honorless Target") then
    -- If you gained the Honorless Target debuff, it probably means you've been
    -- teleported.
    playerTeleported()
  end
end)
