#ifndef SCENE_INCLUDED
#define SCENE_INCLUDED

#include <common/node.hpp>
#include <gfx/camera.hpp>
#include <gfx/light.hpp>
#include <gfx/mesh.hpp>
#include <utils/math.hpp>

#include <filesystem>
#include <memory>
#include <string>
#include <vector>

using camera_ptr = std::shared_ptr<Camera>;
using camera_weak_ptr = std::weak_ptr<Camera>;
using light_ptr = std::shared_ptr<Light>;
using mesh_ptr = std::shared_ptr<Mesh>;

class Scene
{
private:
    std::vector<node_ptr> scene;
    std::vector<node_ptr> selections = { nullptr };
    std::vector<node_ptr> selection_AABBs = { nullptr };
    std::vector<node_ptr> side_highlights = { nullptr };
    std::vector<glm::vec3> highlight_directions  = { glm::vec3(0.0f) };
    node_ptr pre_add_AABB;


    node_ptr rotation_axis;

    node_ptr object_axes;

    bool camera_axes_enabled = false;
    node_ptr camera_axes;

    node_ptr camera_axes_debug;

    node_ptr grid;

    std::vector<light_ptr> lights;

    std::vector<camera_ptr> cameras;
    std::vector<camera_weak_ptr> active_cameras;

    node_ptr skybox;

public:
    Scene();

    void createScene();

    /*const*/ std::vector<node_ptr>& getScene() { return scene; }
    std::vector<mesh_ptr> getNodeMesh(node_ptr node) const;
    node_ptr getSelection() const { return selections[0]; }
    node_ptr getSelectionAABB() const { return selection_AABBs[0]; }
    node_ptr getPreAddAABB() const { return pre_add_AABB; }
    node_ptr getGrid() const { return grid; }
    node_ptr getHighlight() const { return side_highlights[0]; }
    node_ptr getObjectAxes() const { return object_axes; }
    node_ptr getCameraAxes() const { return camera_axes; }
    node_ptr getCameraAxesDebug() const { return camera_axes_debug; }
    node_ptr getRotationAxis() const { return rotation_axis; }

    glm::vec3 getHighlightDirection() const { return highlight_directions[0]; } 

    std::vector<light_ptr>& getLights() { return lights; }
    std::vector<camera_ptr>& getCameras() { return cameras; }
    std::vector<camera_weak_ptr>& getActiveCameras() { return active_cameras; }

    node_ptr getSkybox() const { return skybox; }

    void addActiveCamera(camera_ptr camera) { active_cameras.emplace_back(camera); }
    void removeActiveCamera(camera_ptr camera);

    void setSelection (node_ptr selection) { selections[0] = selection; }
    void setSelectionAABB (node_ptr selection_AABB) { selection_AABBs[0] = selection_AABB; }
    void setPreAddAABB (node_ptr _pre_add_AABB) { pre_add_AABB = _pre_add_AABB; }
    void setObjectAxes (node_ptr _object_axes) { object_axes = _object_axes; }
    void setRotationAxis(node_ptr _rotation_axis) { rotation_axis = _rotation_axis; }

    node_ptr addNode(node_ptr node = Node::create());
    node_ptr addNode(glm::vec3 position, glm::vec3 rotation = glm::vec3(0.0f), glm::vec3 scale = glm::vec3(1.0f));

    void removeNode(node_ptr to_remove);
    void removeNodeRec(node_ptr to_remove, std::vector<node_ptr> &where);

    node_ptr addMeshNode(const std::string &file_name, node_ptr node);
    node_ptr addMeshNode(const std::string &file_name, glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f));

    node_ptr addLightNode(glm::vec3 color = glm::vec3(1.0f, 1.0f, 1.0f), node_ptr node = Node::create());
    node_ptr addLightNode(glm::vec3 color = glm::vec3(1.0f, 1.0f, 1.0f), glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f));

    node_ptr addCameraNode(float FOV, node_ptr node);
    node_ptr addCameraNode(float FOV, glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3 rotation = glm::vec3(0.0f, 0.0f, 0.0f));

    mesh_ptr addMesh(node_ptr node, const std::string &file_name, const std::string &texture_name = "");
    void addMesh(node_ptr node, mesh_ptr mesh);
    void addMesh(node_ptr node, Mesh mesh);
    void addMesh(node_ptr node, std::vector<float> vertices, std::vector<float> normals, std::vector<unsigned int> indices);

    void removeMesh(node_ptr node, mesh_ptr mesh);
    void removeAllMesh(node_ptr node);

    void addSkyboxNode(const std::string &file_name, const std::vector<std::filesystem::path> &texture_names);

    void addLight(node_ptr node, light_ptr light);
    void addLight(node_ptr node, Light light);
    void addLight(node_ptr node, glm::vec3 color = glm::vec3(1.0f, 1.0f, 1.0f), bool is_point = true);
    void addLight(node_ptr node, glm::vec4 ambient_color, glm::vec4 diffuse_color, glm::vec4 specular_color, bool _is_point = true, bool use_rotation = false);
    void addLight(node_ptr node, glm::vec4 ambient_color, glm::vec4 diffuse_color, glm::vec4 specular_color, glm::vec3 cone_direction);

    void addCamera(node_ptr node, camera_ptr camera);
    void addCamera(node_ptr node, Camera camera);
    void addCamera(node_ptr node, float FOV);

    void addRay(glm::vec3 ray_position, glm::vec3 ray_direction);
    void addSelectionAABB(node_ptr node, std::size_t index = 0);
    void addPreAddAABB(node_ptr node);
    void addSideHighlight(node_ptr node, glm::vec3 side_direction, std::array<glm::vec3, 4> &side_vertices, std::size_t index = 0);
    void addRotationAxis(glm::vec3 axis_direction, node_ptr selected_node);
    void addObjectAxes(node_ptr node);
    void createCameraAxes();
    void createCameraAxesDebug();

    bool containsMesh(node_ptr node, mesh_ptr mesh);
    void findLights(const std::vector<node_ptr> &nodes);
    void findLightsRec(const std::vector<node_ptr> &nodes);
    void findCameras(const std::vector<node_ptr> &nodes);

    void changeDrawMode(node_ptr node, unsigned int mode);
    void manageSelection(node_ptr nearest_node);
    void manageHighlight(glm::vec3 hit_coord, node_ptr nearest_node);
    void manageAxis(glm::vec3 axis_direction, node_ptr selected_node);
    void manageObjectAxes(node_ptr selected_node);

    void manageSecondarySelection(std::vector<node_ptr> nearest_nodes);
    void manageSecondaryHighlights(std::vector<std::pair<glm::vec3, node_ptr>> hit_node_pairs);

    void manageCameraAxes(bool enabled);
    void manageCameraAxesDebug(bool enabled);
    void updateCameraAxesPosition();
    void updateCameraAxesDebugPosition();

    void toggleVisibility(node_ptr node, bool visible);
};

#endif