use crate::render::BackendState;
use egui::{ClippedPrimitive, Context, FullOutput, TexturesDelta};
use egui_wgpu::{Renderer as EguiRenderer, RendererOptions, ScreenDescriptor};
use egui_winit::{self, EventResponse};
use glam::UVec2;
use wgpu::{CommandEncoder, Device, Queue, TextureFormat, TextureView};
use winit::{event::WindowEvent, window::Window};

/// Egui integration backed by roast2d's wgpu device and swapchain.
pub struct EguiSystem {
    ctx: Context,
    state: egui_winit::State,
    renderer: EguiRenderer,
    screen_desc: ScreenDescriptor,
    pending: Option<(TexturesDelta, Vec<ClippedPrimitive>)>,
    pending_free: Vec<egui::TextureId>,
}

impl EguiSystem {
    /// Create a new egui system using the existing window and backend state.
    pub fn new(window: &Window, backend: &BackendState) -> Self {
        let ctx = Context::default();
        let viewport_id = egui::ViewportId::ROOT;
        let native_pixels_per_point = Some(window.scale_factor() as f32);
        let theme = window.theme();
        let max_texture_side = None;

        let state = egui_winit::State::new(
            ctx.clone(),
            viewport_id,
            window,
            native_pixels_per_point,
            theme,
            max_texture_side,
        );

        let color_format: TextureFormat = backend.surface_view_format;
        let renderer = EguiRenderer::new(&backend.device, color_format, RendererOptions::default());

        let size_in_pixels = [backend.surface_config.width, backend.surface_config.height];
        let pixels_per_point = egui_winit::pixels_per_point(&ctx, window);

        let screen_desc = ScreenDescriptor {
            size_in_pixels,
            pixels_per_point,
        };

        Self {
            ctx,
            state,
            renderer,
            screen_desc,
            pending: None,
            pending_free: Vec::new(),
        }
    }

    pub fn context(&self) -> &Context {
        &self.ctx
    }

    /// Forward a window event to egui.
    pub fn on_window_event(&mut self, window: &Window, event: &WindowEvent) -> EventResponse {
        self.state.on_window_event(window, event)
    }

    /// Begin an egui frame and return the context.
    pub fn begin_frame(&mut self, window: &Window) -> &Context {
        let raw_input = self.state.take_egui_input(window);
        self.ctx.begin_pass(raw_input);
        &self.ctx
    }

    /// End the egui frame and store the tessellated primitives for later painting.
    pub fn end_frame(&mut self, window: &Window) {
        let FullOutput {
            platform_output,
            textures_delta,
            shapes,
            ..
        } = self.ctx.end_pass();

        self.state.handle_platform_output(window, platform_output);

        let pixels_per_point = egui_winit::pixels_per_point(&self.ctx, window);
        let clipped_primitives = self.ctx.tessellate(shapes, pixels_per_point);
        self.pending = Some((textures_delta, clipped_primitives));
    }

    /// Paint the last frame's egui output on top of the given render target.
    pub fn paint(
        &mut self,
        window: &Window,
        device: &Device,
        queue: &Queue,
        encoder: &mut CommandEncoder,
        target_view: &TextureView,
        window_size: UVec2,
    ) {
        let Some((textures_delta, primitives)) = self.pending.take() else {
            return;
        };

        self.screen_desc.size_in_pixels = [window_size.x, window_size.y];
        self.screen_desc.pixels_per_point = egui_winit::pixels_per_point(&self.ctx, window);

        // Upload textures
        for (id, delta) in &textures_delta.set {
            self.renderer.update_texture(device, queue, *id, delta);
        }
        self.pending_free.extend(textures_delta.free);

        // Update GPU buffers
        let _ =
            self.renderer
                .update_buffers(device, queue, encoder, &primitives, &self.screen_desc);

        {
            let pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("egui Render Pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: target_view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Load,
                        store: wgpu::StoreOp::Store,
                    },
                    depth_slice: None,
                })],
                depth_stencil_attachment: None,
                occlusion_query_set: None,
                timestamp_writes: None,
            });

            // Match the lifetime trick used in egui-wgpu's winit painter.
            self.renderer
                .render(&mut pass.forget_lifetime(), &primitives, &self.screen_desc);
        }
    }

    /// Free any textures that egui requested to be dropped.
    ///
    /// Call this after the command buffers have been submitted.
    pub fn after_submit(&mut self) {
        for id in self.pending_free.drain(..) {
            self.renderer.free_texture(&id);
        }
    }
}
