#![allow(
    dead_code,
    unused_variables,
    clippy::too_many_arguments,
    clippy::unnecessary_wraps
)]

use anyhow::{anyhow, Result};
use log::*;
use scene::generators;
use winit::dpi::{LogicalSize, LogicalPosition};
use winit::event::{ElementState, Event, WindowEvent};
use winit::event_loop::EventLoop;
use winit::keyboard::NamedKey;
use winit::window::{Window, WindowBuilder};

use vulkanalia::loader::{LibloadingLoader, LIBRARY};
use vulkanalia::window as vk_window;
use vulkanalia::prelude::v1_0::*;
use vulkanalia::Version;
use vulkanalia::bytecode::Bytecode;

use std::collections::HashSet;
use std::ffi::CStr;
use std::os::raw::c_void;

// extension imports
use vulkanalia::vk::ExtDebugUtilsExtension;
use vulkanalia::vk::KhrSurfaceExtension;
use vulkanalia::vk::KhrSwapchainExtension;

use cgmath::Matrix;
use std::mem::size_of;
use std::ptr::copy_nonoverlapping as memcpy;

use std::time::Instant;
use crate::vertex::VertexContainer;

pub mod app_data;
pub mod errors;
pub mod swapchain;
pub mod queue_family_indices;
pub mod vertex;
pub mod buffer;
pub mod image;
pub mod command_buffer;
pub mod depth_buffer;
pub mod load_model;
pub mod scene;
pub mod primitives;

const PORTABILITY_MACOS_VERSION: Version = Version::new(1, 3, 216);
const VALIDATION_ENABLED: bool =
    cfg!(debug_assertions);

const VALIDATION_LAYER: vk::ExtensionName =
    vk::ExtensionName::from_bytes(b"VK_LAYER_KHRONOS_validation");
const DEVICE_EXTENSIONS: &[vk::ExtensionName] = &[
    vk::KHR_SWAPCHAIN_EXTENSION.name
];

const MAX_FRAMES_IN_FLIGHT: usize = 30;

fn main() -> Result<()> {
    pretty_env_logger::init();

    // Window

    let event_loop = EventLoop::new()?;
    let window = WindowBuilder::new()
        .with_title("Vulkan Tutorial (Rust)")
        .with_inner_size(LogicalSize::new(1024, 768))
        .build(&event_loop)?;
    //window.set_cursor_visible(false);

    // event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
    // App

    let mut app = unsafe { App::create(&window)? };
    event_loop.run(move |event, elwt| {
        match event {
            // Request a redraw when all events were processed.
            Event::AboutToWait => window.request_redraw(),
            Event::WindowEvent { event, .. } => match event {
                // Render a frame if our Vulkan app is not being destroyed.
                WindowEvent::RedrawRequested if !elwt.exiting() && !app.minimized => unsafe { app.render(&window) }.unwrap(),
                // Destroy our Vulkan app.
                WindowEvent::CloseRequested => {
                    elwt.exit();
                    unsafe { app.device.device_wait_idle().unwrap(); }
                    unsafe { app.destroy(); }
                },
                WindowEvent::Resized(size) => {
                    if size.width == 0 || size.height == 0 {
                        app.minimized = true;
                    } else {
                        app.minimized = false;
                        app.resized = true;
                    }
                },
                WindowEvent::CursorMoved { device_id, position } => {
                    let log_pos: LogicalPosition<f32> = position.to_logical(window.scale_factor());
                    if app.last_pos.x != -1.0 {
                        app.cam_angle_x += ((log_pos.x - (window.inner_size().width as f32 / 2.0)) / (window.inner_size().width as f32)) * 180.0;
                        app.cam_angle_y += ((log_pos.y - (window.inner_size().height as f32 / 2.0)) / (window.inner_size().height as f32)) * 180.0;

                        if app.cam_angle_x >= 360.0 {
                            app.cam_angle_x -= 360.0;
                        }
                        if app.cam_angle_x <= -360.0 {
                            app.cam_angle_x += 360.0;
                        }
                        app.cam_angle_y = app.cam_angle_y.max(-90.0).min(90.0);
                    }

                    let cursor_res = window.set_cursor_position(LogicalPosition::new(window.inner_size().width / 2, window.inner_size().height / 2));
                    if cursor_res.is_err() {
                        println!("Attempted to move cursor while not in possession")
                    }

                    app.last_pos = LogicalPosition::new(window.inner_size().width as f32 / 2 as f32, window.inner_size().height as f32 / 2 as f32);
                },
                WindowEvent::KeyboardInput { device_id, event, is_synthetic } => {
                    if event.logical_key == "w" {
                        app.cur_pos += app.view_direction * 0.1;
                    }
                    if event.logical_key == "s" {
                        app.cur_pos -= app.view_direction * 0.1;
                    }
                    if event.logical_key == "a" {
                        app.cur_pos -= app.view_direction.cross(vertex::Vec3::new(0.0, 0.0, 1.0)) * 0.1;
                    }
                    if event.logical_key == "d" {
                        app.cur_pos += app.view_direction.cross(vertex::Vec3::new(0.0, 0.0, 1.0)) * 0.1;
                    }
                    if event.logical_key == "f" && event.state == ElementState::Pressed && event.repeat == false {
                        app.show_frame_rate = !app.show_frame_rate;
                    }
                    if event.logical_key == "+" && event.state == ElementState::Pressed && event.repeat == false {
                        app.data.diffuse_raster_steps += 1;
                        app.scene_handler.rt_memory[2] = app.data.diffuse_raster_steps;
                        app.synchronized = 0;
                        println!("Set diffuse tracing raster size to {}", app.scene_handler.rt_memory[2]);
                    }
                    if event.logical_key == "-" && event.state == ElementState::Pressed && event.repeat == false {
                        app.data.diffuse_raster_steps = (app.data.diffuse_raster_steps).max(1) - 1;
                        app.scene_handler.rt_memory[2] = app.data.diffuse_raster_steps;
                        app.synchronized = 0;
                        println!("Set diffuse tracing raster size to {}", app.scene_handler.rt_memory[2]);
                    }
                    if event.logical_key == NamedKey::Escape {
                        elwt.exit();
                        unsafe { app.device.device_wait_idle().unwrap(); }
                        unsafe { app.destroy(); }
                    }
                },
                _ => {}
            }
            _ => {}
        }
    })?;

    Ok(())
}

