/*
 * Copyright 2023 TU Wien, Institute of Visual Computing & Human-Centered Technology.
 * This file is part of the GCG Lab Framework and must not be redistributed.
 *
 * Original version created by Lukas Gersthofer and Bernhard Steiner.
 * Vulkan edition created by Johannes Unterguggenberger (junt@cg.tuwien.ac.at).
 */
#include "PathUtils.h"
#include "Utils.h"

#include <vulkan/vulkan.h>

#include <vector>
#include <array>
#include <sstream>
#include <random>
#include <ctime>

#include "playerCamera.h"
#include "vulkanInitialization.h"
#include "light.h"
#include "descriptorsetHandling.h"
#include "inputManager.h"
#include "geometry.h"
#include "frameTimer.h"
#include "GameLogic/SceneGraph/SceneNode.h"
#include "GameLogic/SceneGraph/StreetNode.h"
#include "GameLogic/SceneGraph/HouseObject.h"
#include "GameLogic/SceneGraph/RobotObjects.h"
#include "GameLogic/SceneGraph/PizzaObject.h"
#include "GameLogic/SceneGraph/crossObject.h"
#include "GameLogic/SceneGraph/GunObject.h"
#include "GameLogic/SceneGraph/SpaceshipObject.h"
#include "GameLogic/SceneGraph/ScooterObject.h"
#include "GameLogic/SceneGraph/HUDObject.h"
#include "GameLogic/SceneGraph/TreeObject.h"
#include "GameLogic/SceneGraph/startScreen.h"
#include "GameLogic/SceneGraph/RockObject.h"
#include "GameLogic/SceneGraph/WallObject.h"
#include "physics.h"
#include "GameLogic/NavGraph/NavGraph.h"
#include "GameLogic/MovingLogic copy/MovementManager.h"
#include <tchar.h>
#include <chrono>
#include "pizzaManager.h"

#undef min
#undef max

PFN_vkCmdPipelineBarrier2KHR g_vkCmdPipelineBarrier2KHR;
bool g_synchronization2_supported;


// Initial rendering values
int cullingMode = 0; //culling mode set to NONE 3.3
bool  wireframeMode = false;
bool normalsMode = false;
std::string init_renderer_filepath = "assets/settings/renderer_standard.ini";




void errorCallbackFromGlfw(int error, const char* description) { std::cout << "GLFW error " << error << ": " << description << std::endl; }

/* --------------------------------------------- */
// Main
/* --------------------------------------------- */
int main(int argc, char** argv) {

    CMDLineArgs cmdline_args;
    gcgParseArgs(cmdline_args, argc, argv);

    /* --------------------------------------------- */
    // Load Display Settings From File
    /* --------------------------------------------- */
    int window_width = 800;
    int window_height = 800;
    bool fullscreen = false;
    std::string window_title = "Alien Robot Pizza Panic";
    INIReader window_reader("assets/settings/window.ini");
    window_width = window_reader.GetInteger("window", "width", 800);
    window_height = window_reader.GetInteger("window", "height", 800);
    fullscreen = window_reader.GetBoolean("window", "fullscreen", false);
    window_title = window_reader.Get("window", "title", "Alien Robot Pizza Panic");
    std::string init_camera_filepath = "/settings/camera_front.ini";
     
    /* --------------------------------------------- */
    // Load Renderer Settings From File
    /* --------------------------------------------- */
    if (cmdline_args.init_renderer) 
        init_renderer_filepath = cmdline_args.init_renderer_filepath;
    INIReader renderer_reader(init_renderer_filepath);
    bool depthtest = renderer_reader.GetBoolean("renderer", "depthtest", true);
    bool as_wireframe = renderer_reader.GetBoolean("renderer", "wireframe", false);
    bool with_backface_culling = renderer_reader.GetBoolean("renderer",
    "backface_culling", false);
    bool wireframe_renderer = as_wireframe;
    bool culling_renderer = with_backface_culling;
    if (culling_renderer) {
        cullingMode = 1;
    }
    bool draw_normals = renderer_reader.GetBoolean("renderer", "normals", false);
    normalsMode = draw_normals;

    /* --------------------------------------------- */
    // Subtask 1.2: Create a Window with GLFW
    /* --------------------------------------------- */
    // Install a callback function, which gets invoked whenever a GLFW error occurred.
    // This callback function gets invoked by GLFW whenever a GLFW error occured.
    
    glfwSetErrorCallback(errorCallbackFromGlfw);

    if (!glfwInit()) {
        VKL_EXIT_WITH_ERROR("Failed to init GLFW");
    }

    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); // No need to create a graphics context for Vulkan
    glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);

    // Use a monitor if we'd like to open the window in fullscreen mode:
    GLFWmonitor* monitor = nullptr;
    if (fullscreen) {
        monitor = glfwGetPrimaryMonitor();
    }

    GLFWwindow* window = nullptr;
    window = glfwCreateWindow(window_width, window_height, window_title.c_str(), monitor, nullptr);

    VkResult result;
    VkInstance vk_instance = VK_NULL_HANDLE;             
    VkSurfaceKHR vk_surface = VK_NULL_HANDLE;             
    VkPhysicalDevice vk_physical_device = VK_NULL_HANDLE; 
    VkDevice vk_device = VK_NULL_HANDLE;                  
    VkQueue vk_queue = VK_NULL_HANDLE;                    
    VkSwapchainKHR vk_swapchain = VK_NULL_HANDLE;

    /* --------------------------------------------- */
    // Subtask 1.3: Create a Vulkan Instance
    /* --------------------------------------------- */
    VkApplicationInfo application_info = {};                     // Zero-initialize every member
    application_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; // Set this struct instance's type
    application_info.pEngineName = "GCG_VK_Library";             // Set some properties...
    application_info.engineVersion = VK_MAKE_API_VERSION(0, 2023, 9, 1);
    application_info.pApplicationName = "Alien Robot Pizza Panic";
    application_info.applicationVersion = VK_MAKE_API_VERSION(0, 2023, 9, 19);
    application_info.apiVersion = VK_API_VERSION_1_1;            // Your system needs to support this Vulkan API version.

    VkInstanceCreateInfo instance_create_info = {};                      // Zero-initialize every member
    instance_create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; // Set the struct's type
    instance_create_info.pApplicationInfo = &application_info;

    // A vector to hold all our requested instance extensions:
    std::vector<const char*> instance_extensions;

    // Query extensions which are required by GLFW:
    uint32_t glfw_instance_extensions_count;
    const char** glfw_instance_extensions_names = glfwGetRequiredInstanceExtensions(&glfw_instance_extensions_count);
    // And add them all to our vector:
    for (uint32_t i = 0; i < glfw_instance_extensions_count; ++i) {
        addInstanceExtensionToVectorIfSupported(glfw_instance_extensions_names[i], instance_extensions);
    }

    // Query extensions which are required by the framework:
    uint32_t framework_instance_extensions_count;
    const char** framework_instance_extensions_names = vklGetRequiredInstanceExtensions(&framework_instance_extensions_count);
    // And add them all to our vector:
    for (uint32_t i = 0; i < framework_instance_extensions_count; ++i) {
        addInstanceExtensionToVectorIfSupported(framework_instance_extensions_names[i], instance_extensions);
    }
#ifdef __APPLE__
    addInstanceExtensionToVectorIfSupported(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, instance_extensions);
