#pragma once

#include <vulkan/vulkan.h>
#include <vector>

#include <array>
#include <glm/glm.hpp>
#include <unordered_map>

#include "PathUtils.h"
#include "Utils.h"
#include "SceneNode.h"
#include "descriptorSetHandling.h"
#include "playerCamera.h"
#include "GameLogic/NavGraph/NavGraphNode.h"

#include "tinyobjloader-release/tiny_obj_loader.h"
#include "ImageView.h"


/*!
 *	It matches the definition and sizes of the corresponding GPU-side struct exactly, which is used in shaders.
 */
struct UniformBuffer {
    glm::mat4 viewProjMatrixShadow; // we need this in the grphics render pass to calculate shadows
    glm::vec4 spotLightPositionWS; //w = farPlane
    glm::vec4 spotlightDirectionWS;
    glm::vec4 color;/*! A color values, stored as four floats, to properly align to 16 byte boundaries. */  
    glm::mat4 modelViewProjMatrix; /*! Storage for a matrix consisting of 16 float values (inherently aligned to 16 bytes) */
    glm::mat4 modelMatrix;
    glm::vec4 cameraPosition;
    glm::ivec4 userInput; // x = ??? y= windowWidth, z= windowHeight  , w = draw cross
    float ambientConstant;
    float diffuseConstant;
    float specularConstant;
    float shininessConstant;
};

/**
 * Represents a renderable scene object, including mesh data and uniform data for shaders.
 * It is designed to be a base class for specific types of objects that can be rendered in a scene.
 */
class SceneObject: public SceneNode {
public:
    std::vector<Vertex> vertices; // Vertex data.
    std::vector<uint32_t> indices; // Index data for indexed drawing.
    VkDevice device; // Vulkan device.
    VkPhysicalDevice physicalDevice; // Vulkan physical device.
    VkDescriptorPool descriptorPool; // Descriptor pool for allocating descriptor sets.
    VkQueue graphicsQueue; // Graphics queue for command submission.
    VkBuffer indexBuffer; // Buffer for index data.
    VkBuffer vertexBuffer; // Buffer for vertex data.
    VkBuffer uniformBuffer; // Buffer for uniform data.
    UniformBuffer ubo; // Struct instance for uniform buffer data.
    VkDescriptorSet descriptorSet; // Descriptor set for binding resources.
    VkDescriptorSetLayout descriptorSetLayout; // Layout of the descriptor set.
    VkSampler sampler;
    Geometry sceneObjectGeometry;
    GeometryData data;
    VkCommandPool cmdPool;
    bool sync;
    PFN_vkCmdPipelineBarrier2KHR pfn;


    std::shared_ptr<NavGraphNode> assignedNavNode;
    glm::vec2 objectNavOrigin;
    bool isMoving;

    std::unordered_map<int, VkDescriptorSet> materialDescriptorSets;
    std::vector<tinyobj::material_t> materials;




    
    /**
     * Constructor that initializes Vulkan resources and buffer data.
     */
    SceneObject(VkDevice dev, VkPhysicalDevice phyDev, 
                VkDescriptorPool descPool, 
                VkDescriptorSetLayout descSetLayout,
                VkQueue queue,
                const std::vector<Vertex>& initialVertices,
                const std::vector<uint32_t>& initialIndices,
                VkSampler texSampler,
                VkCommandPool commandPool,
                bool g_synchronization2_supported,
                PFN_vkCmdPipelineBarrier2KHR g_vkCmdPipelineBarrier2KHR,
                ShadowMap& shadowMap
                ):
        SceneNode(),
        device(dev), 
        physicalDevice(phyDev), 
        descriptorPool(descPool), 
        descriptorSetLayout(descSetLayout), 
        graphicsQueue(queue), 
        vertices(initialVertices), 
        indices(initialIndices), 
        sampler(texSampler),
        cmdPool(commandPool),
        sync(g_synchronization2_supported),
        pfn(g_vkCmdPipelineBarrier2KHR)

    {

        
    }

