//! Desktop simulator backend using `winit` and `wgpu`.
//!
//! Provides a simple window that presents the contents of an RGBA frame buffer
//! generated by rlvgl widgets. Input events from the window are translated to
//! [`InputEvent`] instances.

// NOTE: Module gating is handled in `platform/src/lib.rs` via
// `#[cfg(feature = "simulator")] pub mod simulator;`.

use alloc::{borrow::Cow, boxed::Box, format, string::String, vec, vec::Vec};
use eframe::{self, egui};
use image::{ColorType, ImageError, save_buffer};
use pollster::block_on;
use rlvgl_core::event::Key;
use std::{backtrace::Backtrace, eprintln, panic, path::Path};
use tracing_subscriber::EnvFilter;
use winit::{
    dpi::{LogicalSize, PhysicalSize},
    event::{ElementState, Event, KeyEvent, MouseButton, WindowEvent},
    event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
    keyboard::{KeyCode, PhysicalKey},
    window::{Fullscreen, Window},
};

use crate::input::InputEvent;
use crate::screen::{ColorFormat, Screen};

/// Initialize logging for `wgpu` validation messages.
fn init_wgpu_logger() {
    let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("wgpu=warn"));
    let _ = tracing_subscriber::fmt().with_env_filter(filter).try_init();
}

/// Display a panic message in a scrollable window limited to the screen size.
fn show_panic_window(message: String) {
    /// Simple `eframe` application rendering the panic text.
    struct PanicApp {
        msg: String,
    }

    impl eframe::App for PanicApp {
        fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
            egui::CentralPanel::default().show(ctx, |ui| {
                ui.heading("rlvgl panic");
                egui::ScrollArea::vertical().show(ui, |ui| {
                    ui.add(egui::TextEdit::multiline(&mut self.msg).desired_width(f32::INFINITY));
                });
                ui.horizontal(|ui| {
                    if ui.button("Copy").clicked() {
                        ctx.output_mut(|o| o.copied_text = self.msg.clone());
                    }
                    if ui.button("Close").clicked() {
                        ctx.send_viewport_cmd(egui::ViewportCommand::Close);
                    }
                });
            });
        }
    }

    let event_loop = EventLoop::new().expect("failed to create event loop");
    #[allow(deprecated)]
    let hidden_window = event_loop
        .create_window(Window::default_attributes().with_visible(false))
        .expect("failed to create window");
    let monitor_size = hidden_window
        .current_monitor()
        .map(|m| m.size())
        .unwrap_or(PhysicalSize::new(800, 600));
    drop(hidden_window);
    drop(event_loop);

    let max = egui::vec2(monitor_size.width as f32, monitor_size.height as f32);
    let initial = egui::vec2(max.x * 0.8, max.y * 0.8);

    let viewport = egui::ViewportBuilder::default()
        .with_inner_size(initial)
        .with_max_inner_size(max)
        .with_decorations(true)
        .with_resizable(true);

    let options = eframe::NativeOptions {
        viewport,
        ..Default::default()
    };

    let msg_copy = message.clone();
    if let Err(e) = eframe::run_native(
        "rlvgl panic",
        options,
        Box::new(|_| Box::new(PanicApp { msg: message })),
    ) {
        eprintln!("{msg_copy}\nfailed to show panic window: {e}");
    }
}

/// Minimal state required to present an RGBA frame using `wgpu`.
struct WgpuState {
    surface: wgpu::Surface<'static>,
    device: wgpu::Device,
    queue: wgpu::Queue,
    config: wgpu::SurfaceConfiguration,
    frame: Vec<u8>,
    fb_width: u32,
    fb_height: u32,
    blit_buf: Vec<u8>,
    max_texture_dim: u32,
    /// Target panel colour format. The CPU framebuffer is always
    /// 8-bit-per-channel; this field tells the simulator which colour
    /// format to **simulate** by quantizing each presented frame so the
    /// preview matches what the target panel would actually display.
    color_format: ColorFormat,
}

