pub mod camera;
mod render;
mod body_render; 
mod fleet_render;
mod orbit_render;

use std::time::Duration;

use cgmath::InnerSpace;
use winit::event::ElementState;
use winit::keyboard::KeyCode;

use crate::fleet::FleetsManager;
use crate::solar_system::SolarSystem;
use crate::solar_system::SystemId;
use crate::texture::Texture;
use crate::timeman::Second;
use crate::timeman::TimeMan;
use crate::ui;
use crate::wgpuctx::RenderPassBuilder;
use crate::wgpuctx::SceneCtx;
use crate::wgpuctx::WgpuCtx;
use crate::wgpuctx::pipeline::RenderPipelineBuilder;

use render::*;
use body_render::*;
use fleet_render::*;
use orbit_render::*;
use camera::*;

pub struct TacticalMap
{
    canvas_size: winit::dpi::PhysicalSize<u32>,
    canvas_texture: Option<Texture>,
    canvas_pipeline: wgpu::RenderPipeline,
    canvas_buffer: wgpu::Buffer,
    canvas_bindgroup: wgpu::BindGroup,

    camera: Camera,
    pmatrix: Projection,
 
    camera_controller: CameraController,

    system_for_render: Option<SystemId>,
    body_renderer: BodyRenderer,
    fleet_renderer: FleetRenderer,
    grid_renderer: GridRenderer,
    orbit_renderer: OrbitRenderer
}