/// Our Vulkan app.
#[derive(Clone, Debug)]
struct App {
    entry: Entry,
    instance: Instance,
    data: app_data::AppData,
    device: Device,
    frame: usize,
    resized: bool,
    minimized: bool,
    start: Instant,
    cam_angle_x: f32,
    cam_angle_y: f32,
    last_pos: LogicalPosition<f32>,
    view_direction: vertex::Vec3,
    cur_pos: cgmath::Point3<f32>,
    scene_handler: scene::Scene,
    show_frame_rate: bool,
    synchronized: usize,
}

impl App {
    /// Creates our Vulkan app.
    unsafe fn create(window: &Window) -> Result<Self> {
        let loader = LibloadingLoader::new(LIBRARY)?;
        let entry = Entry::new(loader).map_err(|b| anyhow!("{}", b))?;
        let mut data = app_data::AppData::default();
        data.use_geometry_shader = false;
        data.num_lights_per_volume = 5;
        data.min_light_weight = 0.0001;
        data.max_iterations_per_light = 20;
        data.diffuse_raster_steps = 0;
        data.diffuse_raster_size = 0.01;
        data.max_recursive_rays = 10;
        data.diffuse_rays_per_hit = 1;
        let mut scene_handler = scene::Scene::default();
        
        //load_model::load_model(&mut data)?;

        let instance = create_instance(window, &entry, &mut data)?;
        data.surface = vk_window::create_surface(&instance, &window, &window)?;
        pick_physical_device(&instance, &mut data)?;
        let device = create_logical_device(&entry, &instance, &mut data)?;
        swapchain::create_swapchain(window, &instance, &device, &mut data)?;
        swapchain::create_swapchain_image_views(&device, &mut data)?;
        create_render_pass(&instance, &device, &mut data)?;
        
        buffer::create_descriptor_set_layout(&device, &mut data)?;
        create_pipeline(&device, &mut data)?;

        command_buffer::create_command_pool(&instance, &device, &mut data)?;

        create_color_objects(&instance, &device, &mut data)?;
        depth_buffer::create_depth_objects(&instance, &device, &mut data)?;
        create_framebuffers(&device, &mut data)?;


        image::create_texture_image(&instance, &device, &mut data)?;
        image::create_texture_image_view(&device, &mut data)?;
        image::create_texture_sampler(&device, &mut data)?;
        
        //let cur_pos = generators::generate_test_scene(&mut scene_handler, &mut data)?;
        let cur_pos = generators::generate_test_scene2(&mut scene_handler, &mut data, 31, 31,1, 5)?;
        scene_handler.prepare_data(&instance, &device, &mut data)?;

        buffer::create_uniform_buffers(&instance, &device, &mut data)?;
        buffer::create_storage_buffers(&instance, &device, &mut data)?;
        buffer::create_descriptor_pool(&device, &mut data)?;
        buffer::create_descriptor_sets(&device, &mut data)?;

        command_buffer::create_command_buffers(&device, &mut data, &scene_handler)?;

        create_sync_objects(&device, &mut data)?;

        Ok(Self { entry, instance, data, device, frame: 0 , resized: false, minimized: false, start: Instant::now(),
            cam_angle_x: 0.0, cam_angle_y: 0.0, 
            last_pos: LogicalPosition::new(-1 as f32, -1 as f32), 
            view_direction: vertex::Vec3::new(0.0, 0.0, 0.0),
            cur_pos: cur_pos,
            scene_handler,
            show_frame_rate: false,
            synchronized: 0
        })
    }

    /// Renders a frame for our Vulkan app.
    unsafe fn render(&mut self, window: &Window) -> Result<()> {
        let start_time = Instant::now();
        let in_flight_fence = self.data.in_flight_fences[self.frame];

        self.device.wait_for_fences(&[in_flight_fence], true, u64::MAX)?;

        let result = self.device.acquire_next_image_khr(
            self.data.swapchain,
            u64::MAX,
            self.data.image_available_semaphores[self.frame],
            vk::Fence::null(),
        );

        let image_index = match result {
            Ok((image_index, _)) => image_index as usize,
            Err(vk::ErrorCode::OUT_OF_DATE_KHR) => return self.recreate_swapchain(window),
            Err(e) => return Err(anyhow!(e)),
        };

        let image_in_flight = self.data.images_in_flight[image_index];
        if !image_in_flight.is_null() {
            self.device.wait_for_fences(&[image_in_flight], true, u64::MAX)?;
        }

        self.data.images_in_flight[image_index] = in_flight_fence;

        self.update_uniform_buffer(image_index)?;
        if self.synchronized < MAX_FRAMES_IN_FLIGHT {
            buffer::update_storage_buffer(&self.instance, &self.device, &self.data, image_index, &self.scene_handler)?;
            self.synchronized += 1
        }

        let wait_semaphores = &[self.data.image_available_semaphores[self.frame]];
        let wait_stages = &[vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT];
        let command_buffers = &[self.data.command_buffers[image_index]];
        let signal_semaphores = &[self.data.render_finished_semaphores[self.frame]];
        let submit_info = vk::SubmitInfo::builder()
            .wait_semaphores(wait_semaphores)
            .wait_dst_stage_mask(wait_stages)
            .command_buffers(command_buffers)
            .signal_semaphores(signal_semaphores);

        self.device.reset_fences(&[in_flight_fence])?;

        self.device
            .queue_submit(self.data.graphics_queue, &[submit_info], in_flight_fence)?;

        let swapchains = &[self.data.swapchain];
        let image_indices = &[image_index as u32];
        let present_info = vk::PresentInfoKHR::builder()
            .wait_semaphores(signal_semaphores)
            .swapchains(swapchains)
            .image_indices(image_indices);

        let result = self.device.queue_present_khr(self.data.present_queue, &present_info);
        let changed = result == Ok(vk::SuccessCode::SUBOPTIMAL_KHR) || result == Err(vk::ErrorCode::OUT_OF_DATE_KHR);
        if self.resized || changed {
            self.resized = false;
            self.recreate_swapchain(window)?;
        } else if let Err(e) = result {
            return Err(anyhow!(e));
        }

        self.frame = (self.frame + 1) % MAX_FRAMES_IN_FLIGHT;

        if self.show_frame_rate {
            println!("{}", 1000000.0 / start_time.elapsed().as_micros() as f32);
        }
        Ok(())
    }