#endif

    // Set info in the VkInstanceCreateInfo struct:
    instance_create_info.enabledExtensionCount = instance_extensions.size();
    instance_create_info.ppEnabledExtensionNames = instance_extensions.data();

    // A vector to hold all our requested validation layers, add standard validation, and set info in the VkInstanceCreateInfo struct:
    std::vector<const char*> enabled_layer_names;
    addValidationLayerNameToVectorIfSupported("VK_LAYER_KHRONOS_validation", enabled_layer_names);
    instance_create_info.enabledLayerCount = enabled_layer_names.size();
    instance_create_info.ppEnabledLayerNames = enabled_layer_names.data();
#ifdef __APPLE__
    instance_create_info.flags = VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
#endif

    // Create the instance
    result = vkCreateInstance(&instance_create_info, nullptr, &vk_instance);
    VKL_CHECK_VULKAN_RESULT(result);
    if (!vk_instance) {
        VKL_EXIT_WITH_ERROR("No VkInstance created or handle not assigned.");
    }

    /* --------------------------------------------- */
    // Subtask 1.4: Create a Vulkan Window Surface
    /* --------------------------------------------- */
    result = glfwCreateWindowSurface(vk_instance, window, nullptr, &vk_surface);
    VKL_CHECK_VULKAN_RESULT(result);
    if (!vk_surface) {
        VKL_EXIT_WITH_ERROR("No VkSurfaceKHR created or handle not assigned.");
    }

    /* --------------------------------------------- */
    // Subtask 1.5: Pick a Physical Device
    /* --------------------------------------------- */
    // Query the number of physical devices:
    uint32_t physical_devices_count;
    vkEnumeratePhysicalDevices(vk_instance, &physical_devices_count, nullptr);

    if (physical_devices_count == 0) {
        VKL_EXIT_WITH_ERROR("Vulkan does not recognize any physical devices.");
    }

    std::vector<VkPhysicalDevice> physical_devices(physical_devices_count);
    vkEnumeratePhysicalDevices(vk_instance, &physical_devices_count, physical_devices.data());

    uint32_t selected_physical_device_index = selectPhysicalDeviceIndex(physical_devices, vk_surface);
    vk_physical_device = physical_devices[selected_physical_device_index];
    if (!vk_physical_device) {
        VKL_EXIT_WITH_ERROR("No VkPhysicalDevice selected or handle not assigned.");
    }

    /* --------------------------------------------- */
    // Subtask 1.6: Select a Queue Family
    /* --------------------------------------------- */
    std::array<float, 1> queue_priorities = {1.0f};

    uint32_t selected_queue_family_index = selectQueueFamilyIndex(vk_physical_device, vk_surface);
    VkDeviceQueueCreateInfo device_queue_create_info = {};
    device_queue_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    device_queue_create_info.queueFamilyIndex = selected_queue_family_index;
    device_queue_create_info.queueCount = 1u;
    device_queue_create_info.pQueuePriorities = queue_priorities.data();
    

    //3.1 create struct for wireframe mode
    VkPhysicalDeviceFeatures device_features = {};
    device_features.fillModeNonSolid = true;

    // Sanity check if we have selected a valid queue family index:
    uint32_t queue_family_count = 0;
    vkGetPhysicalDeviceQueueFamilyProperties(vk_physical_device, &queue_family_count, nullptr);
    if (selected_queue_family_index >= queue_family_count) {
        VKL_EXIT_WITH_ERROR("Invalid queue family index selected.");
    }

    /* --------------------------------------------- */
    // Subtask 1.7: Create a Logical Device and Get Queue
    /* --------------------------------------------- */
        std::vector<const char*> device_extensions;
    addDeviceExtensionToVectorIfSupported(VK_KHR_SWAPCHAIN_EXTENSION_NAME, vk_physical_device, device_extensions);
    addDeviceExtensionToVectorIfSupported(VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME, vk_physical_device, device_extensions);
    // Looks like SDK 1.2.170 also requires
    addDeviceExtensionToVectorIfSupported(VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME, vk_physical_device, device_extensions);
    // See if this device supports Synchronization2, and if so, set a flag to indicate that we are going to use Synchronization2:
    if (std::find(std::begin(device_extensions), std::end(device_extensions), std::string(VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME)) !=
        std::end(device_extensions)) {
        g_synchronization2_supported = true;
    }
#ifdef __APPLE__
    addDeviceExtensionToVectorIfSupported("VK_KHR_portability_subset", vk_physical_device, device_extensions);
