use std::error::Error;

use super::*;

use crate::fleet::FleetId;
use crate::solar_system::body::{BodyId, OrbitalBody};
use crate::solar_system::orbit::StaticOrbiter;
use crate::timeman::{self, Second};
use crate::wgpuctx::{WgpuCtx, pipeline::RenderPipelineBuilder};

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

    pipeline: wgpu::RenderPipeline,

    body_orbit_vertex_buffers: Option<Vec<(BodyId, wgpu::Buffer)>>,
    fleet_orbit_vertex_buffers: Option<Vec<(FleetId, wgpu::Buffer)>>,

    origin_buffer: Option<wgpu::Buffer>,

    origin_bind_group: Option<wgpu::BindGroup>,
    origin_bind_group_layout: wgpu::BindGroupLayout
}

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

        let origin_bind_group_layout = wgpuctx.device().create_bind_group_layout(
            &wgpu::BindGroupLayoutDescriptor {
                label: None,
                entries: &[
                    wgpu::BindGroupLayoutEntry {
                        binding: 0,
                        visibility: wgpu::ShaderStages::VERTEX,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Storage { read_only: true },
                            has_dynamic_offset: false,
                            min_binding_size: None
                        },
                        count: None
                    }
                ]
            }
        );

        let render_pipeline = RenderPipelineBuilder::new(&shader)
            .add_bindgroup(&Camera::bindgroup_layout(wgpuctx))
            .add_bindgroup(&origin_bind_group_layout)
            .add_vertex_layout(OrbitVertex::descr())
            .primitive_topology(wgpu::PrimitiveTopology::TriangleStrip)
            .cull_mode(None)
            .build(Some("Tactical map orbit render pipeline"), wgpuctx);

        Self {
            needs_rebuild: true,
            last_time: None,
            pipeline: render_pipeline,

            body_orbit_vertex_buffers: None,
            fleet_orbit_vertex_buffers: None,

            origin_buffer: None,
            origin_bind_group: None,
            origin_bind_group_layout: origin_bind_group_layout
        }
    }

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

    fn create_orbit_buffer<T: StaticOrbiter>(
        &self,
        wgpuctx: &WgpuCtx,
        orbiter: &T,
        time: Second)
    -> Option<(usize, wgpu::Buffer)>
    {
        let orbit = match orbiter.orbit() {
            Some(orbit) => orbit,
            None => { return None; }
        };

        let period = orbit.period(orbiter);

        let num_points = ((period / timeman::HOUR) as usize).clamp(90, 360);
        let period_interval = period as f64 / num_points as f64;

        let mut points = vec![
            OrbitVertex::default();(num_points+1)*2];

        for i in 0..num_points {
            let position = orbit.calculate_position_at(
                orbiter,
                time + (i as f64 * period_interval) as Second);

            points[i*2].origin_id = orbit.parent() as _;
            points[i*2].position = [
                position.x as f32,
                position.y as f32,
                position.z as f32
            ];
            
            points[(i*2)+1] = points[i*2];
        }
        points[num_points*2] = points[0];
        points[num_points*2+1] = points[1];

        let buffer = wgpuctx.create_buffer_init(
            &wgpu::util::BufferInitDescriptor {
                label: None,
                usage: wgpu::BufferUsages::VERTEX,
                contents: bytemuck::cast_slice(&points)
            }
        );
        Some(((num_points+1)*2, buffer))
    }

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

        if let Some(buffers) = &self.body_orbit_vertex_buffers {
            for buffer in buffers {
                buffer.1.destroy();
            }
            self.body_orbit_vertex_buffers = None;
        };

        if let Some(buffers) = &self.fleet_orbit_vertex_buffers {
            for buffer in buffers {
                buffer.1.destroy();
            }
            self.fleet_orbit_vertex_buffers = None;
        };

        //From the solar system's bodies, calculate the orbits.
        let bodies = solar_system.bodies();

        match &self.origin_buffer {
            Some(buffer) => {
                buffer.destroy();
            }
            None => {}
        };
        let origin_buffer = wgpuctx.create_buffer(
            &wgpu::BufferDescriptor {
                label: None,
                size: (std::mem::size_of::<[f32;3]>() * bodies.len()) as _,
                usage: wgpu::BufferUsages::COPY_DST
                    | wgpu::BufferUsages::STORAGE,
                mapped_at_creation: false
            }
        );

        self.origin_bind_group = Some(wgpuctx.device().create_bind_group(
            &wgpu::BindGroupDescriptor {
                label: None,
                layout: &self.origin_bind_group_layout,
                entries: &[
                    wgpu::BindGroupEntry {
                        binding: 0,
                        resource: origin_buffer.as_entire_binding()
                    }
                ]
            }
        ));
        self.origin_buffer = Some(origin_buffer);

        self.body_orbit_vertex_buffers = Some(bodies.iter().filter_map(|body| {
            self.create_orbit_buffer(wgpuctx, body, 0)
        }).collect::<Vec<_>>());
    }

    pub fn update(
        &mut self,
        wgpuctx: &WgpuCtx,
        fleets_man: &FleetsManager,
        solar_system: &SolarSystem,
        time: Second)
    -> Result<(), Box<dyn Error>>
    {
        if self.last_time.is_some_and(|t| { t == time }) {
            return Ok(());
        }

        self.fleet_orbit_vertex_buffers =
            Some(fleets_man.all_in_system(solar_system.id()).iter().filter_map(|id| {
            let fleet = fleets_man.fleet(*id).unwrap();
            self.create_orbit_buffer(wgpuctx, fleet, time)
        }).collect::<Vec<_>>());
        
        let body_tick = time / OrbitalBody::TICK_DURATION;
        let last_body_tick = self.last_time.unwrap_or(i64::MAX) / OrbitalBody::TICK_DURATION;
        self.last_time = Some(time);

        if body_tick == last_body_tick {
            return Ok(());
        }

        let positions = solar_system.bodies().iter().map(|body| {
            let pos = solar_system.body_position(body, time);
            [ pos.x as f32, pos.y as f32, pos.z as f32 ]
        }).collect::<Vec<_>>();

        match &self.origin_buffer {
            Some(buffer) => {
                wgpuctx.queue().write_buffer(buffer, 0, bytemuck::cast_slice(&positions));
            },
            None => { return Err(Box::new(NeedsRebuildError)); }
        };

        Ok(())
    }

    pub fn render(
        &self,
        pass: &mut wgpu::RenderPass,
        camera: &Camera)
    {
        pass.set_pipeline(&self.pipeline);
        pass.set_bind_group(0, camera.bindgroup(), &[]);

        match &self.origin_bind_group {
            Some(bind_group) => { pass.set_bind_group(1, bind_group, &[]); },
            None => { return; }
        }

        if let Some(body_buffers) = &self.body_orbit_vertex_buffers {
            for (num_points, buffer) in body_buffers {
                pass.set_vertex_buffer(0, buffer.slice(..));
                pass.draw(0..(*num_points as u32), 0..1);
            }
        }
        if let Some(fleet_buffers) = &self.fleet_orbit_vertex_buffers {
            for (num_points, buffer) in fleet_buffers {
                pass.set_vertex_buffer(0, buffer.slice(..));
                pass.draw(0..(*num_points as u32), 0..1);
            }
        }
    }
}

#[repr(C)]
#[derive(Copy, Clone, Default, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct OrbitVertex
{
    pub origin_id: u32,
    pub position: [f32;3]
}

impl OrbitVertex
{
    const ATTRIBS: [wgpu::VertexAttribute;2] =
        wgpu::vertex_attr_array![0 => Uint32, 1 => Float32x3];

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

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