    /// Destroys our Vulkan app.
    unsafe fn destroy(&mut self) {
        self.destroy_swapchain();

        self.device.destroy_sampler(self.data.texture_sampler, None);

        self.device.destroy_image_view(self.data.texture_image_view, None);

        self.device.destroy_image(self.data.texture_image, None);
        self.device.free_memory(self.data.texture_image_memory, None);

        self.device.destroy_descriptor_set_layout(self.data.descriptor_set_layout, None);

        self.scene_handler.destroy(&self.device);

        self.data.in_flight_fences
            .iter()
            .for_each(|f| self.device.destroy_fence(*f, None));
        self.data.render_finished_semaphores
            .iter()
            .for_each(|s| self.device.destroy_semaphore(*s, None));
        self.data.image_available_semaphores
            .iter()
            .for_each(|s| self.device.destroy_semaphore(*s, None));
        self.device.destroy_command_pool(self.data.command_pool, None);
        self.device.destroy_device(None);
        self.instance.destroy_surface_khr(self.data.surface, None);

        if VALIDATION_ENABLED {
            self.instance.destroy_debug_utils_messenger_ext(self.data.messenger, None);
        }

        self.instance.destroy_instance(None);
    }

    unsafe fn recreate_swapchain(&mut self, window: &Window) -> Result<()> {
        self.device.device_wait_idle()?;
        self.destroy_swapchain();
        swapchain::create_swapchain(window, &self.instance, &self.device, &mut self.data)?;
        swapchain::create_swapchain_image_views(&self.device, &mut self.data)?;
        create_render_pass(&self.instance, &self.device, &mut self.data)?;
        create_pipeline(&self.device, &mut self.data)?;
        buffer::create_descriptor_pool(&self.device, &mut self.data)?;
        create_color_objects(&self.instance, &self.device, &mut self.data)?;
        depth_buffer::create_depth_objects(&self.instance, &self.device, &mut self.data)?;
        create_framebuffers(&self.device, &mut self.data)?;
        buffer::create_uniform_buffers(&self.instance, &self.device, &mut self.data)?;
        buffer::create_storage_buffers(&self.instance, &self.device, &mut self.data)?;
        buffer::create_descriptor_sets(&self.device, &mut self.data)?;
        command_buffer::create_command_buffers(&self.device, &mut self.data, &self.scene_handler)?;
        self.data
            .images_in_flight
            .resize(self.data.swapchain_images.len(), vk::Fence::null());
        self.synchronized = 0;
        Ok(())
    }

    unsafe fn destroy_swapchain(&mut self) {
        self.device.destroy_image_view(self.data.color_image_view, None);
        self.device.free_memory(self.data.color_image_memory, None);
        self.device.destroy_image(self.data.color_image, None);

        self.device.destroy_image_view(self.data.depth_image_view, None);

        self.device.destroy_image(self.data.depth_image, None);
        self.device.free_memory(self.data.depth_image_memory, None);

        self.device.destroy_descriptor_pool(self.data.descriptor_pool, None);
        self.data.uniform_buffers
            .iter()
            .for_each(|b| self.device.destroy_buffer(*b, None));
        self.data.uniform_buffers_memory
            .iter()
            .for_each(|m| self.device.free_memory(*m, None));
        self.data.storage_buffers
            .iter()
            .for_each(|b| self.device.destroy_buffer(*b, None));
        self.data.storage_buffers_memory
            .iter()
            .for_each(|m| self.device.free_memory(*m, None));
        
        self.data.framebuffers
            .iter()
            .for_each(|f| self.device.destroy_framebuffer(*f, None));
        self.device.free_command_buffers(self.data.command_pool, &self.data.command_buffers);
        self.device.destroy_pipeline(self.data.pipeline_cube, None);
        self.device.destroy_pipeline(self.data.pipeline_cuboid, None);
        self.device.destroy_pipeline(self.data.pipeline_quad, None);
        self.device.destroy_pipeline_layout(self.data.pipeline_layout, None);
        self.device.destroy_render_pass(self.data.render_pass, None);
        self.data.swapchain_image_views
            .iter()
            .for_each(|v| self.device.destroy_image_view(*v, None));
        self.device.destroy_swapchain_khr(self.data.swapchain, None);

    }