    void applyTransformationsLocal(std::vector<glm::vec3>& positions, const glm::mat4& transform) {
        for (auto& pos : positions) {
            glm::vec4 transformedPos = transform * glm::vec4(pos, 1.0f);
            pos = glm::vec3(transformedPos);
        }
    }



    void setIsMoving(bool isMoving){
        this->isMoving = isMoving;
    }

    glm::vec2 getObjectNavOrigin(){
        return objectNavOrigin;
    }

    void setObjectNavOrigin(glm::vec2 origin){
        VKL_LOG("Nav origin set" + glm::to_string(origin));
        objectNavOrigin = origin;
    }


    void setAssignedNavNode (std::shared_ptr <NavGraphNode> node){
        this->assignedNavNode = node;
    }

    void startPath(const std::vector<std::shared_ptr<NavGraphNode>>& path) {
        this->path = path;
        this->currentPathIndex = 0;
        this->isMoving = true;
    }

    std::shared_ptr<NavGraphNode> getCurrentNavNode() const {
        if (!path.empty() && currentPathIndex < path.size()) {
            return path[currentPathIndex];
        }
        return assignedNavNode;
    }


    void updatePosition(const FrameTimer& frametimer) {

        float deltaTime = frametimer.timePassedSinceLastFrame;
        if (!isMoving || currentPathIndex >= path.size()) {
            return; 
        }

        glm::vec2 targetPos = path[currentPathIndex]->getPosition();
        glm::vec3 currentPos = glm::vec3(this->physicsObject->getPosition()); 

        float baseMovementSpeed = 1.0f; 
        float movementSpeed = baseMovementSpeed * deltaTime;
        glm::vec2 currentPos2D(currentPos.x, currentPos.z);
        glm::vec2 targetPos2D(targetPos.x, targetPos.y);
        glm::vec2 direction2D = glm::normalize(targetPos2D - currentPos2D);
        glm::vec3 movement = glm::vec3(direction2D.x, 0, direction2D.y) * movementSpeed;

        if (glm::distance(currentPos2D, targetPos2D) > movementSpeed) {
            // If not close to target, apply calculated movement
            glm::mat4 translation = glm::translate(glm::mat4(1.0f), movement);
            localTransform = this->physicsObject->getModelMatrix() * translation;
        } else {
            // If close to or at the target, finalize position
            localTransform = glm::translate(this->physicsObject->getModelMatrix(), glm::vec3(targetPos2D.x - currentPos2D.x, 0, targetPos2D.y - currentPos2D.y));
            currentPathIndex++;
            if (currentPathIndex >= path.size()) {
                isMoving = false;
                setObjectState(ObjectState::PhysicsControlled);
            }
        }

        physicsObject->updateKinematicPosition(physicsObject->mRigidBody, localTransform[3]);
    }

    /**
     * Updates the uniform buffer with transformation matrices and other data.
     * This function should be called every frame to update shader data based on transformations.
     */
    virtual void updateUniformBuffer(Student::CameraForGLFW& camera, const glm::vec4& windowSize, const FrameTimer& frameTimer) {       
          
        

        ubo.modelMatrix = getWorldTransform();  // Use world transformation
        ubo.modelViewProjMatrix = camera.getViewProjectionMatrix() * getWorldTransform();  // Combine with the camera matrix
        ubo.cameraPosition = glm::vec4(camera.getPosition(), 1.0);
        ubo.userInput = glm::ivec4(0, windowSize.x, windowSize.y, 0 ); // last component is 1 if screequad is rendered
        vklCopyDataIntoHostCoherentBuffer(this->uniformBuffer, &ubo, sizeof(ubo));

        for (auto& child : children) {
            child->updateUniformBuffer(camera, windowSize, frameTimer);
        }
    }

