#pragma once

#include <vulkan/vulkan.h>
#include <vector>
#include <memory>
#include "SceneObject.h"
#include "geometry.h"
#include "TextureLoader.h"

/**
 * Represents a street object within a Vulkan-rendered scene.
 * Inherits from SceneObject to leverage common object properties and functionalities.
 */
class WallObject : public SceneObjectWithPhysics {
public:
    glm::mat4 modelMatrix = glm::mat4(1.0f);


    /**
     * Constructor for the StreetNode.
     * Initializes the street geometry and invokes the base SceneObject constructor.
     * 
     * @param dev The Vulkan device.
     * @param phyDev The physical Vulkan device.
     * @param descPool The descriptor pool for allocating descriptor sets.
     * @param descSetLayout The layout of the descriptor set.
     * @param queue The Vulkan queue to which rendering commands will be submitted.
     */
    WallObject(VkDevice dev, VkPhysicalDevice phyDev,
    VkDescriptorPool descPool, 
    VkDescriptorSetLayout descSetLayout,
    VkQueue queue,
    Physics::GamePhysicsManager& inputPhysicsManager,
    VkSampler sampler,
    VkSampler normalSampler,
    VkSampler normalheightSampler,
    VkCommandPool cmdPool,
    bool sync,
    PFN_vkCmdPipelineBarrier2KHR pfn,
    ShadowMap& shadowMap,
    const glm::vec3& initialCollisionBoxPosition,
    const glm::vec3& initialCollisionBoxHalfSidelengths,
    TextureLoader& textureLoader,
    int wallIndex,
    const float& inputMass = 0.f)
    : SceneObjectWithPhysics(dev, phyDev, descPool, descSetLayout, queue,
    {
                // Vertices for the top and bottom face of the pizza box
                {{-0.1f, 0.1f, 0.251f}, {1.0f, 0.0f, 0.0f}},  // Top face top-left corner
                {{0.1f, 0.1f, 0.251f}, {0.0f, 1.0f, 0.0f}},   // Top face top-right corner
                {{0.1f, 0.1f, -0.251f}, {0.0f, 0.0f, 1.0f}},  // Top face bottom-right corner
                {{-0.1f, 0.1f, -0.251f}, {1.0f, 1.0f, 1.0f}}, // Top face bottom-left corner
                {{-0.1f, -0.1f, 0.251f}, {1.0f, 1.0f, 0.0f}}, // Bottom face top-left corner
                {{0.1f, -0.1f, 0.251f}, {0.0f, 0.0f, 0.0f}},  // Bottom face top-right corner
                {{0.1f, -0.1f, -0.251f}, {1.0f, 0.0f, 1.0f}}, // Bottom face bottom-right corner
                {{-0.1f, -0.1f, -0.251f}, {0.0f, 1.0f, 1.0f}} // Bottom face bottom-left corner
            },
            // Indices for drawing the box with triangles
            {
                0, 1, 2, 0, 2, 3, // Top face
                4, 5, 6, 4, 6, 7, // Bottom face
                0, 4, 5, 0, 5, 1, // Side faces
                1, 5, 6, 1, 6, 2,
                2, 6, 7, 2, 7, 3,
                3, 7, 4, 3, 4, 0
            },
    
            inputPhysicsManager, initialCollisionBoxPosition, initialCollisionBoxHalfSidelengths, shadowMap, inputMass)
    {
        data.positions = { {-70.0f,  10.0f, 0.0f}, {-70.0f, -10.0f, 0.0f}, { 70.0f,  10.0f, 0.0f}, {70.0f, -10.0f, 0.0f} };
        data.textureCoordinates = { {0,1}, {0,0}, {7,1}, {7,0} };
        data.indices = { 0, 1, 2, 1, 2, 3 };
        data.normals = {
            {0.0f, 0.0f, 1.0f}, // Normal for all vertices (facing forward)
            {0.0f, 0.0f, 1.0f},
            {0.0f, 0.0f, 1.0f},
            {0.0f, 0.0f, 1.0f}
        };
        
            // Calculate tangents for each face
        data.tangents.resize(data.positions.size());

        for (size_t i = 0; i < data.indices.size(); i += 3) {
            glm::vec3 pos1 = data.positions[data.indices[i]];
            glm::vec3 pos2 = data.positions[data.indices[i + 1]];
            glm::vec3 pos3 = data.positions[data.indices[i + 2]];

            glm::vec2 uv1 = data.textureCoordinates[data.indices[i]];
            glm::vec2 uv2 = data.textureCoordinates[data.indices[i + 1]];
            glm::vec2 uv3 = data.textureCoordinates[data.indices[i + 2]];

            glm::vec3 edge1 = pos2 - pos1;
            glm::vec3 edge2 = pos3 - pos1;
            glm::vec2 deltaUV1 = uv2 - uv1;
            glm::vec2 deltaUV2 = uv3 - uv1;

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

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

            data.tangents[data.indices[i]] = tangent;
            data.tangents[data.indices[i + 1]] = tangent;
            data.tangents[data.indices[i + 2]] = tangent;
            };
            
            this->sceneObjectGeometry = createAndUploadIntoGpuMemory(this->data);

            uniformBuffer = vklCreateHostCoherentBufferWithBackingMemory(
                sizeof(UniformBuffer), VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT
            );

            ubo.ambientConstant = 0.2f;
            ubo.diffuseConstant = 0.8f;
            ubo.specularConstant = 0.3f;
            ubo.shininessConstant = 32.0f;

            // Load texture data into provided structs
            TextureData textureDataAlbedo(dev);
            textureLoader.load("assets/textures/brickwall.jpg", textureDataAlbedo);
            TextureData textureDataSpecular(dev);
            textureLoader.load("assets/textures/brickwall_specular.png", textureDataSpecular);
            TextureData textureDataNormal(dev);
            textureLoader.load("assets/textures/brickwall_normal.jpg", textureDataNormal);

            descriptorSet = allocDescriptorSet(dev, descPool, descriptorSetLayout);
            writeDescriptorSet(device, descriptorSet, uniformBuffer,
                textureDataAlbedo.textureImageView, textureDataAlbedo.textureSampler,
                textureDataSpecular.textureImageView, textureDataSpecular.textureSampler,
                textureDataNormal.textureImageView, textureDataNormal.textureSampler);
            
            // write shadowmap to correct binding in the descriptorset (default binding = 4)
            writeDescriptorSet(device, descriptorSet, shadowMap.m_image_view, shadowMap.m_image_sampler, shadowMap.m_descriptorBinding);
            
            glm::mat4 rotation;
            switch (wallIndex)
            {
                case 0:
                    modelMatrix =  glm::translate(glm::mat4(1), glm::vec3(0, 0, -65));
                    break;
                case 1: 
                    rotation = glm::rotate(glm::mat4(1.0f), glm::radians(90.0f), glm::vec3(0.0f,1.0f,0.0f));
                    modelMatrix = glm::translate(rotation, glm::vec3(0, 0, -60));
                    break;
                case 2:
                    modelMatrix = glm::translate(glm::mat4(1), glm::vec3(0, 0, 65));
                    break;
                case 3:
                    rotation = glm::rotate(glm::mat4(1.0f), glm::radians(90.0f), glm::vec3(0.0f, 1.0f, 0.0f));
                    modelMatrix = glm::translate(rotation, glm::vec3(0, 0, 60));
                    break;

            
                default: modelMatrix = glm::mat4(1); break;
            }          
    }


    /**
     * Records the Vulkan rendering commands for the street object.
     * Binds the necessary Vulkan resources and issues the draw call.
     * 
     * @param commandBuffer The command buffer to record the rendering commands.
     * @param pipeline_layout The pipeline layout used for descriptor binding.
     * @param pipeline The graphics pipeline to use for rendering the street.
     */
    void recordRenderingCommands(VkCommandBuffer commandBuffer, VkPipeline hudPipeline, VkPipeline specularPipeline, VkPipeline normalPipeline, VkPipeline normalheightPipeline, const FrameTimer& frameTimer) {
        drawGeometryWithMaterial(normalPipeline, this->sceneObjectGeometry, descriptorSet);
    }

    virtual void updateUniformBuffer(Student::CameraForGLFW& camera, const glm::vec4& windowSize, const FrameTimer& frameTimer) {
        ubo.modelMatrix = modelMatrix;  // Use world transformation
        ubo.modelViewProjMatrix = camera.getViewProjectionMatrix() * modelMatrix;  // 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);
        }
    }
};