    unsafe fn update_uniform_buffer(&mut self, image_index: usize) -> Result<()> {
        let time = self.start.elapsed().as_secs_f32();
        
        /*let model = buffer::Mat4::from_axis_angle(
            vec3(0.0, 1.0, 0.0),
            cgmath::Deg(90.0) * 0.0 //time
        );*/

        let rot_mat = cgmath::Matrix3::from_angle_y(cgmath::Deg(-self.cam_angle_y)) * cgmath::Matrix3::from_angle_z(cgmath::Deg(self.cam_angle_x));
        let rot_mat4 = cgmath::Matrix4::from_angle_y(cgmath::Deg(-self.cam_angle_y)) * cgmath::Matrix4::from_angle_z(cgmath::Deg(self.cam_angle_x));
        self.view_direction = rot_mat.transpose() * vertex::Vec3::new(1.0, 0.0, 0.0);
        let model = cgmath::Matrix4::from_translation( cgmath::Point3::new(0.0, 0.0, 0.0) - self.cur_pos );

        let view = buffer::Mat4::look_to_rh(
            cgmath::point3(0.0, 0.0, 0.0),
            vertex::Vec3::new(1.0, 0.0, 0.0),
            vertex::Vec3::new(0.0, 0.0, 1.0)
        );
        

        let correction = buffer::Mat4::new( //column major order, matrix looks transposed
            1.0,  0.0,       0.0, 0.0,
            // We're also flipping the Y-axis with this line's `-1.0`.
            0.0, -1.0,       0.0, 0.0,
            0.0,  0.0, 1.0 / 2.0, 0.0,
            0.0,  0.0, 1.0 / 2.0, 1.0,
        );
        
        let proj = correction
            * cgmath::perspective(
                cgmath::Deg(45.0),
                self.data.swapchain_extent.width as f32 / self.data.swapchain_extent.height as f32,
                0.1,
                10000.0,
            );

        let ubo = buffer::UniformBufferObject { model, geom_rot: rot_mat4, view, proj, 
            use_geom_shader: [self.data.use_geometry_shader, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], 
            camera_pos: self.cur_pos.clone()};

        let memory = self.device.map_memory(
            self.data.uniform_buffers_memory[image_index],
            0,
            size_of::<buffer::UniformBufferObject>() as u64,
            vk::MemoryMapFlags::empty(),
        )?;
        
        memcpy(&ubo, memory.cast(), 1);
        
        self.device.unmap_memory(self.data.uniform_buffers_memory[image_index]);

        Ok(())
    }
}

//================================================
// MARK: Instance
//================================================

unsafe fn create_instance(window: &Window, entry: &Entry, data: &mut app_data::AppData) -> Result<Instance> {
    let application_info = vk::ApplicationInfo::builder()
        .application_name(b"Vulkan Tutorial\0")
        .application_version(vk::make_version(1, 0, 0))
        .engine_name(b"No Engine\0")
        .engine_version(vk::make_version(1, 0, 0))
        .api_version(vk::make_version(1, 1, 0));

    // Validation layers

    let available_layers = entry
        .enumerate_instance_layer_properties()?
        .iter()
        .map(|l| l.layer_name)
        .collect::<HashSet<_>>();

    if VALIDATION_ENABLED && !available_layers.contains(&VALIDATION_LAYER) {
        return Err(anyhow!("Validation layer requested but not supported."));
    }

    let layers = if VALIDATION_ENABLED {
        vec![VALIDATION_LAYER.as_ptr()]
    } else {
        Vec::new()
    };

    // Extension
    let mut extensions = vk_window::get_required_instance_extensions(window)
        .iter()
        .map(|e| e.as_ptr())
        .collect::<Vec<_>>();

    if VALIDATION_ENABLED {
        extensions.push(vk::EXT_DEBUG_UTILS_EXTENSION.name.as_ptr());
    }

    // Required by Vulkan SDK on macOS since 1.3.216.
    let flags = if 
        cfg!(target_os = "macos") && 
        entry.version()? >= PORTABILITY_MACOS_VERSION
    {
        info!("Enabling extensions for macOS portability.");
        extensions.push(vk::KHR_GET_PHYSICAL_DEVICE_PROPERTIES2_EXTENSION.name.as_ptr());
        extensions.push(vk::KHR_PORTABILITY_ENUMERATION_EXTENSION.name.as_ptr());
        vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR
    } else {
        vk::InstanceCreateFlags::empty()
    };

    let mut info = vk::InstanceCreateInfo::builder()
    .application_info(&application_info)
    .enabled_layer_names(&layers)
    .enabled_extension_names(&extensions)
    .flags(flags);

    let mut debug_info = vk::DebugUtilsMessengerCreateInfoEXT::builder()
        .message_severity(vk::DebugUtilsMessageSeverityFlagsEXT::all())
        .message_type(vk::DebugUtilsMessageTypeFlagsEXT::all())
        .user_callback(Some(debug_callback));

    if VALIDATION_ENABLED {
        info = info.push_next(&mut debug_info);
    }
    
    let instance = entry.create_instance(&info, None)?;

    if VALIDATION_ENABLED {
        data.messenger = instance.create_debug_utils_messenger_ext(&debug_info, None)?;
    }

    Ok(instance)
}

extern "system" fn debug_callback(
    severity: vk::DebugUtilsMessageSeverityFlagsEXT,
    type_: vk::DebugUtilsMessageTypeFlagsEXT,
    data: *const vk::DebugUtilsMessengerCallbackDataEXT,
    _: *mut c_void,
) -> vk::Bool32 {
    let data = unsafe { *data };
    let message = unsafe { CStr::from_ptr(data.message) }.to_string_lossy();

    if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::ERROR {
        error!("({:?}) {}", type_, message);
    } else if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::WARNING {
        warn!("({:?}) {}", type_, message);
    } else if severity >= vk::DebugUtilsMessageSeverityFlagsEXT::INFO {
        debug!("({:?}) {}", type_, message);
    } else {
        trace!("({:?}) {}", type_, message);
    }

    vk::FALSE
}

