use std::collections::BTreeMap;

use rand::{Rng, SeedableRng};
use renderer::{Renderer2, render_graph, swapchain::WindowSurface};
use tracing::info;
use tracing_subscriber::EnvFilter;
use winit::{
    application::ApplicationHandler,
    dpi::{LogicalSize, PhysicalSize},
    event::ElementState,
    event_loop::EventLoop,
    raw_window_handle::{DisplayHandle, HasDisplayHandle, HasWindowHandle},
    window::{Window, WindowAttributes, WindowId},
};

struct WindowState {
    window: Window,
    egui_platform: egui_winit_platform::Platform,
    demo_app: egui_demo_lib::DemoWindows,
    scale_factor: f64,
    surface: WindowSurface,
}

struct WinitState {
    last_resize_events: BTreeMap<WindowId, PhysicalSize<u32>>,
    window_attrs: WindowAttributes,
    windows: BTreeMap<WindowId, WindowState>,
    renderer: Renderer2,
    last_redraw: std::time::Instant,
    egui_state: renderer::EguiState,
}

impl WinitState {
    const BASE_WIDTH: u32 = 800;
    const BASE_HEIGHT: u32 = 600;
    fn new(window_title: String, display: DisplayHandle) -> WinitState {
        let renderer = Renderer2::new(display.as_raw()).expect("renderer");

        Self {
            windows: BTreeMap::new(),
            last_resize_events: BTreeMap::new(),
            window_attrs: WindowAttributes::default()
                .with_title(window_title)
                .with_resizable(true)
                .with_inner_size(LogicalSize::new(Self::BASE_WIDTH, Self::BASE_HEIGHT)),
            // TODO: pass down this error and add some kind of error handling UI or dump
            egui_state: renderer::EguiState::new(renderer.device().clone()).unwrap(),
            renderer,
            last_redraw: std::time::Instant::now(),
        }
    }

    fn handle_final_resize(&mut self, window_id: WindowId, new_size: PhysicalSize<u32>) {
        _ = (window_id, new_size);
        info!("TODO: implement resize events");
        if let Some(window) = self.windows.get(&window_id) {
            window
                .surface
                .recreate_with(Some(renderer::Extent2D {
                    width: new_size.width,
                    height: new_size.height,
                }))
                .expect("swapchain recreation");
        }
    }

    fn handle_draw_request(&mut self, window_id: WindowId) {
        _ = window_id;
        tracing::debug!(
            window_id = u64::from(window_id),
            "TODO: implement draw request {}ms",
            self.last_redraw.elapsed().as_millis()
        );
        self.last_redraw = std::time::Instant::now();
        if let Some(window) = self.windows.get_mut(&window_id) {
            // egui
            window.egui_platform.begin_pass();
            window.demo_app.ui(&window.egui_platform.context());
            let output = window.egui_platform.end_pass(Some(&window.window));

            let textures_to_remove = output
                .textures_delta
                .free
                .iter()
                .filter_map(|id| self.egui_state.lookup_texture(*id))
                .collect::<Vec<_>>();
            // rendering
            let render = self.renderer.draw_graph(&window.surface, |renderer, rg| {
                use renderer::rendering::{egui_pass, egui_pre_pass};
                let framebuffer = rg.get_framebuffer().unwrap();
                let dev = renderer.device().clone();

                use renderer::ash::vk::Handle;
                use renderer::device::DeviceOwned;
                let [r, g, b]: [f32; 3] =
                    rand::prelude::StdRng::seed_from_u64(window.surface.surface.handle().as_raw())
                        .random();
                render_graph::clear_pass(rg, renderer::util::Rgba([r, g, b, 1.0]), framebuffer);
                egui_pre_pass(
                    &dev,
                    rg,
                    &mut renderer.texture_manager,
                    &mut self.egui_state,
                    &output,
                )?;

                let _ = egui_pass(
                    &dev,
                    rg,
                    &mut renderer.texture_manager,
                    &mut renderer.samplers,
                    &mut self.egui_state,
                    &window.egui_platform.context(),
                    output,
                    framebuffer,
                )?;

                renderer::Result::Ok(())
            });

            match smol::block_on(render).flatten() {
                Ok(_) => {}
                Err(e) => {
                    tracing::error!("encountered error while rendering: {e}");
                }
            }

            for tid in textures_to_remove {
                self.renderer.texture_manager_mut().remove_texture(tid);
            }

            window.window.request_redraw();
        }
    }