impl WgpuState {
    /// Create a new `wgpu` state for the given window and dimensions.
    fn new(
        window: &'static Window,
        fb_width: u32,
        fb_height: u32,
        surface_width: u32,
        surface_height: u32,
        color_format: ColorFormat,
    ) -> Self {
        init_wgpu_logger();
        let instance = wgpu::Instance::default();
        let surface = instance
            .create_surface(window)
            .expect("failed to create surface");
        let adapter = block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
            power_preference: wgpu::PowerPreference::HighPerformance,
            compatible_surface: Some(&surface),
            force_fallback_adapter: false,
        }))
        .expect("failed to find adapter");
        let (device, queue) = block_on(adapter.request_device(
            &wgpu::DeviceDescriptor {
                label: None,
                required_features: wgpu::Features::empty(),
                required_limits: wgpu::Limits::default(),
            },
            None,
        ))
        .expect("failed to create device");
        device.on_uncaptured_error(Box::new(|e| {
            eprintln!("wgpu uncaptured error: {e:?}");
            debug_assert!(false, "wgpu uncaptured error: {e:?}");
        }));
        let caps = surface.get_capabilities(&adapter);
        let format = caps
            .formats
            .iter()
            .copied()
            .find(|f| f.is_srgb())
            .unwrap_or(caps.formats[0]);
        let present_mode = if caps.present_modes.contains(&wgpu::PresentMode::Fifo) {
            wgpu::PresentMode::Fifo
        } else {
            caps.present_modes[0]
        };
        let config = wgpu::SurfaceConfiguration {
            usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::RENDER_ATTACHMENT,
            format,
            width: surface_width,
            height: surface_height,
            present_mode,
            alpha_mode: caps.alpha_modes[0],
            view_formats: vec![],
            desired_maximum_frame_latency: 1,
        };
        surface.configure(&device, &config);
        eprintln!("Surface format: {:?}", config.format);
        let max_texture_dim = device.limits().max_texture_dimension_2d;
        let frame = vec![0; (fb_width * fb_height * 4) as usize];
        Self {
            surface,
            device,
            queue,
            config,
            frame,
            fb_width,
            fb_height,
            blit_buf: Vec::new(),
            max_texture_dim,
            color_format,
        }
    }

    /// Access the mutable frame buffer.
    fn frame_mut(&mut self) -> &mut [u8] {
        &mut self.frame
    }

    /// Resize the surface but keep the logical frame size unchanged.
    fn resize(&mut self, width: u32, height: u32) {
        self.config.width = width;
        self.config.height = height;
        self.surface.configure(&self.device, &self.config);
    }

    /// Present the current frame buffer to the window.
    ///
    /// `dst_width` and `dst_height` specify the destination rectangle size
    /// inside the surface. `(offset_x, offset_y)` positions this rectangle
    /// within the surface. Any area outside the rectangle is filled with a
    /// solid black color.
    fn render(&mut self, dst_width: u32, dst_height: u32, offset_x: u32, offset_y: u32) {
        let output = match self.surface.get_current_texture() {
            Ok(frame) => frame,
            Err(e) => {
                eprintln!("surface error: {e:?}");
                debug_assert!(false, "surface error: {e:?}");
                self.surface.configure(&self.device, &self.config);
                self.surface
                    .get_current_texture()
                    .expect("failed to acquire surface texture")
            }
        };
        let surface_w = self.config.width;
        let surface_h = self.config.height;
        let required = (surface_w * surface_h * 4) as usize;
        if self.blit_buf.len() != required {
            self.blit_buf.resize(required, 0);
            for px in self.blit_buf.chunks_exact_mut(4) {
                px[3] = 0xff;
            }
        } else {
            for px in self.blit_buf.chunks_exact_mut(4) {
                px[0] = 0;
                px[1] = 0;
                px[2] = 0;
                px[3] = 0xff;
            }
        }
        for y in 0..dst_height.min(surface_h) {
            let src_y = y * self.fb_height / dst_height;
            for x in 0..dst_width.min(surface_w) {
                let src_x = x * self.fb_width / dst_width;
                let src_idx = ((src_y * self.fb_width + src_x) * 4) as usize;
                let dst_x = x + offset_x;
                let dst_y = y + offset_y;
                if dst_x < surface_w && dst_y < surface_h {
                    let dst_idx = ((dst_y * surface_w + dst_x) * 4) as usize;
                    self.blit_buf[dst_idx..dst_idx + 4]
                        .copy_from_slice(&self.frame[src_idx..src_idx + 4]);
                }
            }
        }
        // Apply target colour-format quantization. blit_buf is in BGRA
        // byte order; quantize through (R, G, B) and write back. For
        // ColorFormat::Argb8888 this is a no-op so we skip the loop.
        if self.color_format != ColorFormat::Argb8888 {
            for px in self.blit_buf.chunks_exact_mut(4) {
                let (qr, qg, qb) = self.color_format.quantize(px[2], px[1], px[0]);
                px[0] = qb;
                px[1] = qg;
                px[2] = qr;
            }
        }

        // The CPU framebuffer is filled by CpuBlitter via to_argb8888()+
        // to_le_bytes(), which produces BGRA byte order in memory. The
        // wgpu surface format we select is also BGRA (Bgra8UnormSrgb),
        // so byte order matches and no swap is required. If a non-BGRA
        // surface is ever selected, swap R/B before upload.
        let data: Cow<[u8]> = match self.config.format {
            wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb => {
                Cow::Borrowed(&self.blit_buf)
            }
            wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => {
                for px in self.blit_buf.chunks_exact_mut(4) {
                    px.swap(0, 2);
                }
                Cow::Borrowed(&self.blit_buf)
            }
            _ => Cow::Borrowed(&self.blit_buf),
        };
        let data = data.as_ref();
        let row_bytes = 4 * surface_w as usize;
        let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize;
        if row_bytes.is_multiple_of(align) {
            self.queue.write_texture(
                wgpu::ImageCopyTexture {
                    texture: &output.texture,
                    mip_level: 0,
                    origin: wgpu::Origin3d::ZERO,
                    aspect: wgpu::TextureAspect::All,
                },
                data,
                wgpu::ImageDataLayout {
                    offset: 0,
                    bytes_per_row: Some(row_bytes as u32),
                    rows_per_image: Some(surface_h),
                },
                wgpu::Extent3d {
                    width: surface_w,
                    height: surface_h,
                    depth_or_array_layers: 1,
                },
            );
        } else {
            let stride = row_bytes.div_ceil(align) * align;
            let mut padded = vec![0u8; stride * surface_h as usize];
            for y in 0..surface_h as usize {
                let src_off = y * row_bytes;
                let dst_off = y * stride;
                padded[dst_off..dst_off + row_bytes]
                    .copy_from_slice(&data[src_off..src_off + row_bytes]);
            }
            self.queue.write_texture(
                wgpu::ImageCopyTexture {
                    texture: &output.texture,
                    mip_level: 0,
                    origin: wgpu::Origin3d::ZERO,
                    aspect: wgpu::TextureAspect::All,
                },
                &padded,
                wgpu::ImageDataLayout {
                    offset: 0,
                    bytes_per_row: Some(stride as u32),
                    rows_per_image: Some(surface_h),
                },
                wgpu::Extent3d {
                    width: surface_w,
                    height: surface_h,
                    depth_or_array_layers: 1,
                },
            );
        }
        // Submit to ensure the texture write completes before presenting.
        // Set a breakpoint on this line to verify submission and inspect errors.
        self.queue.submit(std::iter::empty());
        output.present();
    }

    /// Maximum supported texture dimension for the device.
    fn max_texture_dimension(&self) -> u32 {
        self.max_texture_dim
    }
}