    virtual void updateUniformBufferForShadow(ShadowMap shadowMap, Spotlight spotlight) {
        // Update viewProjectionMatrix
        glm::mat4 viewMatrixLightSource = glm::lookAt(glm::vec3(spotlight.position), glm::vec3(spotlight.position) + glm::normalize(glm::vec3(spotlight.direction)), glm::vec3(spotlight.up));
        float aspectRatio = (float)shadowMap.SHADOW_MAP_WIDTH / (float)shadowMap.SHADOW_MAP_HEIGHT;
        glm::mat4 projectionMatrixLightSource = gcgCreatePerspectiveProjectionMatrix(glm::radians(spotlight.fov), aspectRatio, shadowMap.m_near_plane, shadowMap.m_far_plane);

        // Update data for 2nd renderpass
        ubo.viewProjMatrixShadow = projectionMatrixLightSource * viewMatrixLightSource; // we need this in the grphics render pass to claculate shadows
        ubo.spotLightPositionWS = spotlight.position;
        ubo.spotLightPositionWS.w = shadowMap.m_far_plane;
        ubo.spotlightDirectionWS = spotlight.direction;
        vklCopyDataIntoHostCoherentBuffer(this->uniformBuffer, &ubo, sizeof(ubo));

        // update children
        for (auto& child : children) {
            child->updateUniformBufferForShadow(shadowMap, spotlight);
        }
    }

    virtual void recordRenderingCommandsForShadowMap(ShadowMap& shadowMap, const Spotlight& spotlight)
    {
        drawObjectShadow(shadowMap);

        // Draw children
        for (auto& child : children)
        {
            child->recordRenderingCommandsForShadowMap(shadowMap, spotlight);
        }
    }

