-- baseworld.lua :: the base world object, that contain just a fast implementation
-- of a 2D world. It doesn't support collision and stuff.

--[[
  Copyright © 2019 Kazhnuz

  Permission is hereby granted, free of charge, to any person obtaining a copy of
  this software and associated documentation files (the "Software"), to deal in
  the Software without restriction, including without limitation the rights to
  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  the Software, and to permit persons to whom the Software is furnished to do so,
  subject to the following conditions:

  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]

local cwd   = (...):gsub('%.baseworld$', '') .. "."

local BaseWorld = Object:extend()

local Sti           = require(cwd .. "libs.sti")
local CameraSystem  = require(cwd .. "camera")

local PADDING_VALUE = 10/100

-- INIT FUNCTIONS
-- All functions to init the world and the map

function BaseWorld:new(scene, actorlist, mapfile)
  self.scene      = scene
  self.actorlist  = actorlist
  self.mapfile    = mapfile

  self.cameras  = CameraSystem(self)
  self:initActors()

  self:initPlayers()
  self:setActorList(self.actorlist)
  self:initMap(self.mapfile)
  self:setGravity()

  self:register()

  self.isActive = true
end

function BaseWorld:setActorList(actorlist)
  if actorlist == nil then
    error("FATAL: You must set an actorlist to your world")
  else
    self.obj = require(actorlist)
  end
end

function BaseWorld:initMap(mapfile)
  self.haveMap              = false
  self.haveBackgroundColor  = false
  self.backcolor            = {128, 128, 128}
  self.mapfile              = mapfile
end

function BaseWorld:setGravity(xgrav, ygrav, isDefault)
  local xgrav     = xgrav or 0
  local ygrav     = ygrav or 0
  local isDefault = isDefault or 0

  self.gravity = {}
  self.gravity.xgrav, self.gravity.ygrav = xgrav, ygrav
  self.gravity.isDefault = isDefault
end

function BaseWorld:register()
  self.scene:registerWorld(self)
end

function BaseWorld:reset()
  self:initActors()
  self:initPlayers()
  self:initMap(self.mapfile)
  self.cameras:initViews()
  collectgarbage()

  self:loadMap()
end

-- ACTOR MANAGEMENT FUNCTIONS
-- Basic function to handle actors

function BaseWorld:initActors( )
  self.actors = {}
  self.currentCreationID = 0
end

function BaseWorld:newActor(name, x, y)
  self.obj.index[name](self, x, y)
end

function BaseWorld:newCollision(name, x, y, w, h)
  self.obj.collisions[name](self, x, y, w, h)
end

function BaseWorld:registerActor(actor)
  actor.creationID = self.currentCreationID
  self.currentCreationID = self.currentCreationID + 1
  table.insert(self.actors, actor)
end

function BaseWorld:removeActor(actor)
  for i,v in ipairs(self.actors) do
    if v == actor then
      table.remove(self.actors, i)
    end
  end
end

function BaseWorld:moveActor(actor, x, y, filter)
  -- as the baseworld have no collision function, we return empty collision
  -- datas, but from the same type than bump2D will return
  return x, y, {}, 0
end

function BaseWorld:checkCollision(actor, x, y, filter)
  -- as the baseworld have no collision function, we return empty collision
  -- datas, but from the same type than bump2D will return
  return x, y, {}, 0
end

function BaseWorld:queryRect(x, y, w, h)
  local query = {}
  local x2, y2 = x + w, y + h
  for i,v in ipairs(self.actors) do
    if (v.x >= x) and (v.x + v.w >= x1) and
       (v.y >= y) and (v.y + v.h >= y1) then

      table.insert(query, v)
    end
  end

  return v
end

function BaseWorld:countActors()
  return #self.actors
end

function BaseWorld:getActors()
  return self.actors
end

function BaseWorld:getVisibleActors(id)
  local camx, camy, camw, camh = self.cameras:getViewCoordinate(id)
  local paddingw  = camw * PADDING_VALUE
  local paddingh  = camh * PADDING_VALUE
  local x = camx - paddingw
  local y = camy - paddingh
  local w = camw + paddingw * 2
  local h = camh + paddingh * 2
  return self:queryRect(x, y, w, h)
end

-- INFO FUNCTIONS
-- Give infos about the world

function BaseWorld:isActorIndexed(name)
  return (self.obj.index[name]      ~= nil)
end

function BaseWorld:isCollisionIndexed(name)
  return (self.obj.collisions[name] ~= nil)
end

-- PLAYER MANAGEMENT FUNCTIONS
-- Basic function to handle player actors

function BaseWorld:loadMap()
  local mapfile = self.mapfile
  if mapfile == nil then
    self.haveMap = false
    self.haveBackgroundColor = false
    self.backcolor = {128, 128, 128}
  else
    self.haveMap = true
    self.map        = Sti(mapfile)
    self.haveBackgroundColor = true
    self.backcolor  = self.map.backgroundcolor or {128, 128, 128}
    self:loadMapObjects()
  end
end

function BaseWorld:initPlayers()
  self.players = {}
  self.playerNumber = 1
end

function BaseWorld:setPlayerNumber(playerNumber)
  self.playerNumber = playerNumber or 1
end

function BaseWorld:addPlayer(actor, sourceid, haveCam)
  local player = {}
  player.actor    = actor
  player.sourceid = sourceid or 1

  table.insert(self.players, player)

  if (haveCam) then
    local xx, yy = player.actor:getViewCenter()
    self.cameras:addView(xx, yy, player.actor)
  end
end

function BaseWorld:sendInputToPlayers(actor)
  for i,v in ipairs(self.players) do
    --TODO: make the player get from a selected source inputs
    local keys = self.scene.sources[v.sourceid].keys
    v.actor:getInput(keys)
  end
end

function BaseWorld:removePlayer(actor)
  for i,v in ipairs(self.players) do
    if (v.actor == actor) then
      table.remove(self.players, i)
    end
  end
end

-- MAP FUNCTIONS
-- All map wrappers

function BaseWorld:loadMapObjects()
  self:loadMapCollisions()
  self:loadMapPlayers()
  self:loadMapActors()
end

function BaseWorld:loadMapCollisions()
  for k, objectlayer in pairs(self.map.layers) do
    if self:isCollisionIndexed(objectlayer.name) then
      print("DEBUG: loading actors in " .. objectlayer.name .. " collision layer")
        for k, object in pairs(objectlayer.objects) do
          self:newCollision(objectlayer.name, object.x, object.y, object.width, object.height)
        end
        self.map:removeLayer(objectlayer.name)
    end
  end
end

function BaseWorld:loadMapActors()
  for k, objectlayer in pairs(self.map.layers) do
    if self:isActorIndexed(objectlayer.name) then
      print("DEBUG: loading actors in " .. objectlayer.name .. " actor layer")
        for k, object in pairs(objectlayer.objects) do
          if (object.properties.batchActor) then
            self:batchActor(objectlayer.name, object)
          else
            self:newActor(objectlayer.name, object.x, object.y)
          end
        end
        self.map:removeLayer(objectlayer.name)
    end
  end
end

function BaseWorld:batchActor(name, object)
  local gwidth   = object.properties.gwidth  or self.map.tilewidth
  local gheight  = object.properties.gheight or self.map.tileheight
  local x = object.x
  local y = object.y
  local w = object.width
  local h = object.height

  local cellHor   = math.ceil(w / gwidth)
  local cellVert  = math.ceil(h / gheight)

  for i=1, cellHor do
    for j=1, cellVert do
      self:newActor(name, x + (i-1)*gwidth, y + (j-1)*gheight)
    end
  end
end

function BaseWorld:loadMapPlayers()
  for k, objectlayer in pairs(self.map.layers) do
    if (objectlayer.name == "player") then
      print("DEBUG: loading actors in player layer")
      local i = 1
        for k, object in pairs(objectlayer.objects) do
          if (i <= self.playerNumber) then
          -- TODO: don't hardcode camera handling
            self:addPlayer(self.obj.Player(self, object.x, object.y), i, true)
          end
          i = i + 1
        end
        self.map:removeLayer(objectlayer.name)
    end
  end
end

function BaseWorld:getDimensions()
  if self.haveMap then
    return  self.map.width  * self.map.tilewidth,
            self.map.height * self.map.tileheight
  else
    return core.screen:getDimensions()
  end
end

function BaseWorld:setBackgroundColor(r, g, b)
  self.backcolor = {r, g, b}
  self.haveBackgroundColor = true
end

function BaseWorld:removeBackgroundColor()
  self.haveBackgroundColor = false
end

function BaseWorld:getBackgroundColor()
  return self.backcolor[1]/256, self.backcolor[2]/256, self.backcolor[3]/256
end

-- Lock MANAGEMENT FUNCTIONS
-- Basic function to handle the lock

function BaseWorld:setActivity(activity)
  self.isActive = activity
end

function BaseWorld:switchActivity()
  self.isActive = (self.isActive == false)
end

function BaseWorld:getActivity()
  return self.isActive
end

-- UPDATE FUNCTIONS
-- All update functions

function BaseWorld:update(dt)
  self:updateMap(dt)
  self:sendInputToPlayers(dt)
  self:updateActors(dt)
  self.cameras:update(dt)
end

function BaseWorld:updateActors(dt)
  local actors = self:getActors()
  for i,v in ipairs(actors) do
    v:update(dt)
  end
end

function BaseWorld:updateMap(dt)
  if self.haveMap then
    self.map:update(dt)
  end
end


-- DRAW FUNCTIONS
-- All function to draw the map, world and actors

function BaseWorld:draw(dt)
  self:drawBackgroundColor()
  local camNumber = self.cameras:getViewNumber()

  if (camNumber == 0) then
    self:drawMap()
    self:drawActors()
  else
    for i=1, camNumber do
      self.cameras:attachView(i)
      self:drawMap(i)
      self:drawActors(i)
      self.cameras:detachView(i)
      self.cameras:drawHUD(i)
    end
  end
end

function BaseWorld:drawActors(id)
  local actors
  if (id == nil) then
    actors = self:getActors()
  else
    actors = self:getVisibleActors(id)
  end

  table.sort(actors, function(a,b)
    if (a.depth == b.depth) then
      return a.creationID < b.creationID
    else
      return a.depth > b.depth
    end
  end)

  for i,v in ipairs(actors) do
    v:draw()
  end
end

function BaseWorld:drawMap(id)
  local tx, ty, scale = 0, 0, 1
  if id ~= nil then
    -- Du à la manière dont fonctionne STI, on est obligé de récupérer les info
    -- de position de camera pour afficher la carte par rapport à ces infos
    tx, ty  = self.cameras:getViewCoordinate(id)
    scale   = self.cameras:getViewScale(id) or 1
    local vx, vy  = self.cameras:getOnScreenViewRelativePosition(id)
  	tx = math.floor(tx - math.abs(vx))
  	ty = math.floor(ty - math.abs(vy))
  end

  if self.haveMap then
    self.map:draw(-tx, -ty, scale, scale)
  end
end

function BaseWorld:drawBackgroundColor()
  if self.haveBackgroundColor then
    local r, g, b = self.backcolor[1], self.backcolor[2], self.backcolor[3]
    love.graphics.setColor(r/256, g/256, b/256)
    love.graphics.rectangle("fill", 0, 0, 480, 272)
    utils.graphics.resetColor()
  end
end

return BaseWorld