/// Desktop simulator display backed by `winit` and `wgpu`.
pub struct WgpuDisplay {
    screen: Screen,
    width: usize,
    height: usize,
    event_loop: EventLoop<()>,
    window: &'static Window,
    state: WgpuState,
    scale: (f64, f64),
    present_scale: (f64, f64),
    dest_size: (u32, u32),
    surface_offset: (f64, f64),
}

impl WgpuDisplay {
    /// Return the [`Screen`] this window was constructed for.
    ///
    /// Simulator windows always use [`crate::screen::Rotation::Deg0`] —
    /// the window size matches the application's logical coordinates.
    pub fn screen(&self) -> Screen {
        self.screen
    }

    /// Create a new window sized to `screen.logical_size()`.
    ///
    /// The simulator always uses `Rotation::Deg0`, so the window
    /// reflects the application's logical coordinate space directly.
    /// The screen's [`ColorFormat`](crate::screen::ColorFormat) is
    /// applied as a per-pixel quantization on the preview, allowing
    /// the host to faithfully simulate target panels with reduced
    /// colour depth (e.g. RGB565 banding).
    pub fn with_screen(screen: Screen) -> Self {
        panic::set_hook(Box::new(|info| {
            let backtrace = Backtrace::force_capture();
            let message = format!("{info}\n\n{backtrace}");
            eprintln!("{message}");
            show_panic_window(message);
            std::process::exit(1);
        }));
        let (logical_w, logical_h) = screen.logical_size();
        let width = logical_w as usize;
        let height = logical_h as usize;
        let event_loop = EventLoop::new().expect("failed to create event loop");
        #[allow(deprecated)]
        let window = event_loop
            .create_window(
                Window::default_attributes()
                    .with_title("rlvgl simulator")
                    .with_inner_size(LogicalSize::new(width as f64, height as f64)),
            )
            .expect("failed to create window");
        let window = Box::leak(Box::new(window));
        let phys = window.inner_size();
        let state = WgpuState::new(
            window,
            width as u32,
            height as u32,
            phys.width,
            phys.height,
            screen.color_format,
        );
        let window: &'static Window = &*window;
        let scale = (
            phys.width as f64 / width as f64,
            phys.height as f64 / height as f64,
        );
        Self {
            screen,
            width,
            height,
            event_loop,
            window,
            state,
            scale,
            present_scale: (1.0, 1.0),
            dest_size: (phys.width, phys.height),
            surface_offset: (0.0, 0.0),
        }
    }

