//! Structured errors, debug overlays, capability reports, and renderer stats.

use crate::animation::{AnimationClipKey, AnimationMixerKey};
use crate::assets::{EnvironmentHandle, GeometryHandle, MaterialHandle, TextureHandle};
use crate::geometry::{Aabb, GeometryTopology};
use crate::material::{AlphaMode, MaterialKind};
use crate::scene::{
    CameraKey, ClippingPlaneKey, InstanceSetKey, LabelKey, NodeKey, SourceCoordinateSystem,
    SourceUnits, Transform,
};

#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
mod browser_timing;
mod capabilities;
mod capability_status;
mod diagnostic;
mod display;
mod help;
#[cfg(all(target_arch = "wasm32", feature = "demo-page"))]
pub(crate) use browser_timing::browser_timing_enabled;
pub use capabilities::{
    AdapterLimitsReport, AlphaPipelineStatus, Backend, Capabilities, CapabilityReport,
    CapabilityStatus, GpuAdapterReport, HardwareTier, OutputColorSpace, OutputStageStatus,
};
pub use diagnostic::{Diagnostic, DiagnosticCode, DiagnosticSeverity};

#[derive(Debug, Clone, PartialEq)]
pub enum Error {
    Build(BuildError),
    Asset(AssetError),
    Import(ImportError),
    Instantiate(InstantiateError),
    Prepare(PrepareError),
    Render(RenderError),
    Lookup(LookupError),
    Animation(AnimationError),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BuildError {
    InvalidTargetSize { width: u32, height: u32 },
    AsyncSurfaceRequired { backend: Backend },
    CreateSurface { backend: Backend },
    NoAdapter { backend: Backend },
    RequestDevice { backend: Backend },
    SurfaceUnsupported { backend: Backend },
    UnsupportedBackend { backend: Backend },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AssetError {
    NotFound {
        path: String,
    },
    Io {
        path: String,
        reason: String,
    },
    Parse {
        path: String,
        reason: String,
    },
    UnsupportedRequiredExtension {
        path: String,
        extension: String,
    },
    UnsupportedOptionalExtensionUsed {
        path: String,
        extension: String,
        help: String,
    },
    MissingTexture {
        path: String,
        material_slot: String,
        texture_index: usize,
        help: &'static str,
    },
    UnsupportedTextureFormat {
        path: String,
        help: &'static str,
    },
    Cancelled {
        path: String,
        help: &'static str,
    },
    UnsupportedEnvironmentFormat {
        path: String,
        help: &'static str,
    },
    ReloadRequiresRetain {
        path: String,
        help: &'static str,
    },
    GeometryHandleNotFound {
        geometry: GeometryHandle,
    },
    MaterialHandleNotFound {
        material: MaterialHandle,
    },
    TextureHandleNotFound {
        texture: TextureHandle,
    },
    EnvironmentHandleNotFound {
        environment: EnvironmentHandle,
    },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImportError {
    Asset(AssetError),
    Instantiate(InstantiateError),
}

#[derive(Debug, Clone, PartialEq)]
pub enum PrepareError {
    InvalidTargetSize {
        width: u32,
        height: u32,
    },
    AssetsRequired {
        node: NodeKey,
    },
    GeometryNotFound {
        node: NodeKey,
        geometry: GeometryHandle,
    },
    MaterialNotFound {
        node: NodeKey,
        material: MaterialHandle,
    },
    TextureNotFound {
        node: NodeKey,
        material: MaterialHandle,
        texture: TextureHandle,
        slot: &'static str,
    },
    EnvironmentAssetsRequired {
        environment: EnvironmentHandle,
    },
    EnvironmentNotFound {
        environment: EnvironmentHandle,
    },
    UnsupportedGeometryTopology {
        node: NodeKey,
        topology: GeometryTopology,
    },
    UnsupportedMaterialKind {
        node: NodeKey,
        kind: MaterialKind,
    },
    UnsupportedAlphaMode {
        node: NodeKey,
        alpha_mode: AlphaMode,
    },
    UnsupportedModelNode {
        node: NodeKey,
    },
    MultipleShadowedDirectionalLights {
        first: NodeKey,
        second: NodeKey,
    },
    InvalidSkinGeometry {
        node: NodeKey,
        reason: String,
    },
    BackendCapabilityMismatch {
        feature: &'static str,
        backend: Backend,
        help: String,
    },
    GpuResourceUpload {
        backend: Backend,
        reason: String,
    },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenderError {
    NotPrepared { reason: NotPreparedReason },
    NoActiveCamera,
    CameraNotFound(CameraKey),
    InvalidSurfaceSize { width: u32, height: u32 },
    SurfaceLost { recoverable: bool },
    ContextLost { recoverable: bool },
    GpuDeviceLost { recoverable: bool },
    GpuResourcesNotPrepared { backend: Backend },
    GpuReadback { backend: Backend },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InstantiateError {
    InvalidChildIndex {
        parent: usize,
        child: usize,
    },
    InvalidSkinIndex {
        node: usize,
        skin: usize,
    },
    InvalidSkinJointIndex {
        skin: usize,
        joint: usize,
    },
    InvalidAnchorExtras {
        node: String,
        reason: String,
    },
    UnsupportedCoordinateSystem {
        coordinate_system: SourceCoordinateSystem,
        reason: String,
    },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NotPreparedReason {
    NeverPrepared,
    DifferentScene,
    SceneChanged {
        prepared_revision: u64,
        current_revision: u64,
        change: ChangeKind,
    },
    EnvironmentChanged {
        prepared_revision: u64,
        current_revision: u64,
        change: ChangeKind,
    },
    TargetChanged {
        prepared_revision: u64,
        current_revision: u64,
        change: ChangeKind,
    },
    RendererChanged {
        prepared_revision: u64,
        current_revision: u64,
        change: ChangeKind,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeKind {
    SceneStructure,
    Transform,
    Environment,
    RenderTarget,
    DebugOverlay,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum DebugOverlay {
    #[default]
    None,
    Wireframe,
    Normals,
    BoundingBoxes,
    ShadowMap,
    LightCount,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LookupError {
    NodeNotFound(NodeKey),
    NodeNameNotFound {
        name: String,
    },
    AmbiguousNodeName {
        name: String,
        matches: Vec<NodeKey>,
    },
    AnchorNotFound {
        name: String,
    },
    AmbiguousAnchorName {
        name: String,
        hosts: Vec<NodeKey>,
    },
    ConnectorNotFound {
        name: String,
    },
    AmbiguousConnectorName {
        name: String,
        hosts: Vec<NodeKey>,
    },
    ClipNotFound {
        name: String,
    },
    AmbiguousClipName {
        name: String,
        matches: Vec<AnimationClipKey>,
    },
    /// Phase 2B step 3: a variant name passed to
    /// `Scene::set_active_variant` does not appear in the
    /// `SceneImport::material_variants` list. Returned instead of
    /// silently no-oping so callers know the asset doesn't carry
    /// that KHR_materials_variants name.
    VariantNotFound {
        name: String,
    },
    PathNotFound {
        path: String,
    },
    /// A viewport width or height was zero where projection/framing needs pixels.
    InvalidViewport {
        width: u32,
        height: u32,
    },
    /// Bounds were empty, non-finite, or otherwise unsuitable for framing.
    InvalidBounds {
        reason: &'static str,
    },
    /// A named framing option failed validation before camera state was changed.
    InvalidFramingOption {
        field: &'static str,
        reason: &'static str,
    },
    /// The requested operation does not support the camera type yet.
    UnsupportedCameraType {
        camera: CameraKey,
        operation: &'static str,
        supported: &'static str,
    },
    ImportHasNoBounds,
    StaleImport,
    NodeIsNotMesh {
        node: NodeKey,
    },
    NonInvertibleParentTransform {
        node: NodeKey,
        parent: NodeKey,
    },
    GeometryNotFound {
        node: NodeKey,
        geometry: GeometryHandle,
    },
    CameraNotFound(CameraKey),
    ClippingPlaneNotFound(ClippingPlaneKey),
    InstanceSetNotFound(InstanceSetKey),
    LabelNotFound(LabelKey),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AnimationError {
    ClipNotFound { name: String },
    MixerNotFound(AnimationMixerKey),
    StaleMixer(AnimationMixerKey),
}

#[derive(Debug, Clone, PartialEq)]
pub struct ImportDiagnosticOverlay {
    kind: ImportDiagnosticOverlayKind,
    node: NodeKey,
    transform: Transform,
    bounds: Option<Aabb>,
    label: Option<String>,
    source_units: SourceUnits,
    source_coordinate_system: SourceCoordinateSystem,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportDiagnosticOverlayKind {
    Origin,
    Axes,
    Bounds,
    Anchor,
    Connector,
    Pivot,
}

#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct RendererStats {
    pub buffers: u64,
    pub textures: u64,
    pub materials: u64,
    pub material_bindings: u64,
    pub material_texture_bindings: u64,
    pub material_sampler_bindings: u64,
    pub material_textures_missing_decoded_pixels: u64,
    /// Plan line 778 step 2: number of layers a `texture_2d_array` per
    /// material role carries when the prepared materials share
    /// `(sampler, format, dimensions)` for every populated role. Zero when
    /// the materials are not array-compatible or the scene has no
    /// materials. Commit 2 wires the GPU pipeline to consume the shared
    /// `texture_2d_array<f32>` + dynamic-offset uniform; the layer count
    /// reported here matches the layer count actually allocated when
    /// `material_batch_layers >= 2`.
    pub material_batch_layers: u32,
    /// Plan line 778 step 2: actual material bind-group count consumed by
    /// the GPU pipeline. Equals 1 when the renderer chose the batched
    /// `texture_2d_array<f32>` path (one shared bind group services every
    /// draw via dynamic-offset uniforms) and `material_count + 1` when
    /// the per-material fall-back path is in use (one synthetic fallback +
    /// one bind group per material slot). Drops to 0 only when the GPU
    /// is absent (CPU rasterizer path). Closes the
    /// `texture_array_batching_collapses_to_single_bind` proof.
    pub material_bind_groups: u32,
    pub render_targets: u64,
    pub pipelines: u64,
    pub bind_groups: u64,
    pub shader_modules: u64,
    pub environments: u64,
    pub environment_cubemaps: u64,
    pub environment_prefilter_passes: u64,
    pub environment_brdf_luts: u64,
    pub scene_imports: u64,
    pub shadow_maps: u64,
    pub depth_prepass_passes: u64,
    pub depth_prepass_draws: u64,
    pub ambient_occlusion_passes: u64,
    pub order_independent_transparency_passes: u64,
    pub bloom_passes: u64,
    pub fxaa_passes: u64,
    pub live_logical_handles: u64,
    pub pending_destructions: u64,
    pub frames_rendered: u64,
    pub draw_calls: u64,
    pub triangles: u64,
    pub culled_objects: u64,
    pub gpu_culling_dispatches: u64,
    pub skipped_frames: u64,
    pub gpu_submissions: u64,
    pub approximate_gpu_memory_bytes: Option<u64>,
    pub cpu_frame_ms: f32,
    pub gpu_frame_ms: Option<f32>,
    pub primitives: u64,
    pub target_width: u32,
    pub target_height: u32,
    pub directional_shadow_map_resolution: Option<u32>,
    pub directional_shadow_pcf_kernel: Option<u8>,
}

impl ImportDiagnosticOverlay {
    pub fn new(
        kind: ImportDiagnosticOverlayKind,
        node: NodeKey,
        transform: Transform,
        bounds: Option<Aabb>,
        label: Option<String>,
    ) -> Self {
        Self {
            kind,
            node,
            transform,
            bounds,
            label,
            source_units: SourceUnits::Meters,
            source_coordinate_system: SourceCoordinateSystem::GltfYUpRightHanded,
        }
    }

    pub const fn with_source_metadata(
        mut self,
        units: SourceUnits,
        coordinate_system: SourceCoordinateSystem,
    ) -> Self {
        self.source_units = units;
        self.source_coordinate_system = coordinate_system;
        self
    }

    pub const fn kind(&self) -> ImportDiagnosticOverlayKind {
        self.kind
    }

    pub const fn node(&self) -> NodeKey {
        self.node
    }

    pub const fn transform(&self) -> Transform {
        self.transform
    }

    pub const fn bounds(&self) -> Option<Aabb> {
        self.bounds
    }

    pub fn label(&self) -> Option<&str> {
        self.label.as_deref()
    }

    pub const fn source_units(&self) -> SourceUnits {
        self.source_units
    }

    pub const fn source_coordinate_system(&self) -> SourceCoordinateSystem {
        self.source_coordinate_system
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DevicePoll {
    pub pending_destructions_before: u64,
    pub pending_destructions_after: u64,
    pub destroyed_resources: u64,
    pub gpu_polled: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RenderOutcome {
    pub width: u32,
    pub height: u32,
    pub draw_calls: u64,
    pub primitives: u64,
    pub skipped: bool,
}

impl From<BuildError> for Error {
    fn from(error: BuildError) -> Self {
        Self::Build(error)
    }
}

impl From<AssetError> for Error {
    fn from(error: AssetError) -> Self {
        Self::Asset(error)
    }
}

impl From<ImportError> for Error {
    fn from(error: ImportError) -> Self {
        Self::Import(error)
    }
}

impl From<AnimationError> for Error {
    fn from(error: AnimationError) -> Self {
        Self::Animation(error)
    }
}

impl From<InstantiateError> for Error {
    fn from(error: InstantiateError) -> Self {
        Self::Instantiate(error)
    }
}

impl From<PrepareError> for Error {
    fn from(error: PrepareError) -> Self {
        Self::Prepare(error)
    }
}

impl From<RenderError> for Error {
    fn from(error: RenderError) -> Self {
        Self::Render(error)
    }
}

impl From<LookupError> for Error {
    fn from(error: LookupError) -> Self {
        Self::Lookup(error)
    }
}