#endif

    VkDeviceCreateInfo device_create_info = {};
    device_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
    // Add information about queues to be created, and extensions to be enabled:
    device_create_info.queueCreateInfoCount = 1u;
    device_create_info.pQueueCreateInfos = &device_queue_create_info;
    device_create_info.enabledExtensionCount = device_extensions.size();
    device_create_info.ppEnabledExtensionNames = device_extensions.data();

    VkPhysicalDeviceFeatures enabled_physical_device_features = {};
    enabled_physical_device_features.fillModeNonSolid = VK_TRUE;
    device_create_info.pEnabledFeatures = &enabled_physical_device_features;

    // Enable Synchronization2 and hook it into the pNext chain:
    VkPhysicalDeviceSynchronization2FeaturesKHR physical_device_sync2_features = {};
    physical_device_sync2_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SYNCHRONIZATION_2_FEATURES_KHR;
    physical_device_sync2_features.synchronization2 = VK_TRUE;
    if (g_synchronization2_supported) {
        device_create_info.pNext = &physical_device_sync2_features;
    }

    // Create the logical device
    result = vkCreateDevice(vk_physical_device, &device_create_info, nullptr, &vk_device);
    VKL_CHECK_VULKAN_RESULT(result);

    if (g_synchronization2_supported) {
        auto* procAddr = vkGetDeviceProcAddr(vk_device, "vkCmdPipelineBarrier2KHR");
        if (procAddr == nullptr) {
            // Couldn't get the function pointer to vkCmdPipelineBarrier2KHR
            g_synchronization2_supported = false;
        } else {
            g_vkCmdPipelineBarrier2KHR = reinterpret_cast<PFN_vkCmdPipelineBarrier2KHR>(procAddr);
        }
    }
    if (!vk_device) {
        VKL_EXIT_WITH_ERROR("No VkDevice created or handle not assigned.");
    }

    // Get the handle of the queue that was requested:
    vkGetDeviceQueue(vk_device, selected_queue_family_index, 0u, &vk_queue);
    if (!vk_queue) {
        VKL_EXIT_WITH_ERROR("No VkQueue selected or handle not assigned.");
    }
    VKL_LOG("Subtask 1.7 done.");

    /* --------------------------------------------- */
    // Subtask 1.8: Create a Swapchain
    /* --------------------------------------------- */
    uint32_t queueFamilyIndexCount = 0u;
    std::vector<uint32_t> queueFamilyIndices;
    VkSurfaceFormatKHR surface_format = getSurfaceImageFormat(vk_physical_device, vk_surface);
    queueFamilyIndices.push_back(selected_queue_family_index);
    queueFamilyIndexCount = 1u;
    VkSurfaceCapabilitiesKHR surface_capabilities = getPhysicalDeviceSurfaceCapabilities(vk_physical_device, vk_surface);
    // Build the swapchain config struct:
    VkSwapchainCreateInfoKHR swapchain_create_info = {};
    swapchain_create_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
    swapchain_create_info.surface = vk_surface;
    swapchain_create_info.minImageCount = surface_capabilities.minImageCount;
    swapchain_create_info.imageArrayLayers = 1u;
    swapchain_create_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
    if (surface_capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT) {
        swapchain_create_info.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
    } else {
        std::cout << "Warning:  VK_IMAGE_USAGE_TRANSFER_SRC_BIT image usage is not supported" << std::endl;
    }
    swapchain_create_info.preTransform = surface_capabilities.currentTransform;
    swapchain_create_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
    swapchain_create_info.clipped = VK_TRUE;
    swapchain_create_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
    swapchain_create_info.queueFamilyIndexCount = queueFamilyIndexCount;
    swapchain_create_info.pQueueFamilyIndices = queueFamilyIndices.data();
    swapchain_create_info.imageFormat = surface_format.format;
    swapchain_create_info.imageColorSpace = surface_format.colorSpace;
    swapchain_create_info.imageExtent = VkExtent2D{static_cast<uint32_t>(window_width), static_cast<uint32_t>(window_height)};
    swapchain_create_info.presentMode = VK_PRESENT_MODE_FIFO_KHR;

    // Create the swapchain:
    result = vkCreateSwapchainKHR(vk_device, &swapchain_create_info, nullptr, &vk_swapchain);
    VKL_CHECK_VULKAN_RESULT(result);
    if (!vk_swapchain) {
        VKL_EXIT_WITH_ERROR("No VkSwapchainKHR created or handle not assigned.");
    }

    // Query how many swapchain images we got:
    uint32_t swapchain_image_count;
    vkGetSwapchainImagesKHR(vk_device, vk_swapchain, &swapchain_image_count, nullptr);

    // Retrieve the swapchain images:
    std::vector<VkImage> swapchain_image_handles(swapchain_image_count);
    vkGetSwapchainImagesKHR(vk_device, vk_swapchain, &swapchain_image_count, swapchain_image_handles.data());

    /* --------------------------------------------- */
    // Subtask 2.7: Depth Test
    /* --------------------------------------------- */
    VkImage depth_buffer = vklCreateDeviceLocalImageWithBackingMemory(
        vk_physical_device, vk_device, window_width, window_height, VK_FORMAT_D32_SFLOAT, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT);
    VkClearValue depth_clear_value;
    depth_clear_value.depthStencil.depth = 1.0f;
    depth_clear_value.depthStencil.stencil = 0u;

    /* --------------------------------------------- */
    // Subtask 1.9: Init GCG Framework
    /* --------------------------------------------- */

    // Gather swapchain config as required by the framework:
    VklSwapchainConfig swapchain_config = {};

    VkClearValue color_clear_value;
    color_clear_value.color.float32[0] = 0.05;   //0.8f; // for light blue sky
    color_clear_value.color.float32[1] = 0.05;   //1.0f; // for light blue sky
    color_clear_value.color.float32[2] = 0.05;   //1.0f; // for light blue sky
    color_clear_value.color.float32[3] = 1.0f;

    swapchain_config.swapchainHandle = vk_swapchain;
    swapchain_config.imageExtent = swapchain_create_info.imageExtent;
    for (const VkImage& img : swapchain_image_handles) {
        VklSwapchainFramebufferComposition framebufferComposition;
        framebufferComposition.colorAttachmentImageDetails.imageHandle = img;
        framebufferComposition.colorAttachmentImageDetails.imageFormat = swapchain_create_info.imageFormat;
        framebufferComposition.colorAttachmentImageDetails.imageUsage = swapchain_create_info.imageUsage;
        framebufferComposition.colorAttachmentImageDetails.clearValue = color_clear_value;
        if (depthtest) {
            // If we also set the data of the depth buffer, our framebuffer will consist of two images:
            framebufferComposition.depthAttachmentImageDetails.imageHandle = depth_buffer;
            framebufferComposition.depthAttachmentImageDetails.imageFormat = VK_FORMAT_D32_SFLOAT;
            framebufferComposition.depthAttachmentImageDetails.imageUsage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
            framebufferComposition.depthAttachmentImageDetails.clearValue = depth_clear_value;
        }
        swapchain_config.swapchainImages.push_back(framebufferComposition);
    }

    // Init the framework:
    if (!gcgInitFramework(vk_instance, vk_surface, vk_physical_device, vk_device, vk_queue, swapchain_config)) {
        VKL_EXIT_WITH_ERROR("Failed to init GCG framework");
    }

    /* --------------------------------------------- */
    // Subtask 2.1: Create a Custom Graphics Pipeline
    /* --------------------------------------------- */
    std::vector<VkDescriptorSetLayoutBinding> descriptor_set_layout_bindings = {
        VkDescriptorSetLayoutBinding{0u, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1u, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, nullptr},
        VkDescriptorSetLayoutBinding{1u, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1u, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}, //texture
        VkDescriptorSetLayoutBinding{2u, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1u, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}, //specular
        VkDescriptorSetLayoutBinding{3u, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1u, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}, //normal
        VkDescriptorSetLayoutBinding{4u, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1u, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr} //Shadow map
    };

    // HUD pipeline
    std::string vertexShaderPath = gcgLoadShaderFilePath("assets/shaders_vk/hud.vert");
    std::string fragmentShaderPath = gcgLoadShaderFilePath("assets/shaders_vk/hud.frag");
    VklGraphicsPipelineConfig pipeline_hud{
           vertexShaderPath.c_str(),
           fragmentShaderPath.c_str(),
           {
               VkVertexInputBindingDescription{0u, sizeof(float) * 3, VK_VERTEX_INPUT_RATE_VERTEX},
               VkVertexInputBindingDescription{1u, sizeof(float) * 3, VK_VERTEX_INPUT_RATE_VERTEX},
               VkVertexInputBindingDescription{2u, sizeof(float) * 2, VK_VERTEX_INPUT_RATE_VERTEX},
               VkVertexInputBindingDescription{3u, sizeof(float) * 3, VK_VERTEX_INPUT_RATE_VERTEX},
           },
           {
               VkVertexInputAttributeDescription{0u, 0u, VK_FORMAT_R32G32B32_SFLOAT, 0u},
               VkVertexInputAttributeDescription{1u, 1u, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 3},
               VkVertexInputAttributeDescription{2u, 2u, VK_FORMAT_R32G32_SFLOAT, 0u},
               VkVertexInputAttributeDescription{3u, 3u, VK_FORMAT_R32G32_SFLOAT, 0u}
           },
           VK_POLYGON_MODE_FILL,
           VK_CULL_MODE_NONE,
           descriptor_set_layout_bindings };
    VkPipeline hudPipeline = vklCreateGraphicsPipeline(pipeline_hud, false);

    //Normal Pipeline
    vertexShaderPath    = gcgLoadShaderFilePath("assets/shaders_vk/normal.vert");
    fragmentShaderPath  = gcgLoadShaderFilePath("assets/shaders_vk/normal.frag");

     VklGraphicsPipelineConfig pipeline_normal_config_no_culling{
            vertexShaderPath.c_str(),
            fragmentShaderPath.c_str(),
            {
                VkVertexInputBindingDescription{0u, sizeof(float) * 3, VK_VERTEX_INPUT_RATE_VERTEX},
                VkVertexInputBindingDescription{1u, sizeof(float) * 3, VK_VERTEX_INPUT_RATE_VERTEX},
                VkVertexInputBindingDescription{2u, sizeof(float) * 2, VK_VERTEX_INPUT_RATE_VERTEX},
                VkVertexInputBindingDescription{3u, sizeof(float) * 3, VK_VERTEX_INPUT_RATE_VERTEX},
            },
            {
                VkVertexInputAttributeDescription{0u, 0u, VK_FORMAT_R32G32B32_SFLOAT, 0u},
                VkVertexInputAttributeDescription{1u, 1u, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 3},
                VkVertexInputAttributeDescription{2u, 2u, VK_FORMAT_R32G32_SFLOAT, 0u},
                VkVertexInputAttributeDescription{3u, 3u, VK_FORMAT_R32G32_SFLOAT, 0u}
            },
            VK_POLYGON_MODE_FILL,
            VK_CULL_MODE_NONE,
            descriptor_set_layout_bindings};
    VkPipeline normalPipeline = vklCreateGraphicsPipeline(pipeline_normal_config_no_culling, false);

    //NormalHeight Pipeline
    vertexShaderPath = gcgLoadShaderFilePath("assets/shaders_vk/normal_height.vert");
    fragmentShaderPath = gcgLoadShaderFilePath("assets/shaders_vk/normal_height.frag");


     VklGraphicsPipelineConfig pipeline_normal_height_config_no_culling{
            vertexShaderPath.c_str(),
            fragmentShaderPath.c_str(),
            {
                VkVertexInputBindingDescription{0u, sizeof(float) * 3, VK_VERTEX_INPUT_RATE_VERTEX},
                VkVertexInputBindingDescription{1u, sizeof(float) * 3, VK_VERTEX_INPUT_RATE_VERTEX},
                VkVertexInputBindingDescription{2u, sizeof(float) * 2, VK_VERTEX_INPUT_RATE_VERTEX},
                VkVertexInputBindingDescription{3u, sizeof(float) * 3, VK_VERTEX_INPUT_RATE_VERTEX},
            },
            {
                VkVertexInputAttributeDescription{0u, 0u, VK_FORMAT_R32G32B32_SFLOAT, 0u},
                VkVertexInputAttributeDescription{1u, 1u, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 3},
                VkVertexInputAttributeDescription{2u, 2u, VK_FORMAT_R32G32_SFLOAT, 0u},
                VkVertexInputAttributeDescription{3u, 3u, VK_FORMAT_R32G32_SFLOAT, 0u}
            },
            VK_POLYGON_MODE_FILL,
            VK_CULL_MODE_NONE,
            descriptor_set_layout_bindings};
    VkPipeline normalheightPipeline = vklCreateGraphicsPipeline(pipeline_normal_height_config_no_culling, false);



    //SPECULAR PIPELINE
    vertexShaderPath = gcgLoadShaderFilePath("assets/shaders_vk/specular.vert");
    fragmentShaderPath = gcgLoadShaderFilePath("assets/shaders_vk/specular.frag");


     VklGraphicsPipelineConfig pipeline_specular_config_no_culling{
            vertexShaderPath.c_str(),
            fragmentShaderPath.c_str(),
            {
                VkVertexInputBindingDescription{0u, sizeof(float) * 3, VK_VERTEX_INPUT_RATE_VERTEX},
                VkVertexInputBindingDescription{1u, sizeof(float) * 3, VK_VERTEX_INPUT_RATE_VERTEX},
                VkVertexInputBindingDescription{2u, sizeof(float) * 2, VK_VERTEX_INPUT_RATE_VERTEX},
                VkVertexInputBindingDescription{3u, sizeof(float) * 3, VK_VERTEX_INPUT_RATE_VERTEX},
            },
            {
                VkVertexInputAttributeDescription{0u, 0u, VK_FORMAT_R32G32B32_SFLOAT, 0u},
                VkVertexInputAttributeDescription{1u, 1u, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 3},
                VkVertexInputAttributeDescription{2u, 2u, VK_FORMAT_R32G32_SFLOAT, 0u},
                VkVertexInputAttributeDescription{3u, 3u, VK_FORMAT_R32G32_SFLOAT, 0u}
            },
            VK_POLYGON_MODE_FILL, //3.3 FILL
            VK_CULL_MODE_NONE, //3.3 NONE
            descriptor_set_layout_bindings};
    VkPipeline specularPipeline = vklCreateGraphicsPipeline(pipeline_specular_config_no_culling, false);


    /* --------------------------------------------- */
    // Subtask 2.3: Allocate and Write Descriptors
    /* --------------------------------------------- */
    VkBuffer uniform_buffer = vklCreateHostCoherentBufferWithBackingMemory(
        static_cast<VkDeviceSize>(sizeof(UniformBuffer)), VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT
    );

    std::vector<VkDescriptorPoolSize> pool_sizes{
        VkDescriptorPoolSize{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 500u},
        VkDescriptorPoolSize{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 500u} //changed that from 30 to 50, is there a better way to handle this
    };

    VkDescriptorPoolCreateInfo descriptor_pool_create_info = {};
    descriptor_pool_create_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
    descriptor_pool_create_info.maxSets = 100u;
    descriptor_pool_create_info.poolSizeCount = static_cast<uint32_t>(pool_sizes.size());
    descriptor_pool_create_info.pPoolSizes = pool_sizes.data();

    VkDescriptorPool vk_descriptor_pool;
    result = vkCreateDescriptorPool(vk_device, &descriptor_pool_create_info, nullptr, &vk_descriptor_pool);
    VKL_CHECK_VULKAN_RESULT(result);

    VkDescriptorSetLayoutCreateInfo descriptor_set_layout_create_info = {};
    descriptor_set_layout_create_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
    // We can reuse the same layout description that we have passed to graphics pipeline creation:
    descriptor_set_layout_create_info.bindingCount = static_cast<uint32_t>(descriptor_set_layout_bindings.size());
    descriptor_set_layout_create_info.pBindings = descriptor_set_layout_bindings.data();
    
    // JCR:This is meant to fix the validation error from updateing the hud but does not work so far
    /*
    VkDescriptorBindingFlags flags = VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT | VK_DESCRIPTOR_BINDING_UPDATE_UNUSED_WHILE_PENDING_BIT;
    VkDescriptorSetLayoutBindingFlagsCreateInfo vkDescriptorSetLayoutBindingFlagsCreateInfo;
    vkDescriptorSetLayoutBindingFlagsCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO;
    vkDescriptorSetLayoutBindingFlagsCreateInfo.pBindingFlags = &flags;
    descriptor_set_layout_create_info.pNext = &vkDescriptorSetLayoutBindingFlagsCreateInfo;
    */

    VkDescriptorSetLayout vk_descriptor_set_layout;
    result = vkCreateDescriptorSetLayout(vk_device, &descriptor_set_layout_create_info, nullptr, &vk_descriptor_set_layout);
    VKL_CHECK_VULKAN_RESULT(result);

    VkCommandPoolCreateInfo command_pool_create_info = {};
    command_pool_create_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
    command_pool_create_info.queueFamilyIndex = selected_queue_family_index;

    VkCommandPool command_pool;
    vkCreateCommandPool(vk_device, &command_pool_create_info, nullptr, &command_pool);

    VkSamplerCreateInfo sampler_create_info = {};
    sampler_create_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
    sampler_create_info.magFilter = VK_FILTER_LINEAR;
    sampler_create_info.minFilter = VK_FILTER_LINEAR;
    sampler_create_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    sampler_create_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    sampler_create_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
    sampler_create_info.minLod = 0.0f;
    sampler_create_info.maxLod = VK_LOD_CLAMP_NONE;
    VkSampler sampler;
    result = vkCreateSampler(vk_device, &sampler_create_info, nullptr, &sampler);
    VKL_CHECK_VULKAN_RESULT(result);

    VkSamplerCreateInfo specularSamplerInfo = {};
    specularSamplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
    specularSamplerInfo.magFilter = VK_FILTER_LINEAR;
    specularSamplerInfo.minFilter = VK_FILTER_LINEAR;
    specularSamplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    specularSamplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    specularSamplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    specularSamplerInfo.anisotropyEnable = VK_TRUE;
    specularSamplerInfo.maxAnisotropy = 16;
    specularSamplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
    specularSamplerInfo.unnormalizedCoordinates = VK_FALSE;
    specularSamplerInfo.compareEnable = VK_FALSE;
    specularSamplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
    specularSamplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
    VkSampler specularSampler;
    result = vkCreateSampler(vk_device, &specularSamplerInfo, nullptr, &specularSampler);
    VKL_CHECK_VULKAN_RESULT(result);

    VkSamplerCreateInfo normalSamplerInfo = {};
    normalSamplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
    normalSamplerInfo.magFilter = VK_FILTER_LINEAR;
    normalSamplerInfo.minFilter = VK_FILTER_LINEAR;
    normalSamplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    normalSamplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    normalSamplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    normalSamplerInfo.anisotropyEnable = VK_TRUE;
    normalSamplerInfo.maxAnisotropy = 16;
    normalSamplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
    normalSamplerInfo.unnormalizedCoordinates = VK_FALSE;
    normalSamplerInfo.compareEnable = VK_FALSE;
    normalSamplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
    normalSamplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
    VkSampler normalSampler;
    result = vkCreateSampler(vk_device, &normalSamplerInfo, nullptr, &normalSampler);
    VKL_CHECK_VULKAN_RESULT(result);

    VkSamplerCreateInfo normalheightSamplerInfo = {};
    normalheightSamplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
    normalheightSamplerInfo.magFilter = VK_FILTER_LINEAR;
    normalheightSamplerInfo.minFilter = VK_FILTER_LINEAR;
    normalheightSamplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    normalheightSamplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    normalheightSamplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
    normalheightSamplerInfo.anisotropyEnable = VK_TRUE;
    normalheightSamplerInfo.maxAnisotropy = 16;
    normalheightSamplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
    normalheightSamplerInfo.unnormalizedCoordinates = VK_FALSE;
    normalheightSamplerInfo.compareEnable = VK_FALSE;
    normalheightSamplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
    normalheightSamplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
    VkSampler normalheightSampler;
    result = vkCreateSampler(vk_device, &normalheightSamplerInfo, nullptr, &normalheightSampler);
    VKL_CHECK_VULKAN_RESULT(result);


    /* --------------------------------------------- */
    // Player and Physics Setup 
    /* --------------------------------------------- */
    // JCR: Initialze the engine that calculates physics and the physical objects
    Physics::GamePhysicsManager physicsManager; // position of ground along y-axis. In out case: 0

    // Add player (with built in camera) to physics simulation
    glm::vec3 worldUp(0, 1, 0);
    glm::vec3 cameraFront(0, 0, -1);
    float aspectRatio = (float)window_width / (float)window_height;
    Physics::Player player(
        physicsManager,
        glm::vec3(0, 2, 5),    // initial player position (box center) 
        glm::vec3(0.2, 1, 0.2),     // half Sizes of player collision box
        1,                      // mass of player , 0 = static
        glm::vec3(0, 2, 0),     // local camOffset from center of player colission box
        5,                     // speed of player when buttons pressed must be positive
        50,                    // Jump FOrce of player
        window,
        aspectRatio,
        worldUp,
        cameraFront
    );
        VKL_LOG("PASSES physics");


    // Establish a callback function for handling mouse button events:
    glfwSetMouseButtonCallback(window, mouseButtonCallbackFromGlfw);

    // Establish a callback function for handling mouse scroll events:
    glfwSetScrollCallback(window, scrollCallbackFromGlfw);

    /* --------------------------------------------- */
    // Register a Key Callback
    /* --------------------------------------------- */
    glfwSetKeyCallback(window, [](GLFWwindow* glfw_window, int key, int scancode, int action, int mods) {
        if (action != GLFW_RELEASE)
            return;
        if (key == GLFW_KEY_ESCAPE) {
            glfwSetWindowShouldClose(glfw_window, true);
        }
        if (key == GLFW_KEY_F1){
            wireframeMode = !wireframeMode; //toggle wireframe mode
        }
        if (key == GLFW_KEY_F2){
            // Cycle through different culling modes
            cullingMode = (cullingMode + 1) % 3; // Cycle between 0, 1, 2 for different culling modes
        }
        if (key == GLFW_KEY_N) {
            normalsMode = !normalsMode; // toggle normal drawing mode
        }
    });

    // Setup stuff for the render loop
    VkPipelineLayout pipeline_layout;
    VkPipelineLayout pipeline_layout_cornell;
    VkCommandBuffer cmdBuffer;
    VkBuffer vertexBuffer;
    VkBuffer indexBuffer;
    VkDeviceSize offsets[] = { 0 };

    // Setup vector with piplines for Destruction etc.
    std::vector<VkPipeline> pipelines;
    pipelines.push_back(hudPipeline);
    pipelines.push_back(specularPipeline);
    pipelines.push_back(normalPipeline);
    pipelines.push_back(normalheightPipeline);

            VKL_LOG("PASSES pipelines");



    // Create texture loader object that is used by HUd and screen quads to load their textures
    TextureLoader textureLoader(vk_physical_device, vk_device, vk_queue, command_pool);
   std::vector<std::shared_ptr<SceneObject>> sceneObjects;


    // Create shadowmap and spotlight that throws shadow
    ShadowMap shadowMap(vk_physical_device, vk_device, selected_queue_family_index, vk_queue, vk_descriptor_set_layout, vk_descriptor_pool);
    Spotlight spotlight;
    
    // Render 1st Start screen. Does not belong to scengraph!
    auto startScreen = std::make_shared<StartScreen>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, shadowMap, glm::vec2(window_width, window_height), textureLoader);
    startScreen->updateUniformBuffer(player.mCamera, glm::vec4(window_width, window_height, 0, 0));
    startScreen->render(hudPipeline);


    // Create the root of the scene graph
    std::shared_ptr<SceneNode> root = std::make_shared<SceneNode>();

    // Create Gun
    std::shared_ptr<const Student::CameraForGLFW>sharedPtrToCamera(&(player.mCamera));
    std::shared_ptr<GunObject> gun = std::make_shared<GunObject>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, sampler, normalSampler, command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR, sharedPtrToCamera, shadowMap);
    root->addChild(gun);
    VKL_LOG("PASSES GUN");

    
    // Create and add the street node (plane) as a child of the root
    std::shared_ptr<StreetNode> street = std::make_shared<StreetNode>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, physicsManager, sampler, normalSampler, normalheightSampler, command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR, shadowMap);
    VKL_LOG("PASSES STREET");

    auto frontWall = std::make_shared<WallObject>(
    vk_device, 
    vk_physical_device, 
    vk_descriptor_pool, 
    vk_descriptor_set_layout, 
    vk_queue, 
    physicsManager, 
    sampler, 
    normalSampler, 
    normalheightSampler, 
    command_pool, 
    g_synchronization2_supported, 
    g_vkCmdPipelineBarrier2KHR, 
    shadowMap, 
    glm::vec3(0.0f, 10.0f, -65.0f), 
    glm::vec3(60.0f, 10.0f, 1.0f),
    textureLoader,
    0
);