    /// Create a new window with the given size.
    ///
    /// Equivalent to [`Self::with_screen`] with `Screen::landscape(width, height)`.
    ///
    /// Any panic during simulator execution opens a resizable window
    /// displaying the panic and a captured call stack. The window is
    /// constrained to the visible screen, provides a scrollbar for long
    /// messages, and offers copy and close controls. Closing the window
    /// terminates the process.
    pub fn new(width: usize, height: usize) -> Self {
        Self::with_screen(Screen::landscape(width as u32, height as u32))
    }

    /// Run the simulator event loop.
    ///
    /// `frame_callback` is invoked whenever the window needs to be redrawn,
    /// providing mutable access to the RGBA pixel buffer along with the current
    /// framebuffer width and height. `event_callback` receives input events
    /// converted from the underlying `winit` window.
    pub fn run(
        self,
        mut frame_callback: impl FnMut(&mut [u8], usize, usize) + 'static,
        mut event_callback: impl FnMut(InputEvent) + 'static,
    ) {
        let WgpuDisplay {
            screen: _,
            width,
            height,
            event_loop,
            window,
            mut state,
            scale: initial_scale,
            present_scale: initial_present_scale,
            mut dest_size,
            mut surface_offset,
        } = self;
        event_loop.set_control_flow(ControlFlow::Poll);
        // Request an initial redraw so the window displays its first frame
        // immediately on creation. Without this, some platforms may present
        // a blank window until the next event triggers a redraw.
        window.request_redraw();

        let mut pointer_pos = (0.0f64, 0.0f64);
        let mut pointer_down = false;
        // Ratio between window pixels and logical display coordinates
        let mut scale = initial_scale;
        // Additional scale applied when the window exceeds device texture limits
        let mut present_scale = initial_present_scale;
        let aspect_ratio = width as f64 / height as f64;
        let max_dim = state.max_texture_dimension();
        let mut fullscreen = false;

        fn key_from_event(event: &KeyEvent) -> Key {
            if let Some(text) = &event.text
                && let Some(ch) = text.chars().next()
            {
                return Key::Character(ch);
            }
            match event.physical_key {
                PhysicalKey::Code(code) => match code {
                    KeyCode::Escape => Key::Escape,
                    KeyCode::Enter => Key::Enter,
                    KeyCode::Space => Key::Space,
                    KeyCode::ArrowUp => Key::ArrowUp,
                    KeyCode::ArrowDown => Key::ArrowDown,
                    KeyCode::ArrowLeft => Key::ArrowLeft,
                    KeyCode::ArrowRight => Key::ArrowRight,
                    KeyCode::F1 => Key::Function(1),
                    KeyCode::F2 => Key::Function(2),
                    KeyCode::F3 => Key::Function(3),
                    KeyCode::F4 => Key::Function(4),
                    KeyCode::F5 => Key::Function(5),
                    KeyCode::F6 => Key::Function(6),
                    KeyCode::F7 => Key::Function(7),
                    KeyCode::F8 => Key::Function(8),
                    KeyCode::F9 => Key::Function(9),
                    KeyCode::F10 => Key::Function(10),
                    KeyCode::F11 => Key::Function(11),
                    KeyCode::F12 => Key::Function(12),
                    _ => Key::Other(code as u32),
                },
                _ => Key::Other(0),
            }
        }

        #[allow(deprecated)]
        event_loop
            .run(move |event, target: &ActiveEventLoop| match event {
                Event::WindowEvent {
                    event: WindowEvent::CloseRequested,
                    ..
                } => target.exit(),
                Event::WindowEvent {
                    event: WindowEvent::RedrawRequested,
                    ..
                } => {
                    frame_callback(state.frame_mut(), width, height);
                    state.render(
                        dest_size.0,
                        dest_size.1,
                        surface_offset.0.round() as u32,
                        surface_offset.1.round() as u32,
                    );
                }
                Event::WindowEvent {
                    event: WindowEvent::Resized(size),
                    ..
                } => {
                    let surf_w = size.width.min(max_dim);
                    let surf_h = size.height.min(max_dim);
                    state.resize(surf_w, surf_h);
                    let mut w = surf_w;
                    let mut h = surf_h;
                    if (w as f64 / h as f64 - aspect_ratio).abs() > f64::EPSILON {
                        if w as f64 / h as f64 > aspect_ratio {
                            w = (h as f64 * aspect_ratio).round() as u32;
                        } else {
                            h = (w as f64 / aspect_ratio).round() as u32;
                        }
                    }
                    let logical = (
                        (pointer_pos.0 - surface_offset.0) / scale.0,
                        (pointer_pos.1 - surface_offset.1) / scale.1,
                    );
                    surface_offset = (
                        (surf_w as f64 - w as f64) / 2.0,
                        (surf_h as f64 - h as f64) / 2.0,
                    );
                    dest_size = (w, h);
                    present_scale = (
                        size.width as f64 / surf_w as f64,
                        size.height as f64 / surf_h as f64,
                    );
                    scale = (w as f64 / width as f64, h as f64 / height as f64);
                    pointer_pos = (
                        logical.0 * scale.0 + surface_offset.0,
                        logical.1 * scale.1 + surface_offset.1,
                    );
                    window.request_redraw();
                }
                Event::WindowEvent {
                    event: WindowEvent::KeyboardInput { event, .. },
                    ..
                } => {
                    if let PhysicalKey::Code(code) = event.physical_key {
                        if code == KeyCode::F11 && event.state == ElementState::Pressed {
                            fullscreen = !fullscreen;
                            if fullscreen {
                                window.set_fullscreen(Some(Fullscreen::Borderless(None)));
                            } else {
                                window.set_fullscreen(None);
                            }
                        }
                        let key = key_from_event(&event);
                        match event.state {
                            ElementState::Pressed => {
                                event_callback(InputEvent::KeyDown { key });
                            }
                            ElementState::Released => {
                                event_callback(InputEvent::KeyUp { key });
                            }
                        }
                    }
                }
                Event::WindowEvent {
                    event: WindowEvent::CursorMoved { position, .. },
                    ..
                } => {
                    let surf_x = position.x / present_scale.0;
                    let surf_y = position.y / present_scale.1;
                    pointer_pos = (surf_x, surf_y);
                    let adj_x = surf_x - surface_offset.0;
                    let adj_y = surf_y - surface_offset.1;
                    let x = (adj_x / scale.0).clamp(0.0, width as f64 - 1.0) as i32;
                    let y = (adj_y / scale.1).clamp(0.0, height as f64 - 1.0) as i32;
                    if pointer_down {
                        event_callback(InputEvent::PointerMove { x, y });
                    }
                }
                Event::WindowEvent {
                    event:
                        WindowEvent::MouseInput {
                            state: ElementState::Pressed,
                            button: MouseButton::Left,
                            ..
                        },
                    ..
                } => {
                    pointer_down = true;
                    let adj_x = pointer_pos.0 - surface_offset.0;
                    let adj_y = pointer_pos.1 - surface_offset.1;
                    let x = (adj_x / scale.0).clamp(0.0, width as f64 - 1.0) as i32;
                    let y = (adj_y / scale.1).clamp(0.0, height as f64 - 1.0) as i32;
                    event_callback(InputEvent::PointerDown { x, y });
                }
                Event::WindowEvent {
                    event:
                        WindowEvent::MouseInput {
                            state: ElementState::Released,
                            button: MouseButton::Left,
                            ..
                        },
                    ..
                } => {
                    let adj_x = pointer_pos.0 - surface_offset.0;
                    let adj_y = pointer_pos.1 - surface_offset.1;
                    let x = (adj_x / scale.0).clamp(0.0, width as f64 - 1.0) as i32;
                    let y = (adj_y / scale.1).clamp(0.0, height as f64 - 1.0) as i32;
                    pointer_down = false;
                    event_callback(InputEvent::PointerUp { x, y });
                }
                Event::AboutToWait => {
                    window.request_redraw();
                }
                _ => {}
            })
            .expect("event loop error");
    }

