//! Reusable, windowless `OpenSim` scene rendering on stable `wgpu`.

#![allow(clippy::missing_errors_doc)]

type CameraBasis = ([f32; 3], [f32; 3], [f32; 3]);

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Camera {
    pub position: [f32; 3],
    pub forward: [f32; 3],
    pub up: [f32; 3],
    pub vertical_fov_degrees: f32,
}

impl Camera {
    pub fn look_at(
        position: [f32; 3],
        target: [f32; 3],
        up: [f32; 3],
        vertical_fov_degrees: f32,
    ) -> Result<Self, RenderError> {
        let camera = Self {
            position,
            forward: normalize(sub(target, position))?,
            up: normalize(up)?,
            vertical_fov_degrees,
        };
        camera.basis()?;
        Ok(camera)
    }

    fn basis(self) -> Result<CameraBasis, RenderError> {
        if !(10.0..=140.0).contains(&self.vertical_fov_degrees)
            || self
                .position
                .iter()
                .chain(self.forward.iter())
                .chain(self.up.iter())
                .any(|value| !value.is_finite())
        {
            return Err(RenderError::InvalidScene);
        }
        let forward = normalize(self.forward)?;
        let right = normalize(cross(forward, self.up))?;
        Ok((right, cross(right, forward), forward))
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Vertex {
    pub position: [f32; 3],
    pub normal: [f32; 3],
    pub tex_coord: [f32; 2],
    pub color_srgb: [u8; 4],
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct Mesh {
    pub vertices: Vec<Vertex>,
    pub indices: Vec<u32>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct Texture {
    pub width: u32,
    pub height: u32,
    pub rgba: Vec<u8>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct UvTransform {
    pub scale: [f32; 2],
    pub offset: [f32; 2],
    pub rotation_radians: f32,
}

impl Default for UvTransform {
    fn default() -> Self {
        Self {
            scale: [1.0; 2],
            offset: [0.0; 2],
            rotation_radians: 0.0,
        }
    }
}

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct TextureSlot {
    pub texture: Option<usize>,
    pub transform: UvTransform,
}

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum AlphaMode {
    #[default]
    Opaque,
    Mask {
        cutoff: f32,
    },
    Blend,
}

#[derive(Clone, Debug, PartialEq)]
pub struct BlinnPhongMaterial {
    pub diffuse_color_srgb: [u8; 4],
    pub diffuse: TextureSlot,
    pub normal: TextureSlot,
    pub specular: TextureSlot,
    pub specular_color_srgb: [u8; 3],
    pub shininess: f32,
    pub environment_intensity: f32,
    pub fullbright: bool,
    pub double_sided: bool,
    pub alpha_mode: AlphaMode,
    /// Optional diffuse-texture cutout applied before face-alpha blending.
    pub alpha_cutoff: Option<f32>,
}

impl Default for BlinnPhongMaterial {
    fn default() -> Self {
        Self {
            diffuse_color_srgb: [255; 4],
            diffuse: TextureSlot::default(),
            normal: TextureSlot::default(),
            specular: TextureSlot::default(),
            specular_color_srgb: [0; 3],
            shininess: 32.0,
            environment_intensity: 0.0,
            fullbright: false,
            double_sided: false,
            alpha_mode: AlphaMode::Opaque,
            alpha_cutoff: None,
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct PbrMaterial {
    pub base_color_srgb: [u8; 4],
    pub base_color: TextureSlot,
    pub normal: TextureSlot,
    /// glTF convention: roughness in G and metallic in B.
    pub metallic_roughness: TextureSlot,
    pub emissive: TextureSlot,
    pub metallic_factor: f32,
    pub roughness_factor: f32,
    pub emissive_factor_srgb: [u8; 3],
    pub double_sided: bool,
    pub alpha_mode: AlphaMode,
}

impl Default for PbrMaterial {
    fn default() -> Self {
        Self {
            base_color_srgb: [255; 4],
            base_color: TextureSlot::default(),
            normal: TextureSlot::default(),
            metallic_roughness: TextureSlot::default(),
            emissive: TextureSlot::default(),
            metallic_factor: 1.0,
            roughness_factor: 1.0,
            emissive_factor_srgb: [0; 3],
            double_sided: false,
            alpha_mode: AlphaMode::Opaque,
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub enum Material {
    BlinnPhong(BlinnPhongMaterial),
    Pbr(PbrMaterial),
}

impl Default for Material {
    fn default() -> Self {
        Self::BlinnPhong(BlinnPhongMaterial::default())
    }
}

impl Material {
    #[must_use]
    pub fn alpha_mode(&self) -> AlphaMode {
        match self {
            Self::BlinnPhong(material) => material.alpha_mode,
            Self::Pbr(material) => material.alpha_mode,
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
pub struct Renderable {
    pub mesh: Mesh,
    pub material: Material,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RenderLimits {
    pub width: u32,
    pub height: u32,
    pub max_triangles: usize,
    pub max_texture_bytes: usize,
    pub max_texture_pixels: usize,
    pub far_distance: u32,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RenderError {
    AdapterUnavailable,
    InvalidScene,
    ResourceLimit,
    TimedOut,
    Readback,
    Device(String),
}

impl std::fmt::Display for RenderError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(match self {
            Self::AdapterUnavailable => "wgpu adapter is unavailable",
            Self::InvalidScene => "render scene is invalid",
            Self::ResourceLimit => "render resource limit exceeded",
            Self::TimedOut => "wgpu render timed out",
            Self::Readback => "wgpu readback failed",
            Self::Device(_) => "wgpu device failed",
        })?;
        if let Self::Device(detail) = self {
            write!(formatter, ": {detail}")?;
        }
        Ok(())
    }
}

impl std::error::Error for RenderError {}

fn sub(left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
    std::array::from_fn(|axis| left[axis] - right[axis])
}

fn normalize(value: [f32; 3]) -> Result<[f32; 3], RenderError> {
    let length = dot(value, value).sqrt();
    if length < 0.0001 || !length.is_finite() {
        return Err(RenderError::InvalidScene);
    }
    Ok(value.map(|component| component / length))
}

fn dot(left: [f32; 3], right: [f32; 3]) -> f32 {
    left[0] * right[0] + left[1] * right[1] + left[2] * right[2]
}

fn cross(left: [f32; 3], right: [f32; 3]) -> [f32; 3] {
    [
        left[1] * right[2] - left[2] * right[1],
        left[2] * right[0] - left[0] * right[2],
        left[0] * right[1] - left[1] * right[0],
    ]
}

#[cfg(feature = "wgpu")]
mod gpu;
#[cfg(feature = "wgpu")]
pub use gpu::{RenderTimings, RenderedFrame, Renderer};

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn camera_and_both_material_models_are_explicit() {
        let camera =
            Camera::look_at([1.0, 2.0, 3.0], [5.0, 2.0, 3.0], [0.0, 0.0, 1.0], 60.0).unwrap();
        assert!(
            camera
                .forward
                .iter()
                .zip([1.0, 0.0, 0.0])
                .all(|(actual, expected)| (actual - expected).abs() < f32::EPSILON)
        );
        assert!(Camera::look_at([0.0; 3], [0.0; 3], [0.0, 0.0, 1.0], 60.0).is_err());
        assert!(matches!(Material::default(), Material::BlinnPhong(_)));
        assert!(matches!(
            Material::Pbr(PbrMaterial::default()),
            Material::Pbr(_)
        ));
    }
}