impl TacticalMap
{
    fn bindgroup_layout(device: &wgpu::Device)
    -> wgpu::BindGroupLayout
    {
        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::Uniform, 
                            has_dynamic_offset: false, 
                            min_binding_size: None 
                        },
                        count: None,
                    }
                ]
            }
        )
    }

    pub fn new(wgpuctx: &WgpuCtx)
    -> Self {
        let surface_size = winit::dpi::PhysicalSize::new(
            wgpuctx.surface_config().width,
            wgpuctx.surface_config().height
        );

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

        let canvas_texture_bindgroup_layout = Texture::bindgroup_layout(wgpuctx.device());
        let canvas_position_bindgroup_layout = Self::bindgroup_layout(wgpuctx.device());

        let canvas_pipeline = RenderPipelineBuilder::new(&canvas_shader)
            .primitive_topology(wgpu::PrimitiveTopology::TriangleStrip)
            .cull_mode(None)
            .add_bindgroup(&canvas_texture_bindgroup_layout)
            .add_bindgroup(&canvas_position_bindgroup_layout)
            .build(Some("Tactical map canvas pipeline"), wgpuctx);

        let canvas_buffer = wgpuctx.create_buffer(
            &wgpu::BufferDescriptor {
                label: None,
                size: 4 * std::mem::size_of::<f32>() as u64,
                usage: wgpu::BufferUsages::UNIFORM |
                       wgpu::BufferUsages::COPY_DST,
                mapped_at_creation: false,
            }
        );

        let canvas_bindgroup = wgpuctx.device().create_bind_group(
            &wgpu::BindGroupDescriptor {
                label: None,
                layout: &canvas_position_bindgroup_layout,
                entries: &[ wgpu::BindGroupEntry {
                    binding: 0,
                    resource: canvas_buffer.as_entire_binding(),
                }],
            }
        );
    
        let camera = Camera::new(
            wgpuctx,
            cgmath::Vector3::new(0.0, 0.0, 1.0),
            cgmath::Deg(0.0),
            cgmath::Rad(0.0));

        let projection = Projection::new(
            surface_size.width,
            surface_size.height,
            cgmath::Deg(60.0),
            (0.01, 1000.0));

        Self {
            canvas_size: winit::dpi::PhysicalSize::new(0, 0),
            canvas_texture: None,
            canvas_pipeline: canvas_pipeline,
            canvas_buffer: canvas_buffer,
            canvas_bindgroup: canvas_bindgroup,
                
            camera: camera,
            pmatrix: projection,
            camera_controller: CameraController::new(),

            system_for_render: None,
            body_renderer: BodyRenderer::new(wgpuctx),
            fleet_renderer: FleetRenderer::new(wgpuctx),
            grid_renderer: GridRenderer::new(wgpuctx),
            orbit_renderer: OrbitRenderer::new(wgpuctx),
        }
    }

    pub fn resize(
        &mut self,
        width: u32,
        height: u32)
    {
        let new_size = winit::dpi::PhysicalSize::new(width, height);
        if self.canvas_size != new_size {
            self.pmatrix.resize(width, height);
            self.canvas_size = new_size;
            self.canvas_texture = None;
        }
    }

    pub fn update(
        &mut self,
        star_system: &SolarSystem,
        fleets_man: &FleetsManager,
        ui_state: &mut ui::State,
        _time: Second,
        _dt: Duration)
    {
        self.camera.set_target(ui_state.camera_target);
    }

    pub fn keyboard_input(
        &mut self,
        key_code: KeyCode,
        key_state: ElementState)
    {
        self.camera_controller.keyboard_input(key_code, key_state);
    }

    pub fn prepare(
        &mut self,
        rect: egui::Rect,
        wgpuctx: &WgpuCtx,
        fleets_man: &FleetsManager,
        solar_system: &SolarSystem,
        timeman: &TimeMan,
        dt: Duration,
    ) -> Result<(), ()>
    {
        //Prepare canvas
        if self.canvas_texture.is_none() {
            println!("Rebuilding canvas texture to {:?}", rect);
            self.canvas_texture = Some(wgpuctx.new_texture(
                wgpu::TextureDescriptor {
                    label: None,
                    size: wgpu::Extent3d {
                        width: self.canvas_size.width,
                        height: self.canvas_size.height,
                        depth_or_array_layers: 1
                    },
                    mip_level_count: 1,
                    sample_count: 1,
                    dimension: wgpu::TextureDimension::D2,
                    format: wgpuctx.surface_config().format,
                    usage: wgpu::TextureUsages::RENDER_ATTACHMENT |
                           wgpu::TextureUsages::TEXTURE_BINDING,
                    view_formats: &wgpuctx.surface_config().view_formats
                }
            ));
        }

        self.camera_controller.update(
            &mut self.camera, 
            solar_system,
            fleets_man,
            timeman.seconds(),
            dt);

        self.camera.update_buffer(wgpuctx, &self.pmatrix);

        if self.system_for_render.is_none() 
            || self.system_for_render.unwrap() != solar_system.id() {

                self.body_renderer.mark_to_rebuild();
                self.orbit_renderer.mark_to_rebuild();
        }
        self.system_for_render = Some(solar_system.id());

        //Update buffers for the current system and time.
        self.body_renderer.rebuild(wgpuctx, solar_system);
        self.orbit_renderer.rebuild(wgpuctx, solar_system);

        match self.orbit_renderer.update(
            wgpuctx, 
            fleets_man,
            solar_system,
            timeman.seconds()
        ){
            Ok(()) => {},
            Err(e) => println!("Tactical map orbit render update error: {}", e)
        }
        match self.body_renderer.update(
            wgpuctx, solar_system, timeman.seconds())
        {
            Ok(()) => {},
            Err(e) => println!("Tactical map body render update error: {}", e)
        }

        match self.fleet_renderer.update(
            wgpuctx,
            fleets_man,
            solar_system, 
            timeman.seconds())
        {
            Ok(()) => {},
            Err(e) => println!("Tactical map fleet render update error: {}", e)
        }

        let window_width = wgpuctx.surface_config().width as f32;
        let window_height = wgpuctx.surface_config().height as f32;
        wgpuctx.queue().write_buffer(&self.canvas_buffer, 0, 
            bytemuck::cast_slice(&[
                rect.min.x / window_width,
                rect.min.y / window_height,
                rect.width() / window_width,
                rect.height() / window_height
            ]));

        let canvas_view = self.canvas_texture.as_ref().unwrap().view();
        let mut canvas_scene = SceneCtx::from_view_default(wgpuctx, canvas_view, Some("Tactical map canvas scene"));

        self.camera.stage_changes(canvas_scene.encoder_mut());

        {
            let mut pass = RenderPassBuilder::new(Some("Tactical map render pass"), canvas_view)
                   .clear_color(wgpu::Color { r: 0.0, g: 0.0, b: 0.1, a: 1.0 })
                   .build_from_scene(&mut canvas_scene);

            self.grid_renderer.render( &mut pass, &self.camera);
            self.orbit_renderer.render(&mut pass, &self.camera);
            self.fleet_renderer.render(&mut pass, &self.camera);
            self.body_renderer.render( &mut pass, &self.camera);
        }

        canvas_scene.submit(wgpuctx);

        Ok(())
    }

    pub fn paint(
        &self,
        pass: &mut wgpu::RenderPass<'_>)
    {
        pass.set_pipeline(&self.canvas_pipeline);
        
        self.canvas_texture.as_ref().unwrap().use_on_pass(pass, 0);
        pass.set_bind_group(1, &self.canvas_bindgroup, &[]);

        pass.draw(0..4, 0..1);
    }

    pub fn paint_labels(
        &self,
        solar_system: &SolarSystem,
        time: Second,
        panel_rect: &egui::Rect,
        ui: &mut egui::Ui)
    {
        let view_matrix = self.camera.view_matrix();
        let proj_matrix = self.pmatrix.projection_matrix();
        let pv_matrix = proj_matrix * view_matrix;

        //Paint body labels
        let bodies = solar_system.bodies();
        bodies.iter().for_each(|body| {
            let scaled_radius = (panel_rect.height() * body.radius() * self.camera.get_scale()).max(16.0);

            let offset_pos = body.offset_position(time);
            let origin_pos = match body.get_orbit() {
                Some(orbit) => {
                    solar_system.body_position(&bodies[orbit.parent()], time)
                },
                None => cgmath::vec3(0.0, 0.0, 0.0)
            };
            let origin_local_pos = origin_pos - self.camera.get_combined_position();

            let offset_pos_scaled = offset_pos * self.camera.get_scale() as f64;
            let origin_pos_scaled = origin_local_pos * self.camera.get_scale() as f64;

            let origin_clip_pos = pv_matrix * origin_pos_scaled.map(|v| { v as f32 }).extend(1.0);
            let offset_clip_pos = pv_matrix * offset_pos_scaled.map(|v| { v as f32}).extend(0.0);
            let clip_pos = origin_clip_pos + offset_clip_pos;

            let ndc_pos = clip_pos.truncate() / clip_pos.w;
            let offset_ndc = offset_clip_pos.truncate() / origin_clip_pos.w;

            if offset_ndc.truncate().magnitude() < 0.1 {
                if offset_pos != cgmath::vec3(0.0, 0.0, 0.0) {
                    return;
                }
            }

            let screen_pos = egui::pos2(
                ((ndc_pos.x + 1.0) * 0.5) * panel_rect.width() + panel_rect.min.x,
                (((-ndc_pos.y + 1.0) * 0.5) * panel_rect.height() + panel_rect.min.y) 
                    + (scaled_radius / clip_pos.w).max(16.0));
            if clip_pos.z < 0.0 { return; }

            ui.put(
                egui::Rect::from_pos(screen_pos).expand(64.0), 
                egui::Label::new(
                    egui::RichText::new(body.name())
                        .color(egui::Color32::WHITE)
                    ).selectable(false).sense(egui::Sense::empty()));
        });
    }
} // impl SystemViewport
