/*Copyright (C) 2010-2023 Duncan Deveaux

This file is part of Hikou no mizu.

Hikou no mizu 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.

Hikou no mizu 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 Hikou no mizu.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "PhysicsObject.hpp"
#include "PhysicsWorld.hpp"
#include "Network/Serialization/PhysicsObjectStatus.hpp"

#include <cmath>

PhysicsObject::PhysicsObject() :
m_physicsWorld(nullptr), m_fixed(false),
m_airPenetration(0.f), m_material(MATERIAL_NONE)
{

}

void PhysicsObject::setPhysicsWorld(PhysicsWorld &physicsWorld)
{
    m_physicsWorld = &physicsWorld;
}

const PhysicsWorld *PhysicsObject::getPhysicsWorld() const
{
    return m_physicsWorld;
}

void PhysicsObject::applyPhysicsStatus(const PhysicsObjectStatus &status)
{
    //Force update the object position to match
    //server status, bypassing client collision detection
    m_box.left = status.getPosition().x;
    m_box.top = status.getPosition().y;

    setVelocity(status.getVelocity());
}

void PhysicsObject::setPosition(float xPosition, float yPosition)
{
    if (m_physicsWorld == nullptr)
        return;

    const Box targetBox(xPosition, yPosition, m_box.width, m_box.height);
    if (m_physicsWorld->worldContains(m_box) && hitTest(targetBox).empty())
    {
        m_box.left = xPosition;
        m_box.top = yPosition;
    }
}

void PhysicsObject::moveXPosition(float distance, bool push)
{
    movePosition(distance, PhysicsRoutines::BoxView1D(Axis::Horizontal), push);
}

void PhysicsObject::moveYPosition(float distance, bool push)
{
    movePosition(distance, PhysicsRoutines::BoxView1D(Axis::Vertical), push);
}

void PhysicsObject::movePosition(float distance,
                                 PhysicsRoutines::BoxView1D view, bool push)
{
    if (m_physicsWorld == nullptr)
        return;

    //To avoid teleporting through other objects,
    //make the object cover the distance by steps of its size

    /// Number of times it will move by its size
    const int stepsNo_signed = static_cast<int>(distance / view.size(m_box));

    /// Remainder to move to reach the expected distance
    const float remainder =
        distance - static_cast<float>(stepsNo_signed) * view.size(m_box);

    const int stepsNo = (stepsNo_signed > 0) ? stepsNo_signed : -stepsNo_signed;
    for (int i = 0; i < stepsNo; i++)
    {
        if (!attempt_movePosition((stepsNo_signed > 0) ? view.size(m_box)
                                                       : -view.size(m_box),
                                  view, push))
            return;
    }

    attempt_movePosition(remainder, view, push);
}

bool PhysicsObject::attempt_movePosition(float distance,
                                         PhysicsRoutines::BoxView1D view,
                                         bool push)
{
    if (m_physicsWorld == nullptr)
        return false;

    const float initialPosition = view.position(m_box);

    //Compute initial hits with a small margin to avoid detecting
    //collisions due to rounding errors in networked mode
    const std::vector<PhysicsObject*> initialHits =
        hitTest(view.shrink(m_box, 1.f));

    view.setPosition(m_box, view.position(m_box) + distance);

    //Objects
    std::vector<PhysicsObject*> finalHits = hitTest();
    PhysicsRoutines::soustractVectors(finalHits, initialHits);

    if (push)
    {
        //Move hit objects
        for (PhysicsObject *hitObject : finalHits)
        {
            if (hitObject->isFixed())
                continue;

            const Box &hitBox = hitObject->getBox();

            const float movement = (distance > 0.f) ?
                view.bound(m_box) - view.position(hitBox) :
                view.position(m_box) - view.bound(hitBox);

            hitObject->movePosition(movement, view, true);
        }

        //Update hits
        finalHits = hitTest();
        PhysicsRoutines::soustractVectors(finalHits, initialHits);
    }

    if (!finalHits.empty())
    {
        view.setPosition(m_box, initialPosition); //Return to initial position

        if (view.axis == Axis::Horizontal)
            PhysicsRoutines::sortObjectsByX(finalHits, distance < 0.f ? 1 : 0);
        else if (view.axis == Axis::Vertical)
            PhysicsRoutines::sortObjectsByY(finalHits, distance < 0.f ? 1 : 0);

        //Collide callback
        for (PhysicsObject *hitObject : finalHits)
        {
            hitObject->collide((*this)); //The object collided with this
            collide((*hitObject)); //This collided with the object
        }

        // Attempt to fix the position.
        const PhysicsObject *nearestObject = (distance < 0.f) ?
            finalHits.back() : finalHits.front();

        const Box &hitBox = nearestObject->getBox();

        const float updatedPosition = (distance > 0.f) ?
            view.position(hitBox) - view.size(m_box) :
            view.bound(hitBox);

        if (view.axis == Axis::Horizontal)
            setPosition(updatedPosition, m_box.top);
        else if (view.axis == Axis::Vertical)
            setPosition(m_box.left, updatedPosition);

        return false; ///< Move was corrected not to induce a collision.
    }

    //World
    if (!m_physicsWorld->worldContains(m_box))
    {
        view.setPosition(m_box, initialPosition); //Return to initial position

        //Fix the position
        const Box &worldBox = m_physicsWorld->getWorldBox();
        const float updatedPosition = (distance > 0.f) ?
            view.bound(worldBox) - view.size(m_box) :
            view.position(worldBox);

        if (view.axis == Axis::Horizontal)
        {
            setPosition(updatedPosition, m_box.top);
            collideWorld(false);
        }
        else if (view.axis == Axis::Vertical)
        {
            setPosition(m_box.left, updatedPosition);
            collideWorld((distance > 0.f)); //Collision with the ground
        }

        return false; ///< Move was corrected not to induce a collision
    }

    return true; ///< Move happened as asked
}

void PhysicsObject::setWidth(float width, bool reverseSide)
{
    setSize(width, PhysicsRoutines::BoxView1D(Axis::Horizontal), reverseSide);
}

void PhysicsObject::setHeight(float height, bool reverseSide)
{
    setSize(height, PhysicsRoutines::BoxView1D(Axis::Vertical), reverseSide);
}

void PhysicsObject::setSize(float size,
                            PhysicsRoutines::BoxView1D view, bool reverseSide)
{
    if (m_physicsWorld == nullptr || size <= 0.f)
        return;

    const Box initialBox = m_box;
    view.setSize(m_box, size);
    if (reverseSide)
        view.setPosition(m_box, view.position(m_box) +
                                view.size(initialBox) - view.size(m_box));

    //Lower width: no collision, return
    if (view.size(initialBox) >= size)
        return;

    //Leave world: abort
    if (!m_physicsWorld->worldContains(m_box))
    {
        m_box = initialBox;
        return;
    }

    //Potential Object hit
    const std::vector<PhysicsObject*> initialHits = hitTest(initialBox);
    std::vector<PhysicsObject*> finalHits = hitTest();

    PhysicsRoutines::soustractVectors(finalHits, initialHits);

    if (!finalHits.empty()) //Hits
    {
        //If fixed objects are hit, abort to avoid
        //moving objects past the fixed objects
        if (PhysicsRoutines::containsFixedObject(finalHits))
        {
            m_box = initialBox;
            return;
        }

        //Otherwise, try to move the objects
        for (PhysicsObject *hitObject : finalHits)
        {
            if (hitObject->isFixed())
                continue;

            const Box &hitBox = hitObject->getBox();
            const float movement = reverseSide ?
                view.position(m_box) - view.bound(hitBox) :
                view.bound(m_box) - view.position(hitBox);

            hitObject->movePosition(movement, view, true);
        }

        //Still a hit (may involve a fixed object): abort
        finalHits = hitTest();
        PhysicsRoutines::soustractVectors(finalHits, initialHits);

        if (!finalHits.empty())
            m_box = initialBox;
    }
}

void PhysicsObject::setVelocity(const Vector &velocity)
{
    if (!isFixed())
        m_velocity = velocity;
}

const Vector &PhysicsObject::getVelocity() const
{
    return m_velocity;
}

void PhysicsObject::setAirPenetration(float airPenetration)
{
    if (airPenetration >= 0.f && airPenetration <= 1.f)
        m_airPenetration = airPenetration;
}

float PhysicsObject::getAirPenetration() const
{
    return m_airPenetration;
}

void PhysicsObject::setMaterial(const std::string &material)
{
    m_material = material;
}

const std::string &PhysicsObject::getMaterial() const
{
    return m_material;
}

void PhysicsObject::setFixed(bool fixed)
{
    m_fixed = fixed;
}

bool PhysicsObject::isFixed() const
{
    return m_fixed;
}

float PhysicsObject::shadowProjection() const
{
    if (m_physicsWorld == nullptr)
        return 0.f;

    // The object touches the ground
    const Box &worldBox = m_physicsWorld->getWorldBox();
    if (m_box.top + m_box.height >= worldBox.top + worldBox.height)
        return 0.f;

    std::vector<PhysicsObject*> objectsUnder = hitTest(
        Box(m_box.left + m_box.width*.2f, m_box.top, m_box.width*.6f,
            worldBox.top + worldBox.height - m_box.top)
        );
    PhysicsRoutines::sortObjectsByY(objectsUnder, 0);

    // The object is over another object
    for (const PhysicsObject *under : objectsUnder)
    {
        if (under->interceptsShadows())
            return under->getBox().top - (m_box.top + m_box.height);
    }

    //The shadow should be projected on the ground
    return worldBox.top + worldBox.height - (m_box.top + m_box.height);
}

bool PhysicsObject::interceptsShadows() const
{
    return false;
}

bool PhysicsObject::isOnGround(std::string *material) const
{
    if (m_physicsWorld == nullptr)
        return false;

    constexpr float groundCollisionMargin = .1f;
    const Box &worldBox = m_physicsWorld->getWorldBox();

    //The object does not touch the ground
    if (m_box.top + m_box.height + groundCollisionMargin <
        worldBox.top + worldBox.height)
    {
        Box upperBox = m_box; //Box slightly up compared to m_box
        upperBox.top += groundCollisionMargin;

        const std::vector<PhysicsObject*> initialHits = hitTest();
        std::vector<PhysicsObject*> hitObjects = hitTest(upperBox);

        PhysicsRoutines::soustractVectors(hitObjects, initialHits);

        if (hitObjects.empty()) //The object does not touch another one
            return false;
        else
        {
            if (material != nullptr)
            {
                PhysicsRoutines::sortObjectsByY(hitObjects, 0);
                (*material) = hitObjects.front()->getMaterial();
            }

            return true;
        }
    }

    //The object is on the world's ground
    if (material != nullptr)
        (*material) = m_physicsWorld->getGroundMaterial();

    return true;
}

const Box &PhysicsObject::getBox() const
{
    return m_box;
}

std::vector<PhysicsObject*> PhysicsObject::hitTest() const
{
    return hitTest(m_box);
}

std::vector<PhysicsObject*> PhysicsObject::hitTest(const Box &box) const
{
    return (m_physicsWorld != nullptr) ?
        m_physicsWorld->hitTest((*this), box) : std::vector<PhysicsObject*>();
}