    fn remove_window(&mut self, window_id: WindowId) {
        tracing::info!(window = u64::from(window_id), "window close requested");

        self.windows.remove(&window_id);
    }

    fn create_window(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
        let window = event_loop
            .create_window(
                self.window_attrs
                    .clone()
                    .with_window_level(winit::window::WindowLevel::AlwaysOnTop),
            )
            .expect("new window");
        let window_id = window.id();
        tracing::info!(window = u64::from(window_id), "created new window");

        let size = self
            .window_attrs
            .inner_size
            .map(|size| size.to_physical(1.0))
            .unwrap_or(PhysicalSize::new(0, 0));
        let extent = renderer::Extent2D {
            width: size.width,
            height: size.height,
        };

        let surface = self
            .renderer
            .create_surface(
                window.window_handle().expect("window handle").as_raw(),
                extent,
            )
            .expect("surface");

        let scale_factor = window.scale_factor();
        self.windows.insert(
            window_id,
            WindowState {
                window,
                scale_factor,
                demo_app: egui_demo_lib::DemoWindows::default(),
                egui_platform: egui_winit_platform::Platform::new(
                    egui_winit_platform::PlatformDescriptor {
                        physical_width: size.width,
                        physical_height: size.height,
                        scale_factor,
                        ..Default::default()
                    },
                ),
                surface,
            },
        );
    }
}

impl ApplicationHandler for WinitState {
    fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
        tracing::debug!("winit::resumed");

        self.create_window(event_loop);
    }

    fn about_to_wait(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
        tracing::trace!("winit::about_to_wait");
        for (&window, &resize) in self.last_resize_events.clone().iter() {
            self.handle_final_resize(window, resize);
        }

        self.last_resize_events.clear();

        if self.windows.is_empty() {
            event_loop.exit();
        }
    }

    fn window_event(
        &mut self,
        event_loop: &winit::event_loop::ActiveEventLoop,
        window_id: winit::window::WindowId,
        event: winit::event::WindowEvent,
    ) {
        // if !matches!(event, winit::event::WindowEvent::Resized(_)) {
        //     if let Some(resize) = self.last_resize_events.remove(&window_id) {
        //         self.handle_final_resize(window_id, resize);
        //     }
        // }

        if let Some(window) = self.windows.get_mut(&window_id) {
            window.egui_platform.handle_event(&event);
        }

        match event {
            winit::event::WindowEvent::Resized(physical_size) => {
                _ = self.last_resize_events.insert(window_id, physical_size);
            }
            winit::event::WindowEvent::CloseRequested
            | winit::event::WindowEvent::KeyboardInput {
                event:
                    winit::event::KeyEvent {
                        physical_key:
                            winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::KeyQ),
                        state: ElementState::Pressed,
                        repeat: false,
                        ..
                    },
                ..
            } => {
                self.remove_window(window_id);
            }
            winit::event::WindowEvent::KeyboardInput {
                event:
                    winit::event::KeyEvent {
                        physical_key:
                            winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::Space),
                        state: ElementState::Pressed,
                        repeat: false,
                        ..
                    },
                ..
            } => {
                self.create_window(event_loop);
            }
            winit::event::WindowEvent::KeyboardInput {
                device_id,
                event,
                is_synthetic,
            } => {
                _ = (device_id, event, is_synthetic);
            }
            winit::event::WindowEvent::CursorMoved {
                device_id,
                position,
            } => {
                _ = (device_id, position);
            }
            winit::event::WindowEvent::MouseInput {
                device_id,
                state,
                button,
            } => {
                _ = (device_id, state, button);
            }
            winit::event::WindowEvent::RedrawRequested => {
                self.handle_draw_request(window_id);
            }
            winit::event::WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
                if let Some(window) = self.windows.get_mut(&window_id) {
                    window.scale_factor = scale_factor;
                }
            }
            _ => {} // unhandled event
        }
    }
}

fn main() {
    let _ = tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env())
        .init();
    let ev = EventLoop::new().unwrap();
    ev.set_control_flow(winit::event_loop::ControlFlow::Poll);

    let display = ev.display_handle().expect("display handle");
    let mut game = WinitState::new("Vidya".to_owned(), display);
    ev.run_app(&mut game).unwrap();
}
