use vulkanalia::prelude::v1_0::*;
use anyhow::Result;

use cgmath::{vec2, vec3};

use std::collections::HashMap;

use crate::app_data::AppData;
use crate::buffer;
use crate::vertex;
use crate::primitives::cube::Cube;

extern crate rand;
use rand::Rng;

const CHUNK_SIZE: usize = 100;

#[derive(Clone, Debug, Default)]
pub struct Scene {
    pub vertices: Vec<vertex::Vertex>,
    pub indices: Vec<u32>,

    pub vertex_buffer: vk::Buffer,
    pub vertex_buffer_memory: vk::DeviceMemory,

    pub index_buffer: vk::Buffer,
    pub index_buffer_memory: vk::DeviceMemory,
}

impl Scene {
    pub unsafe fn prepare_data(&mut self, instance: &vulkanalia::Instance, device: &vulkanalia::Device, data: &AppData) -> Result<()> {
        let mut rng = rand::thread_rng();
        let grid_size = CHUNK_SIZE as i32;

        // todo store the chunks somewhere (or only use them as intermediary for neighbouthood calculation idc)
        let mut chunks = vec![Chunk::create()?];

        //todo use the 14 vertice box method. Not using geometry shaders seems to be faster... make this a setting?
        // have cube elements with a method asking for vertices, while giving a primitive type -> method for preferred primitive type as well as one collecting all primitives
        for x_index in 0..grid_size {
            for y_index in 0..grid_size {
                let shade = (rng.gen_range(0..25) as f32) / 100.0;
                let cube = Cube {
                    pos: vec3(x_index as f32, y_index as f32, 0.0),
                    color: vec3(shade, 1.0, shade),
                    tex_coord: vec2(0.0, 0.0)
                };
                chunks[0].set_cube(cube);
            }
        }

        let chunk  = &chunks[0];
        let chunk_iter = ChunkIter::create(chunk)?;
        for item in  chunk_iter {
            let index = self.vertices.len();
            match item {
                Some(cube) => {
                    cube.draw(&data.topology, index, self);
                }
                None => {}
            }
        }
        
        (self.vertex_buffer, self.vertex_buffer_memory) = buffer::create_vertex_buffer(instance, device, &data, &self.vertices)?;
        (self.index_buffer, self.index_buffer_memory) = buffer::create_index_buffer(&instance, &device, &data, &self.indices)?;

        Ok(())

    }

    pub unsafe fn destroy(&mut self, device: &vulkanalia::Device) {
        device.destroy_buffer(self.index_buffer, None);
        device.free_memory(self.index_buffer_memory, None);

        device.destroy_buffer(self.vertex_buffer, None);
        device.free_memory(self.vertex_buffer_memory, None);
    }
}

#[derive(Clone, Debug)]
struct Chunk {
    //todo change to hashmap?
    blocks: HashMap<cgmath::Vector3<u32>, Cube>,
}

impl Chunk {
    pub fn create() -> Result<Self> {
        let mut map = HashMap::new();
        Ok(Self {
            blocks: map
        })
    }

    pub fn set_cube(&mut self, cube: Cube) {
        let x = cube.pos.x as usize;
        let y = cube.pos.y as usize;
        let z = cube.pos.z as usize;
        assert!(x < CHUNK_SIZE, "x value out of range!");
        assert!(y < CHUNK_SIZE, "y value out of range!");
        assert!(z < CHUNK_SIZE, "z value out of range!");
        self.blocks.insert(vec3(x as u32, y as u32, z as u32), cube);
    }

    pub fn clear_cube(&mut self, x: usize, y: usize, z: usize) {
        assert!(x < CHUNK_SIZE, "x value out of range!");
        assert!(y < CHUNK_SIZE, "y value out of range!");
        assert!(z < CHUNK_SIZE, "z value out of range!");
        self.blocks.remove(&vec3(x as u32, y as u32, z as u32));
    }

}

struct ChunkIter<'a> {
    iter_x: usize,
    iter_y: usize,
    iter_z: usize,
    chunk: &'a Chunk
}

impl<'a> ChunkIter<'a> {
    pub fn create(chunk: &'a Chunk) -> Result<Self> {

        Ok(Self {
            iter_x: 0,
            iter_y: 0,
            iter_z: 0,
            chunk
        })
    }
}

impl<'a> Iterator for ChunkIter<'a> {
    type Item = Option<&'a Cube>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.iter_x < CHUNK_SIZE && self.iter_y < CHUNK_SIZE && self.iter_z < CHUNK_SIZE {
            let result = self.chunk.blocks.get(&vec3(self.iter_x as u32, self.iter_y as u32, self.iter_z as u32));

            self.iter_x += 1;
            if self.iter_x >= CHUNK_SIZE {
                self.iter_x = 0;
                self.iter_y += 1;
            }
            if self.iter_y >= CHUNK_SIZE {
                self.iter_y = 0;
                self.iter_z += 1;
            }
            return Some(result.clone())
        }
        self.iter_x = 0;
        self.iter_y = 0;
        self.iter_z = 0;
        None
    }
}