use glam::UVec2;

use crate::commands::{Command, RemoveTexture, ResourceCommand};
use crate::engine::Engine;
use crate::platform::types::TextureResource;
use crate::render::BackendState;
use crate::renderer::resource::TextureCache;
use crate::renderer::shader2d::runner::Renderer2DRunner;
use crate::renderer::shader3d::draw3d::Draw3D;
use crate::renderer::shader3d::runner::Shader3DRunner;

pub struct RenderDriver {
    device: wgpu::Device,
    queue: wgpu::Queue,
    surface_view_format: wgpu::TextureFormat,
    depth_format: wgpu::TextureFormat,
    post_sampler: wgpu::Sampler,
    textures: TextureCache,
    renderer2d: Renderer2DRunner,
    renderer_ui: Renderer2DRunner,
    // Cached offscreen
    render_target: Option<wgpu::Texture>,
    depth_target: Option<wgpu::Texture>,
    render_target_size: UVec2,
    renderer3d: Option<Shader3DRunner>,
}

impl RenderDriver {
    pub fn new(backend: &BackendState) -> Self {
        let device = backend.device.clone();
        let queue = backend.queue.clone();
        let surface_view_format = backend.surface_view_format;
        let depth_format = wgpu::TextureFormat::Depth32Float;
        let renderer2d = Renderer2DRunner::setup(backend, false, depth_format);
        let renderer_ui = Renderer2DRunner::setup(backend, true, depth_format);
        let post_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some("Post Render Sampler"),
            address_mode_u: wgpu::AddressMode::ClampToEdge,
            address_mode_v: wgpu::AddressMode::ClampToEdge,
            address_mode_w: wgpu::AddressMode::ClampToEdge,
            mag_filter: wgpu::FilterMode::Nearest,
            min_filter: wgpu::FilterMode::Nearest,
            mipmap_filter: wgpu::FilterMode::Nearest,
            ..Default::default()
        });
        Self {
            device,
            queue,
            surface_view_format,
            depth_format,
            post_sampler,
            textures: TextureCache::default(),
            renderer2d,
            renderer_ui,
            render_target: None,
            depth_target: None,
            render_target_size: UVec2::new(0, 0),
            renderer3d: None,
        }
    }

    pub fn resize(&mut self) {
        // Drop cached offscreen so it will be recreated with the new size
        self.render_target = None;
        self.depth_target = None;
        self.render_target_size = UVec2::new(0, 0);
    }

    pub fn apply_resource_cmds(&mut self, cmds: Vec<ResourceCommand>) {
        for cmd in cmds {
            match cmd {
                ResourceCommand::CreateTexture(c) => {
                    let tex = TextureCache::create_texture(&self.device, &self.queue, c.data);
                    self.textures.insert(c.handle.id(), tex);
                }
                ResourceCommand::CreateRawTexture(c) => {
                    let tex = TextureCache::create_raw_texture(&self.device, c.texture);
                    self.textures.insert(c.handle.id(), tex);
                }
                ResourceCommand::RemoveTexture(RemoveTexture(id)) => {
                    self.textures.remove(&id);
                }
            }
        }
    }

    fn ensure_render_target(
        &mut self,
        g: &Engine,
        frame: &wgpu::SurfaceTexture,
    ) -> (wgpu::Texture, wgpu::TextureView) {
        let desired = g.render.logical_size().as_uvec2();
        let need_recreate = self
            .render_target
            .as_ref()
            .map(|_| self.render_target_size != desired)
            .unwrap_or(true);
        if need_recreate {
            let size = wgpu::Extent3d {
                width: desired.x,
                height: desired.y,
                depth_or_array_layers: 1,
            };
            let dimension = frame.texture.dimension();
            let texture = self.device.create_texture(&wgpu::TextureDescriptor {
                label: Some("Render Texture"),
                size,
                mip_level_count: 1,
                sample_count: 1,
                dimension,
                format: self.surface_view_format,
                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
                    | wgpu::TextureUsages::TEXTURE_BINDING,
                view_formats: &[],
            });
            self.render_target = Some(texture);

            let depth_texture = self.device.create_texture(&wgpu::TextureDescriptor {
                label: Some("depth_texture"),
                size,
                mip_level_count: 1,
                sample_count: 1,
                dimension,
                format: self.depth_format,
                usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
                view_formats: &[self.depth_format],
            });
            self.depth_target = Some(depth_texture);
            self.render_target_size = desired;
        }
        (
            self.render_target.as_ref().unwrap().clone(),
            self.depth_target
                .as_ref()
                .unwrap()
                .create_view(&wgpu::TextureViewDescriptor::default()),
        )
    }

    // NOTE: texture resolution is handled in record_frame when splitting draw commands.

    pub fn record_frame(
        &mut self,
        g: &Engine,
        draw_commands: Vec<Command>,
        draws3d: Vec<Draw3D>,
        encoder: &mut wgpu::CommandEncoder,
        frame: &wgpu::SurfaceTexture,
    ) {
        let (render_target, depth_view) = self.ensure_render_target(g, frame);
        // Split draws into world/ui and resolve textures once
        let mut world_draws: Vec<(crate::commands::Draw, Option<TextureResource>)> = Vec::new();
        let mut ui_draws: Vec<(crate::commands::Draw, Option<TextureResource>)> = Vec::new();
        for cmd in draw_commands.into_iter() {
            match cmd {
                Command::Draw(d) => {
                    let tex = d
                        .content
                        .texture_handle()
                        .and_then(|h| self.textures.get(&h.id()))
                        .cloned();
                    world_draws.push((d, tex));
                }
                Command::DrawUi(d) => {
                    let tex = d
                        .content
                        .texture_handle()
                        .and_then(|h| self.textures.get(&h.id()))
                        .cloned();
                    ui_draws.push((d, tex));
                }
            }
        }
        let has_3d = !draws3d.is_empty();

        // Clear the offscreen once at the beginning of the frame
        {
            let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("Offscreen Clear Pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &render_target.create_view(&wgpu::TextureViewDescriptor::default()),
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color {
                            r: 0.0,
                            g: 0.0,
                            b: 0.0,
                            a: 0.0,
                        }),
                        store: wgpu::StoreOp::Store,
                    },
                    depth_slice: None,
                })],
                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
                    view: &depth_view,
                    depth_ops: Some(wgpu::Operations {
                        load: wgpu::LoadOp::Clear(1.0),
                        store: wgpu::StoreOp::Store,
                    }),
                    stencil_ops: None,
                }),
                occlusion_query_set: None,
                timestamp_writes: None,
            });
            // pass dropped here
        }

        if has_3d {
            if self.renderer3d.is_none() {
                self.renderer3d = Some(Shader3DRunner::new(g));
            }
            self.renderer3d.as_mut().unwrap().record(
                g,
                &render_target,
                &depth_view,
                &draws3d,
                encoder,
            );
        }

        // Record 2D world and UI draws, if any
        if !world_draws.is_empty() {
            self.renderer2d
                .record(g, &render_target, &depth_view, world_draws, encoder);
        }
        if !ui_draws.is_empty() {
            self.renderer_ui
                .record(g, &render_target, &depth_view, ui_draws, encoder);
        }

        // Post pass to present to the frame texture
        let frame_view = frame.texture.create_view(&wgpu::TextureViewDescriptor {
            label: Some("Swapchain sRGB View"),
            format: Some(self.surface_view_format),
            ..Default::default()
        });
        let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
            label: Some("Post Render Pass"),
            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                view: &frame_view,
                resolve_target: None,
                ops: wgpu::Operations {
                    // Clear the swapchain so transparent offscreen regions don't preserve the prior frame
                    load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
                    store: wgpu::StoreOp::Store,
                },
                depth_slice: None,
            })],
            depth_stencil_attachment: None,
            occlusion_query_set: None,
            timestamp_writes: None,
        });
        // Update post renderer per-frame state here (unified spot)
        g.render.post_shader.frame_pass(g, &mut render_pass);
        let view = render_target.create_view(&wgpu::TextureViewDescriptor::default());
        let tex = TextureResource {
            texture: render_target,
            view,
            sampler: self.post_sampler.clone(),
        };
        g.render.post_shader.draw(&tex, &mut render_pass);
        g.render.post_shader.frame_end(&mut render_pass);
    }
}