auto backWall = std::make_shared<WallObject>(
    vk_device, 
    vk_physical_device, 
    vk_descriptor_pool, 
    vk_descriptor_set_layout, 
    vk_queue, 
    physicsManager, 
    sampler, 
    normalSampler, 
    normalheightSampler, 
    command_pool, 
    g_synchronization2_supported, 
    g_vkCmdPipelineBarrier2KHR, 
    shadowMap, 
    glm::vec3(0.0f, 10.0f, 65.0f), 
    glm::vec3(60.0f, 10.0f, 1.0f) ,
    textureLoader,
    2
);

auto leftWall = std::make_shared<WallObject>(
    vk_device, 
    vk_physical_device, 
    vk_descriptor_pool, 
    vk_descriptor_set_layout, 
    vk_queue, 
    physicsManager, 
    sampler, 
    normalSampler, 
    normalheightSampler, 
    command_pool, 
    g_synchronization2_supported, 
    g_vkCmdPipelineBarrier2KHR, 
    shadowMap, 
    glm::vec3(-60.0f, 10.0f, 0.0f), 
    glm::vec3(1.0f, 10.0f, 65.0f),
    textureLoader,
    1
);

auto rightWall = std::make_shared<WallObject>(
    vk_device, 
    vk_physical_device, 
    vk_descriptor_pool, 
    vk_descriptor_set_layout, 
    vk_queue, 
    physicsManager, 
    sampler, 
    normalSampler, 
    normalheightSampler, 
    command_pool, 
    g_synchronization2_supported, 
    g_vkCmdPipelineBarrier2KHR, 
    shadowMap, 
    glm::vec3(60.0f, 10.0f, 0.0f), 
    glm::vec3(1.0f, 10.0f, 65.0f),
    textureLoader,
    3
);
    root->addChild(frontWall);
    root->addChild(leftWall);
    root->addChild(backWall);
    root->addChild(rightWall);
    //NavGraph testing
    std::shared_ptr<NavGraph> navGraph = std::make_shared<NavGraph>(street);

    root->addChild(street);


    glm::vec3 treeScale(3.5f, 3.5f, 3.5f);
    glm::vec3 treePositionIncrement(5.0f, 0.0f, 9.0f);
    // // Create and add 3 houses on one side of the street
    glm::vec3 collisionBoxHouseHalfSideLengths(5, 2, 6);
    for (int i = 0; i < 3; ++i) {
        glm::vec3 translationVector(-30.0f, 0.0f, i * 30.0f - 20.0f);
        glm::vec3 collisionBoxPosition = glm::vec3(0, 1, 0) + translationVector;
        glm::vec3 collisionBoxPositionTree = glm::vec3(0, 0, 0) + translationVector;
        std::shared_ptr<HouseObject> houseLeft = std::make_shared<HouseObject>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, 
            physicsManager, collisionBoxPosition, collisionBoxHouseHalfSideLengths, sampler, normalSampler, command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR,
            glm::rotate(glm::mat4(1.0f), glm::radians(-90.0f), glm::vec3(0.0f, 1.0f, 0.0f)), shadowMap);
        // Set the house's position to the left of the street
        root->addChild(houseLeft);
        navGraph->addObstacles(houseLeft);
        sceneObjects.push_back(houseLeft);

    glm::vec3 treeLeftPosition = collisionBoxPositionTree + glm::vec3(0.0f, 0.0f, -treePositionIncrement.z);
    glm::vec3 treeRightPosition = collisionBoxPositionTree + glm::vec3(0.0f, 0.0f, treePositionIncrement.z);

    glm::mat4 treeTransformation = glm::scale(glm::mat4(1.0f), treeScale);

    auto treeLeft = std::make_shared<TreeObject>(
        vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue,
        physicsManager, treeLeftPosition, glm::vec3(1, 0, 1), sampler, normalSampler,
        command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR,
        treeTransformation,
        shadowMap, 1, i % 4 // Rotate through different tree models and textures
    );

    auto treeRight = std::make_shared<TreeObject>(
        vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue,
        physicsManager, treeRightPosition, glm::vec3(1, 0, 1), sampler, normalSampler,
        command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR,
        treeTransformation,
        shadowMap, 1, (i + 1) % 4 // Rotate through different tree models and textures
    );

    root->addChild(treeLeft);
    root->addChild(treeRight);
    navGraph->addObstacles(treeLeft);
    navGraph->addObstacles(treeRight);
    }
    VKL_LOG("PASSES HOUSES");

    // Create and add 3 houses on the other side of the street
    for (int i = 0; i < 3; ++i) {
        glm::vec3 translationVector(30.0f, 0.0f, i * 30.0f - 20.0f);
        glm::vec3 collisionBoxPosition = glm::vec3(0, 1, 0) + translationVector;
        glm::vec3 collisionBoxPositionTree = glm::vec3(0, 0, 0) + translationVector;
        std::shared_ptr<HouseObject> houseRight = std::make_shared<HouseObject>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, 
            physicsManager, collisionBoxPosition, collisionBoxHouseHalfSideLengths, sampler, normalSampler, command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR,
            glm::rotate(glm::mat4(1.0f), glm::radians(90.0f), glm::vec3(0.0f, 1.0f, 0.0f)), shadowMap);
        // Set the house's position to the right of the street
        root->addChild(houseRight);
        navGraph->addObstacles(houseRight);

    glm::vec3 treeLeftPosition = collisionBoxPositionTree + glm::vec3(0.0f, 0.0f, -treePositionIncrement.z);
    glm::vec3 treeRightPosition = collisionBoxPositionTree + glm::vec3(0.0f, 0.0f, treePositionIncrement.z);

    glm::mat4 treeTransformation = glm::scale(glm::mat4(1.0f), treeScale);

    auto treeLeft = std::make_shared<TreeObject>(
        vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue,
        physicsManager, treeLeftPosition, glm::vec3(1, 0, 1), sampler, normalSampler,
        command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR,
        treeTransformation,
        shadowMap, 1, i % 4 // Rotate through different tree models and textures
    );

    auto treeRight = std::make_shared<TreeObject>(
        vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue,
        physicsManager, treeRightPosition, glm::vec3(1, 0, 1), sampler, normalSampler,
        command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR,
        treeTransformation,
        shadowMap, 1, (i + 1) % 4 // Rotate through different tree models and textures
    );

    root->addChild(treeLeft);
    root->addChild(treeRight);
    navGraph->addObstacles(treeLeft);
    navGraph->addObstacles(treeRight);
    }
    VKL_LOG("PASSES HOUSES");

    
    std::shared_ptr<SceneObject> spaceship = std::make_shared<SpaceshipObject>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, 
             sampler, specularSampler, normalSampler, command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR,
            shadowMap);

    root->addChild(spaceship);

    glm::vec3 collisionBoxHalfLengthsScooter = glm::vec3(1, 1, 1);
    glm::vec3 collisionBoxPositionScooter = glm::vec3(3, 1, 5);
    auto scooter = std::make_shared<ScooterObject>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, 
            physicsManager, collisionBoxPositionScooter, collisionBoxHalfLengthsScooter, sampler, specularSampler, normalSampler, command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR, shadowMap);
    root->addChild(scooter);
    navGraph->addObstacles(scooter);

    auto pizzaPile = std::make_shared<PizzaPile>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, sampler, normalSampler, command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR, shadowMap);
    pizzaPile->createPizzaStack(5, vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, sampler, normalSampler, command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR, shadowMap, scooter);




    //trees


    // Random number generator
    std::mt19937 rng(static_cast<unsigned int>(std::time(0)));
    std::uniform_real_distribution<float> distXTrees(-20.0f, 25.0f); // Random x coordinate within plane bounds
    std::uniform_real_distribution<float> distZTrees(-45.0f, 35.0f); // Random z coordinate within plane bounds
    std::uniform_real_distribution<float> distScaleTrees(1.5f, 3.5f); // Random scale factor for rocks
    std::uniform_real_distribution<float> distRotationTrees(0.0f, 360.0f); // Random rotation angle for rocks

    const int numberOfTrees = 15;
    glm::vec3 basePosition = glm::vec3(1, 1, 1);
    glm::vec3 positionIncrement = glm::vec3(0.5, 0, 2); 

    for (int i = 0; i < numberOfTrees; ++i) {
        
        glm::vec3 currentPosition(distXTrees(rng), 1.0f, distZTrees(rng));
        float scale = distScaleTrees(rng);
        float rotationAngle = distRotationTrees(rng);

        glm::mat4 transformation = glm::scale(glm::mat4(1.0f), glm::vec3(scale)) * glm::rotate(glm::mat4(1.0f), glm::radians(rotationAngle), glm::vec3(0.0f, 1.0f, 0.0f));
           
        
        auto tree = std::make_shared<TreeObject>(
            vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue,
            physicsManager, currentPosition, glm::vec3(0.5*scale, scale*2, 0.5*scale), sampler, normalSampler,
            command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR, 
            transformation,
            shadowMap, i % 4 // Rotate through 3 different tree models and textures
        );

        root->addChild(tree);
        navGraph->addObstacles(tree);
    }

    //rocks
    int numberOfRocks = 15;

    // Random number generator
    std::uniform_real_distribution<float> distX(-20.0f, 25.0f); // Random x coordinate within plane bounds
    std::uniform_real_distribution<float> distZ(-45.0f, 35.0f); // Random z coordinate within plane bounds
    std::uniform_real_distribution<float> distScale(0.1f, 0.7f); // Random scale factor for rocks
    std::uniform_real_distribution<float> distRotation(0.0f, 360.0f); // Random rotation angle for rocks

    for (int i = 0; i < numberOfRocks; ++i) {
        glm::vec3 currentPosition(distX(rng), 1.0f, distZ(rng)); // Y coordinate is set to 1.0 to match plane height
        
        float scale = distScale(rng);
        float rotationAngle = distRotation(rng);

        glm::mat4 transformation = glm::scale(glm::mat4(1.0f), glm::vec3(scale)) * glm::rotate(glm::mat4(1.0f), glm::radians(rotationAngle), glm::vec3(0.0f, 1.0f, 0.0f));
        auto rock = std::make_shared<RockObject>(
            vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue,
            physicsManager, currentPosition, glm::vec3(4*scale, 0.25, 4*scale), sampler, normalSampler,
            command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR, 
            transformation,
            shadowMap
        );

        root->addChild(rock);
        navGraph->addObstacles(rock);
    }

    