unsafe fn pick_physical_device(instance: &Instance, data: &mut app_data::AppData) -> Result<()> {
    for physical_device in instance.enumerate_physical_devices()? {
        let properties = instance.get_physical_device_properties(physical_device);

        if let Err(error) = check_physical_device(instance, data, physical_device) {
            warn!("Skipping physical device (`{}`): {}", properties.device_name, error);
        } else {
            info!("Selected physical device (`{}`).", properties.device_name);
            data.physical_device = physical_device;
            data.msaa_samples = get_max_msaa_samples(instance, data);
            return Ok(());
        }
    }

    Err(anyhow!("Failed to find suitable physical device."))
}

unsafe fn check_physical_device(
    instance: &Instance,
    data: &app_data::AppData,
    physical_device: vk::PhysicalDevice,
) -> Result<()> {
    let properties = instance.get_physical_device_properties(physical_device);
    if properties.device_type != vk::PhysicalDeviceType::DISCRETE_GPU {
        return Err(anyhow!(errors::SuitabilityError("Only discrete GPUs are supported.")));
    }

    let features = instance.get_physical_device_features(physical_device);
    if features.geometry_shader != vk::TRUE {
        return Err(anyhow!(errors::SuitabilityError("Missing geometry shader support.")));
    }
    if features.sampler_anisotropy != vk::TRUE {
        return Err(anyhow!(errors::SuitabilityError("No sampler anisotropy.")));
    }

    queue_family_indices::QueueFamilyIndices::get(instance, data, physical_device)?;
    check_physical_device_extensions(instance, physical_device)?;

    let support = swapchain::SwapchainSupport::get(instance, data, physical_device)?;
    if support.formats.is_empty() || support.present_modes.is_empty() {
        return Err(anyhow!(errors::SuitabilityError("Insufficient swapchain support.")));
    }

    Ok(())
}

unsafe fn check_physical_device_extensions(
    instance: &Instance,
    physical_device: vk::PhysicalDevice,
) -> Result<()> {
    let extensions = instance
        .enumerate_device_extension_properties(physical_device, None)?
        .iter()
        .map(|e| e.extension_name)
        .collect::<HashSet<_>>();
    if DEVICE_EXTENSIONS.iter().all(|e| extensions.contains(e)) {
        Ok(())
    } else {
        let missing = DEVICE_EXTENSIONS.iter().filter(|e| !extensions.contains(e)).collect::<Vec<&vk::ExtensionName>>();
        for missing_extension in missing {
            println!("Missing extension: {}", missing_extension);
        }
        Err(anyhow!(errors::SuitabilityError("Missing required device extensions.")))
    }
}


unsafe fn create_logical_device(
    entry: &Entry,
    instance: &Instance,
    data: &mut app_data::AppData,
) -> Result<Device> {
    let indices = queue_family_indices::QueueFamilyIndices::get(instance, data, data.physical_device)?;

    let queue_priorities = &[1.0];
    let queue_info = vk::DeviceQueueCreateInfo::builder()
        .queue_family_index(indices.graphics)
        .queue_priorities(queue_priorities);

    let layers = if VALIDATION_ENABLED {
        vec![VALIDATION_LAYER.as_ptr()]
    } else {
        vec![]
    };

    let mut extensions = DEVICE_EXTENSIONS
        .iter()
        .map(|n| n.as_ptr())
        .collect::<Vec<_>>();

    // Required by Vulkan SDK on macOS since 1.3.216.
    if cfg!(target_os = "macos") && entry.version()? >= PORTABILITY_MACOS_VERSION {
        extensions.push(vk::KHR_PORTABILITY_SUBSET_EXTENSION.name.as_ptr());
    };

    let features = vk::PhysicalDeviceFeatures::builder()
        .sampler_anisotropy(true)
        .geometry_shader(true);

    let indices = queue_family_indices::QueueFamilyIndices::get(instance, data, data.physical_device)?;

    let mut unique_indices = HashSet::new();
    unique_indices.insert(indices.graphics);
    unique_indices.insert(indices.present);

    let queue_priorities = &[1.0];
    let queue_infos = unique_indices
        .iter()
        .map(|i| {
            vk::DeviceQueueCreateInfo::builder()
                .queue_family_index(*i)
                .queue_priorities(queue_priorities)
        })
        .collect::<Vec<_>>();


    let info = vk::DeviceCreateInfo::builder()
        .queue_create_infos(&queue_infos)
        .enabled_layer_names(&layers)
        .enabled_extension_names(&extensions)
        .enabled_features(&features);
    let device = instance.create_device(data.physical_device, &info, None)?;

    data.graphics_queue = device.get_device_queue(indices.graphics, 0);
    data.present_queue = device.get_device_queue(indices.present, 0);

    Ok(device)
}