    /// Render a single frame off-screen and save it as a PNG.
    ///
    /// This helper enables headless CI tests that compare rendered
    /// output against golden images. Defaults to true-colour ARGB8888;
    /// use [`Self::headless_with_color_format`] to bake target panel
    /// quantization into the saved image.
    pub fn headless(
        width: usize,
        height: usize,
        frame_callback: impl FnMut(&mut [u8]),
        path: impl AsRef<Path>,
    ) -> Result<(), ImageError> {
        Self::headless_with_color_format(width, height, ColorFormat::Argb8888, frame_callback, path)
    }

    /// Render a single frame off-screen, quantize through `color_format`,
    /// and save it as a PNG.
    ///
    /// The CPU framebuffer is filled by [`crate::CpuBlitter`] via
    /// `Color::to_argb8888().to_le_bytes()`, which produces **BGRA**
    /// byte order in memory. PNG output expects RGBA8, so we apply the
    /// quantization (operating on R/G/B in the BGRA buffer) and then
    /// swap red and blue per pixel before encoding.
    pub fn headless_with_color_format(
        width: usize,
        height: usize,
        color_format: ColorFormat,
        mut frame_callback: impl FnMut(&mut [u8]),
        path: impl AsRef<Path>,
    ) -> Result<(), ImageError> {
        let mut frame = vec![0u8; width * height * 4];
        frame_callback(&mut frame);
        if color_format != ColorFormat::Argb8888 {
            for px in frame.chunks_exact_mut(4) {
                let (qr, qg, qb) = color_format.quantize(px[2], px[1], px[0]);
                px[0] = qb;
                px[1] = qg;
                px[2] = qr;
            }
        }
        for px in frame.chunks_exact_mut(4) {
            px.swap(0, 2);
        }
        save_buffer(path, &frame, width as u32, height as u32, ColorType::Rgba8)
    }
}