    bool load_from_obj(const char* filename) {
        VKL_LOG("IN OBJ LOADER");

        // Use absolute path for the OBJ file
        std::string full_path = std::filesystem::absolute(filename).string();
        std::cout << "Trying to load file: " << full_path << std::endl;

        if (!std::filesystem::exists(full_path)) {
            std::cerr << "File does not exist: " << full_path << std::endl;
            return false;
        }

        tinyobj::attrib_t attrib;
        std::vector<tinyobj::shape_t> shapes;
        //std::vector<tinyobj::material_t> materials;
        std::string warn, err;
        std::unordered_map<std::string, ImageAndView> texture_map;


        // Extract directory path from filename for material file search
        std::string base_dir = full_path.substr(0, full_path.find_last_of("/\\"));

        std::cout << "Base directory for materials: " << base_dir << std::endl;

        if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, full_path.c_str(), base_dir.c_str())) {
            std::cerr << "Failed to load/parse .obj file: " << err << std::endl;
            return false;
        }

        if (!warn.empty()) {
            std::cout << "WARN: " << warn << std::endl;
        }


        //GeometryData data;

        vertices.clear();
        indices.clear();

        std::unordered_map<Vertex, uint32_t> uniqueVertices{};
        for (const auto& shape : shapes) {
        for (const auto& index : shape.mesh.indices) {
            Vertex vertex{};
            vertex.position = {
                attrib.vertices[3 * index.vertex_index + 0],
                attrib.vertices[3 * index.vertex_index + 1],
                attrib.vertices[3 * index.vertex_index + 2]
            };
            
            if (index.normal_index >= 0) {
                vertex.normal = {
                    attrib.normals[3 * index.normal_index + 0],
                    attrib.normals[3 * index.normal_index + 1],
                    attrib.normals[3 * index.normal_index + 2],
                };
            } else {
                vertex.normal = { 0.0f, 0.0f, 0.0f };
            }
            

            vertex.texCoord = {
                attrib.texcoords[2 * index.texcoord_index + 0],
                1.0f - attrib.texcoords[2 * index.texcoord_index + 1]
            };

            
            

            
            if (uniqueVertices.count(vertex) == 0) {
                uniqueVertices[vertex] = static_cast<uint32_t>(vertices.size());
                vertices.push_back(vertex);

                data.positions.push_back(vertex.position);
                data.normals.push_back(vertex.normal);
                data.textureCoordinates.push_back(vertex.texCoord);
            }

            indices.push_back(uniqueVertices[vertex]);
            data.indices.push_back(uniqueVertices[vertex]);
        }
    }
        VKL_LOG("LOADING MESH");

        //calculate tangents

        std::vector<glm::vec3> tan1(vertices.size(), glm::vec3(0.0f));
        

        for (size_t i = 0; i < indices.size(); i += 3) {
            Vertex& v0 = vertices[indices[i]];
            Vertex& v1 = vertices[indices[i + 1]];
            Vertex& v2 = vertices[indices[i + 2]];

            glm::vec3 edge1 = v1.position - v0.position;
            glm::vec3 edge2 = v2.position - v0.position;

            glm::vec2 deltaUV1 = v1.texCoord - v0.texCoord;
            glm::vec2 deltaUV2 = v2.texCoord - v0.texCoord;

            float f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

            glm::vec3 tangent;
            tangent.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
            tangent.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
            tangent.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);

            tan1[indices[i]] += tangent;
            tan1[indices[i + 1]] += tangent;
            tan1[indices[i + 2]] += tangent;
        }

        for (size_t i = 0; i < vertices.size(); ++i) {
            vertices[i].tangent = glm::normalize(tan1[i]);
            data.tangents.push_back(glm::normalize(tan1[i]));
        }


        return true;
    }

    void applyTransformations(std::vector<glm::vec3>& positions, glm::mat4 rotation, glm::mat4 scale, glm::mat4 translate) {

        for (auto& pos : positions) {
            glm::vec4 transformedPos = translate * rotation * scale * glm::vec4(pos, 1.0f);
            pos = glm::vec3(transformedPos);
        }
    }

    void drawObject(VkPipeline pipeline) {
        VkCommandBuffer cb = vklGetCurrentCommandBuffer();
        VkPipelineLayout pipeline_layout = vklGetLayoutForPipeline(pipeline);

        for (const auto& [materialID, descriptorSet] : this->materialDescriptorSets) {
            vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptorSet, 0, nullptr);
            drawGeometryWithMaterial(pipeline, this->sceneObjectGeometry, descriptorSet);
        }
    }

    void drawObjectShadow(ShadowMap& shadowMap)
    {
        VkCommandBuffer cb = shadowMap.m_command_buffer;
        VkPipelineLayout pipeline_layout = shadowMap.m_pipeline_layout;
        Geometry geometry = this->sceneObjectGeometry;
        
        if (materialDescriptorSets.size() > 0)
        {
            for (const auto& [materialID, descriptorSet] : this->materialDescriptorSets) {
                vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptorSet, 0, nullptr);

                // Record the draw call into the command buffer, which uses vertex and index buffers of the geometry:
                vklCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowMap.m_pipeline);
                if (VK_NULL_HANDLE == geometry.colorsBuffer) {
                    VkBuffer vertex_buffers[4] = { geometry.positionsBuffer, geometry.normalsBuffer, geometry.textureCoordinatesBuffer, geometry.tangentBuffer };
                    VkDeviceSize offsets[4] = { 0, 0, 0, 0 };
                    vkCmdBindVertexBuffers(cb, 0u, 4u, vertex_buffers, offsets);
                }
                else {
                    VkBuffer vertex_buffers[3] = { geometry.positionsBuffer, geometry.normalsBuffer, geometry.colorsBuffer };
                    VkDeviceSize offsets[3] = { 0, 0, 0 };
                    vkCmdBindVertexBuffers(cb, 0u, 3u, vertex_buffers, offsets);
                }

                vkCmdBindIndexBuffer(cb, geometry.indicesBuffer, 0, VK_INDEX_TYPE_UINT32);
                vkCmdDrawIndexed(cb, geometry.numberOfIndices, 1, 0u, 0u, 0u);
            }
        }
        else
        {
            vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, 1, &descriptorSet, 0, nullptr);

            // Record the draw call into the command buffer, which uses vertex and index buffers of the geometry:
            vklCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, shadowMap.m_pipeline);
            if (VK_NULL_HANDLE == geometry.colorsBuffer) {
                VkBuffer vertex_buffers[4] = { geometry.positionsBuffer, geometry.normalsBuffer, geometry.textureCoordinatesBuffer, geometry.tangentBuffer };
                VkDeviceSize offsets[4] = { 0, 0, 0, 0 };
                vkCmdBindVertexBuffers(cb, 0u, 4u, vertex_buffers, offsets);
            }
            else {
                VkBuffer vertex_buffers[3] = { geometry.positionsBuffer, geometry.normalsBuffer, geometry.colorsBuffer };
                VkDeviceSize offsets[3] = { 0, 0, 0 };
                vkCmdBindVertexBuffers(cb, 0u, 3u, vertex_buffers, offsets);
            }

            vkCmdBindIndexBuffer(cb, geometry.indicesBuffer, 0, VK_INDEX_TYPE_UINT32);
            vkCmdDrawIndexed(cb, geometry.numberOfIndices, 1, 0u, 0u, 0u);
        }

    }

    void drawGeometryWithMaterial(VkPipeline pipeline, const Geometry& geometry, VkDescriptorSet material, uint32_t num_instances = 1u) {
       
        // Get the current command buffer:
        VkCommandBuffer cb = vklGetCurrentCommandBuffer();

        // Record binding the descriptor set for subsequent draw calls into the command buffer:
        VkPipelineLayout pipeline_layout = vklGetLayoutForPipeline(pipeline);
        vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0u, 1u, &material, 0u, nullptr);

        // Record the draw call into the command buffer, which uses vertex and index buffers of the geometry:
        vklCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
        VkBuffer vertex_buffers[4] = {geometry.positionsBuffer, geometry.normalsBuffer, geometry.textureCoordinatesBuffer, geometry.tangentBuffer};
        VkDeviceSize offsets[4] = {0, 0, 0, 0};
        vkCmdBindVertexBuffers(cb, 0u, 4u, vertex_buffers, offsets);
        vkCmdBindIndexBuffer(cb, geometry.indicesBuffer, 0, VK_INDEX_TYPE_UINT32);
        vkCmdDrawIndexed(cb, geometry.numberOfIndices, num_instances, 0u, 0u, 0u);
    }
    

     /**
     * Destructor that cleans up Vulkan resources.
     */
    ~SceneObject() {
        vklDestroyHostCoherentBufferAndItsBackingMemory(vertexBuffer);
        vklDestroyHostCoherentBufferAndItsBackingMemory(indexBuffer);
        vklDestroyHostCoherentBufferAndItsBackingMemory(uniformBuffer);
    }


    private: 
    std::vector<std::shared_ptr<NavGraphNode>> path;
    size_t currentPathIndex;
    

    

};



