use crate::assets::{Assets, RetainPolicy};
use crate::diagnostics::{Backend, BuildError, Capabilities, PrepareError, RenderError};
use crate::material::Color;
use crate::platform::{PlatformSurface, PlatformSurfaceAttachment, SurfaceEvent};
use crate::scene::Scene;

#[cfg(not(target_arch = "wasm32"))]
use super::gpu;
use super::{RasterTarget, Renderer, backend_for_attached_surface, validate_target_size};

impl Renderer {
    pub fn handle_surface_event(&mut self, event: SurfaceEvent) -> Result<(), RenderError> {
        match event {
            SurfaceEvent::Resize { width, height } => {
                self.resize_target(width, height)?;
            }
            SurfaceEvent::ViewportChanged(viewport) => {
                let size = viewport.physical_size();
                self.resize_target(size.width, size.height)?;
            }
            SurfaceEvent::ScaleFactorChanged { .. } | SurfaceEvent::Occluded { .. } => {
                self.target_revision = self.target_revision.saturating_add(1);
            }
            SurfaceEvent::Hidden | SurfaceEvent::Shown => {}
            SurfaceEvent::Lost => {
                self.surface_lost = Some(true);
                self.target_revision = self.target_revision.saturating_add(1);
            }
            SurfaceEvent::ContextLost { recoverable } => {
                self.context_lost = Some(recoverable);
                self.target_revision = self.target_revision.saturating_add(1);
            }
            SurfaceEvent::ContextRestored => {
                if self.context_lost == Some(true) {
                    self.context_lost = None;
                    self.target_revision = self.target_revision.saturating_add(1);
                }
            }
            SurfaceEvent::DeviceLost { recoverable } => {
                self.device_lost = Some(recoverable);
                self.target_revision = self.target_revision.saturating_add(1);
            }
        }
        Ok(())
    }

    pub fn recover_surface(&mut self, surface: PlatformSurface) -> Result<(), BuildError> {
        let (kind, size, attachment) = surface.into_parts();
        let (backend, gpu, attached) = match attachment {
            PlatformSurfaceAttachment::Descriptor => (Backend::SurfaceDescriptor, None, false),
            #[cfg(not(target_arch = "wasm32"))]
            PlatformSurfaceAttachment::NativeWindow(window) => {
                let backend = backend_for_attached_surface(kind);
                let gpu =
                    pollster::block_on(gpu::request_native_surface_gpu(backend, size, window))?;
                (backend, Some(gpu), true)
            }
            #[cfg(target_arch = "wasm32")]
            PlatformSurfaceAttachment::BrowserWebGpuCanvas(_)
            | PlatformSurfaceAttachment::BrowserWebGl2Canvas(_) => {
                let backend = backend_for_attached_surface(kind);
                return Err(BuildError::AsyncSurfaceRequired { backend });
            }
        };
        validate_target_size(size.width, size.height).map_err(|()| {
            BuildError::InvalidTargetSize {
                width: size.width,
                height: size.height,
            }
        })?;

        self.target = RasterTarget {
            width: size.width,
            height: size.height,
            backend,
        };
        self.frame.resize(self.target.byte_len(), 0);
        self.fxaa_scratch.resize(self.target.byte_len(), 0);
        self.bloom_scratch.resize(self.target.byte_len(), 0);
        self.oit_scratch.resize(
            self.target.pixel_len(),
            super::cpu::OitAccumPixel::default(),
        );
        if let Some(linear_frame) = &mut self.linear_frame {
            linear_frame.resize(self.target.pixel_len(), Color::BLACK);
        }
        if let Some(depth_frame) = &mut self.depth_frame {
            depth_frame.resize(self.target.pixel_len(), f32::INFINITY);
        }
        if gpu.is_some() && self.linear_frame.is_some() {
            self.linear_frame = None;
            self.depth_frame = None;
        } else if gpu.is_none() && self.linear_frame.is_none() {
            self.linear_frame = Some(vec![Color::BLACK; self.target.pixel_len()]);
            self.depth_frame = Some(vec![f32::INFINITY; self.target.pixel_len()]);
        }
        self.gpu = gpu;
        self.capabilities = if attached {
            Capabilities::for_attached_gpu_backend(backend)
        } else if self.gpu.is_some() {
            Capabilities::for_gpu_backend(backend)
        } else {
            Capabilities::for_backend(backend)
        }
        .with_display_p3_output(false);
        self.stats.target_width = size.width;
        self.stats.target_height = size.height;
        self.surface_lost = None;
        self.target_revision = self.target_revision.saturating_add(1);
        self.prepared = None;
        self.last_rendered_generation = None;
        Ok(())
    }

    pub fn recover_context<F>(
        &mut self,
        assets: &Assets<F>,
        _scene: &mut Scene,
    ) -> Result<(), PrepareError> {
        if assets.retain_policy() == RetainPolicy::Never {
            return Err(PrepareError::BackendCapabilityMismatch {
                feature: "context recovery",
                backend: self.target.backend,
                help: "Assets uses RetainPolicy::Never; recreate assets or retain CPU data for recovery"
                    .to_string(),
            });
        }
        match self.context_lost.or(self.device_lost) {
            Some(false) => Err(PrepareError::BackendCapabilityMismatch {
                feature: "context recovery",
                backend: self.target.backend,
                help: "the host reported the GPU context as unrecoverable; rebuild Renderer"
                    .to_string(),
            }),
            Some(true) | None => {
                if let Some(gpu) = &mut self.gpu {
                    gpu.clear_prepared_resources_for_context_recovery();
                }
                self.context_lost = None;
                self.device_lost = None;
                self.target_revision = self.target_revision.saturating_add(1);
                self.prepared = None;
                self.last_rendered_generation = None;
                Ok(())
            }
        }
    }

    pub(super) fn resize_target(&mut self, width: u32, height: u32) -> Result<(), RenderError> {
        validate_target_size(width, height)
            .map_err(|()| RenderError::InvalidSurfaceSize { width, height })?;
        self.target.width = width;
        self.target.height = height;
        self.frame.resize(self.target.byte_len(), 0);
        self.fxaa_scratch.resize(self.target.byte_len(), 0);
        self.bloom_scratch.resize(self.target.byte_len(), 0);
        self.oit_scratch.resize(
            self.target.pixel_len(),
            super::cpu::OitAccumPixel::default(),
        );
        if let Some(linear_frame) = &mut self.linear_frame {
            linear_frame.resize(self.target.pixel_len(), Color::BLACK);
        }
        if let Some(depth_frame) = &mut self.depth_frame {
            depth_frame.resize(self.target.pixel_len(), f32::INFINITY);
        }
        self.stats.target_width = width;
        self.stats.target_height = height;
        self.target_revision = self.target_revision.saturating_add(1);
        Ok(())
    }

    pub(super) fn loss_error(&self) -> Result<(), RenderError> {
        if let Some(recoverable) = self.surface_lost {
            return Err(RenderError::SurfaceLost { recoverable });
        }
        if let Some(recoverable) = self.context_lost {
            return Err(RenderError::ContextLost { recoverable });
        }
        if let Some(recoverable) = self.device_lost {
            return Err(RenderError::GpuDeviceLost { recoverable });
        }
        Ok(())
    }
}