unsafe fn create_pipeline(device: &Device, data: &mut app_data::AppData) -> Result<()> {
    // set up shaders for cubes
    // load the byte data
    let vert_cube = include_bytes!("../shaders/compiled/vert_cube.spv");
    let geo_cube = include_bytes!("../shaders/compiled/geo_cube.spv");
    let frag_cube = include_bytes!("../shaders/compiled/frag_cube.spv");
    // create the shaders
    let vert_shader_module_cube = create_shader_module(device, &vert_cube[..])?;
    let geo_shader_module_cube = create_shader_module(device, &geo_cube[..])?;
    let frag_shader_module_cube = create_shader_module(device, &frag_cube[..])?;
    //create the shader stage for the vertex shader
    let vert_stage_cube = vk::PipelineShaderStageCreateInfo::builder()
        .stage(vk::ShaderStageFlags::VERTEX)
        .module(vert_shader_module_cube)
        .name(b"main\0");
    //create the shader stage for the geometry shader
    let geo_stage_cube = vk::PipelineShaderStageCreateInfo::builder()
        .stage(vk::ShaderStageFlags::GEOMETRY)
        .module(geo_shader_module_cube)
        .name(b"main\0");
    //create the shader stage for the fragment shader
    let frag_stage_cube = vk::PipelineShaderStageCreateInfo::builder()
        .stage(vk::ShaderStageFlags::FRAGMENT)
        .module(frag_shader_module_cube)
        .name(b"main\0");
    // create the binding description for the cube vertex
    let binding_descriptions_cube = &[vertex::Vertex::binding_description()];
    let attribute_descriptions_cube = vertex::Vertex::attribute_descriptions();
    let vertex_input_state_cube = vk::PipelineVertexInputStateCreateInfo::builder()
        .vertex_binding_descriptions(binding_descriptions_cube)
        .vertex_attribute_descriptions(&attribute_descriptions_cube);

    // set up shaders for cuboids
    // load the byte data
    let vert_cuboid = include_bytes!("../shaders/compiled/vert_cuboid.spv");
    let geo_cuboid = include_bytes!("../shaders/compiled/geo_cuboid.spv");
    let frag_cuboid = include_bytes!("../shaders/compiled/frag_cuboid.spv");
    // create the shaders
    let vert_shader_module_cuboid = create_shader_module(device, &vert_cuboid[..])?;
    let geo_shader_module_cuboid = create_shader_module(device, &geo_cuboid[..])?;
    let frag_shader_module_cuboid = create_shader_module(device, &frag_cuboid[..])?;
    //create the shader stage for the vertex shader
    let vert_stage_cuboid = vk::PipelineShaderStageCreateInfo::builder()
        .stage(vk::ShaderStageFlags::VERTEX)
        .module(vert_shader_module_cuboid)
        .name(b"main\0");
    //create the shader stage for the geometry shader
    let geo_stage_cuboid = vk::PipelineShaderStageCreateInfo::builder()
        .stage(vk::ShaderStageFlags::GEOMETRY)
        .module(geo_shader_module_cuboid)
        .name(b"main\0");
    //create the shader stage for the fragment shader
    let frag_stage_cuboid = vk::PipelineShaderStageCreateInfo::builder()
        .stage(vk::ShaderStageFlags::FRAGMENT)
        .module(frag_shader_module_cuboid)
        .name(b"main\0");
    // create the binding description for the sized vertex
    let binding_descriptions_cuboid = &[vertex::SizedVertex::binding_description()];
    let attribute_descriptions_cuboid = vertex::SizedVertex::attribute_descriptions();
    let vertex_input_state_cuboid = vk::PipelineVertexInputStateCreateInfo::builder()
        .vertex_binding_descriptions(binding_descriptions_cuboid)
        .vertex_attribute_descriptions(&attribute_descriptions_cuboid);

    // set up shaders for quads/raytracing
    // load the byte data
    let vert_quad = include_bytes!("../shaders/compiled/vert_rt_quad.spv");
    let frag_quad = include_bytes!("../shaders/compiled/frag_rt_quad.spv");
    // create the shaders
    let vert_shader_module_quad = create_shader_module(device, &vert_quad[..])?;
    let frag_shader_module_quad = create_shader_module(device, &frag_quad[..])?;
    //create the shader stage for the vertex shader
    let vert_stage_quad = vk::PipelineShaderStageCreateInfo::builder()
        .stage(vk::ShaderStageFlags::VERTEX)
        .module(vert_shader_module_quad)
        .name(b"main\0");
    //create the shader stage for the fragment shader
    let frag_stage_quad = vk::PipelineShaderStageCreateInfo::builder()
        .stage(vk::ShaderStageFlags::FRAGMENT)
        .module(frag_shader_module_quad)
        .name(b"main\0");
    // create the binding description for the quad vertex
    let binding_descriptions_quad = &[vertex::RTVertex::binding_description()];
    let attribute_descriptions_quad = vertex::RTVertex::attribute_descriptions();
    let vertex_input_state_quad = vk::PipelineVertexInputStateCreateInfo::builder()
        .vertex_binding_descriptions(binding_descriptions_quad)
        .vertex_attribute_descriptions(&attribute_descriptions_quad);

    // define input assembly and object type. This is altered when using geometry shader
    let mut topology = vk::PrimitiveTopology::TRIANGLE_LIST;
    if data.use_geometry_shader {
        topology = vk::PrimitiveTopology::POINT_LIST;
    } 
    data.topology = topology;

    let input_assembly_state = vk::PipelineInputAssemblyStateCreateInfo::builder()
        .topology(topology)
        .primitive_restart_enable(false);

    // define viewport and other transformations when projecting onto the screen
    let viewport = vk::Viewport::builder()
        .x(0.0)
        .y(0.0)
        .width(data.swapchain_extent.width as f32)
        .height(data.swapchain_extent.height as f32)
        .min_depth(0.0)
        .max_depth(1.0);

    let scissor = vk::Rect2D::builder()
        .offset(vk::Offset2D { x: 0, y: 0 })
        .extent(data.swapchain_extent);

    let viewports = &[viewport];
    let scissors = &[scissor];
    let viewport_state = vk::PipelineViewportStateCreateInfo::builder()
        .viewports(viewports)
        .scissors(scissors);

    let rasterization_state = vk::PipelineRasterizationStateCreateInfo::builder()
        .depth_clamp_enable(false)
        .rasterizer_discard_enable(false)
        .polygon_mode(vk::PolygonMode::FILL)
        .line_width(1.0)
        .cull_mode(vk::CullModeFlags::BACK)
        .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
        .depth_bias_enable(false);

    let multisample_state = vk::PipelineMultisampleStateCreateInfo::builder()
        .sample_shading_enable(false)
        .rasterization_samples(data.msaa_samples);

    let depth_stencil_state = vk::PipelineDepthStencilStateCreateInfo::builder()
        .depth_test_enable(true)
        .depth_write_enable(true)
        .depth_compare_op(vk::CompareOp::LESS)
        .depth_bounds_test_enable(false)
        .min_depth_bounds(0.0) // Optional.
        .max_depth_bounds(1.0) // Optional.
        .stencil_test_enable(false);

    let attachment = vk::PipelineColorBlendAttachmentState::builder()
        .color_write_mask(vk::ColorComponentFlags::all())
        .blend_enable(false)
        .src_color_blend_factor(vk::BlendFactor::ONE)  // Optional
        .dst_color_blend_factor(vk::BlendFactor::ZERO) // Optional
        .color_blend_op(vk::BlendOp::ADD)              // Optional
        .src_alpha_blend_factor(vk::BlendFactor::ONE)  // Optional
        .dst_alpha_blend_factor(vk::BlendFactor::ZERO) // Optional
        .alpha_blend_op(vk::BlendOp::ADD);             // Optional
    
    let attachments = &[attachment];
    let color_blend_state = vk::PipelineColorBlendStateCreateInfo::builder()
        .logic_op_enable(false)
        .logic_op(vk::LogicOp::COPY)
        .attachments(attachments)
        .blend_constants([0.0, 0.0, 0.0, 0.0]);

    // define the work pipeline
    let set_layouts = &[data.descriptor_set_layout];
    let layout_info = vk::PipelineLayoutCreateInfo::builder()
        .set_layouts(set_layouts);

    data.pipeline_layout = device.create_pipeline_layout(&layout_info, None)?;
    // define stages for the cubes pipeline
    let stages_cube = &[vert_stage_cube, frag_stage_cube];
    let stages_geom_cube = &[vert_stage_cube, geo_stage_cube,frag_stage_cube];

    let mut info_cube = vk::GraphicsPipelineCreateInfo::builder()
        .vertex_input_state(&vertex_input_state_cube)
        .input_assembly_state(&input_assembly_state)
        .viewport_state(&viewport_state)
        .rasterization_state(&rasterization_state)
        .multisample_state(&multisample_state)
        .depth_stencil_state(&depth_stencil_state)
        .color_blend_state(&color_blend_state)
        .layout(data.pipeline_layout)
        .render_pass(data.render_pass)
        .subpass(0);

    if data.use_geometry_shader {
        info_cube = info_cube.stages(stages_geom_cube);
    }
    else {
        info_cube = info_cube.stages(stages_cube);
    }
    // define stages for the cuboid pipeline
    let stages_cuboid = &[vert_stage_cuboid, frag_stage_cuboid];
    let stages_geom_cuboid = &[vert_stage_cuboid, geo_stage_cuboid,frag_stage_cuboid];

    let mut info_cuboid = vk::GraphicsPipelineCreateInfo::builder()
        .vertex_input_state(&vertex_input_state_cuboid)
        .input_assembly_state(&input_assembly_state)
        .viewport_state(&viewport_state)
        .rasterization_state(&rasterization_state)
        .multisample_state(&multisample_state)
        .depth_stencil_state(&depth_stencil_state)
        .color_blend_state(&color_blend_state)
        .layout(data.pipeline_layout)
        .render_pass(data.render_pass)
        .subpass(0);

    if data.use_geometry_shader {
        info_cuboid = info_cuboid.stages(stages_geom_cuboid);
    }
    else {
        info_cuboid = info_cuboid.stages(stages_cuboid);
    }
    // define stages for the quad/rt pipeline
    let stages_quad = &[vert_stage_quad, frag_stage_quad];

    let mut info_quad = vk::GraphicsPipelineCreateInfo::builder()
        .vertex_input_state(&vertex_input_state_quad)
        .input_assembly_state(&input_assembly_state)
        .viewport_state(&viewport_state)
        .rasterization_state(&rasterization_state)
        .multisample_state(&multisample_state)
        .depth_stencil_state(&depth_stencil_state)
        .color_blend_state(&color_blend_state)
        .layout(data.pipeline_layout)
        .render_pass(data.render_pass)
        .subpass(0);


    info_quad = info_quad.stages(stages_quad);

    // create the pipeline
    let pipelines = device.create_graphics_pipelines(vk::PipelineCache::null(), &[info_cube, info_cuboid, info_quad], None)?.0;

    data.pipeline_cube = pipelines[0];
    data.pipeline_cuboid = pipelines[1];
    data.pipeline_quad = pipelines[2];

    device.destroy_shader_module(vert_shader_module_cube, None);
    device.destroy_shader_module(geo_shader_module_cube, None);
    device.destroy_shader_module(frag_shader_module_cube, None);

    device.destroy_shader_module(vert_shader_module_cuboid, None);
    device.destroy_shader_module(geo_shader_module_cuboid, None);
    device.destroy_shader_module(frag_shader_module_cuboid, None);

    device.destroy_shader_module(vert_shader_module_quad, None);
    device.destroy_shader_module(frag_shader_module_quad, None);

    Ok(())
}