// //BEWARE FLOATING TREES
// // Number of trees to create
// const int numberOfTrees = 20;

// // Base positions for left and right sides
// glm::vec3 basePositionLeft = glm::vec3(5, 1, 7);
// glm::vec3 basePositionRight = glm::vec3(-5, 1, 7);

// // Random number generator for placement
// std::mt19937 rng(static_cast<unsigned int>(std::time(0)));
// std::uniform_real_distribution<float> distLeft(-10.0f, -5.0f); 
// std::uniform_real_distribution<float> distRight(5.0f, 10.0f);
// std::uniform_real_distribution<float> distDepth(-10.0f, 10.0f);

// // Loop to create and place trees
// for (int i = 0; i < numberOfTrees; ++i) {
//     // Randomly decide whether to place on left or right
//     bool placeOnLeft = (i % 2 == 0);

//     glm::vec3 currentPosition;
//     if (placeOnLeft) {
//         currentPosition = basePositionLeft + glm::vec3(distLeft(rng), 0, distDepth(rng));
//     } else {
//         currentPosition = basePositionRight + glm::vec3(distRight(rng), 0, distDepth(rng));
//     }

//     // Apply scaling transformation
//     glm::mat4 transformation = glm::scale(glm::mat4(1.0f), glm::vec3(2.5f)) * glm::rotate(glm::mat4(1.0f), glm::radians(90.0f), glm::vec3(0.0f, 1.0f, 0.0f));

