use crate::camera::*;
use crate::grid::*;
use crate::pipelines::{raymarch::*, render::*, ssao::*};
use crate::utils::*;
use lib3dmol::structures::GetAtom;
use nalgebra_glm as glm;
use std::convert::TryInto;
use std::time::SystemTime;
use wgpu;
pub struct Application {
    
    width: u32,
    
    height: u32,
    
    device: wgpu::Device,
    
    queue: wgpu::Queue,
    
    start_time: SystemTime,
    
    pub camera: RotationCamera,
    
    pub camera_changed: bool,
    
    voxel_grid: VoxelGrid,
    
    raymarch_globals: RaymarchGlobals,
    
    raymarch_globals_buffer: wgpu::Buffer,
    
    ssao_globals_buffer: wgpu::Buffer,
    
    raymarch_pipeline: RaymarchPipeline,
    
    render_pipeline: RenderPipeline,
    
    ssao_pipeline: SsaoPipeline,
    gbuffer_positions: wgpu::TextureView,
    gbuffer_normals: wgpu::TextureView,
    output_texture: wgpu::TextureView,
    sdf_default: wgpu::Buffer,
    
    
    sdf_texture: wgpu::Texture,
    sdf_texture_view: wgpu::TextureView,
    mouse_pressed: bool,
    mouse_position: winit::dpi::PhysicalPosition<f64>,
}
impl Application {
    
    
    
    pub async fn new(width: u32, height: u32, surface: &wgpu::Surface, file_name: Option<&str>) -> Self {
        
        let adapter = wgpu::Adapter::request(
            &wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::HighPerformance,
                compatible_surface: Some(&surface),
            },
            wgpu::BackendBit::PRIMARY,
        )
        .await
        .unwrap();
        println!("{}", adapter.get_info().name);
        let (device, queue) = adapter
            .request_device(&wgpu::DeviceDescriptor {
                extensions: wgpu::Extensions {
                    anisotropic_filtering: false,
                },
                limits: wgpu::Limits::default(),
            })
            .await;
        let raymarch_pipeline = RaymarchPipeline::new(&device);
        let render_pipeline = RenderPipeline::new(&device);
        let ssao_pipeline = SsaoPipeline::new(&device);
        
        
        
