#include "scenenode.h"
#include "scenetree.h"
#include "glm/ext.hpp"
#include <iostream>
#include <imgui/imgui.h>
#include <SparrowRenderer/mesh.h>
#include <bullet/btBulletCollisionCommon.h>
#include <bullet/btBulletDynamicsCommon.h>
#include "tools/utils.h"

Engine &SceneNode::getEngine()
{
    return m_scene->getEngine();
}

void SceneNode::setParent(SceneNode* parent)
{
    if(m_toDestroy && parent == nullptr)
        delete this;
    else
        m_parent = parent;
}

SceneNode::SceneNode(Script* script) :
    m_toDestroy(false),
    m_parentVisible(true),
    m_visible(true),
    m_enabled(true),
    m_transformChanged(true),
    m_parent(nullptr),
    m_scene(nullptr),
    m_geometry(nullptr, glm::mat4()),
    m_light(nullptr),
    m_transform(glm::mat4()),
    m_script(script),
    m_rigidBody(nullptr),
    m_motionState(this)
{
    if(m_script != nullptr)
        m_script->begin(this);
}

SceneNode::~SceneNode()
{
    if(m_script != nullptr)
        m_script->end(this);
    setVisible(false);
    setSceneTree(nullptr);
    for(SceneNode* child : m_children)
        delete child;
}

void SceneNode::updateLightSource()
{
    switch(m_light->getType())
    {
    case Light::DIRECTIONNAL :
        {
            DirectionnalLight *l = (DirectionnalLight*)m_light;
            l->setDir(glm::mat3(m_geometry.modelMatrix) * l->getDir());
            if(l->isShadowCaster())
                l->updateShadowMap(getScene());
        }
        break;

    case Light::POINT :
        {
            PointLight *l = (PointLight*)m_light;
            l->setPos(l->getPos() + glm::vec3(m_geometry.modelMatrix[3]));
        }
        break;

    case Light::SPOT :
        // TODO
        break;

    case Light::AMBIENT :
    default:
        break;
    }
}

void SceneNode::update()
{
    for(SceneNode* node : m_nodesToRemove)
    {
        for(auto it = m_children.begin(); it != m_children.end();)
        {
            if(*it == node)
            {
                it = m_children.erase(it);
                node->setSceneTree(nullptr);
                node->setParent(nullptr);
            }
            else
                ++it;
        }
    }
    m_nodesToRemove.clear();

    for(SceneNode* node : m_nodesToAdd)
    {
        if(m_scene != nullptr)
            node->setSceneTree(m_scene);
        m_children.push_back(node);
        node->setParent(this);
        node->setParentTransform(m_transform);
        node->setParentVisible(isVisible());
    }
    m_nodesToAdd.clear();

    if(m_enabled)
    {
        if(m_transformChanged)
        {
            m_geometry.modelMatrix = m_parentTransform * m_transform;
            if(m_light != nullptr)
                updateLightSource();
        }
        if(m_script != nullptr)
            m_script->update(this);
        for(SceneNode* child : m_children)
        {
            if(m_transformChanged)
                child->setParentTransform(m_geometry.modelMatrix);
            child->update();
        }
    }
}

void SceneNode::setSceneTree(SceneTree *tree)
{
    if(isVisible())
    {
        if(m_scene != nullptr)
            m_scene->removeFromIndex(this);
        if(tree != nullptr)
            tree->addToIndex(this);
    }
    m_scene = tree;
    if(m_scene != nullptr && m_geometry.mesh != nullptr)
        m_scene->registerMeshType(m_geometry.mesh->getFlags());
    if(m_scene != nullptr && m_light != nullptr)
        m_scene->registerLightType(m_light->getFlags());
    for(SceneNode* child : m_children)
        child->setSceneTree(tree);
}

void SceneNode::resetTransform()
{
    setTransform(glm::mat4());
}

// tools
void SceneNode::moveTo(const glm::vec3 &position)
{
    m_transform = glm::translate(glm::mat4(), position);
    m_transformChanged = true;
}

void SceneNode::translate(const glm::vec3 &vector)
{
    m_transform = glm::translate(m_transform, vector);
    m_transformChanged = true;
}

void SceneNode::lookAt(const glm::vec3 &target, const glm::vec3 &upVector)
{
    glm::vec3 pos = glm::vec3(m_transform[3]);
    m_transform = glm::lookAt(pos, target, upVector);
    m_transformChanged = true;
}

void SceneNode::rotate(float angle, const glm::vec3 &vector)
{
    m_transform = glm::rotate(m_transform, angle, vector);
    m_transformChanged = true;
}

void SceneNode::scale(const glm::vec3 &scaleFactor)
{
    m_transform = glm::scale(m_transform, scaleFactor);
    m_transformChanged = true;
}

float SceneNode::getDepth()
{
    m_geometry.mesh->getDepth();
}

void SceneNode::setDepth(float depth)
{
    m_geometry.mesh->setDepth(depth);
}

// setters
void SceneNode::setTransform(const glm::mat4 &transform)
{
    m_transform = transform;
    m_transformChanged = true;
}

// could be optimised by just storing a pointer, but unexpected behavior may happen ?
void SceneNode::setParentTransform(const glm::mat4 &transform)
{
    m_parentTransform = transform;
    m_transformChanged = true;
}

void SceneNode::setVisible(bool visible)
{
    if(m_parentVisible)
    {
        if(m_visible && !visible)
            updateVisibility(false);
        if(visible && !m_visible)
            updateVisibility(true);
    }
    m_visible = visible;
}

void SceneNode::setParentVisible(bool visible)
{
    if(m_visible)
    {
        if(m_parentVisible && !visible)
            updateVisibility(false);
        if(visible && !m_parentVisible)
            updateVisibility(true);
    }
    m_parentVisible = visible;
}

void SceneNode::addChild(SceneNode *node)
{
    if(node != nullptr)
        m_nodesToAdd.push_back(node);
}

void SceneNode::removeChild(SceneNode *node)
{
    if(node != nullptr)
        m_nodesToRemove.push_back(node);
}

void SceneNode::updateVisibility(bool visible)
{
    for(SceneNode* child : m_children)
        child->setParentVisible(visible);
    if(m_scene != nullptr)
    {
        if(visible)
            m_scene->addToIndex(this);
        else
            m_scene->removeFromIndex(this);
    }
}

void SceneNode::SparrowMotionState::getWorldTransform(btTransform& worldTrans ) const
{
    worldTrans.setFromOpenGLMatrix(glm::value_ptr(m_node->getTransform()));
}

void SceneNode::SparrowMotionState::setWorldTransform(const btTransform& worldTrans)
{
    glm::mat4 t;
    worldTrans.getOpenGLMatrix(glm::value_ptr(t));
    m_node->setTransform(t);
}

void SceneNode::setRigidBody(btRigidBody* body)
{
    m_rigidBody = body;
    if(m_rigidBody != nullptr)
        m_rigidBody->setUserPointer(this);
}

SceneNode* SceneNode::clone()
{
    SceneNode* node = new SceneNode(m_script);
    node->setMesh(getMesh());
    node->setTransform(getTransform());
    node->setLight(getLight());
    if(m_rigidBody != nullptr)
        node->setRigidBody(new btRigidBody(m_rigidBody->getInvMass(), node->getMotionState(), m_rigidBody->getCollisionShape()));
    for(SceneNode* child : m_children)
        node->addChild(child->clone());
    for(SceneNode* child : m_nodesToAdd)
        node->addChild(child->clone());
    return node;
}
