local World = Object:extend()

local bump3dpd = require 'libs.level-libs.bump-3dpd'
local bump2d   = require 'libs.level-libs.bump'
local tsort    = require 'libs.level-libs.tsort'

local actor = require "scenes.subgame.sonic-boost.actors"

local tilesize = 31

---------- WORLD FUNCTIONS ----------

function World:new(controller)
  self.controller = controller
  self.creationID = 1

  self.entities = bump3dpd.newWorld()
  self.chunks   = {}

  self:loadAssets()

  self.worldloaded = false
end

function World:destroy()
  self.chunks = {}
  for i,v in ipairs(self.entities:getItems()) do
    v:destroy( )
  end
end

---------- ENTITIES MANAGEMENT FUNCTIONS ----------

function World:addEntitiesFromChunk(chunkpos)
  local chunkdata, isTrue = self:getChunkData(chunkpos, 1)
  local objectdata       = chunkdata.objects

  local chunkx = (chunkpos - 1) * (8*31)
  print("trying to get objects from chunk at pos " .. chunkpos)

  for i=1, 5 do
    for j=1, 8 do
      local y = (i-1)*20 + 10
      local x = chunkx + (j-1)*31 + 16

      local id = objectdata[i][j]
      --print(chunkpos)
      self:addEntityFromID(id, x, y)
    end
  end
end

function World:addRailsFromChunk(chunkpos)
  local chunkdata, isTrue = self:getChunkData(chunkpos, 1)
  local objectdata       = chunkdata.grind

  local chunkx = (chunkpos - 1) * (8*31)
  print("trying to get rails from chunk at pos " .. chunkpos)

  for i=1, 5 do
    for j=1, 8 do
      local y = (i-1)*20 + 10
      local x = chunkx + (j-1)*31

      local id = objectdata[i][j]
      --print(chunkpos)
      if id ~= 0 then
        actor.Rail(self, x, y, 10, id)
      end
    end
  end
end

function World:addEntityFromID(id, x, y)
  if actor.Index[id] == nil then
    if (id ~= 0) then
      print("WARNING: Object " .. id .. " doesn't exist")
    end
    return false
  end

  actor.Index[id](self, x, y)
end

function World:register(entity)
  self.entities:add(entity, entity.x, entity.y, entity.z, entity.w, entity.h, entity.d)
end

function World:remove(entity)
  if entity.destroyed == false then
    self.entities:remove(entity)
    print("entity " .. entity.id .. " removed")
  end
end

function World:move(entity, dx, dy, dz, filter)
  local dx, dy, dz = dx, dy, dz
  local newx, newy, newz
  newx, newy, newz, cols, coll_number = self.entities:move(entity, dx, dy, dz, filter)
  return newx, newy, newz, cols, coll_number
end

function World:updateEntity(entity, dx, dy, dz)
  newx, newy, newz, cols, coll_number = self.entities:update(entity, dx, dy, dz)
end

---------- UPDATE FUNCTIONS ----------

function World:update(dt)
  for _, item in ipairs(self.entities:getItems()) do
    item:update(dt)
    --self.world2D:update(item, item.x, item.y)
  end
end

---------- DRAW FUNCTIONS ----------


function World:draw()
  local chunksize = 8 * tilesize
  local camstart = math.floor(math.max(0, (self.controller.camera.view.x - 212) / chunksize)) --

  self:drawBorder(-10)

  for i=0, 3 do
    -- on fait commencer la boucle à 0 pour regarder celui *avant* la caméra,
    -- pour éviter des soucis d'affichage.
    local chunkid = camstart + i
    self:drawChunk((chunkid - 1) * chunksize, 0, chunkid)
  end

  local itemsToDraw = self.entities:getItems()

  for _, item in ipairs(itemsToDraw) do
    item:drawShadow()
    if self.controller.camera.turn < self.controller.missiondata.turns then
      item:drawShadowEcho()
    end
  end

  table.sort(itemsToDraw, sortItemsToDraw)

  for _, item in ipairs(itemsToDraw) do
    item:draw()
    if self.controller.camera.turn < self.controller.missiondata.turns then
      item:drawEcho()
    end
    --item:drawHitBox()
  end

  self:drawBorder(100)