unsafe fn create_shader_module(
    device: &Device,
    bytecode: &[u8],
) -> Result<vk::ShaderModule> {
    let bytecode = Bytecode::new(bytecode).unwrap();
    let info = vk::ShaderModuleCreateInfo::builder()
        .code_size(bytecode.code_size())
        .code(bytecode.code());

    Ok(device.create_shader_module(&info, None)?)
}

unsafe fn create_render_pass(
    instance: &Instance,
    device: &Device,
    data: &mut app_data::AppData,
) -> Result<()> {
    let color_attachment = vk::AttachmentDescription::builder()
        .format(data.swapchain_format)
        .samples(data.msaa_samples)
        .load_op(vk::AttachmentLoadOp::CLEAR)
        .store_op(vk::AttachmentStoreOp::STORE)
        .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
        .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
        .initial_layout(vk::ImageLayout::UNDEFINED)
        .final_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);

    let color_attachment_ref = vk::AttachmentReference::builder()
        .attachment(0)
        .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);

    let color_resolve_attachment = vk::AttachmentDescription::builder()
        .format(data.swapchain_format)
        .samples(vk::SampleCountFlags::_1)
        .load_op(vk::AttachmentLoadOp::DONT_CARE)
        .store_op(vk::AttachmentStoreOp::STORE)
        .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
        .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
        .initial_layout(vk::ImageLayout::UNDEFINED)
        .final_layout(vk::ImageLayout::PRESENT_SRC_KHR);

    let color_resolve_attachment_ref = vk::AttachmentReference::builder()
        .attachment(2)
        .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL);

    let depth_stencil_attachment = vk::AttachmentDescription::builder()
        .format(depth_buffer::get_depth_format(instance, data)?)
        .samples(data.msaa_samples)
        .load_op(vk::AttachmentLoadOp::CLEAR)
        .store_op(vk::AttachmentStoreOp::DONT_CARE)
        .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE)
        .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE)
        .initial_layout(vk::ImageLayout::UNDEFINED)
        .final_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);

    let depth_stencil_attachment_ref = vk::AttachmentReference::builder()
        .attachment(1)
        .layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL);

    let color_attachments = &[color_attachment_ref];
    let resolve_attachments = &[color_resolve_attachment_ref];
    let subpass = vk::SubpassDescription::builder()
        .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS)
        .color_attachments(color_attachments)
        .depth_stencil_attachment(&depth_stencil_attachment_ref)
        .resolve_attachments(resolve_attachments);
    
    let dependency = vk::SubpassDependency::builder()
        .src_subpass(vk::SUBPASS_EXTERNAL)
        .dst_subpass(0)
        .src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
            | vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS)
        .src_access_mask(vk::AccessFlags::empty())
        .dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT
            | vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS)
        .dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE
            | vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE);

    let attachments = &[color_attachment, depth_stencil_attachment, color_resolve_attachment];
    let subpasses = &[subpass];
    let dependencies = &[dependency];
    let info = vk::RenderPassCreateInfo::builder()
        .attachments(attachments)
        .subpasses(subpasses)
        .dependencies(dependencies);
    
    data.render_pass = device.create_render_pass(&info, None)?;

    Ok(())
}

