/*Copyright (C) 2010-2012 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 "PhysicsRoutines.hpp"

PhysicsObject::PhysicsObject() : m_physicsWorld(NULL), m_fixed(false)
{
    
}

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

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

void PhysicsObject::setPosition(float xPosition, float yPosition)
{
    if (m_physicsWorld != NULL)
    {
        Box initialBox = m_box;
        
        m_box.left = xPosition;
        m_box.top = yPosition;
        
        const Box &worldBox = m_physicsWorld->getWorldBox();
        
        if (!worldBox.contains(m_box) || hitTest().size() > 0)
            m_box = initialBox;
    }
}

void PhysicsObject::moveXPosition(float distance, bool push)
{
    if (m_physicsWorld != NULL && distance != 0)
    {
        std::vector<PhysicsObject*> initialHits = hitTest();
        m_box.left += distance;
        
        //Objects
        std::vector<PhysicsObject*> finalHits = hitTest();
        PhysicsRoutines::soustractVectors(finalHits, initialHits);
        
        if (push)
        {
            std::vector<PhysicsObject*>::iterator it;
            
            for (it = finalHits.begin(); it != finalHits.end(); it++) //Move hit objects
            {
                PhysicsObject *hitObject = (*it);
                
                if (!hitObject->isFixed())
                {
                    const Box &hitBox = hitObject->getBox();
                    
                    if (distance > 0)
                        hitObject->moveXPosition((m_box.left + m_box.width) - hitBox.left, true);
                    else if (distance < 0)
                        hitObject->moveXPosition(m_box.left - (hitBox.left + hitBox.width), true);
                }
            }
            
            //Update hits
            finalHits = hitTest();
            PhysicsRoutines::soustractVectors(finalHits, initialHits);
        }
        
        if (finalHits.size() > 0)
        {
            int checkedSide = 0;
            if (distance < 0) checkedSide = 1;
            
            PhysicsRoutines::sortObjectsByX(finalHits, checkedSide);
            
            PhysicsObject *nearestObject = finalHits.front();
            if (distance < 0) nearestObject = finalHits.back();
            
            const Box &hitBox = nearestObject->getBox();
            
            if (distance > 0)
                m_box.left = hitBox.left - m_box.width;
            else if (distance < 0)
                m_box.left = hitBox.left + hitBox.width;
        }
        
        //World
        const Box &worldBox = m_physicsWorld->getWorldBox();
        
        if (!worldBox.contains(m_box))
        {
            if (distance > 0)
                m_box.left = worldBox.left + worldBox.width - m_box.width;
            else if (distance < 0)
                m_box.left = worldBox.left;
        }
    }
}

void PhysicsObject::moveYPosition(float distance, bool push)
{
    if (m_physicsWorld != NULL && distance != 0)
    {
        std::vector<PhysicsObject*> initialHits = hitTest();
        m_box.top += distance;
        
        //Objects
        std::vector<PhysicsObject*> finalHits = hitTest();
        PhysicsRoutines::soustractVectors(finalHits, initialHits);
        
        if (push)
        {
            std::vector<PhysicsObject*>::iterator it;
            
            for (it = finalHits.begin(); it != finalHits.end(); it++) //Move hit objects
            {
                PhysicsObject *hitObject = (*it);
                
                if (!hitObject->isFixed())
                {
                    const Box &hitBox = hitObject->getBox();
                    
                    if (distance > 0)
                        hitObject->moveYPosition((m_box.top + m_box.height) - hitBox.top, true);
                    else if (distance < 0)
                        hitObject->moveYPosition(m_box.top - (hitBox.top + hitBox.height), true);
                }
            }
            
            //Update hits
            finalHits = hitTest();
            PhysicsRoutines::soustractVectors(finalHits, initialHits);
        }
        
        if (finalHits.size() > 0)
        {
            int checkedSide = 0;
            if (distance < 0) checkedSide = 1;
            
            PhysicsRoutines::sortObjectsByY(finalHits, checkedSide);
            
            PhysicsObject *nearestObject = finalHits.front();
            if (distance < 0) nearestObject = finalHits.back();
            
            const Box &hitBox = nearestObject->getBox();
            
            if (distance > 0)
                m_box.top = hitBox.top - m_box.height;
            else if (distance < 0)
                m_box.top = hitBox.top + hitBox.height;
        }
        
        //World
        const Box &worldBox = m_physicsWorld->getWorldBox();
        
        if (!worldBox.contains(m_box))
        {
            if (distance > 0)
                m_box.top = worldBox.top + worldBox.height - m_box.height;
            else if (distance < 0)
                m_box.top = worldBox.top;
        }
    }
}

void PhysicsObject::setWidth(float width, bool reverseSide)
{
    if (m_physicsWorld != NULL && width > 0)
    {
        Box initialBox = m_box;
        m_box.width = width;
        if (reverseSide) m_box.left += initialBox.width - m_box.width;
        
        //Lower width : no collision, change & abort
        if (initialBox.width >= width)
            return;
        
        //Leave world : abort
        const Box &worldBox = m_physicsWorld->getWorldBox();
        
        if (!worldBox.contains(m_box))
        {
            m_box = initialBox;
            return;
        }
        
        //Potential Object hit
        std::vector<PhysicsObject*> initialHits = hitTest(initialBox);
        std::vector<PhysicsObject*> finalHits = hitTest();
        
        PhysicsRoutines::soustractVectors(finalHits, initialHits);
        
        if (finalHits.size() > 0) //Hits
        {
            std::vector<PhysicsObject*>::iterator it;
            
            for (it = finalHits.begin(); it != finalHits.end(); it++) //Move hit objects
            {
                if (!(*it)->isFixed())
                {
                    if (!reverseSide)
                        (*it)->moveXPosition(m_box.width - initialBox.width, true);
                    else
                        (*it)->moveXPosition(initialBox.width - m_box.width, true);
                }
            }
            
            //If still hit : abort
            finalHits = hitTest();
            PhysicsRoutines::soustractVectors(finalHits, initialHits);
            
            if (finalHits.size() > 0)
                m_box = initialBox;
        }
    }
}

void PhysicsObject::setHeight(float height, bool reverseSide)
{
    if (m_physicsWorld != NULL && height > 0)
    {
        Box initialBox = m_box;
        
        m_box.height = height;
        if (reverseSide) m_box.top += initialBox.height - m_box.height;
        
        //Lower height : no collision, abort
        if (initialBox.height >= height)
            return;
        
        //Leave world : abort
        const Box &worldBox = m_physicsWorld->getWorldBox();
        
        if (!worldBox.contains(m_box))
        {
            m_box = initialBox;
            return;
        }
        
        //Potential Object hit
        std::vector<PhysicsObject*> initialHits = hitTest(initialBox);
        std::vector<PhysicsObject*> finalHits = hitTest();
        
        PhysicsRoutines::soustractVectors(finalHits, initialHits);
        
        if (finalHits.size() > 0) //Hits
        {
            std::vector<PhysicsObject*>::iterator it;
            
            for (it = finalHits.begin(); it != finalHits.end(); it++) //Move hit objects
            {
                if (!(*it)->isFixed())
                {
                    if (!reverseSide)
                        (*it)->moveYPosition(m_box.height - initialBox.height, true);
                    else
                        (*it)->moveYPosition(initialBox.height - m_box.height, true);
                }
            }
            
            //If still hit : abort
            finalHits = hitTest();
            PhysicsRoutines::soustractVectors(finalHits, initialHits);
            
            if (finalHits.size() > 0)
                m_box = initialBox;
        }
    }
}

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

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

bool PhysicsObject::isOnGround()
{
    if (m_physicsWorld != NULL)
    {
        const Box &worldBox = m_physicsWorld->getWorldBox();
        
        if (m_box.top + m_box.height < worldBox.top + worldBox.height) // The object doesn't touch the ground
        {
            std::vector<PhysicsObject*> initialHits = hitTest();
            m_box.top += 1;
            
            std::vector<PhysicsObject*> hitObjects = hitTest();
            PhysicsRoutines::soustractVectors(hitObjects, initialHits);
            
            m_box.top -= 1;
            if (hitObjects.size() == 0) //The object doesn't touch another one
                return false;
        }
        
        return true;
    }
    
    return true;
}

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

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

std::vector<PhysicsObject*> PhysicsObject::hitTest(const Box &box)
{
    std::vector<PhysicsObject*> hitObjects;
    
    if (m_physicsWorld != NULL)
    {
        std::vector<PhysicsObject*> &allObjects = m_physicsWorld->getObjects();
        std::vector<PhysicsObject*>::iterator it;
        
        for (it = allObjects.begin(); it != allObjects.end(); it++)
        {
            if ((*it) != this)
            {
                if (box.hits( (*it)->getBox() ))
                    hitObjects.push_back((*it));
            }
        }
    }
    
    return hitObjects;
}
