-- 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 functions = {}

shared.meta.Building = {
  __index = functions,
  name = "Building"
}

local entrance_tree = {}
local building_tree = {}

local buildings = shared.static.buildings

for map, list in pairs(shared.static.buildings) do
  local btree, etree = shared.createQuadTree(), shared.createQuadTree()
  building_tree[map] = btree
  entrance_tree[map] = etree
  
  for i, building in ipairs(list) do
    -- If this isn't set, then the object in the tree points references the save data directly.
    building.static = true
    
    -- If building.static is true, then building may also have a building.saved pointing to a building in the saved data;
    -- in which case this building is actually a hybrid between the static data and the saved data.
    
    btree:addObject(setmetatable(building, shared.meta.Building), setmetatable(building.volume, static.meta.ConvexPoly):boundingBox())
    for i, entrance in ipairs(building.entrances) do
      entrance.building = building
      etree:addObject(entrance, entrance.x, entrance.y)
    end
  end
end

-- Returns a building near the specified location, or nil if we don't know of a building near there.
function shared.getBuilding(area_id, level, x, y)
  local x, y, map = shared.mapPosition(area_id, level, x, y)
  
  local tree = building_tree[map]
  
  if tree then
    local nearest, nearest_d2
    
    for building in tree:iterateObjects(x-8,y-8,x+8,y+8) do
      local d2 = building.volume:distance(x,y)
      
      if not nearest or d2 < nearest_d2 then
        nearest, nearest_d2 = building, d2
      end
    end
    
    if nearest and nearest_d2 < 64 then
      return nearest
    end
  end
end

local function getEntrance(x, y, map)
  local tree = entrance_tree[map]
  
  if tree then
    local nearest, nearest_d2
    
    for entrance in tree:iterateObjects(x-8,y-8,x+8,y+8) do
      local dx, dy = x-entrance.x, y-entrance.y
      local d2 = dx*dx+dy*dy
      if not nearest or d2 < nearest_d2 then
        nearest, nearest_d2 = entrance, d2
      end
    end
    
    if nearest and nearest_d2 < 64 then
      return nearest, nearest_d2
    end
  end
end