end

function World:drawChunk(x, y, chunkid, turn)
  local chunkdata, isTrue = self:getChunkData(chunkid, self.controller.camera.turn)
  local terraindata       = chunkdata.terrain

  for i=1, 5 do
    for j=1, 8 do
      local tiley = (i-1)*20 - 4
      local tilex = x + (j-1)*31 + (i-1)*10
      local tileid = terraindata[i][j]

      local variant = 1 + ((i + j) % 2)
      if (isTrue == false) then
        love.graphics.setColor(.75, .75, .75, 1)
      end
      self:drawTile(tilex, tiley, tileid, variant)
      utils.graphics.resetColor()
    end
  end
end


function World:drawTile(x, y, type, variant)
  if type == 0 then
    love.graphics.draw(self.textures.tiles,   self.quads.tiles[variant],  x, y)
  else
    love.graphics.draw(self.textures.sptiles, self.quads.sptiles[type],   x, y-2)
  end
end

function World:drawBorder(y)
  local x = math.floor((self.controller.camera.view.x - 212) / 80) * 80

  for i=1, 7 do
    love.graphics.draw(self.textures.borders, self.quads.borders, x + (i-1)*80, y, 0, 1, 1)
  end
end



---------- LOADING FUNCTIONS ----------

function World:loadWorld()
  self.chunks = {}

  self:addChunk("basics", 00)

  local layout = self.controller.layout
  for i,v in ipairs(layout) do
    --print("Decompressing group of parts " .. i .. " located in " .. v[1] .. " (containing " .. #v[2] .. " parts)")

    -- self.controller.layout est un tableau composée d'une série de tableau,
    -- chacuns en deux partie : le premier (v[1]) contient le nom du fichier
    -- où se trouve des partie de niveau, et la seconde partie (v[2]) une liste
    -- d'identifiant de partie de niveau à rajouter.

    -- Ce format permet de simplifier l'écriture de layout, si on a besoin de
    -- plusieur partie de niveau se trouvant dans le même fichier.

    for j,w in ipairs(v[2]) do
      -- On dit au gestionnaire de terrain d'ajouter la partie numéro j se trouvant
      -- dans v[2]. Le nom du fichier contenant les parties sera toujours v[1]
      self:addPart(v[1], w)
    end
  end
end

function World:addPart(filename, part)
  local filename = filename or "self"
  local partdata = {}

  -- On recupère les données de chunks

  if (filename == "self") or (filename == "") then

    -- Si le nom est "self", cela veut dire que fichier est le même que celui ou
    -- se trouve le layout, qui est forcément le fichier du niveau. Cela veut dire
    -- qu'il faut utiliser les parties de niveau du niveau actuel, localisée dans
    -- self.controller.parts

    partdata = self.controller.parts
  else

    -- Si c'est un autre nom, on charge dans "partdata" la liste des partie de niveau
    -- du fichier qu'on réfère.

    local chunkfile = require("datas.subgame.sonic-boost.chunks." .. filename)
    partdata = chunkfile.parts
  end

  -- On charge ensuite la partie de niveau voulue, qui correspond à l'identifiant
  -- qu'on a envoyé dans cette fonction (part) dans la liste partdata.

  local chunklist = partdata[part]

  for i, v in ipairs(chunklist) do
    -- chunklist fonctionne de la même manière que partlist :
    -- chaque entrée dans la liste (v) contient deux partie.
    -- v[1]: l'identifiant du fichier de chunk où se trouve le chunk voulu
    -- v[2]: la liste de chunks voulu. chaque entrée dedans est un nombre,
    -- correspondant à un chunk

    --print("- Decompressing group of chunk " .. i .. " located in " .. v[1] .. " (containing " .. #v[2] .. " chunk)")

    for j, w in ipairs(v[2]) do
      local chunkfile = v[1]
      if (chunkfile == "") or (chunkfile == "self") then
        -- si le fichier de chunk est "self", on lui indique que le chunk se trouve
        -- dans le même fichier que le fichier de partie.
        chunkfile = filename
      end
      self:addChunk(chunkfile, w)
    end
  end
end


function World:addChunk(filename, chunk)
  if filename == "self" then filename = "basic" end
  --print("--- Adding chunk id " .. chunk .. " located in " .. filename)

  local chunkfile = require("datas.subgame.sonic-boost.chunks." .. filename)
  chunkdata = chunkfile.chunks

  local chunkpos = #self.chunks

  table.insert(self.chunks, chunkdata[chunk])
  self:addEntitiesFromChunk(chunkpos)
  self:addRailsFromChunk(chunkpos)
  self:updateSize()
end

---------- ASSETS LOADING FUNCTIONS ----------

function World:loadAssets()
  self.textures = {}
  local folder = "assets/backgrounds/sideview/"
  self.textures.tiles   = love.graphics.newImage(folder .. "normaltile.png")
  self.textures.sptiles = love.graphics.newImage(folder .. "specialtile.png")
  self.textures.borders = love.graphics.newImage(folder .. "borders.png")

  self.textures.rail    = love.graphics.newImage(folder .. "rails/basic.png")

  self.quads = {}
  self.quads.tiles    = {}
  self.quads.sptiles  = {}
  self.quads.rails    = {}

  local tileline    = self.controller.datas.tiles or 0
  local borderline  = self.controller.datas.borders or 0


  local w, h = self.textures.tiles:getDimensions()
  self.quads.tiles[1] = love.graphics.newQuad( 0, tileline*24, 40, 24, w, h)
  self.quads.tiles[2] = love.graphics.newQuad(40, tileline*24, 40, 24, w, h)

  local w, h = self.textures.sptiles:getDimensions()
  local h2   = math.floor(h / 26)
  for i=1, h2 do
    self.quads.sptiles[i] = love.graphics.newQuad(0, (i-1)*26, 40, 26, w, h)
  end

  local w, h = self.textures.borders:getDimensions()
  self.quads.borders = love.graphics.newQuad(0, borderline*10, 80, 10, w, h)

  local w, h = self.textures.rail:getDimensions()
  for i=1, 3 do
    self.quads.rails[i] = love.graphics.newQuad((i-1)*31, 0, 31, 8, w, h)
  end
end

---------- DATA FUNCTIONS ----------

function World:getChunkData(chunkid, turn)
  local turn = turn or 1
  local currentTurn, maxTurn = turn, self.controller.missiondata.turns

  if chunkid <= 0 then
    if currentTurn <= 1 then
      return self:getFakeChunk()
    else
      chunkid = (chunkid - 1)
      chunkid = utils.math.wrap(chunkid, 0, #self.chunks)
      return self.chunks[chunkid + 1], true
    end
  elseif chunkid > #self.chunks then
    if currentTurn >= maxTurn then
      return self:getFakeChunk()
    else
      chunkid = (chunkid - 1)
      chunkid = utils.math.wrap(chunkid, 0, #self.chunks)
      return self.chunks[chunkid + 1], true
    end
  else
    return self.chunks[chunkid], true
  end

end

function World:getFakeChunk()
  local fakedata = {}
  local emptyline = {00, 00, 00, 00, 00, 00, 00, 00}
  fakedata.objects = {emptyline, emptyline, emptyline, emptyline, emptyline}
  fakedata.terrain = {emptyline, emptyline, emptyline, emptyline, emptyline}
  fakedata.grind   = {emptyline, emptyline, emptyline, emptyline, emptyline}

  return fakedata, false
end

function World:getTerrainAtPoint(x, y, turn)
  local chunkid           = self:getChunkAtPoint(x, y)
  local chunkdata, isTrue = self:getChunkData(chunkid, turn)

  local i = math.floor(y / 20) + 1
  local j = math.floor((x % (8*31)) / 31) + 1

  local terrain = chunkdata.terrain[i][j]

  --print(terrain)
  return terrain
end

function World:getGrindAtPoint(x, y, turn)
  local chunkid           = self:getChunkAtPoint(x, y)
  local chunkdata, isTrue = self:getChunkData(chunkid, turn)

  local i = math.floor(y / 20) + 1
  local j = math.floor((x % (8*31)) / 31) + 1

  local grind = chunkdata.grind[i][j]

  --print(terrain)
  return grind > 0
end

function World:getChunkAtPoint(x, y)
  return math.floor(x / (8*31)) + 1
end


---------- USEFULL FUNCTIONS ----------

function World:updateSize()
  self.size   = #self.chunks * 8
  self.width  = self.size * 31
end

function World:getZSortedItems()
  local bump2d   = require 'libs.bump'
  local tsort    = require 'libs.tsort'
  local world2d = bump2d.newWorld()

  local world = self.entities

  for _, item in ipairs(world:getItems()) do
    if item.invisible ~= true and item.destroyed ~= true then
      local x,y,z,w,h,d = world:getCube(item)
      if world2d:hasItem(item) then
        world2d:update(item, x, y + z)
      else
        world2d:add(item, x, y + z, w, h + d)
      end
    end
  end

  local graph = tsort.new()
  local noOverlap = {}

  -- Iterate through all visible items, and calculate ordering of all pairs
  -- of overlapping items.
  -- TODO: Each pair is calculated twice currently. Maybe this is slow?
  for _, itemA in ipairs(world2d:getItems()) do repeat
    local x, y, w, h = world2d:getRect(itemA)
    local otherItemsFilter = function(other) return other ~= itemA end
    local overlapping, len = world2d:queryRect(x, y, w, h, otherItemsFilter)

    if len == 0 then
      table.insert(noOverlap, itemA)

      break
    end

    local _, aY, aZ, _, aH, aD = world:getCube(itemA)
    aDepth  = itemA.depth
    aID     = itemA.id
    aType   = itemA.type

    --print("testing overlap with itemA " .. aID .. " (" .. aType .. ")")


    for _, itemB in ipairs(overlapping) do
      local _, bY, bZ, _, bH, bD = world:getCube(itemB)
      bDepth = itemB.depth
      bID    = itemB.id
      bType  = itemB.type

      --print("itemB " .. aID .. " overlap with item " .. bID .. " (" .. bType .. ")")

      if aZ + aD <= bZ then
        -- item A is completely above item B
        graph:add(itemB, itemA)
        --print("itemA is completely above itemB")
      elseif bZ + bD <= aZ then
        -- item B is completely above item A
        graph:add(itemA, itemB)
        --print("itemB is completely above itemA")
      elseif aY + aH <= bY then
        -- item A is completely behind item B
        graph:add(itemA, itemB)
        --print("itemA is completely behind itemB")
      elseif bY + bH <= aY then
        -- item B is completely behind item A
        graph:add(itemB, itemA)
        --print("itemB is completely behind itemA")
      elseif aY + aZ + aH + aD == bY + bZ + bH + bD then
        -- item A's forward-most point is the same than item B's forward-most point
        --print("itemA's forward-most point is the same than itemB's forward-most point")
        if aDepth > bDepth then
          graph:add(itemB, itemA)
        elseif aDepth < bDepth then
          graph:add(itemA, itemB)
        else
          if aID > bID then
            graph:add(itemA, itemB)
          elseif aID < bID then
            graph:add(itemB, itemA)
          else
            err("two object can't have the same ID")
          end
        end
      elseif aY + aZ + aH + aD >= bY + bZ + bH + bD then
        -- item A's forward-most point is in front of item B's forward-most point
        graph:add(itemB, itemA)
        --print("itemA's forward-most point is in front of itemB's forward-most point")
      else
        -- item B's forward-most point is in front of item A's forward-most point
        graph:add(itemA, itemB)
      --print("itemB's forward-most point is in front of itemA's forward-most point")
      end
    end
  until true end

  local sorted, err = graph:sort()
  if err then
    error(err)
  end
  for _, item in ipairs(noOverlap) do
    table.insert(sorted, item)
  end

  return sorted

end

function sortItemsToDraw(a, b)
  return a.y < b.y
end

return World
