//! wgpu-Zustand: Surface, Device und der zweistufige Render-Pfad
//! aus dem Renderer-Plan:
//!
//!   Pass 1 (intern):  640×480 RGBA8 + Depth — hier entsteht das Bild
//!                     ([`ScenePass`], PS1-Shader).
//!   Pass 2 (Fenster): Nearest-Upscale des internen Targets mit
//!                     4:3-Letterbox (via Viewport) auf die Surface.
//!
//! 640×480 (VGA) statt 320×240: das Spiel ist text-getrieben — die höhere
//! Auflösung trägt den 80-Spalten-Textmodus-Look (EGA 8×14), der Lo-Fi-Rest
//! (RGB555 + Bayer-Dither, affines Mapping) bleibt.

use std::sync::Arc;

use winit::window::Window;

use crate::engine::tga::Image;
use crate::render::math::Mat4;
use crate::render::scene::{Mesh, ScenePass};
use crate::render::sprite::SpritePass;
use crate::render::ui::Ui;

pub const INTERNAL_W: u32 = 640;
pub const INTERNAL_H: u32 = 480;

/// D16 reicht für PS1-Geometrieskalen und ist das älteste, überall
/// (auch GL-Fallback) unterstützte Depth-Format.
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth16Unorm;
const INTERNAL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;

pub struct Gpu {
    surface: wgpu::Surface<'static>,
    device:  wgpu::Device,
    queue:   wgpu::Queue,
    config:  wgpu::SurfaceConfiguration,

    internal_view: wgpu::TextureView,
    depth_view:    wgpu::TextureView,

    scene:         ScenePass,
    sprite:        SpritePass,
    blit_pipeline: wgpu::RenderPipeline,
    blit_bind:     wgpu::BindGroup,
}

