use std::sync::Arc;

use color_eyre::eyre::eyre;
use winit::window::Window;

/// This is the main external interface for graphics interactions.
pub enum Context {
    // Winit has not sent a resume event, so the graphics context is empty.
    Uninitialized {
        creation_size: Option<winit::dpi::PhysicalSize<u32>>,
    },
    // Winit has initialized our graphics context.
    #[allow(private_interfaces)]
    Initialized(ActiveContext),
}

impl Default for Context {
    fn default() -> Self {
        Self::Uninitialized {
            creation_size: None,
        }
    }
}

impl Context {
    pub async fn initialize(&mut self, window: Window) -> color_eyre::Result<()> {
        match self {
            Self::Uninitialized { creation_size } => {
                let mut context = ActiveContext::new(window).await?;
                if let Some(new_size) = creation_size.take() {
                    context.resize(new_size);
                }
                *self = Self::Initialized(context);
                Ok(())
            }
            Self::Initialized(_) => {
                // Re-init
                *self = Self::default();
                let context = ActiveContext::new(window).await?;
                *self = Self::Initialized(context);
                Ok(())
            }
        }
    }

    pub fn on_window<F: FnOnce(Arc<Window>) -> O, O>(&self, f: F) -> Option<O> {
        self.window().map(f)
    }

    pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
        match self {
            Self::Uninitialized { .. } => Ok(()),
            Self::Initialized(ac) => ac.render(),
        }
    }

    pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
        match self {
            Self::Uninitialized { creation_size } => {
                *creation_size = Some(new_size);
            }

            Self::Initialized(ac) => {
                ac.resize(new_size);
            }
        }
    }

    pub fn window(&self) -> Option<Arc<Window>> {
        match self {
            Self::Uninitialized { .. } => None,
            Self::Initialized(ctx) => Some(ctx.window.clone()),
        }
    }
}

struct ActiveContext {
    instance: wgpu::Instance,
    surface: wgpu::Surface<'static>,
    config: wgpu::SurfaceConfiguration,
    adapter: wgpu::Adapter,
    device: wgpu::Device,
    queue: wgpu::Queue,

    window: Arc<Window>,
}

impl ActiveContext {
    pub async fn new(window: Window) -> color_eyre::Result<Self> {
        let size = window.inner_size();
        let width = size.width.max(1);
        let height = size.height.max(1);

        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
            backends: wgpu::Backends::PRIMARY,
            ..Default::default()
        });

        let window = Arc::new(window);
        let surface = instance.create_surface(window.clone())?;
        let adapter = instance
            .request_adapter(&wgpu::RequestAdapterOptions {
                power_preference: wgpu::PowerPreference::default(),
                force_fallback_adapter: false,
                compatible_surface: Some(&surface),
            })
            .await
            .ok_or(eyre!("Failed to find an adapter for surface"))?;

        let (device, queue) = adapter
            .request_device(
                &wgpu::DeviceDescriptor {
                    label: None,
                    required_features: wgpu::Features::empty(),
                    required_limits: wgpu::Limits::default().using_resolution(adapter.limits()),
                },
                None,
            )
            .await?;

        let swapchain_caps = surface.get_capabilities(&adapter);
        let _swapchain_format = swapchain_caps.formats[0];

        let config = surface
            .get_default_config(&adapter, width, height)
            .ok_or(eyre!("Failed to configure surface for adapter"))?;
        surface.configure(&device, &config);

        Ok(Self {
            instance,
            surface,
            adapter,
            window,
            device,
            queue,
            config,
        })
    }

    pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
        if new_size.width > 0 && new_size.height > 0 {
            self.config.width = new_size.width;
            self.config.height = new_size.height;
            self.surface.configure(&self.device, &self.config);
        }
    }

    pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
        let output = self.surface.get_current_texture()?;
        let view = output
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());
        let mut encoder = self
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("Render Encoder"),
            });

        {
            let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("Render Pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view: &view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(wgpu::Color {
                            r: 0.01,
                            g: 0.005,
                            b: 0.005,
                            a: 1.0,
                        }),
                        store: wgpu::StoreOp::Store,
                    },
                })],
                depth_stencil_attachment: None,
                occlusion_query_set: None,
                timestamp_writes: None,
            });
        }

        // submit will accept anything that implements IntoIter
        self.queue.submit(std::iter::once(encoder.finish()));
        output.present();

        Ok(())
    }
}