/**
 * Represents a renderable scene object with physiscs simulation funtionality, 
 * including mesh data and uniform data for shaders and bounding box for collision detection
 * It is designed to be a base class for specific types of objects that can be rendered in a scene.
 */
class SceneObjectWithPhysics : public SceneObject {
public:
    glm::vec3 mCollisionShapeExtents = glm::vec3(1,1,1);

    /**
     * Constructor that initializes Vulkan resources and buffer data.
     */
    SceneObjectWithPhysics(VkDevice dev, VkPhysicalDevice phyDev, 
                           VkDescriptorPool descPool, 
                           VkDescriptorSetLayout descSetLayout,
                           VkQueue queue,
                           const std::vector<Vertex>& initialVertices,
                           const std::vector<uint32_t>& initialIndices,
                           Physics::GamePhysicsManager& inputPhysicsManager,
                           const glm::vec3& initialCollisionBoxPosition,
                           const glm::vec3& initialCollisionBoxHalfSidelengths,
                           ShadowMap& shadowMap,
                           const float& inputMass = 0.f,
                           const Physics::ShapeType& collisionShape = Physics::ShapeType::CUBE):
        SceneObject(dev, phyDev, descPool, descSetLayout, queue, initialVertices, initialIndices, sampler, cmdPool, sync, pfn, shadowMap) 
    {
        physicsObject = std::make_shared<Physics::PhysicsObject>(
            inputPhysicsManager, initialCollisionBoxPosition, initialCollisionBoxHalfSidelengths, inputMass);
        attachPhysicsObject(physicsObject);
        glm::vec3 position = this->physicsObject->getPosition();
        glm::vec2 navOrigin(position.x, position.z);    
        setObjectNavOrigin(navOrigin);
        

    }

    std::shared_ptr<Physics::PhysicsObject> getAttachedPhysicssObj(){
        return physicsObject;
    }
};