impl Gpu {
    pub fn new(
        window: Arc<Window>,
        display: winit::event_loop::OwnedDisplayHandle,
        mesh: &Mesh,
        images: &[Image],
        ui_textures: &[Image],
    ) -> Self {
        let size = window.inner_size();
        // Display-Handle mitgeben: für den GL-Fallback (v.a. Wayland)
        // Pflicht; Vulkan ignoriert es. `with_env` erlaubt Overrides wie
        // WGPU_BACKEND=gl zum Testen des Fallback-Pfads.
        let instance = wgpu::Instance::new(
            wgpu::InstanceDescriptor::new_with_display_handle(Box::new(display)).with_env(),
        );
        let surface = instance.create_surface(window).expect("wgpu: Surface");

        let adapter = pollster::block_on(instance.request_adapter(
            &wgpu::RequestAdapterOptions {
                compatible_surface: Some(&surface),
                ..Default::default()
            },
        )).expect("wgpu: kein Adapter");
        let info = adapter.get_info();
        println!("[gpu] {} ({:?}, {:?})", info.name, info.backend, info.device_type);

        // Baseline-Limits/-Features: alles, was der Plan braucht, ist
        // WebGPU-Kern — nichts anfordern, dann läuft es auch auf HD 5500.
        let (device, queue) = pollster::block_on(
            adapter.request_device(&wgpu::DeviceDescriptor::default()),
        ).expect("wgpu: Device");

        // Nicht-sRGB-8-Bit-Surface bevorzugen: der Fragment-Shader
        // quantisiert später selbst auf RGB555 — die Werte sollen
        // unverändert auf den Schirm, ohne Gamma-Umkodierung beim Blit.
        // Explizite Liste statt „erstes nicht-sRGB": Treiber bieten auch
        // 16-Bit-Formate an, die extra Device-Features bräuchten.
        let caps = surface.get_capabilities(&adapter);
        let format = [wgpu::TextureFormat::Bgra8Unorm, wgpu::TextureFormat::Rgba8Unorm]
            .into_iter()
            .find(|f| caps.formats.contains(f))
            .unwrap_or(caps.formats[0]);
        println!("[gpu] Surface-Format {format:?}");
        let config = wgpu::SurfaceConfiguration {
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            format,
            width:  size.width.max(1),
            height: size.height.max(1),
            present_mode: wgpu::PresentMode::AutoVsync,
            alpha_mode: caps.alpha_modes[0],
            view_formats: vec![],
            desired_maximum_frame_latency: 2,
        };
        surface.configure(&device, &config);

        let (internal_view, depth_view) = make_internal_targets(&device);

        // Pass 1: die Szene. Geometrie + Bilder kommen fertig vom Aufrufer
        // (Decode in render::run); hier wird nur hochgeladen.
        let scene = ScenePass::new(&device, &queue, INTERNAL_FORMAT, DEPTH_FORMAT, mesh, images);

        // Overlay-Pass: zeichnet nach der Szene ins selbe interne Target.
        let sprite = SpritePass::new(
            &device, &queue, INTERNAL_FORMAT, DEPTH_FORMAT,
            [INTERNAL_W as f32, INTERNAL_H as f32], ui_textures,
        );

        // Pass 2: internes Target nearest-gesampelt auf die Surface.
        let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
            label: Some("blit"),
            source: wgpu::ShaderSource::Wgsl(include_str!("blit.wgsl").into()),
        });
        let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
            label: Some("blit"),
            layout: None,
            vertex: wgpu::VertexState {
                module: &blit_shader,
                entry_point: Some("vs_main"),
                compilation_options: Default::default(),
                buffers: &[],
            },
            fragment: Some(wgpu::FragmentState {
                module: &blit_shader,
                entry_point: Some("fs_main"),
                compilation_options: Default::default(),
                targets: &[Some(config.format.into())],
            }),
            primitive: wgpu::PrimitiveState::default(),
            depth_stencil: None,
            multisample: wgpu::MultisampleState::default(),
            multiview_mask: None,
            cache: None,
        });
        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("blit nearest"),
            mag_filter: wgpu::FilterMode::Nearest,
            min_filter: wgpu::FilterMode::Nearest,
            ..Default::default()
        });
        let blit_bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("blit"),
            layout: &blit_pipeline.get_bind_group_layout(0),
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(&internal_view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: wgpu::BindingResource::Sampler(&sampler),
                },
            ],
        });

        Self {
            surface, device, queue, config,
            internal_view, depth_view,
            scene, sprite, blit_pipeline, blit_bind,
        }
    }

    pub fn resize(&mut self, width: u32, height: u32) {
        self.config.width  = width.max(1);
        self.config.height = height.max(1);
        self.surface.configure(&self.device, &self.config);
    }

    /// Fenster-Pixel (physisch) → interne Pixel, über dieselbe Letterbox wie
    /// der Blit. Außerhalb des Bildbereichs auf die Ränder geklemmt.
    pub fn map_cursor(&self, win: [f32; 2]) -> [f32; 2] {
        let (vx, vy, vw, vh) = letterbox(self.config.width, self.config.height);
        let ix = (win[0] - vx) / vw * INTERNAL_W as f32;
        let iy = (win[1] - vy) / vh * INTERNAL_H as f32;
        [ix.clamp(0.0, INTERNAL_W as f32), iy.clamp(0.0, INTERNAL_H as f32)]
    }

    /// Einen Frame rendern. Die View-Matrix kommt vom Aufrufer (Flycam);
    /// die Projektion gehört hierher, weil sie an der internen Auflösung
    /// hängt.
    pub fn frame(&mut self, view: &Mat4, ui: &Ui) {
        let proj = Mat4::perspective(
            60f32.to_radians(),
            INTERNAL_W as f32 / INTERNAL_H as f32,
            0.1, 100.0,
        );
        let half_res = [INTERNAL_W as f32 * 0.5, INTERNAL_H as f32 * 0.5];
        self.scene.prepare(&self.queue, &proj.mul(view), half_res);
        self.sprite.prepare(&self.queue, &ui.verts);

        use wgpu::CurrentSurfaceTexture as Cst;
        let frame = match self.surface.get_current_texture() {
            Cst::Success(f) | Cst::Suboptimal(f) => f,
            // Surface veraltet (Resize/Compositor): neu konfigurieren,
            // diesen Frame auslassen.
            Cst::Outdated | Cst::Lost => {
                self.surface.configure(&self.device, &self.config);
                return;
            }
            // Fenster verdeckt/minimiert oder Treiber-Timeout: auslassen.
            Cst::Timeout | Cst::Occluded => return,
            Cst::Validation => panic!("wgpu: Surface-Validation-Fehler"),
        };
        let surface_view = frame.texture.create_view(&Default::default());
        let mut enc = self.device.create_command_encoder(&Default::default());

        // Pass 1: intern.
        {
            let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("internal"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &self.internal_view,
                    depth_slice: None,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                        store: wgpu::StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                    view: &self.depth_view,
                    depth_ops: Some(wgpu::Operations {
                        load: wgpu::LoadOp::Clear(1.0),
                        store: wgpu::StoreOp::Store,
                    }),
                    stencil_ops: None,
                }),
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            self.scene.draw(&mut pass);
            // Overlay obenauf, im selben Pass (kein Tiefentest).
            self.sprite.draw(&mut pass, &ui.batches);
        }

        // Pass 2: Letterbox-Blit aufs Fenster.
        {
            let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("blit"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &surface_view,
                    depth_slice: None,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        // Schwarz = die Letterbox-Balken.
                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                        store: wgpu::StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
            let (vx, vy, vw, vh) = letterbox(self.config.width, self.config.height);
            pass.set_viewport(vx, vy, vw, vh, 0.0, 1.0);
            pass.set_pipeline(&self.blit_pipeline);
            pass.set_bind_group(0, &self.blit_bind, &[]);
            pass.draw(0..3, 0..1);
        }

        self.queue.submit([enc.finish()]);
        frame.present();
    }
}

fn make_internal_targets(device: &wgpu::Device) -> (wgpu::TextureView, wgpu::TextureView) {
    let size = wgpu::Extent3d {
        width: INTERNAL_W, height: INTERNAL_H, depth_or_array_layers: 1,
    };
    let color = device.create_texture(&wgpu::TextureDescriptor {
        label: Some("internal color"),
        size,
        mip_level_count: 1,
        sample_count: 1,
        dimension: wgpu::TextureDimension::D2,
        format: INTERNAL_FORMAT,
        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
        view_formats: &[],
    });
    let depth = device.create_texture(&wgpu::TextureDescriptor {
        label: Some("internal depth"),
        size,
        mip_level_count: 1,
        sample_count: 1,
        dimension: wgpu::TextureDimension::D2,
        format: DEPTH_FORMAT,
        usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
        view_formats: &[],
    });
    (color.create_view(&Default::default()), depth.create_view(&Default::default()))
}

/// Größtes 4:3-Rechteck, das ins Fenster passt, zentriert.
/// Nicht-ganzzahlige Skalierung ist gewollt (volle Fensterausnutzung);
/// die leicht ungleichen Pixel passen zum CRT-Vorbild.
fn letterbox(win_w: u32, win_h: u32) -> (f32, f32, f32, f32) {
    let (w, h) = (win_w as f32, win_h as f32);
    let scale = (w / INTERNAL_W as f32).min(h / INTERNAL_H as f32);
    let vw = INTERNAL_W as f32 * scale;
    let vh = INTERNAL_H as f32 * scale;
    ((w - vw) * 0.5, (h - vh) * 0.5, vw, vh)
}