unsafe fn create_framebuffers(device: &Device, data: &mut app_data::AppData) -> Result<()> {
    data.framebuffers = data
        .swapchain_image_views
        .iter()
        .map(|i| {
            let attachments = &[data.color_image_view, data.depth_image_view, *i];
            let create_info = vk::FramebufferCreateInfo::builder()
                .render_pass(data.render_pass)
                .attachments(attachments)
                .width(data.swapchain_extent.width)
                .height(data.swapchain_extent.height)
                .layers(1);

            device.create_framebuffer(&create_info, None)
        })
        .collect::<Result<Vec<_>, _>>()?;

    Ok(())
}

unsafe fn create_sync_objects(device: &Device, data: &mut app_data::AppData) -> Result<()> {
    let semaphore_info = vk::SemaphoreCreateInfo::builder();
    let fence_info = vk::FenceCreateInfo::builder()
        .flags(vk::FenceCreateFlags::SIGNALED);

    for _ in 0..MAX_FRAMES_IN_FLIGHT {
        data.image_available_semaphores
            .push(device.create_semaphore(&semaphore_info, None)?);
        data.render_finished_semaphores
            .push(device.create_semaphore(&semaphore_info, None)?);

        data.in_flight_fences.push(device.create_fence(&fence_info, None)?);
    }

    data.images_in_flight = data.swapchain_images
        .iter()
        .map(|_| vk::Fence::null())
        .collect();

    Ok(())
}

unsafe fn get_max_msaa_samples(
    instance: &Instance,
    data: &app_data::AppData,
) -> vk::SampleCountFlags {
    let properties = instance.get_physical_device_properties(data.physical_device);
    let counts = properties.limits.framebuffer_color_sample_counts
        & properties.limits.framebuffer_depth_sample_counts;
    [
        vk::SampleCountFlags::_64,
        vk::SampleCountFlags::_32,
        vk::SampleCountFlags::_16,
        vk::SampleCountFlags::_8,
        vk::SampleCountFlags::_4,
        vk::SampleCountFlags::_2,
    ]
    .iter()
    .cloned()
    .find(|c| counts.contains(*c))
    .unwrap_or(vk::SampleCountFlags::_1)
}

unsafe fn create_color_objects(
    instance: &Instance,
    device: &Device,
    data: &mut app_data::AppData,
) -> Result<()> {
    let (color_image, color_image_memory) = image::create_image(
        instance,
        device,
        data,
        data.swapchain_extent.width,
        data.swapchain_extent.height,
        1,
        data.msaa_samples,
        data.swapchain_format,
        vk::ImageTiling::OPTIMAL,
        vk::ImageUsageFlags::COLOR_ATTACHMENT
            | vk::ImageUsageFlags::TRANSIENT_ATTACHMENT,
        vk::MemoryPropertyFlags::DEVICE_LOCAL,
    )?;

    data.color_image = color_image;
    data.color_image_memory = color_image_memory;

    data.color_image_view = image::create_image_view(
        device,
        data.color_image,
        data.swapchain_format,
        vk::ImageAspectFlags::COLOR,
        1,
    )?;

    Ok(())
}