use std::{fmt::Display, num::NonZero};
use std::error::Error;

use crate::canvas::Canvas;
use crate::solar_system::Kilometers;
use crate::tacmap::camera::Camera;
use crate::wgpuctx::RenderPassBuilder;
use crate::{solar_system::{SolarSystem, SystemId}, timeman::Second, vertex::{self, Vertex}, wgpuctx::{WgpuCtx, pipeline::RenderPipelineBuilder}};

struct BodyInstance
{
    position: cgmath::Point3<Kilometers>,
    radius: f32
}

#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct BodyInstanceRaw
{
    position: [f32;3],
    radius: f32
}

#[derive(Debug, Clone)]
pub struct NeedsRebuildError;

impl Display for NeedsRebuildError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "RenderState needs to be rebuilt before updating!")
    }
}

impl Error for NeedsRebuildError {}

pub struct BodyRenderer
{
    needs_rebuild: bool,
    last_time: Option<Second>,

    pipeline: wgpu::RenderPipeline,

    body_vertex_buffer: wgpu::Buffer,
    body_instance_buffer: Option<(usize, wgpu::Buffer)>
}

impl BodyRenderer
{
    pub fn new(
        wgpuctx: &WgpuCtx)
    -> Self {
        let quad_vertices = vertex::QUAD_VERTICES;

        let body_vertex_buffer = wgpuctx.create_buffer_init(
            &wgpu::util::BufferInitDescriptor {
                label: None,
                contents: bytemuck::cast_slice(quad_vertices),
                usage: wgpu::BufferUsages::VERTEX
            }
        );

        let shader = wgpuctx.create_shader(
            wgpu::include_wgsl!(concat!(
                    env!("CARGO_MANIFEST_DIR"),
                    "/assets/shaders/tacbody.wgsl")
        ));

        let render_pipeline = RenderPipelineBuilder::new(&shader)
            .add_bindgroup(&Camera::bindgroup_layout(wgpuctx))
            .add_vertex_layout(Vertex::descr())
            .add_vertex_layout(BodyInstanceRaw::descr())
            .build(Some("Tactical map render pipeline"), wgpuctx);

        Self {
            needs_rebuild: true,
            last_time: None,
            pipeline: render_pipeline,
            body_vertex_buffer,
            body_instance_buffer: None
        }
    }

    pub fn mark_to_rebuild(&mut self)
    { self.needs_rebuild = true; }

    pub fn rebuild(
        &mut self,
        wgpuctx: &WgpuCtx,
        solar_system: &SolarSystem)
    {
        if self.body_instance_buffer.is_some() && !self.needs_rebuild {
            return;
        }

        match self.body_instance_buffer.as_mut() {
            Some(buffer) => {
                buffer.1.destroy();
                self.body_instance_buffer = None;
            }
            None => {}
        }

        let bodies = solar_system.bodies();
        let buffer_len = bodies.len() * size_of::<BodyInstanceRaw>();

        let buffer = wgpuctx.create_buffer(
            &wgpu::BufferDescriptor {
                label: Some("Tactical map bodies instance buffer"),
                size: buffer_len as u64,
                usage: wgpu::BufferUsages::COPY_DST |
                       wgpu::BufferUsages::VERTEX,
                mapped_at_creation: false
            }
        );

        self.last_time = None;
        self.body_instance_buffer = Some((bodies.len(), buffer));
    }

    pub fn update(
        &mut self,
        wgpuctx: &WgpuCtx,
        solar_system: &SolarSystem,
        time: Second)
    -> Result<(), Box<dyn Error>>
    {
        //If the last updated time is the same, we don't need to update
        //the positions of all the bodies.
        match self.last_time {
            Some(last_time) => {
                if last_time == time {
                    return Ok(());
                }
            }
            None => {}
        }

        self.last_time = Some(time);
        let (_, bodies_buffer) = match &self.body_instance_buffer {
            Some(tuple) => tuple,
            None => return Err(Box::new(NeedsRebuildError))
        };

        let bodies = solar_system.bodies();
        let body_instances = bodies.iter().map(|body| {
            let position = match body.position(time) {
                Some(pos) => pos,
                None => return BodyInstanceRaw { 
                    position: [0.0, 0.0, 0.0], 
                    radius: body.radius() }
            };
            BodyInstance {
                position: position,
                radius: body.radius()
            }.raw()
        }).collect::<Vec<_>>();

        wgpuctx.queue().write_buffer(bodies_buffer, 0, bytemuck::cast_slice(&body_instances));
        //Update the canvas texture.

        Ok(())
    }

    pub fn render(
        &self,
        wgpuctx: &WgpuCtx,
        canvas: &Canvas,
        camera: &Camera)
    {
        let (num_bodies, bodies_buffer) = match &self.body_instance_buffer {
            Some(tuple) => tuple,
            None => return
        };

        let mut encoder = wgpuctx.create_default_encoder("Tactical map renderer encoder");
        let view = canvas.view();

        {
            camera.stage_changes(&mut encoder);
        }
        {
            let mut pass = RenderPassBuilder::new("Tactiacal map render pass", view)
                .clear_color(wgpu::Color { r: 0.0, g: 0.0, b: 0.1, a: 1.0 })
                .build(&mut encoder);

            pass.set_pipeline(&self.pipeline);
            pass.set_bind_group(0, camera.bindgroup(), &[]);

            pass.set_vertex_buffer(0, self.body_vertex_buffer.slice(..));
            pass.set_vertex_buffer(1, bodies_buffer.slice(..));

            pass.draw(0..6, 0..num_bodies.clone() as _);
        }

        wgpuctx.submit_encoder(encoder);
    }
} // impl RenderState

impl BodyInstance 
{
    fn raw(&self) -> BodyInstanceRaw
    {
        BodyInstanceRaw {
            position: [ 
                self.position.x as f32, 
                self.position.y as f32, 
                self.position.z as f32 ],
            radius: self.radius
        }
    }
} // impl BodyInstance

impl BodyInstanceRaw 
{
    fn descr() -> wgpu::VertexBufferLayout<'static> {
        use std::mem;
        wgpu::VertexBufferLayout {
            array_stride: mem::size_of::<BodyInstanceRaw>() as wgpu::BufferAddress,
            step_mode: wgpu::VertexStepMode::Instance,
            attributes: &[
                wgpu::VertexAttribute {
                    offset: 0,
                    shader_location: 2,
                    format: wgpu::VertexFormat::Float32x3
                },
                wgpu::VertexAttribute {
                    offset: mem::size_of::<[f32;3]>() as wgpu::BufferAddress,
                    shader_location: 3,
                    format: wgpu::VertexFormat::Float32
                }
            ]
        }
    }
}