        let start_time = SystemTime::now();
        let voxel_grid = if let Some(file_name) = file_name {
            let molecule_structure = lib3dmol::parser::read_pdb(file_name, "");
            let mut atoms = Vec::new();
            for atom in molecule_structure.get_atom() {
                atoms.push(glm::vec4(atom.coord[0], atom.coord[1], atom.coord[2], 0.0));
            }
            VoxelGrid::new(&device, 1.0, atoms)
        } else {
            let mut atoms = Vec::new();
            atoms.push(glm::vec4(1.5, 0.0, 0.0, 1.0));
            atoms.push(glm::vec4(-1.5, 0.0, 0.0, 1.0));
            atoms.push(glm::vec4(0.0, 2.5, 0.0, 1.0));
            VoxelGrid::new(&device, 2.0, atoms)
        };
        let camera = RotationCamera::new(0.5 * glm::distance(&glm::vec3(0.0, 0.0, 0.0), &voxel_grid.bb_diff));
        let projection = glm::perspective(width as f32 / height as f32, 1.57079633 * 0.5, 0.01, 100.0);
        let raymarch_globals = RaymarchGlobals {
            projection: projection.as_slice().try_into().unwrap(),
            camera_origin: camera.eye().as_slice().try_into().expect(""),
            bb_min: voxel_grid.bb_min.into(),
            bb_max: voxel_grid.bb_max.into(),
            bb_diff: voxel_grid.bb_diff.into(),
            bb_size: voxel_grid.bb_size.into(),
            voxel_length: voxel_grid.voxel_length,
            solvent_radius: 0.71590906,
            max_neighbours: 15,
            time: 0.0,
            save: 0,
            ..Default::default()
        };
        let raymarch_globals_buffer = device.create_buffer_with_data(
            bytemuck::cast_slice(&[raymarch_globals]),
            wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
        );
        let ssao_globals = SsaoGlobals {
            projection: projection.as_slice().try_into().unwrap(),
            ..Default::default()
        };
        let ssao_globals_buffer = device.create_buffer_with_data(
            bytemuck::cast_slice(&[ssao_globals]),
            wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
        );
        let gbuffer_positions = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("GBuffer positions texture"),
            size: wgpu::Extent3d { width, height, depth: 1 },
            array_layer_count: 1,
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba32Float,
            usage: wgpu::TextureUsage::STORAGE | wgpu::TextureUsage::SAMPLED,
        });
        let gbuffer_positions = gbuffer_positions.create_default_view();
        let gbuffer_normals = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("GBuffer normals texture"),
            size: wgpu::Extent3d { width, height, depth: 1 },
            array_layer_count: 1,
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba32Float,
            usage: wgpu::TextureUsage::STORAGE | wgpu::TextureUsage::SAMPLED,
        });
        let gbuffer_normals = gbuffer_normals.create_default_view();
        let output_texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("Output texture"),
            size: wgpu::Extent3d { width, height, depth: 1 },
            array_layer_count: 1,
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba32Float,
            usage: wgpu::TextureUsage::STORAGE,
        });
        let output_texture = output_texture.create_default_view();
        let sdf_default_cpu = vec![std::f32::NEG_INFINITY; (width * height) as usize];
        let sdf_default = device.create_buffer_with_data(bytemuck::cast_slice(&sdf_default_cpu), wgpu::BufferUsage::COPY_SRC);
        let sdf_texture = device.create_texture(&wgpu::TextureDescriptor {
            label: Some("SDF texture"),
            size: wgpu::Extent3d { width, height, depth: 1 },
            array_layer_count: 1,
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::R32Float,
            usage: wgpu::TextureUsage::STORAGE | wgpu::TextureUsage::COPY_DST | wgpu::TextureUsage::WRITE_ALL,
        });
        let sdf_texture_view = sdf_texture.create_default_view();
        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("Copy sdf encoder"),
        });
        encoder.copy_buffer_to_texture(
            wgpu::BufferCopyView {
                buffer: &sdf_default,
                offset: 0,
                bytes_per_row: width * 4,
                rows_per_image: height,
            },
            wgpu::TextureCopyView {
                texture: &sdf_texture,
                mip_level: 0,
                array_layer: 0,
                origin: wgpu::Origin3d::ZERO,
            },
            wgpu::Extent3d { width, height, depth: 1 },
        );
        queue.submit(&[encoder.finish()]);
        Self {
            width,
            height,
            device,
            queue,
            start_time,
            camera,
            camera_changed: true,
            voxel_grid,
            raymarch_globals,
            raymarch_globals_buffer,
            ssao_globals_buffer,
            raymarch_pipeline,
            render_pipeline,
            ssao_pipeline,
            gbuffer_positions,
            gbuffer_normals,
            output_texture,
            sdf_default,
            sdf_texture,
            sdf_texture_view,
            mouse_pressed: false,
            mouse_position: winit::dpi::PhysicalPosition { x: 0.0, y: 0.0},
        }
    }
    
    
    
    pub fn resize(&mut self, width: u32, height: u32) {
        self.width = width;
        self.height = height;
        let gbuffer_positions = self.device.create_texture(&wgpu::TextureDescriptor {
            label: Some("GBuffer positions texture"),
            size: wgpu::Extent3d { width, height, depth: 1 },
            array_layer_count: 1,
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba32Float,
            usage: wgpu::TextureUsage::STORAGE | wgpu::TextureUsage::SAMPLED,
        });
        self.gbuffer_positions = gbuffer_positions.create_default_view();
        let gbuffer_normals = self.device.create_texture(&wgpu::TextureDescriptor {
            label: Some("GBuffer normals texture"),
            size: wgpu::Extent3d { width, height, depth: 1 },
            array_layer_count: 1,
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba32Float,
            usage: wgpu::TextureUsage::STORAGE | wgpu::TextureUsage::SAMPLED,
        });
        self.gbuffer_normals = gbuffer_normals.create_default_view();
        let output_texture = self.device.create_texture(&wgpu::TextureDescriptor {
            label: Some("Output texture"),
            size: wgpu::Extent3d { width, height, depth: 1 },
            array_layer_count: 1,
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::Rgba32Float,
            usage: wgpu::TextureUsage::STORAGE,
        });
        self.output_texture = output_texture.create_default_view();
        let sdf_default_cpu = vec![std::f32::NEG_INFINITY; (width * height) as usize];
        self.sdf_default = self
            .device
            .create_buffer_with_data(bytemuck::cast_slice(&sdf_default_cpu), wgpu::BufferUsage::COPY_SRC);
        self.sdf_texture = self.device.create_texture(&wgpu::TextureDescriptor {
            label: Some("SDF texture"),
            size: wgpu::Extent3d { width, height, depth: 1 },
            array_layer_count: 1,
            mip_level_count: 1,
            sample_count: 1,
            dimension: wgpu::TextureDimension::D2,
            format: wgpu::TextureFormat::R32Float,
            usage: wgpu::TextureUsage::STORAGE | wgpu::TextureUsage::COPY_DST | wgpu::TextureUsage::WRITE_ALL,
        });
        self.sdf_texture_view = self.sdf_texture.create_default_view();
        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("Copy sdf encoder"),
        });
        encoder.copy_buffer_to_texture(
            wgpu::BufferCopyView {
                buffer: &self.sdf_default,
                offset: 0,
                bytes_per_row: self.width * 4,
                rows_per_image: self.height,
            },
            wgpu::TextureCopyView {
                texture: &self.sdf_texture,
                mip_level: 0,
                array_layer: 0,
                origin: wgpu::Origin3d::ZERO,
            },
            wgpu::Extent3d { width, height, depth: 1 },
        );
        self.queue.submit(&[encoder.finish()]);
    }
    
    
    
    pub fn render(&mut self, frame: &wgpu::TextureView) {
        let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
            label: Some("Main encoder"),
        });
        
        {
            let now = SystemTime::now();
            self.raymarch_globals.time = now.duration_since(self.start_time).expect("Time went backwards").as_secs_f32();
            if self.camera_changed {
                let eye = self.camera.distance * self.camera.direction_vector();
                self.raymarch_globals.camera_origin = eye.as_slice().try_into().expect("");
                encoder.copy_buffer_to_texture(
                    wgpu::BufferCopyView {
                        buffer: &self.sdf_default,
                        offset: 0,
                        bytes_per_row: self.width * 4,
                        rows_per_image: self.height,
                    },
                    wgpu::TextureCopyView {
                        texture: &self.sdf_texture,
                        mip_level: 0,
                        array_layer: 0,
                        origin: wgpu::Origin3d::ZERO,
                    },
                    wgpu::Extent3d {
                        width: self.width,
                        height: self.height,
                        depth: 1,
                    },
                );
                self.camera_changed = false;
            }
            let raymarch_globals_size = std::mem::size_of::<RaymarchGlobals>();
            let raymarch_globals_buffer = self
                .device
                .create_buffer_with_data(bytemuck::cast_slice(&[self.raymarch_globals]), wgpu::BufferUsage::COPY_SRC);
            encoder.copy_buffer_to_buffer(
                &raymarch_globals_buffer,
                0,
                &self.raymarch_globals_buffer,
                0,
                raymarch_globals_size as wgpu::BufferAddress,
            );
        }
        let raymarch_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("Raymarch bind group"),
            layout: &self.raymarch_pipeline.bind_group_layout,
            bindings: &[
                wgpu::Binding {
                    binding: 0,
                    resource: wgpu::BindingResource::Buffer {
                        buffer: &self.raymarch_globals_buffer,
                        range: 0..std::mem::size_of::<RaymarchGlobals>() as u64,
                    },
                },
                wgpu::Binding {
                    binding: 1,
                    resource: wgpu::BindingResource::TextureView(&self.output_texture),
                },
                wgpu::Binding {
                    binding: 2,
                    resource: wgpu::BindingResource::Buffer {
                        buffer: &self.voxel_grid.voxels,
                        range: 0..(self.voxel_grid.voxels_len * std::mem::size_of::<f32>()) as u64,
                    },
                },
                wgpu::Binding {
                    binding: 3,
                    resource: wgpu::BindingResource::Buffer {
                        buffer: &self.voxel_grid.voxel_pointers,
                        range: 0..(self.voxel_grid.voxel_pointers_len * std::mem::size_of::<VoxelPointer>()) as u64,
                    },
                },
                wgpu::Binding {
                    binding: 4,
                    resource: wgpu::BindingResource::TextureView(&self.sdf_texture_view),
                },
                wgpu::Binding {
                    binding: 5,
                    resource: wgpu::BindingResource::TextureView(&self.gbuffer_positions),
                },
                wgpu::Binding {
                    binding: 6,
                    resource: wgpu::BindingResource::TextureView(&self.gbuffer_normals),
                },
            ],
        });
        let render_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("Render bind group"),
            layout: &self.render_pipeline.bind_group_layout,
            bindings: &[wgpu::Binding {
                binding: 0,
                resource: wgpu::BindingResource::TextureView(&self.output_texture),
            }],
        });
        let ssao_bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("SSAO bind group"),
            layout: &self.ssao_pipeline.bind_group_layout,
            bindings: &[
                wgpu::Binding {
                    binding: 0,
                    resource: wgpu::BindingResource::Buffer {
                        buffer: &self.ssao_globals_buffer,
                        range: 0..std::mem::size_of::<SsaoGlobals>() as u64,
                    },
                },
                wgpu::Binding {
                    binding: 1,
                    resource: wgpu::BindingResource::TextureView(&self.gbuffer_positions),
                },
                wgpu::Binding {
                    binding: 2,
                    resource: wgpu::BindingResource::TextureView(&self.gbuffer_normals),
                },
                wgpu::Binding {
                    binding: 3,
                    resource: wgpu::BindingResource::TextureView(&self.output_texture),
                },
            ],
        });
        
        {
            let mut cpass = encoder.begin_compute_pass();
            cpass.set_pipeline(&self.raymarch_pipeline.pipeline);
            cpass.set_bind_group(0, &raymarch_bind_group, &[]);
            cpass.dispatch((self.width + 31) / 32, (self.height + 32) / 32, 1);
        }
        
        {
            let mut cpass = encoder.begin_compute_pass();
            cpass.set_pipeline(&self.ssao_pipeline.pipeline);
            cpass.set_bind_group(0, &ssao_bind_group, &[]);
            cpass.dispatch((self.width + 31) / 32, (self.height + 32) / 32, 1);
        }
        
        {
            let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor {
                    attachment: &frame,
                    resolve_target: None,
                    load_op: wgpu::LoadOp::Clear,
                    store_op: wgpu::StoreOp::Store,
                    clear_color: wgpu::Color::GREEN,
                }],
                depth_stencil_attachment: None,
            });
            rpass.set_pipeline(&self.render_pipeline.pipeline);
            rpass.set_bind_group(0, &render_bind_group, &[]);
            rpass.draw(0..3, 0..1);
        }
        self.queue.submit(&[encoder.finish()]);
    }
    pub fn window_event(&mut self, event: &winit::event::WindowEvent) {
        match event {
            winit::event::WindowEvent::MouseInput { state, button, .. } => {
                if *button == winit::event::MouseButton::Left {
                    if *state == winit::event::ElementState::Pressed && self.mouse_position.x >= 200.0 {
                        self.mouse_pressed = true;
                    } else {
                        self.mouse_pressed = false;
                    }
                }
            },
            winit::event::WindowEvent::MouseWheel { delta, .. } => {
                if let winit::event::MouseScrollDelta::LineDelta(_, change) = delta {
                    self.camera.distance -= change * self.camera.speed;
                    self.camera_changed = true;
                }
            },
            winit::event::WindowEvent::CursorMoved { position, .. } => {
                self.mouse_position = *position;
            }
            _ => {},
        };
    }
    pub fn device_event(&mut self, event: &winit::event::DeviceEvent) {
        match event {
            winit::event::DeviceEvent::MouseMotion { delta } => {
                if self.mouse_pressed {
                    self.camera.yaw += delta.0 as f32;
                    self.camera.pitch += delta.1 as f32;
                    self.camera_changed = true;
                }
            }
            _ => {}
        };
    }
    
    
    
    pub fn device(&self) -> &wgpu::Device {
        &self.device
    }
    
    
    
    pub fn queue_mut(&self) -> &wgpu::Queue {
        &self.queue
    }
    fn update_raymarch_globals(&mut self) {
        self.raymarch_globals_buffer = self.device.create_buffer_with_data(
            bytemuck::cast_slice(&[self.raymarch_globals]),
            wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST,
        );
    }
    pub fn solvent_radius(&self) -> f32 {
        self.raymarch_globals.solvent_radius
    }
    pub fn set_solvent_radius(&mut self, solvent_radius: f32) {
        self.raymarch_globals.solvent_radius = solvent_radius;
        self.update_raymarch_globals();
        self.camera_changed = true;
    }
    pub fn max_neighbours(&self) -> i32 {
        self.raymarch_globals.max_neighbours
    }
    pub fn set_max_neighbours(&mut self, max_neighbours: i32) {
        self.raymarch_globals.max_neighbours = max_neighbours;
        self.update_raymarch_globals();
        self.camera_changed = true;
    }
}