shared.onLoad(function ()
  local saved_buildings = shared.saved.buildings
  
  local function createBuilding(map)
    local building = setmetatable({
      volume = shared.createConvexPoly(),
      entrances = {}
    }, shared.meta.Building)
    
    if map then
      -- Add the created building to the saved data for the specified map.
      local list = saved_buildings[map]
      
      if not list then
        list = {}
        saved_buildings[map] = list
      end
      
      table.insert(list, building)
    else
      -- The building isn't being saved, so call it static.
      building.static = true
    end
    
    return building
  end

  local function getBuildingSaved(building)
    if building.static then
      return building.saved
    else
      return building
    end
  end

  local function addEntrance(self, x, y, weight, etree)
    weight = weight or 1
    
    self.volume:addVertex(x,y)
    
    local nearest, nearest_d2
    
    for i, entrance in ipairs(self.entrances) do
      local dx, dy = x-entrance.x, y-entrance.y
      local d2 = dx*dx+dy*dy
      if not nearest or d2 < nearest_d2 then
        nearest, nearest_d2 = i, d2
      end
    end
    
    local entrance
    
    if nearest and nearest_d2 < 64 then
      -- Merge with a nearby entrance.
      entrance = self.entrances[nearest]
      local w = entrance.w
      local wsum = w+weight
      entrance.x, entrance.y, entrance.w = (entrance.x*w+x*weight)/wsum, (entrance.y*w+y*weight)/wsum, wsum
    else
      -- Create a new entrance.
      entrance = {x=x,y=y,w=weight,building=self}
      table.insert(self.entrances, entrance)
    end
    
    if etree then
      etree:addObject(entrance, entrance.x, entrance.y)
    end
  end
  
  -- Merges two buildings, and returns the building
  -- that survived the merge process.
  local function merge(b1, b2, btree, etree, map)
    local s1, s2 = getBuildingSaved(b1), getBuildingSaved(b2)
    
    -- If one of the buildings is static, make sure the static building is the one we keep.
    if b1 == s1 and b2 ~= s2 then
      b1, b2, s1, s2 = b2, b1, s2, s1
    end
    
    btree:removeObject(b2)
    
    for i, entrance in ipairs(b2.entrances) do
      etree:removeObject(entrance)
    end
    
    if s1 and s2 and s1 ~= s2 then
      -- Merge the saved data.
      
      for i = 1, s2.volume:vertices() do
        s1.volume:addVertex(s2.volume:vertex(i))
      end
      
      for i, entrance in ipairs(s2.entrances) do
        addEntrance(s1, entrance.x, entrance.y, entrance.w, s1 == b1 and etree or nil)
      end
      
      -- Remove the save we merged with.
      local found = false
      for i, building in ipairs(saved_buildings[map]) do
        if building == s2 then
          table.remove(saved_buildings[map], i)
          found = true
          break
        end
      end
      
      assert(found)
    end
    
    if b1 ~= s1 then
      for i = 1,b2.volume:vertices() do
        b1.volume:addVertex(b2.volume.vertex(i))
      end
      
      for i, entrance in ipairs(b2.entrances) do
        addEntrance(b1, entrance.x, entrance.y, entrance.w, etree)
      end
    end
    
    btree:addObject(b1, b1.volume:boundingBox())
    
    return b1
  end
  
  shared.onBuildingEntry(function (area_id, level, x1, y1, x2, y2, perimeter)
    local map
    x1, y1, map = shared.mapPosition(area_id, level, x1, y1)
    x2, y2 = shared.mapPosition(area_id, level, x2, y2)
    
    local valid_map = map >= 0
    
    local btree, etree = building_tree[map], entrance_tree[map]
    
    if not btree then
      assert(not etree)
      btree, etree = shared.createQuadTree(), shared.createQuadTree()
      building_tree[map], entrance_tree[map] = btree, etree
    end
    
    local e1, e2 = getEntrance(x1, y1, map), getEntrance(x2, y2, map)
    local b1, b2 = e1 and e1.building, e2 and e2.building
    
    local building
    
    if b1 and b2 and b1 ~= b2 then
      building = merge(b1, b2, btree, etree, map)
    elseif b1 or b2 then
      building = b1 or b2
    elseif valid_map then
      building = createBuilding(map)
    else
      building = createBuilding()
    end
    
    addEntrance(building, x1, y1, 1, etree)
    addEntrance(building, x2, y2, 1, etree)
    
    -- Also include the entrances in the volume.
    building.volume:addVertex(x1, y1)
    building.volume:addVertex(x2, y2)
    
    for i=1,perimeter:vertices() do
      building.volume:addVertex(shared.mapPosition(area_id, level, perimeter:vertex(i)))
    end
    
    btree:addObject(building, building.volume:boundingBox())
    
    local saved = getBuildingSaved(building)
    
    if not saved and valid_map then
      assert(building.static)
      saved = createBuilding(map)
      building.saved = saved
    end
    
    if saved and saved ~= building then
      addEntrance(saved, x1, y1, 1)
      addEntrance(saved, x2, y2, 1)
      
      saved.volume:addVertex(x1, y1)
      saved.volume:addVertex(x2, y2)
      
      for i=1,perimeter:vertices() do
        saved.volume:addVertex(shared.mapPosition(area_id, level, perimeter:vertex(i)))
      end
    end
  end)
  
  for map, list in pairs(saved_buildings) do
    local btree, etree = building_tree[map], entrance_tree[map]
    
    if not btree then
      assert(not etree)
      btree, etree = shared.createQuadTree(), shared.createQuadTree()
      building_tree[map], entrance_tree[map] = btree, etree
    end
    
    -- Need to make a copy of the list, because merging might delete buildings while we're iterating through it.
    local list_copy = {}
    for k,v in ipairs(list) do list_copy[k] = v end
    
    for i, building in ipairs(list_copy) do
      local nearest, nearest_d2
      
      setmetatable(building, shared.meta.Building)
      setmetatable(building.volume, shared.meta.ConvexPoly)
      
      for i, entrance in ipairs(building.entrances) do
        entrance.building = building
        local nearest_static_entrance, d2 = getEntrance(entrance.x, entrance.y, map)
        
        if nearest_static_entrance and (not nearest or d2 < nearest_d2) then
          nearest, nearest_d2 = nearest_static_entrance.building, d2
        end
      end
      
      if nearest then
        merge(nearest, building, btree, etree, map)
      else
        btree:addObject(building, building.volume:boundingBox())
        
        for i, entrance in ipairs(building.entrances) do
          etree:addObject(entrance, entrance.x, entrance.y)
        end
      end
    end
  end
end)

-- TODO: this isn't a very useful function because it uses map coordinates.
-- Returns the position of the entrance closest to the specified point.
function functions:getNearestEntrance(x, y)
  local nearest, nearest_d2
  
  for i, entrance in ipairs(self.entrances) do
    local dx, dy = x-entrance.x, y-entrance.y
    local d2 = dx*dx+dy*dy
    if not nearest or d2 < nearest_d2 then
      nearest, nearest_d2 = entrance, d2
    end
  end
  
  return nearest.x, nearest.y
end

function shared.getEntrance(area_id, level, x, y)
  local mx, my, map = shared.mapPosition(area_id, level, x, y)
  
  local entrance, d2 = getEntrance(mx, my, map)
  
  if not entrance then
    local building = shared.getBuilding(area_id, level, x, y)
    
    if building then
      for i, e in ipairs(building.entrances) do
        local dx, dy = mx-e.x, my-e.y
        local ed2 = dx*dx+dy*dy
        if not entrance or ed2 < d2 then
          entrance, d2 = e, ed2
        end
      end
    end
  end
  
  if entrance then
    return shared.areaPosition(entrance.x, entrance.y, map, area_id, level)
  end
end
