-- 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 mapObjects    = require(cwd .. "maps")
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, maptype)
  self.scene      = scene
  self.actorlist  = actorlist
  self.mapfile    = mapfile
  self.mapObjects = mapObjects

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

  self:initPlayers()
  self:setActorList(self.actorlist)
  self:initMap(self.mapfile, maptype)
  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, maptype)
  if (mapfile ~= nil) then
    self.maptype            = maptype or "sti"
  else
    self.maptype            = "empty"
  end

  self.mapfile              = mapfile
end

function BaseWorld:setGravity(grav, isDefault)
  local grav      = grav or 0
  local isDefault = isDefault or false

  self.gravity = {}
  self.gravity.grav = grav
  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.maptype)
  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, z)
  local debugstring  = " at (" .. x .. ";" .. y .. ")."
  core.debug:logInfo("world2D", "adding actor " .. name .. debugstring)
  self.obj.index[name](self, x, y)
end

function BaseWorld:newCollision(name, x, y, z, w, h, d)
  local debugstringpos  = "at (" .. x .. ";" .. y .. ")"
  local debugstringsize = "size is (" .. w .. ";" .. h .. ")"
  local debugstring     = " " .. debugstringpos .. ". " .. debugstringsize .. "."
  core.debug:logInfo("world2D", "creating collision " .. name .. debugstring)
  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:countActors()
  return #self.actors
end

function BaseWorld:getActors()
  return self.actors
end

function BaseWorld:getActorsInRect(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 <= x2) and
       (v.y >= y) and (v.y + v.h <= y2) then

      table.insert(query, v)
    end
  end

  return query
end


function BaseWorld:getVisibleActors(id)
  local actors = {}
  if (id ~= nil) then
    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

    actors = self:getActorsInRect(x, y, w, h)
  else
    actors = self:getActors()
  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)

  return actors
end

-- BODIES MANAGEMENT FUNCTIONS
-- Basic function to handle bodies. Empty function here as baseworld doesn't
-- handle bodies

function BaseWorld:registerBody(body)
  return nil
end

function BaseWorld:updateBody(body)
  return 0, 0, {}, 0
end

function BaseWorld:removeBody(body)
  return nil
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:getBodiesInRect(x, y, w, h)
  return {}
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:initPlayers()
  self.players = {}
  self.playerNumber = 1
end

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

function BaseWorld:addPlayer(x, y, z, id)
  local player = {}
  if id <= self.playerNumber then
    player.actor    = self:newPlayer(x, y, z)
    player.sourceid = id or 1

    table.insert(self.players, player)

    self.cameras:addTarget(player.actor)
  end
end

function BaseWorld:newPlayer(x, y, z)
  return self.obj.Player(self, x, y)
end

function BaseWorld:sendInputToPlayers(actor)
  for i, player in ipairs(self.players) do
    local keys = self.scene:getKeys(player.sourceid)
    player.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:loadMap()
  self:createMapController()
  self:loadMapObjects()
end

function BaseWorld:createMapController()
  if (self.maptype == "empty") then
    self.mapObjects.Base(self)
  elseif (self.maptype == "sti") then
    self.mapObjects.Sti(self, self.mapfile)
  end
end

function BaseWorld:loadMapObjects()
  if (self.map ~= nil) then
    self.map:loadObjects()
  end
end

function BaseWorld:getBox()
  if (self.map ~= nil) then
    return  self.map:getBox()
  else
    local w, h = core.screen:getDimensions()
    return 0, 0, w, h
  end
end

function BaseWorld:getDimensions()
  if (self.map ~= nil) then
    return  self.map:getDimensions()
  else
    return core.screen:getDimensions()
  end
end

function BaseWorld:setBackgroundColor(r, g, b)
  if (self.map ~= nil) then
    self.map:setBackgroundColor(r, g, b)
  end
end

function BaseWorld:removeBackgroundColor()
  if (self.map ~= nil) then
    self.map:setBackgroundColor()
  end
end

function BaseWorld:getBackgroundColor()
  if (self.map ~= nil) then
    return self.map:getBackgroundColor()
  else
    return 0, 0, 0
  end
end

function BaseWorld:resizeMap(w, h)
  if (self.map ~= nil) then
    local w, h = utils.math.floorCoord(w, h)
    self.map:resize(w, h)
  end
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.map ~= nil) 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)
    end
  end
end

function BaseWorld:drawActors(id)
  local actors = self:getVisibleActors(id)

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

function BaseWorld:drawMap(id)
  if (self.map ~= nil) then
    self.map:draw()
  end
end

function BaseWorld:drawBackgroundColor()
  if (self.map ~= nil) then
    self.map:drawBackgroundColor()
  end
end

return BaseWorld