//     // Create tree object
//     auto tree = std::make_shared<TreeObject>(
//         vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue,
//         physicsManager, currentPosition, collisionBoxHouseHalfSideLengths, sampler, normalSampler,
//         command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR,
//         transformation,
//         shadowMap, 1, i % 4 // Rotate through different tree models and textures
//     );

//     root->addChild(tree);
//     //navGraph->addObstacles(tree);
// }


// // Create Robots and add them to vector with all robots
    glm::vec3 initialCollisionBoxRobot = glm::vec3(-15, 3.5, 20);
    std::vector<std::shared_ptr<RobotObject>> robots;
    std::shared_ptr<RobotObject> robotObject1= std::make_shared<RobotObject>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, physicsManager, initialCollisionBoxRobot, sampler, normalSampler, command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR, shadowMap);
    sceneObjects.push_back(robotObject1);    
    root->addChild(robotObject1);
    robots.push_back(robotObject1);
    std::shared_ptr<RobotObject> robotObject2= std::make_shared<RobotObject>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, physicsManager, initialCollisionBoxRobot + glm::vec3(7, 0, 4), sampler, normalSampler, command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR, shadowMap);
    sceneObjects.push_back(robotObject2); 
    root->addChild(robotObject2);
    robots.push_back(robotObject2);
    VKL_LOG("PASSES ROBOTS");

    navGraph->setObjectNavNode(robotObject1);
    navGraph->setObjectNavNode(robotObject2);


    MovementManager movement = MovementManager(navGraph);
    movement.moveObjectToNode(robotObject1, 3.0f, 3.0f);
    movement.moveObjectToNode(robotObject2, 3.0f, 3.0f);

    

    //Create pizza Objects
    /*
    auto pizza = std::make_shared<PizzaObject>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, sampler, normalSampler, command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR, shadowMap);
    pizza->localTransform = glm::translate(glm::mat4(1.0f), glm::vec3(0, 4, -1.3)) * glm::rotate(glm::mat4(1.0f), glm::radians(45.0f), glm::vec3(1.0f, 0.0f, 0.0f));
    robotObject1->mLeftArm->addChild(pizza);
    VKL_LOG("PASSES PIZZA");
    */

    //Create pizza in Hand Object
    auto pizzaInHand = std::make_shared<PizzaInHandObject>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, sampler, normalSampler, command_pool, g_synchronization2_supported, g_vkCmdPipelineBarrier2KHR, shadowMap, sharedPtrToCamera);
    root->addChild(pizzaInHand);

    //Add quad for permanently rendering cross on screen:
    auto cross = std::make_shared<CrossObject>(
        vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout,
        vk_queue, shadowMap);
    root->addChild(cross);

    // add a round HUD display that shows pizza slices as indicator for undelivered pizzas
    auto hud = std::make_shared<HudObject>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, shadowMap, glm::vec2(window_width, window_height), textureLoader);
    root->addChild(hud);

    // Create Timer for animations
    FrameTimer frameTimer;

    // This ensures every scene node has the correct world trafo
    root->updateTransforms(frameTimer); // Update physics-based transformations

    // Create pizza manager in initla state
    std::vector<glm::vec2> locationsOfHouses;
    locationsOfHouses.push_back(glm::vec2(-24, 19 ));
    locationsOfHouses.push_back(glm::vec2( 24,-20 ));
    locationsOfHouses.push_back(glm::vec2( - 23, 11 ));
    locationsOfHouses.push_back(glm::vec2( 24,9 ));
    locationsOfHouses.push_back(glm::vec2( - 25, 40 ));
    locationsOfHouses.push_back(glm::vec2( 24,39 ));
    glm::vec2 scooterLocation(3.0, 4.5);
    PizzaManager pizzaManager;
    pizzaManager.hudObject = hud;
    pizzaManager.pizzaObjectInHand = pizzaInHand;
    pizzaManager.scooterLocation = scooterLocation;
    pizzaManager.houseLocations = locationsOfHouses;
    
    // Render 2nd startscreen and wait for player to press a key
    startScreen->updateTextureIndex(1);
    startScreen->render(hudPipeline);
    while(true)
    {
        glfwPollEvents();
        if (glfwGetKey(window, GLFW_KEY_ENTER) == GLFW_PRESS) break;
    }
    

    // For Debugging: Create screen quad that shows depth (requieres changes in hud frag shader)
    //auto shadowScreen = std::make_shared<ShadowScreen>(vk_device, vk_physical_device, vk_descriptor_pool, vk_descriptor_set_layout, vk_queue, shadowMap, glm::vec2(window_width, window_height), textureLoader);
    //shadowScreen->updateUniformBuffer(player.mCamera, glm::vec4(window_width, window_height, 0, 0));
    //shadowScreen->render(hudPipeline);
    bool useLooseCondition = true;

    /////////////////
    // Render Loop //
    /////////////////
    double mouse_x, mouse_y;
    vklEnablePipelineHotReloading(window, GLFW_KEY_F5);
    while (!glfwWindowShouldClose(window)) {
        
        // For Debugging:
        //std::cout << "\n Player Pos = " << player.getPosition().x << "  ,  " << player.getPosition().z << "   PizzaInHand = " << (pizzaManager.playerHasPizzaInHand ? 1 : 0 ) << "renderPizza = " << pizzaInHand->renderThisPizza ? 1 : 0;
        if (glfwGetKey(window, GLFW_KEY_L) == GLFW_PRESS) useLooseCondition = false;
        
        // Update timer for current frame
        frameTimer.update();

        // Handle user input:
        glfwPollEvents();
        glfwGetCursorPos(window, &mouse_x, &mouse_y);

        
        //Update positions of objects and camera (JCR)
        player.processUserInputs(); // get speed from keyboard input, camera movement from mouse, deltaTime from camera timer
        physicsManager.runStepSimulation(player.mCamera.DeltaTime); // calculates motions and updates position of player and cube object
        player.updateCameraPosition(); // updates camera position with freshly claculated player position

        DirectionalLight directionalLight_data;
        PointLight pointLight_data;

        // Update physics-based transformations and time based animations
            

        
        
          // Update pizza Manager
        std::vector<glm::vec3> robotPositions;
        for (auto robot : robots)
        {
            if (robot->getRobotState() == ObjectState::NeedsUpdate){
                movement.updateObjectPosition(robot, robot->physicsObject->getPosition().x, robot->physicsObject->getPosition().z);
                robot->setRobotState();
            }
            robotPositions.push_back(robot->physicsObject->getPosition());     
        }

        root->updateTransforms(frameTimer);
        gun->updateShot(window, robots);   

        bool gameOver = pizzaManager.update(player.getPosition(), robotPositions);
        if (gameOver && useLooseCondition) break;

        // Update Spotlight position to sit on the already updated gun
        glm::vec4 spotlightLocalOffsetFromGun(0, 0, -0.251, 1);
        glm::vec3 spotlightPosition = gun->worldTransform * spotlightLocalOffsetFromGun;
        glm::vec3 spotlightDirection = gun->mDirection;
        spotlight.update(glm::vec4(spotlightPosition, 1), glm::vec4(spotlightDirection, 1), glm::vec4(player.mCamera.Up, 1));

        // update gpu buffers for shadow map draw call
        root->updateUniformBufferForShadow(shadowMap, spotlight); 

        // Update gpu buffers for scene rendering
        root->updateUniformBuffer(player.mCamera, glm::vec4(window_width, window_height, 0, 0), frameTimer);
        
        // draw shadow map
        shadowMap.startRecordingCommandBuffer();
        root->recordRenderingCommandsForShadowMap(shadowMap, spotlight);
        shadowMap.endRecordingCommandbufferAndSubmitToQueue();
        
        // Wait until we get an image from the swapchain to render into:
        vklWaitForNextSwapchainImage();

        // Render scene to current swap chain image 
        vklStartRecordingCommands();
        root->recordRenderingCommands(vklGetCurrentCommandBuffer(), hudPipeline, specularPipeline, normalPipeline, normalheightPipeline, frameTimer);
        vklEndRecordingCommands();
        
        // Present rendered image to the screen:
        vklPresentCurrentSwapchainImage();
        
        // For debugging: show shdow map
        //shadowScreen->updateUniformBuffer(player.mCamera, glm::vec4(window_width, window_height, 0, 0));
        //shadowScreen->render(hudPipeline);


        if (cmdline_args.run_headless) {
            uint32_t idx = vklGetCurrentSwapChainImageIndex();
            std::string screenshot_filename = "screenshot";
            if (cmdline_args.set_filename) {
                screenshot_filename = cmdline_args.filename;
            }
            gcgSaveScreenshot(screenshot_filename, swapchain_image_handles[idx], window_width, window_height, surface_format.format, vk_device, vk_physical_device,
                vk_queue, selected_queue_family_index);
            break;
        }
    }

    // Game Over: Show correct End Screen if player won or lost game
    while (true)
    {
        if (pizzaManager.allPizzasDelivered)
        {
            startScreen->updateTextureIndex(2);
            startScreen->render(hudPipeline);
        }
        else if (pizzaManager.robotAtePizza)
        {
            startScreen->updateTextureIndex(3);
            startScreen->render(hudPipeline);
        }

        // Close on any key
        glfwPollEvents();
        if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) break;
    }

    // Wait for all GPU work to finish before cleaning up:
    vkDeviceWaitIdle(vk_device);

    // Cleanup out own stuff:
    for(auto& entry : hud->m_textureData) entry.destroyVulkanAssets();
    shadowMap.destroyVulkanAssets();
    vklDestroyDeviceLocalImageAndItsBackingMemory(depth_buffer);
    vkDestroyDescriptorSetLayout(vk_device, vk_descriptor_set_layout, nullptr);
    vkDestroyDescriptorPool(vk_device, vk_descriptor_pool, nullptr);
    vklDestroyHostCoherentBufferAndItsBackingMemory(uniform_buffer);
    // vklDestroyHostCoherentBufferAndItsBackingMemory(vertexBufferCornell);
    // vklDestroyHostCoherentBufferAndItsBackingMemory(indexBufferCornell);
    // vklDestroyHostCoherentBufferAndItsBackingMemory(uniform_cornell_buffer);
    // vklDestroyHostCoherentBufferAndItsBackingMemory(pointLightBuffer_cornell);
    // vklDestroyHostCoherentBufferAndItsBackingMemory(directionalLightBuffer_cornell);
    for (VkPipeline pipeline : pipelines) {
        vklDestroyGraphicsPipeline(pipeline);
    }
    pipelines.clear();

    /* --------------------------------------------- */
    // Cleanup Framework
    /* --------------------------------------------- */
    gcgDestroyFramework();
    vkDestroySwapchainKHR(vk_device, vk_swapchain, nullptr);
    vkDestroyDevice(vk_device, nullptr);
    vkDestroySurfaceKHR(vk_instance, vk_surface, nullptr);
    vkDestroyInstance(vk_instance, nullptr);
    glfwDestroyWindow(window);
    glfwTerminate();

    return EXIT_SUCCESS;  
}
