/// Every relevant part is marked with the prefix `SHADY` so you can just search in this code with `SHADY`.
use std::{borrow::Cow, sync::Arc};

use pollster::FutureExt;
use shady::{Shady, ShadyDescriptor, ShadyRenderPipeline};
use shady_audio::{fetcher::SystemAudioFetcher, SampleProcessor};
use wgpu::{
    Backends, Device, Instance, Queue, ShaderSource, Surface, SurfaceConfiguration,
    TextureViewDescriptor,
};
use winit::{
    application::ApplicationHandler,
    dpi::PhysicalSize,
    event::WindowEvent,
    event_loop::{ActiveEventLoop, EventLoop},
    window::{Window, WindowAttributes, WindowId},
};

struct State<'a> {
    surface: Surface<'a>,
    device: Device,
    queue: Queue,
    config: SurfaceConfiguration,
    window: Arc<Window>,

    // SHADY
    sample_processor: SampleProcessor,
    // SHADY
    shady: Shady,
    // SHADY
    pipeline: ShadyRenderPipeline,
}

impl<'a> State<'a> {
    fn new(window: Window) -> Self {
        let window = Arc::new(window);

        let instance = Instance::new(&wgpu::InstanceDescriptor {
            backends: Backends::PRIMARY,
            ..Default::default()
        });

        let surface = instance.create_surface(window.clone()).unwrap();

        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                compatible_surface: Some(&surface),
                ..Default::default()
            })
            .block_on()
            .unwrap();

        let (device, queue) = adapter
            .request_device(&wgpu::DeviceDescriptor::default(), None)
            .block_on()
            .unwrap();

        let config = {
            let surface_caps = surface.get_capabilities(&adapter);
            let surface_format = surface_caps
                .formats
                .iter()
                .find(|f| f.is_srgb())
                .copied()
                .unwrap_or(surface_caps.formats[0]);

            let size = window.clone().inner_size();

            let config = wgpu::SurfaceConfiguration {
                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
                format: surface_format,
                width: size.width,
                height: size.height,
                present_mode: wgpu::PresentMode::AutoVsync,
                alpha_mode: surface_caps.alpha_modes[0],
                view_formats: vec![],
                desired_maximum_frame_latency: 2,
            };

            config
        };

        // SHADY
        //
        // Create the render pipeline which shady will use.
        let pipeline = {
            let fragment_shader = {
                let template = shady::TemplateLang::Wgsl.generate_to_string(None).unwrap();

                ShaderSource::Wgsl(Cow::Owned(template))
            };

            shady::create_render_pipeline(&device, fragment_shader, &config.format)
        };

        // SHADY
        let sample_processor =
            SampleProcessor::new(SystemAudioFetcher::default(|err| panic!("{}", err)).unwrap());
        // SHADY
        let shady = Shady::new(ShadyDescriptor {
            device: &device,
            sample_processor: &sample_processor,
        });

        Self {
            surface,
            device,
            queue,
            config,
            window,
            sample_processor,
            shady,
            pipeline,
        }
    }

    pub fn prepare_next_frame(&mut self) {
        // SHADY
        //
        // Updates the values inside the uniform buffers.
        {
            self.shady.inc_frame();

            self.sample_processor.process_next_samples();
            self.shady
                .update_audio_buffer(&mut self.queue, &self.sample_processor);
            self.shady.update_frame_buffer(&mut self.queue);
            self.shady.update_mouse_buffer(&mut self.queue);
            self.shady.update_resolution_buffer(&mut self.queue);
            self.shady.update_time_buffer(&mut self.queue);
        }

        self.surface.configure(&self.device, &self.config);
    }

    pub fn render(&mut self) {
        let output = self.surface.get_current_texture().unwrap();
        let view = output
            .texture
            .create_view(&TextureViewDescriptor::default());

        let mut encoder = self
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("Render encoder"),
            });

        // SHADY
        //
        // Add the render pass to the encoder to draw the next frame.
        self.shady
            .add_render_pass(&mut encoder, &view, std::iter::once(&self.pipeline));

        self.queue.submit(std::iter::once(encoder.finish()));
        output.present();
    }

    pub fn window(&self) -> Arc<Window> {
        self.window.clone()
    }

    pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
        // SHADY
        //
        // Update any properties of shady.
        // Note: You need to call the appropriate `update_*_buffer` method to write
        // the new values into the buffers for the next frame you use shady otherwise the previous values in the
        // buffer will be used.
        self.shady.set_resolution(new_size.width, new_size.height);
    }
}

struct App<'a> {
    state: Option<State<'a>>,
}

impl<'a> App<'a> {
    pub fn new() -> Self {
        Self { state: None }
    }
}

impl<'a> ApplicationHandler<()> for App<'a> {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        let window = event_loop
            .create_window(WindowAttributes::default())
            .unwrap();

        self.state = Some(State::new(window));
    }

    fn window_event(
        &mut self,
        event_loop: &ActiveEventLoop,
        _window_id: WindowId,
        event: WindowEvent,
    ) {
        let Some(state) = &mut self.state else { return };
        let window = state.window();

        match event {
            WindowEvent::CloseRequested => event_loop.exit(),
            WindowEvent::RedrawRequested => {
                window.request_redraw();
                state.prepare_next_frame();
                state.render();
            }
            WindowEvent::Resized(new_size) => state.resize(new_size),
            WindowEvent::KeyboardInput { event, .. }
                if event.logical_key.to_text() == Some("q") =>
            {
                event_loop.exit();
            }
            _ => (),
        }
    }
}

fn main() {
    let event_loop = EventLoop::new().unwrap();
    let mut app = App::new();

    event_loop.run_app(&mut app).unwrap();
}