use crate::camera::ViewportExtent;
use crate::gpu::{Capabilities, Gpu};
use crate::pick::{PickIndex, Picker};

const PICK_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;

#[derive(Debug, thiserror::Error)]
pub enum SurfaceError {
    #[error("surface creation: {0}")]
    Create(#[from] wgpu::CreateSurfaceError),
    #[error("no wgpu adapter matched the surface: {0}")]
    NoAdapter(#[from] wgpu::RequestAdapterError),
    #[error("wgpu device request failed: {0}")]
    Device(#[from] wgpu::RequestDeviceError),
    #[error(
        "surface exposes no srgb color format; baseline requires one. available: {available:?}"
    )]
    NoSrgbFormat { available: Vec<wgpu::TextureFormat> },
    #[error("viewport dimension is zero")]
    ZeroExtent,
}

pub struct SurfaceContext {
    gpu: Gpu,
    surface: wgpu::Surface<'static>,
    config: wgpu::SurfaceConfiguration,
    pick: wgpu::Texture,
    pick_staging: wgpu::Buffer,
    extent: ViewportExtent,
    reconfigure_pending: bool,
}

enum AcquireOutcome {
    Frame(wgpu::SurfaceTexture),
    Skip,
    Reconfigure,
}

impl SurfaceContext {
    pub async fn new(
        target: impl Into<wgpu::SurfaceTarget<'static>>,
        extent: ViewportExtent,
    ) -> Result<Self, SurfaceError> {
        if extent.width().value() == 0 || extent.height().value() == 0 {
            return Err(SurfaceError::ZeroExtent);
        }
        let instance =
            wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
        let target: wgpu::SurfaceTarget<'static> = target.into();
        let surface = instance.create_surface(target)?;
        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::LowPower,
                force_fallback_adapter: false,
                compatible_surface: Some(&surface),
            })
            .await?;
        let (device, queue) = adapter
            .request_device(&wgpu::DeviceDescriptor {
                label: Some("bone-render:surface"),
                required_features: wgpu::Features::empty(),
                required_limits: wgpu::Limits::downlevel_defaults(),
                experimental_features: wgpu::ExperimentalFeatures::default(),
                memory_hints: wgpu::MemoryHints::default(),
                trace: wgpu::Trace::Off,
            })
            .await?;
        let capabilities = Capabilities::probe(&adapter);
        let caps = surface.get_capabilities(&adapter);
        let Some(format) = caps
            .formats
            .iter()
            .copied()
            .find(wgpu::TextureFormat::is_srgb)
        else {
            return Err(SurfaceError::NoSrgbFormat {
                available: caps.formats.clone(),
            });
        };
        let present_mode = pick_present_mode(&caps.present_modes);
        let alpha_mode = caps
            .alpha_modes
            .first()
            .copied()
            .unwrap_or(wgpu::CompositeAlphaMode::Auto);
        let config = wgpu::SurfaceConfiguration {
            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
            format,
            width: extent.width().value(),
            height: extent.height().value(),
            present_mode,
            alpha_mode,
            view_formats: vec![],
            desired_maximum_frame_latency: 1,
        };
        surface.configure(&device, &config);
        let pick = create_pick_texture(&device, extent);
        let pick_staging = device.create_buffer(&wgpu::BufferDescriptor {
            label: Some("bone-render:surface-pick-readback"),
            size: u64::from(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT),
            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
            mapped_at_creation: false,
        });
        Ok(Self {
            gpu: Gpu::from_parts(device, queue, capabilities),
            surface,
            config,
            pick,
            pick_staging,
            extent,
            reconfigure_pending: false,
        })
    }

    #[must_use]
    pub fn gpu(&self) -> &Gpu {
        &self.gpu
    }

    #[must_use]
    pub const fn extent(&self) -> ViewportExtent {
        self.extent
    }

    #[must_use]
    pub const fn color_format(&self) -> wgpu::TextureFormat {
        self.config.format
    }

    #[must_use]
    pub fn picker(&self, index: PickIndex) -> Picker<'_> {
        Picker::new(
            &self.gpu,
            &self.pick,
            &self.pick_staging,
            self.extent,
            index,
        )
    }

    pub fn resize(&mut self, extent: ViewportExtent) {
        if extent.width().value() == 0 || extent.height().value() == 0 {
            return;
        }
        self.extent = extent;
        self.config.width = extent.width().value();
        self.config.height = extent.height().value();
        self.surface.configure(self.gpu.device(), &self.config);
        self.pick = create_pick_texture(self.gpu.device(), extent);
        self.reconfigure_pending = false;
    }

    pub fn render<F, G>(&mut self, build_passes: F, pre_present: G)
    where
        F: FnOnce(&mut wgpu::CommandEncoder, &wgpu::TextureView, &wgpu::TextureView),
        G: FnOnce(),
    {
        let Some(frame) = self.acquire_frame() else {
            return;
        };
        let color_view = frame
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());
        let pick_view = self
            .pick
            .create_view(&wgpu::TextureViewDescriptor::default());
        let mut encoder =
            self.gpu
                .device()
                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                    label: Some("bone-render:surface-encoder"),
                });
        build_passes(&mut encoder, &color_view, &pick_view);
        self.gpu.queue().submit(Some(encoder.finish()));
        pre_present();
        frame.present();
    }

    fn acquire_frame(&mut self) -> Option<wgpu::SurfaceTexture> {
        if self.reconfigure_pending {
            self.surface.configure(self.gpu.device(), &self.config);
            self.reconfigure_pending = false;
        }
        match self.classify_acquire() {
            AcquireOutcome::Frame(frame) => Some(frame),
            AcquireOutcome::Skip => None,
            AcquireOutcome::Reconfigure => {
                self.surface.configure(self.gpu.device(), &self.config);
                match self.classify_acquire() {
                    AcquireOutcome::Frame(frame) => Some(frame),
                    AcquireOutcome::Skip => None,
                    AcquireOutcome::Reconfigure => {
                        tracing::warn!(
                            "surface still outdated/lost after reconfigure; skipping frame"
                        );
                        None
                    }
                }
            }
        }
    }

    fn classify_acquire(&mut self) -> AcquireOutcome {
        match self.surface.get_current_texture() {
            wgpu::CurrentSurfaceTexture::Success(frame) => AcquireOutcome::Frame(frame),
            wgpu::CurrentSurfaceTexture::Suboptimal(frame) => {
                self.reconfigure_pending = true;
                AcquireOutcome::Frame(frame)
            }
            wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
                AcquireOutcome::Reconfigure
            }
            wgpu::CurrentSurfaceTexture::Timeout => {
                tracing::warn!("surface frame acquire timed out; skipping frame");
                AcquireOutcome::Skip
            }
            wgpu::CurrentSurfaceTexture::Occluded => {
                tracing::debug!("surface occluded; skipping frame");
                AcquireOutcome::Skip
            }
            wgpu::CurrentSurfaceTexture::Validation => {
                tracing::warn!("surface validation error on acquire; skipping frame");
                AcquireOutcome::Skip
            }
        }
    }
}

fn create_pick_texture(device: &wgpu::Device, extent: ViewportExtent) -> wgpu::Texture {
    device.create_texture(&wgpu::TextureDescriptor {
        label: Some("bone-render:surface-pick"),
        size: wgpu::Extent3d {
            width: extent.width().value(),
            height: extent.height().value(),
            depth_or_array_layers: 1,
        },
        mip_level_count: 1,
        sample_count: 1,
        dimension: wgpu::TextureDimension::D2,
        format: PICK_FORMAT,
        usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
        view_formats: &[],
    })
}

fn pick_present_mode(modes: &[wgpu::PresentMode]) -> wgpu::PresentMode {
    [
        wgpu::PresentMode::Mailbox,
        wgpu::PresentMode::Immediate,
        wgpu::PresentMode::FifoRelaxed,
    ]
    .into_iter()
    .find(|m| modes.contains(m))
    .unwrap_or(wgpu::PresentMode::Fifo)
}
