use std::error::Error;

use crate::solar_system::{Kilometers};
use crate::solar_system::body::OrbitalBody;
use crate::timeman::{Second};
use crate::wgpuctx::{WgpuCtx, pipeline::RenderPipelineBuilder};

use super::*;

pub struct BodyRenderer
{
    needs_rebuild: bool,
    last_tick: Option<i64>,

    pipeline: wgpu::RenderPipeline,

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

impl BodyRenderer
{
    pub fn new(
        wgpuctx: &WgpuCtx)
    -> Self {
        let shader = wgpuctx.create_shader(
            wgpu::include_wgsl!(concat!(
                    env!("CARGO_MANIFEST_DIR"),
                    "/assets/shaders/tacmap/body.wgsl")
        ));

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

        Self {
            needs_rebuild: true,
            last_tick: None,
            pipeline: render_pipeline,
            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;
        }
        self.needs_rebuild = false;

        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_tick = 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.
        let tick = time / OrbitalBody::TICK_DURATION;
        if self.last_tick.is_some_and(|last_tick| { last_tick == tick }) {
            return Ok(());
        }

        self.last_tick = 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 = body.offset_position(time);
            let origin = match body.get_orbit() {
                Some(orbit) => {
                    let origin_body = &bodies[orbit.parent()];
                    origin_body.offset_position(time)
                },
                None => cgmath::Vector3::new(0.0, 0.0, 0.0)
            };
            BodyInstance {
                position: position,
                origin: origin,
                radius: body.radius()
            }.raw()
        }).collect::<Vec<_>>();

        wgpuctx.queue().write_buffer(bodies_buffer, 0, bytemuck::cast_slice(&body_instances));

        Ok(())
    }

    pub fn render(
        &self,
        pass: &mut wgpu::RenderPass,
        camera: &Camera)
    {
        let (num_bodies, bodies_buffer) = match &self.body_instance_buffer {
            Some(tuple) => tuple,
            None => return
        };

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

        pass.set_vertex_buffer(0, bodies_buffer.slice(..));

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

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

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

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

impl BodyInstanceRaw 
{
    const ATTRIBS: [wgpu::VertexAttribute;3] =
        wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32];

    pub fn descr()
    -> wgpu::VertexBufferLayout<'static> {
        use std::mem;

        wgpu::VertexBufferLayout {
            array_stride: mem::size_of::<Self>() as wgpu::BufferAddress,
            step_mode: wgpu::VertexStepMode::Instance,
            attributes: &Self::ATTRIBS
        }
    }
}

