PhysicalActor = Object:extend()

-- PHYSICS FUNCTIONS
-- Raw implementation of everything common in physics

function PhysicalActor:initPhysics(hitboxObj, isSolid)
    self.isSolid = isSolid or false

    self.xsp = 0
    self.ysp = 0
    self.zsp = 0

    self.xfrc = 0
    self.yfrc = 0
    self.zfrc = 0

    self:initGravity()
    self:initHitboxes(hitboxObj)

    self:setBounceFactor()
    self:setFilter()

    self:addUpdateFunction(self.autoMove)
end

function PhysicalActor:setBounceFactor(newBounceFactor)
    self.bounceFactor = newBounceFactor or 0
end

function PhysicalActor:setFilter()
    -- Init the bump filter
    self.filter = function(item, other)
        if (other.owner == self) then
            -- ignore every collision with our own hitboxes
            return nil
        elseif (other.isSolid) then
            return "slide"
        else
            return "cross"
        end
    end
end

function PhysicalActor:getFuturePosition(dt)
    local dx, dy, dz
    dx = self.x + self.xsp * dt
    dy = self.y + self.ysp * dt
    if (self.z ~= nil) then
        dz = self.z + self.zsp * dt
    end

    return dx, dy, dz
end

function PhysicalActor:applyFriction(dt)
    self.xsp = utils.math.toZero(self.xsp, self.xfrc * dt)
    self.ysp = utils.math.toZero(self.ysp, self.yfrc * dt)
    if (self.z ~= nil) then
        self.zsp = utils.math.toZero(self.zsp, self.zfrc * dt)
    end
end

function PhysicalActor:solveAllCollisions(cols)
    for i, col in ipairs(cols) do
        self:collisionResponse(col)
        if (col.type == "touch") or (col.type == "bounce") or (col.type == "slide") then
            self:changeSpeedToCollisionNormal(col.normal)
        end
    end
end

function PhysicalActor:collisionResponse(collision)
    -- here come the response to the collision
end

function PhysicalActor:changeSpeedToCollisionNormal(normal)
    -- Empty function in PhysicalActor
end

-- COORDINATE/MOVEMENT FUNCTIONS
-- Handle coordinate
-- Will be replaced by functions inside Actors or Rects/Point

function PhysicalActor:getViewCenter()
    return self:getCenter()
end

-- GRAVITY SYSTEM FUNCTIONS
-- All functions related to gravity

function PhysicalActor:initGravity()
    if (self.world.gravity.isDefault) then
        self.grav = self.world.gravity.grav
    else
        self.grav = 0
    end

    self.onGround = false
end

function PhysicalActor:setGravity(grav)
    -- It's basically now a function with two roles at once :
    -- - activate the gravity
    -- - use the gravity value the dev want

    self.grav = grav or self.world.gravity.grav
end

function PhysicalActor:applyGravity(dt)
    -- Empty function in PhysicalActor
end

function PhysicalActor:checkGround()
    -- Empty function in PhysicalActor
end

function PhysicalActor:autoMove(dt)
    self:updateHitboxes()
    self.onGround = false
    self:applyGravity(dt)

    local cols, colNumber = self:moveToFuturePosition(dt)

    -- apply after the movement the friction, until the player stop
    -- note: the friction is applied according to the delta time,
    -- thus the friction should be how much speed is substracted in 1 second

    self:solveAllCollisions(cols)

    self:applyFriction(dt)
end

-- HITBOX FUNCTIONS
-- All functions to handle hitboxes

function PhysicalActor:initHitboxes(hitboxObj)
    self.Hitbox = hitboxObj
    self:initMainHitbox()

    self.hitboxes = {}
    self.hitboxListFile = ""
    self.hitboxList = nil
end

function PhysicalActor:initMainHitbox()
    -- Empty function : don't load ANY real hitbox function into PhysicalActor
end

function PhysicalActor:setHitboxFile(file)
    self.hitboxList = require(file)
    self.hitboxListFile = file
end

function PhysicalActor:getAutomaticHitboxLoading()
    return (self.hitboxList ~= nil)
end

function PhysicalActor:getHitboxFile()
    return self.hitboxListFile
end

function PhysicalActor:getHitboxList(animation, frame)
    if (animation == nil) or (self.hitboxList == nil) then
        return self.hitboxList
    else
        local list = self.hitboxList[animation]

        if (frame == nil) or (list == nil) then
            return list
        else
            return list[frame]
        end
    end
end

function PhysicalActor:addHitboxFromFrameData(framedata, animationID, frameID, hitboxID)
    local sx, sy = self.sprite:getScalling()
    local type = framedata[1]
    local box = framedata[2]
    local isSolid = framedata[3] or false
    local anim = animationID or "null"
    local frame = frameID or 0
    local id = hitboxID or 0

    if (type == "main") then
        self.mainHitbox:setFromData(box, sx, sy)
    else
        local hitboxName = anim .. frame .. type .. id
        self:addHitbox(hitboxName, type, box, sx, sy, isSolid)
        return hitboxName
    end
end

function PhysicalActor:initMainHitbox()
    self.mainHitbox = self.Hitbox(self, self.type, self:packForHitbox(), 0, 0, self.isSolid)
    self.mainHitbox:advertiseAsMainHitbox()
end

function PhysicalActor:addHitbox(name, type, data, sx, sy, isSolid)
    if (self.hitboxes[name] ~= nil) then
        core.debug:logWarn("PhysicalActor", "the hitbox " .. name .. " already exists")
    else
        local hitbox = self.Hitbox(self, type, data, sx, sy, isSolid)
        self.hitboxes[name] = hitbox
        return hitbox
    end
end

function PhysicalActor:updateHitboxes()
    if (self.hitboxList ~= nil) then
        self:purgeHitbox()
        local animation, frame
        animation = self.sprite:getCurrentAnimation()
        frame = self.sprite:getRelativeFrame()
        local hitboxList = self:getHitboxList(animation, frame)

        if (hitboxList ~= nil) then
            for i, v in ipairs(hitboxList) do
                self:addHitboxFromFrameData(v, animation, frame, i)
            end
        end
    end
end

function PhysicalActor:checkHitboxesCollisions(filter)
    for k, v in pairs(self.hitboxes) do
        self:applyHitboxCollisions(k, filter)
    end
end

function PhysicalActor:applyHitboxCollisions(name, filter)
    local cols, colNumber = {}, 0
    local filter = filter or self.filter
    if (self.isDestroyed == false) and (self.hitboxes[name] ~= nil) then
        cols, colNumber = self.hitboxes[name]:checkCollision(filter)
        local type = self.hitboxes[name].type

        for i, col in ipairs(cols) do
            self:hitboxResponse(name, type, col)
        end
    end

    return cols, colNumber
end

function PhysicalActor:hitboxResponse(name, type, collision)
    -- just a blank placeholder function
end

function PhysicalActor:removeHitbox(name)
    if (self.hitboxes[name] ~= nil) then
        self.hitboxes[name]:destroy()
        self.hitboxes[name] = nil
    end
end

function PhysicalActor:purgeHitbox()
    for k, v in pairs(self.hitboxes) do
        v:destroy()
    end
    self.hitboxes = {}
end

function PhysicalActor:drawHitboxes()
    for k, v in pairs(self.hitboxes) do
        v:draw()
    end
    self:drawMainHitbox()
end

function PhysicalActor:drawMainHitbox()
    if (self.mainHitbox ~= nil) then
        self.mainHitbox:draw()
    end
end

return PhysicalActor
