//! Graphics settings resources.

use crate::ecs::camera::components::EffectiveShading;
use serde::{Deserialize, Serialize};

/// Controls how the renderer treats the focused viewport when the
/// camera is in a non-`Always` update mode.
///
/// - `FocusedAlways` (default): the focused viewport (active camera, or
///   the camera tile under the mouse) re-renders every frame regardless
///   of its `ViewportUpdateMode`. Background tiles still respect their
///   own update mode.
/// - `Manual`: every viewport, focused or not, respects its own
///   `ViewportUpdateMode` strictly. Useful when the user wants
///   pixel-perfect control over which tiles render each frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ViewportFocusPolicy {
    #[default]
    FocusedAlways,
    Manual,
}

/// Debug visualization mode for PBR rendering components.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum PbrDebugMode {
    #[default]
    None,
    BaseColor,
    Normal,
    Metallic,
    Roughness,
    Occlusion,
    Emissive,
    F,
    G,
    D,
    Diffuse,
    Specular,
    MipRainbow,
}

impl PbrDebugMode {
    pub const ALL: &'static [PbrDebugMode] = &[
        PbrDebugMode::None,
        PbrDebugMode::BaseColor,
        PbrDebugMode::Normal,
        PbrDebugMode::Metallic,
        PbrDebugMode::Roughness,
        PbrDebugMode::Occlusion,
        PbrDebugMode::Emissive,
        PbrDebugMode::F,
        PbrDebugMode::G,
        PbrDebugMode::D,
        PbrDebugMode::Diffuse,
        PbrDebugMode::Specular,
        PbrDebugMode::MipRainbow,
    ];

    pub fn name(&self) -> &'static str {
        match self {
            PbrDebugMode::None => "None",
            PbrDebugMode::BaseColor => "Base Color",
            PbrDebugMode::Normal => "Normal",
            PbrDebugMode::Metallic => "Metallic",
            PbrDebugMode::Roughness => "Roughness",
            PbrDebugMode::Occlusion => "Occlusion",
            PbrDebugMode::Emissive => "Emissive",
            PbrDebugMode::F => "Fresnel (F)",
            PbrDebugMode::G => "Geometry (G)",
            PbrDebugMode::D => "Distribution (D)",
            PbrDebugMode::Diffuse => "Diffuse",
            PbrDebugMode::Specular => "Specular",
            PbrDebugMode::MipRainbow => "Mip Rainbow",
        }
    }

    pub fn as_u32(&self) -> u32 {
        match self {
            PbrDebugMode::None => 0,
            PbrDebugMode::BaseColor => 1,
            PbrDebugMode::Normal => 2,
            PbrDebugMode::Metallic => 3,
            PbrDebugMode::Roughness => 4,
            PbrDebugMode::Occlusion => 5,
            PbrDebugMode::Emissive => 6,
            PbrDebugMode::F => 7,
            PbrDebugMode::G => 8,
            PbrDebugMode::D => 9,
            PbrDebugMode::Diffuse => 10,
            PbrDebugMode::Specular => 11,
            PbrDebugMode::MipRainbow => 12,
        }
    }
}

/// Quality level for depth of field effect.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum DepthOfFieldQuality {
    /// 8 samples per pixel.
    Low,
    /// 16 samples per pixel.
    #[default]
    Medium,
    /// 32 samples per pixel.
    High,
}

impl DepthOfFieldQuality {
    pub const ALL: &'static [DepthOfFieldQuality] = &[
        DepthOfFieldQuality::Low,
        DepthOfFieldQuality::Medium,
        DepthOfFieldQuality::High,
    ];

    pub fn name(&self) -> &'static str {
        match self {
            DepthOfFieldQuality::Low => "Low",
            DepthOfFieldQuality::Medium => "Medium",
            DepthOfFieldQuality::High => "High",
        }
    }

    pub fn sample_count(&self) -> u32 {
        match self {
            DepthOfFieldQuality::Low => 8,
            DepthOfFieldQuality::Medium => 16,
            DepthOfFieldQuality::High => 32,
        }
    }
}

/// Depth of field post-processing settings.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct DepthOfField {
    /// Whether DOF is active.
    pub enabled: bool,
    /// Distance to the focal plane in world units.
    pub focus_distance: f32,
    /// Range around focus distance that remains sharp.
    pub focus_range: f32,
    /// Maximum blur radius in pixels.
    pub max_blur_radius: f32,
    /// Brightness threshold for bokeh highlights.
    pub bokeh_threshold: f32,
    /// Intensity multiplier for bokeh highlights.
    pub bokeh_intensity: f32,
    /// Sample count quality level.
    pub quality: DepthOfFieldQuality,
    /// Debug visualization of circle of confusion.
    pub visualize_coc: bool,
    /// Enable tilt-shift miniature effect.
    pub tilt_shift_enabled: bool,
    /// Angle of the tilt-shift band in radians.
    pub tilt_shift_angle: f32,
    /// Vertical position of the sharp band center (-1 to 1).
    pub tilt_shift_center: f32,
    /// Blur strength outside the sharp band.
    pub tilt_shift_blur_amount: f32,
    /// Debug visualization of tilt-shift mask.
    pub visualize_tilt_shift: bool,
}

impl Default for DepthOfField {
    fn default() -> Self {
        Self {
            enabled: false,
            focus_distance: 10.0,
            focus_range: 5.0,
            max_blur_radius: 8.0,
            bokeh_threshold: 0.8,
            bokeh_intensity: 1.0,
            quality: DepthOfFieldQuality::Medium,
            visualize_coc: false,
            tilt_shift_enabled: false,
            tilt_shift_angle: 0.0,
            tilt_shift_center: 0.0,
            tilt_shift_blur_amount: 1.0,
            visualize_tilt_shift: false,
        }
    }
}

impl DepthOfField {
    /// Preset for close-up portraits with strong background blur.
    pub fn portrait() -> Self {
        Self {
            enabled: true,
            focus_distance: 3.0,
            focus_range: 1.5,
            max_blur_radius: 12.0,
            bokeh_threshold: 0.6,
            bokeh_intensity: 1.2,
            quality: DepthOfFieldQuality::High,
            visualize_coc: false,
            tilt_shift_enabled: false,
            tilt_shift_angle: 0.0,
            tilt_shift_center: 0.0,
            tilt_shift_blur_amount: 1.0,
            visualize_tilt_shift: false,
        }
    }

    /// Preset for cinematic medium-distance focus.
    pub fn cinematic() -> Self {
        Self {
            enabled: true,
            focus_distance: 8.0,
            focus_range: 4.0,
            max_blur_radius: 10.0,
            bokeh_threshold: 0.7,
            bokeh_intensity: 1.0,
            quality: DepthOfFieldQuality::Medium,
            visualize_coc: false,
            tilt_shift_enabled: false,
            tilt_shift_angle: 0.0,
            tilt_shift_center: 0.0,
            tilt_shift_blur_amount: 1.0,
            visualize_tilt_shift: false,
        }
    }

    pub fn macro_shot() -> Self {
        Self {
            enabled: true,
            focus_distance: 0.5,
            focus_range: 0.2,
            max_blur_radius: 16.0,
            bokeh_threshold: 0.5,
            bokeh_intensity: 1.5,
            quality: DepthOfFieldQuality::High,
            visualize_coc: false,
            tilt_shift_enabled: false,
            tilt_shift_angle: 0.0,
            tilt_shift_center: 0.0,
            tilt_shift_blur_amount: 1.0,
            visualize_tilt_shift: false,
        }
    }

    pub fn landscape() -> Self {
        Self {
            enabled: true,
            focus_distance: 50.0,
            focus_range: 100.0,
            max_blur_radius: 4.0,
            bokeh_threshold: 0.9,
            bokeh_intensity: 0.5,
            quality: DepthOfFieldQuality::Low,
            visualize_coc: false,
            tilt_shift_enabled: false,
            tilt_shift_angle: 0.0,
            tilt_shift_center: 0.0,
            tilt_shift_blur_amount: 1.0,
            visualize_tilt_shift: false,
        }
    }

    pub fn tilt_shift() -> Self {
        Self {
            enabled: true,
            focus_distance: 10.0,
            focus_range: 5.0,
            max_blur_radius: 12.0,
            bokeh_threshold: 0.8,
            bokeh_intensity: 0.8,
            quality: DepthOfFieldQuality::Medium,
            visualize_coc: false,
            tilt_shift_enabled: true,
            tilt_shift_angle: 0.0,
            tilt_shift_center: 0.0,
            tilt_shift_blur_amount: 1.0,
            visualize_tilt_shift: false,
        }
    }
}

/// HDR to LDR tonemapping algorithm.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum TonemapAlgorithm {
    /// Academy Color Encoding System (film-like).
    #[default]
    Aces,
    /// Simple Reinhard curve.
    Reinhard,
    /// Extended Reinhard with white point.
    ReinhardExtended,
    /// Filmic curve from Uncharted 2.
    Uncharted2,
    /// AgX display transform.
    AgX,
    /// Neutral (minimal color shift).
    Neutral,
    /// No tonemapping (clamp only).
    None,
}

impl TonemapAlgorithm {
    pub const ALL: &'static [TonemapAlgorithm] = &[
        TonemapAlgorithm::Aces,
        TonemapAlgorithm::Reinhard,
        TonemapAlgorithm::ReinhardExtended,
        TonemapAlgorithm::Uncharted2,
        TonemapAlgorithm::AgX,
        TonemapAlgorithm::Neutral,
        TonemapAlgorithm::None,
    ];

    pub fn as_u32(&self) -> u32 {
        match self {
            TonemapAlgorithm::Aces => 0,
            TonemapAlgorithm::Reinhard => 1,
            TonemapAlgorithm::ReinhardExtended => 2,
            TonemapAlgorithm::Uncharted2 => 3,
            TonemapAlgorithm::AgX => 4,
            TonemapAlgorithm::Neutral => 5,
            TonemapAlgorithm::None => 6,
        }
    }

    pub fn name(&self) -> &'static str {
        match self {
            TonemapAlgorithm::Aces => "ACES",
            TonemapAlgorithm::Reinhard => "Reinhard",
            TonemapAlgorithm::ReinhardExtended => "Reinhard Extended",
            TonemapAlgorithm::Uncharted2 => "Uncharted 2",
            TonemapAlgorithm::AgX => "AgX",
            TonemapAlgorithm::Neutral => "Neutral",
            TonemapAlgorithm::None => "None",
        }
    }
}

/// Pre-configured color grading style.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ColorGradingPreset {
    /// Neutral default settings.
    #[default]
    Default,
    /// High saturation and brightness.
    Vibrant,
    /// Film-like with lifted blacks.
    Cinematic,
    /// Low saturation and contrast.
    Muted,
    /// Strong contrast with deep blacks.
    HighContrast,
    /// Orange-shifted warm tones.
    Warm,
    /// Blue-shifted cool tones.
    Cool,
    /// Faded vintage look.
    Retro,
    /// Nearly black and white.
    Desaturated,
    /// User-defined values.
    Custom,
}

impl ColorGradingPreset {
    pub const ALL: &'static [ColorGradingPreset] = &[
        ColorGradingPreset::Default,
        ColorGradingPreset::Vibrant,
        ColorGradingPreset::Cinematic,
        ColorGradingPreset::Muted,
        ColorGradingPreset::HighContrast,
        ColorGradingPreset::Warm,
        ColorGradingPreset::Cool,
        ColorGradingPreset::Retro,
        ColorGradingPreset::Desaturated,
        ColorGradingPreset::Custom,
    ];

    pub fn name(&self) -> &'static str {
        match self {
            ColorGradingPreset::Default => "Default",
            ColorGradingPreset::Vibrant => "Vibrant",
            ColorGradingPreset::Cinematic => "Cinematic",
            ColorGradingPreset::Muted => "Muted",
            ColorGradingPreset::HighContrast => "High Contrast",
            ColorGradingPreset::Warm => "Warm",
            ColorGradingPreset::Cool => "Cool",
            ColorGradingPreset::Retro => "Retro",
            ColorGradingPreset::Desaturated => "Desaturated",
            ColorGradingPreset::Custom => "Custom",
        }
    }

    pub fn to_color_grading(&self) -> ColorGrading {
        let preset = *self;
        match self {
            ColorGradingPreset::Default => ColorGrading {
                preset,
                ..ColorGrading::default()
            },
            ColorGradingPreset::Vibrant => ColorGrading {
                saturation: 1.3,
                brightness: 0.02,
                contrast: 1.1,
                preset,
                ..ColorGrading::default()
            },
            ColorGradingPreset::Cinematic => ColorGrading {
                gamma: 2.4,
                saturation: 0.9,
                brightness: -0.02,
                contrast: 1.15,
                preset,
                ..ColorGrading::default()
            },
            ColorGradingPreset::Muted => ColorGrading {
                saturation: 0.7,
                contrast: 0.9,
                tonemap_algorithm: TonemapAlgorithm::Reinhard,
                preset,
                ..ColorGrading::default()
            },
            ColorGradingPreset::HighContrast => ColorGrading {
                saturation: 1.1,
                contrast: 1.4,
                preset,
                ..ColorGrading::default()
            },
            ColorGradingPreset::Warm => ColorGrading {
                gamma: 2.1,
                saturation: 1.1,
                brightness: 0.03,
                contrast: 1.05,
                preset,
                ..ColorGrading::default()
            },
            ColorGradingPreset::Cool => ColorGrading {
                gamma: 2.3,
                saturation: 0.95,
                brightness: -0.01,
                contrast: 1.05,
                tonemap_algorithm: TonemapAlgorithm::Neutral,
                preset,
                ..ColorGrading::default()
            },
            ColorGradingPreset::Retro => ColorGrading {
                gamma: 2.0,
                saturation: 0.8,
                brightness: 0.05,
                contrast: 1.2,
                tonemap_algorithm: TonemapAlgorithm::Reinhard,
                preset,
                ..ColorGrading::default()
            },
            ColorGradingPreset::Desaturated => ColorGrading {
                saturation: 0.3,
                preset,
                ..ColorGrading::default()
            },
            ColorGradingPreset::Custom => ColorGrading::default(),
        }
    }
}

/// Color grading and tonemapping settings.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ColorGrading {
    /// Linear exposure multiplier applied before tonemapping. Use small values
    /// (e.g. 0.001) when the scene uses physical glTF light units (lux/candela).
    pub exposure: f32,
    /// Exposure compensation in EV stops applied on top of `exposure` and
    /// auto-exposure. Multiplies the final color by `2^ev`.
    pub exposure_compensation_ev: f32,
    /// When true, derive exposure each frame from average HDR scene luminance
    /// and use `exposure` as a manual compensation multiplier on top.
    pub auto_exposure: bool,
    /// Target luminance the auto-exposure tries to map to middle gray
    /// (typical 0.18 = perceptual middle gray).
    pub auto_exposure_target: f32,
    /// Adaptation speed for the auto-exposure smoothing (1/seconds). 1.0 lerps
    /// ~63% toward the new target each second; 5.0 is snappy; 0.2 is cinematic.
    pub auto_exposure_rate: f32,
    /// Lower bound on the exposure multiplier resolved from auto-exposure,
    /// expressed in EV stops relative to `auto_exposure_target` mapping to
    /// middle gray. Prevents night-vision overshoot in dark scenes.
    pub auto_exposure_min_ev: f32,
    /// Upper bound on the exposure multiplier resolved from auto-exposure,
    /// expressed in EV stops. Prevents specular hot-spots from crushing
    /// bright scenes.
    pub auto_exposure_max_ev: f32,
    /// Gamma correction exponent (typically 2.2).
    pub gamma: f32,
    /// Color saturation multiplier (1.0 = neutral).
    pub saturation: f32,
    /// Brightness offset (-1 to 1).
    pub brightness: f32,
    /// Contrast multiplier (1.0 = neutral).
    pub contrast: f32,
    /// HDR tonemapping algorithm.
    pub tonemap_algorithm: TonemapAlgorithm,
    /// Active preset (Custom if manually adjusted).
    pub preset: ColorGradingPreset,
}

impl Default for ColorGrading {
    fn default() -> Self {
        Self {
            exposure: 1.0,
            exposure_compensation_ev: 0.0,
            auto_exposure: false,
            auto_exposure_target: 0.18,
            auto_exposure_rate: 1.5,
            auto_exposure_min_ev: -3.0,
            auto_exposure_max_ev: 5.0,
            gamma: 2.2,
            saturation: 1.0,
            brightness: 0.0,
            contrast: 1.0,
            tonemap_algorithm: TonemapAlgorithm::Aces,
            preset: ColorGradingPreset::Default,
        }
    }
}

/// PS1-style vertex snapping for retro rendering.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct VertexSnap {
    /// Grid resolution to snap vertices to (lower = more pixelated).
    pub resolution: [f32; 2],
}

impl Default for VertexSnap {
    fn default() -> Self {
        Self {
            resolution: [160.0, 120.0],
        }
    }
}

/// Distance-based fog settings.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Fog {
    /// Fog color (linear RGB).
    pub color: [f32; 3],
    /// Distance where fog begins.
    pub start: f32,
    /// Distance where fog is fully opaque.
    pub end: f32,
}

impl Default for Fog {
    fn default() -> Self {
        Self {
            color: [0.5, 0.5, 0.55],
            start: 2.0,
            end: 15.0,
        }
    }
}

#[derive(Clone)]
pub struct DayNightState {
    pub hour: f32,
    pub speed: f32,
    pub auto_cycle: bool,
    pub sun_entity: Option<crate::ecs::world::Entity>,
}

impl Default for DayNightState {
    fn default() -> Self {
        Self {
            hour: 12.0,
            speed: 0.0,
            auto_cycle: false,
            sun_entity: None,
        }
    }
}

/// Skybox and environment map selection.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum Atmosphere {
    /// Solid background color (no skybox).
    #[default]
    None,
    /// Procedural clear sky gradient.
    Sky,
    /// Procedural sky with volumetric clouds.
    CloudySky,
    /// Procedural starfield.
    Space,
    /// Procedural nebula with stars.
    Nebula,
    /// Procedural sunset gradient.
    Sunset,
    /// Procedural day/night cycle driven by hour parameter.
    DayNight,
    /// HDR environment cubemap.
    Hdr,
    /// Debug: HDR mip level 0.
    HdrMip0,
    /// Debug: HDR mip level 1.
    HdrMip1,
    /// Debug: HDR mip level 2.
    HdrMip2,
    /// Debug: HDR mip level 3.
    HdrMip3,
    /// Debug: HDR mip level 4.
    HdrMip4,
    /// Debug: Diffuse irradiance map.
    Irradiance,
    /// Debug: Prefiltered specular mip 0.
    PrefilterMip0,
    /// Debug: Prefiltered specular mip 1.
    PrefilterMip1,
    /// Debug: Prefiltered specular mip 2.
    PrefilterMip2,
    /// Debug: Prefiltered specular mip 3.
    PrefilterMip3,
    /// Debug: Prefiltered specular mip 4.
    PrefilterMip4,
}

impl Atmosphere {
    pub const ALL: &'static [Atmosphere] = &[
        Atmosphere::None,
        Atmosphere::Sky,
        Atmosphere::CloudySky,
        Atmosphere::Space,
        Atmosphere::Nebula,
        Atmosphere::Sunset,
        Atmosphere::DayNight,
        Atmosphere::Hdr,
        Atmosphere::HdrMip0,
        Atmosphere::HdrMip1,
        Atmosphere::HdrMip2,
        Atmosphere::HdrMip3,
        Atmosphere::HdrMip4,
        Atmosphere::Irradiance,
        Atmosphere::PrefilterMip0,
        Atmosphere::PrefilterMip1,
        Atmosphere::PrefilterMip2,
        Atmosphere::PrefilterMip3,
        Atmosphere::PrefilterMip4,
    ];

    pub fn mip_level(&self) -> f32 {
        match self {
            Atmosphere::HdrMip0 | Atmosphere::PrefilterMip0 => 0.0,
            Atmosphere::HdrMip1 | Atmosphere::PrefilterMip1 => 1.0,
            Atmosphere::HdrMip2 | Atmosphere::PrefilterMip2 => 2.0,
            Atmosphere::HdrMip3 | Atmosphere::PrefilterMip3 => 3.0,
            Atmosphere::HdrMip4 | Atmosphere::PrefilterMip4 => 4.0,
            _ => 0.0,
        }
    }

    pub fn next(self) -> Self {
        let all = Self::ALL;
        let current_index = all.iter().position(|&a| a == self).unwrap_or(0);
        let next_index = (current_index + 1) % all.len();
        all[next_index]
    }

    pub fn previous(self) -> Self {
        let all = Self::ALL;
        let current_index = all.iter().position(|&a| a == self).unwrap_or(0);
        let prev_index = if current_index == 0 {
            all.len() - 1
        } else {
            current_index - 1
        };
        all[prev_index]
    }

    pub fn is_procedural(&self) -> bool {
        matches!(
            self,
            Atmosphere::Sky
                | Atmosphere::CloudySky
                | Atmosphere::Space
                | Atmosphere::Nebula
                | Atmosphere::Sunset
                | Atmosphere::DayNight
        )
    }

    pub fn as_procedural_cubemap_type(&self) -> Option<u32> {
        match self {
            Atmosphere::Sky => Some(0),
            Atmosphere::CloudySky => Some(1),
            Atmosphere::Space => Some(2),
            Atmosphere::Nebula => Some(3),
            Atmosphere::Sunset => Some(4),
            Atmosphere::DayNight => Some(5),
            _ => None,
        }
    }
}

#[derive(Default)]
pub struct IblViews {
    pub brdf_lut_view: Option<wgpu::TextureView>,
    pub irradiance_view: Option<wgpu::TextureView>,
    pub prefiltered_view: Option<wgpu::TextureView>,
}

impl Clone for IblViews {
    fn clone(&self) -> Self {
        Self {
            brdf_lut_view: self.brdf_lut_view.clone(),
            irradiance_view: self.irradiance_view.clone(),
            prefiltered_view: self.prefiltered_view.clone(),
        }
    }
}

#[derive(Clone)]
pub struct MeshLodLevel {
    pub mesh_name: String,
    pub min_screen_pixels: f32,
}

#[derive(Clone)]
pub struct MeshLodChain {
    pub base_mesh: String,
    pub levels: Vec<MeshLodLevel>,
}

/// GPU uniform layout for the configurable post-process effects pass.
/// 38 packed `f32`s mapped 1:1 to the WGSL uniform binding in
/// `crates/nightshade/src/render/wgpu/shaders/effects.wgsl`. The
/// `EffectsPass` writes this buffer each frame from
/// `Graphics::effects.uniforms` plus the live frame time.
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct EffectsUniforms {
    pub time: f32,
    pub chromatic_aberration: f32,
    pub wave_distortion: f32,
    pub color_shift: f32,
    pub kaleidoscope_segments: f32,
    pub crt_scanlines: f32,
    pub vignette: f32,
    pub plasma_intensity: f32,
    pub glitch_intensity: f32,
    pub mirror_mode: f32,
    pub invert: f32,
    pub hue_rotation: f32,
    pub raymarch_mode: f32,
    pub raymarch_blend: f32,
    pub film_grain: f32,
    pub sharpen: f32,
    pub pixelate: f32,
    pub color_posterize: f32,
    pub radial_blur: f32,
    pub tunnel_speed: f32,
    pub fractal_iterations: f32,
    pub glow_intensity: f32,
    pub screen_shake: f32,
    pub zoom_pulse: f32,
    pub speed_lines: f32,
    pub color_grade_mode: f32,
    pub vhs_distortion: f32,
    pub lens_flare: f32,
    pub edge_glow: f32,
    pub saturation: f32,
    pub warp_speed: f32,
    pub pulse_rings: f32,
    pub heat_distortion: f32,
    pub digital_rain: f32,
    pub strobe: f32,
    pub color_cycle_speed: f32,
    pub feedback_amount: f32,
    pub ascii_mode: f32,
}

impl Default for EffectsUniforms {
    fn default() -> Self {
        Self {
            time: 0.0,
            chromatic_aberration: 0.0,
            wave_distortion: 0.0,
            color_shift: 0.0,
            kaleidoscope_segments: 0.0,
            crt_scanlines: 0.0,
            vignette: 0.0,
            plasma_intensity: 0.0,
            glitch_intensity: 0.0,
            mirror_mode: 0.0,
            invert: 0.0,
            hue_rotation: 0.0,
            raymarch_mode: 0.0,
            raymarch_blend: 0.0,
            film_grain: 0.0,
            sharpen: 0.0,
            pixelate: 0.0,
            color_posterize: 0.0,
            radial_blur: 0.0,
            tunnel_speed: 1.0,
            fractal_iterations: 4.0,
            glow_intensity: 0.0,
            screen_shake: 0.0,
            zoom_pulse: 0.0,
            speed_lines: 0.0,
            color_grade_mode: 0.0,
            vhs_distortion: 0.0,
            lens_flare: 0.0,
            edge_glow: 0.0,
            saturation: 1.0,
            warp_speed: 0.0,
            pulse_rings: 0.0,
            heat_distortion: 0.0,
            digital_rain: 0.0,
            strobe: 0.0,
            color_cycle_speed: 1.0,
            feedback_amount: 0.0,
            ascii_mode: 0.0,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RaymarchMode {
    Off = 0,
    Tunnel = 1,
    Fractal = 2,
    Mandelbulb = 3,
    PlasmaVortex = 4,
    Geometric = 5,
}

impl From<u32> for RaymarchMode {
    fn from(value: u32) -> Self {
        match value {
            1 => RaymarchMode::Tunnel,
            2 => RaymarchMode::Fractal,
            3 => RaymarchMode::Mandelbulb,
            4 => RaymarchMode::PlasmaVortex,
            5 => RaymarchMode::Geometric,
            _ => RaymarchMode::Off,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ColorGradeMode {
    None = 0,
    Cyberpunk = 1,
    Sunset = 2,
    Grayscale = 3,
    Sepia = 4,
    Matrix = 5,
    HotMetal = 6,
}

impl From<u32> for ColorGradeMode {
    fn from(value: u32) -> Self {
        match value {
            1 => ColorGradeMode::Cyberpunk,
            2 => ColorGradeMode::Sunset,
            3 => ColorGradeMode::Grayscale,
            4 => ColorGradeMode::Sepia,
            5 => ColorGradeMode::Matrix,
            6 => ColorGradeMode::HotMetal,
            _ => ColorGradeMode::None,
        }
    }
}

/// Post-process effects pass settings. Held in
/// `Graphics::effects` as plain data; the `EffectsPass` reads it from
/// `world.resources.renderer_state.effects` each frame and uploads
/// `uniforms` (with `time`, and optionally `hue_rotation`, overridden
/// for the current frame) to the GPU.
#[derive(Clone, Debug)]
pub struct EffectsState {
    pub uniforms: EffectsUniforms,
    pub enabled: bool,
    pub animate_hue: bool,
}

impl Default for EffectsState {
    fn default() -> Self {
        Self {
            uniforms: EffectsUniforms::default(),
            enabled: true,
            animate_hue: false,
        }
    }
}

/// User-tunable rendering settings. This is the subset of former
/// `Graphics` state that materially controls how a scene is shaded and
/// is safe to persist. Held as `world.resources.render_settings`.
#[derive(Clone, Serialize, Deserialize)]
pub struct RenderSettings {
    /// Software frame rate cap in frames per second. `None` lets the
    /// present mode pace the loop (typically vsync or unbounded under
    /// Mailbox). `Some(fps)` sleeps after each frame to hold the loop
    /// near the target rate.
    ///
    /// On macOS this defaults to `refresh_rate - 1` at window creation
    /// (and re-applies when the window moves to a monitor with a
    /// different refresh rate) when the value still matches what the
    /// engine auto-applied.
    pub frame_rate_limit: Option<f32>,
    /// Active skybox/environment map.
    pub atmosphere: Atmosphere,
    /// When false, the sky pass is skipped so the atmosphere does not render
    /// to the color buffer. IBL contributions are unaffected.
    pub show_sky: bool,
    /// Enable world render layer.
    pub render_layer_world_enabled: bool,
    /// Enable overlay render layer.
    pub render_layer_overlay_enabled: bool,
    /// When true, the world renders to the swapchain even when no viewport tile is requesting
    /// a camera. Default true, which is the normal "game" behaviour: the world fills the window
    /// and any retained UI overlays on top of it. UI-first apps (asset viewers, gallery demos)
    /// can set this to false so an idle frame skips all scene passes and only paints the UI.
    pub render_world_to_swapchain: bool,
    /// Background clear color.
    pub clear_color: [f32; 4],
    /// Override UI scale factor (None = automatic).
    pub ui_scale: Option<f32>,
    /// Disable all lighting calculations.
    pub unlit_mode: bool,
    /// PS1-style vertex snapping settings.
    pub vertex_snap: Option<VertexSnap>,
    /// PS1-style affine texture mapping.
    pub affine_texture_mapping: bool,
    /// Anisotropic filtering clamp for material textures (1 = disabled,
    /// max 16). Default 16 for crisp PBR textures at grazing angles.
    pub material_anisotropy_filtering: u16,
    /// Distance fog settings.
    pub fog: Option<Fog>,
    /// Color grading and tonemapping.
    pub color_grading: ColorGrading,
    /// Enable bloom post-processing.
    pub bloom_enabled: bool,
    /// Bloom intensity multiplier.
    pub bloom_intensity: f32,
    /// Brightness threshold for bloom extraction (pixels below this luminance are excluded).
    pub bloom_threshold: f32,
    /// Soft-knee width around `bloom_threshold` (0 = hard cut).
    pub bloom_knee: f32,
    /// Upsample blur radius in normalized UV units.
    pub bloom_filter_radius: f32,
    /// Depth of field settings.
    pub depth_of_field: DepthOfField,
    /// Enable screen-space ambient occlusion.
    pub ssao_enabled: bool,
    /// SSAO sample radius in world units.
    pub ssao_radius: f32,
    /// SSAO depth bias to reduce self-occlusion.
    pub ssao_bias: f32,
    /// SSAO darkening intensity.
    pub ssao_intensity: f32,
    pub ssao_sample_count: u32,
    pub ssao_blur_depth_threshold: f32,
    pub ssao_blur_normal_power: f32,
    pub ssgi_enabled: bool,
    pub ssgi_radius: f32,
    pub ssgi_intensity: f32,
    pub ssgi_max_steps: u32,
    pub ssr_enabled: bool,
    pub ssr_max_steps: u32,
    pub ssr_thickness: f32,
    pub ssr_max_distance: f32,
    pub ssr_stride: f32,
    pub ssr_fade_start: f32,
    pub ssr_fade_end: f32,
    pub ssr_intensity: f32,
    /// Global ambient light color and intensity.
    pub ambient_light: [f32; 4],
    pub fxaa_enabled: bool,
    /// When true, the swapchain runs in `Fifo` (vsync). When false, the
    /// renderer picks the lowest-latency present mode the adapter
    /// supports. The renderer compares this against the active swapchain
    /// configuration each frame and reconfigures live. No restart required.
    pub vsync_enabled: bool,
    /// Per-camera render-resolution multiplier. Clamped to `[0.25, 4.0]` at use.
    pub render_scale: f32,
    /// IBL blend factor for interpolating between snapshot cubemaps (0.0 - 1.0).
    pub ibl_blend_factor: f32,
}

/// Debug visualization toggles. Held as `world.resources.debug_draw`.
#[derive(Clone)]
pub struct DebugDraw {
    /// Show debug grid overlay.
    pub show_grid: bool,
    /// Show bounding volumes for all entities.
    pub show_bounding_volumes: bool,
    /// Show bounding volume for selected entity only.
    pub show_selected_bounding_volume: bool,
    /// Show vertex normal debug lines.
    pub show_normals: bool,
    /// Length of normal debug lines.
    pub normal_line_length: f32,
    /// Color for normal debug lines.
    pub normal_line_color: [f32; 4],
    /// Enable selection outline effect.
    pub selection_outline_enabled: bool,
    /// Selection outline color.
    pub selection_outline_color: [f32; 4],
    /// PBR debug visualization mode.
    pub pbr_debug_mode: PbrDebugMode,
    /// Show animated debug stripes on textures.
    pub texture_debug_stripes: bool,
    /// Animation speed for debug stripes.
    pub texture_debug_stripes_speed: f32,
    pub ssao_visualization: bool,
}

/// Editor/selection state used by tools that highlight entities. Held
/// as `world.resources.editor_selection`.
#[derive(Clone, Default)]
pub struct EditorSelection {
    /// Entity to highlight with bounding volume.
    pub bounding_volume_selected_entity: Option<freecs::Entity>,
    /// Additional entities to include in the selection outline. The
    /// outline pass renders these alongside `bounding_volume_selected_entity`
    /// and their descendants.
    pub selected_entities: Vec<freecs::Entity>,
    /// When set, frustum culling uses this camera's frustum instead of the active camera's.
    pub culling_camera_override: Option<freecs::Entity>,
}

/// The GPU class the renderer ended up on, mirrored from `wgpu::DeviceType`
/// so apps can pick quality presets without depending on wgpu directly.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GpuDeviceType {
    #[default]
    Other,
    IntegratedGpu,
    DiscreteGpu,
    VirtualGpu,
    Cpu,
}

/// The graphics backend the renderer is running on. `WebGl` is the constrained
/// browser fallback and is the strongest signal that a mobile-class budget is
/// appropriate.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GpuBackend {
    #[default]
    Other,
    Vulkan,
    Metal,
    Dx12,
    Gl,
    WebGpu,
    WebGl,
}

/// A snapshot of the adapter the renderer selected, published into
/// `RendererState` so game code can adapt quality and controls to the device.
#[derive(Debug, Clone, Default)]
pub struct GpuProfile {
    pub backend: GpuBackend,
    pub device_type: GpuDeviceType,
    pub name: String,
}

impl GpuProfile {
    /// True when the renderer fell back to the browser WebGL path, the clearest
    /// indication of a constrained mobile-class GPU budget.
    pub fn is_webgl(&self) -> bool {
        self.backend == GpuBackend::WebGl
    }

    /// True when the adapter is an integrated, virtual, or software device, the
    /// classes that benefit from a reduced render budget.
    pub fn is_low_power(&self) -> bool {
        matches!(
            self.device_type,
            GpuDeviceType::IntegratedGpu | GpuDeviceType::VirtualGpu | GpuDeviceType::Cpu
        )
    }
}

/// Runtime-only renderer bookkeeping that apps generally do not edit
/// directly. Held as `world.resources.renderer_state`.
#[derive(Clone)]
pub struct RendererState {
    /// Internal bookkeeping for the macOS frame-rate auto-default.
    pub auto_frame_rate_limit_baseline: Option<f32>,
    /// Enable GPU frustum culling.
    pub gpu_culling_enabled: bool,
    /// Enable GPU hi-z occlusion culling for the mesh pass.
    pub occlusion_culling_enabled: bool,
    /// Enable the GPU-driven batch table build (classification + combo table on
    /// the GPU). When false the CPU builds the batch tables.
    pub gpu_batching_enabled: bool,
    pub min_screen_pixel_size: f32,
    /// Minimum window size in logical pixels. `None` disables the floor.
    pub min_window_size: Option<(u32, u32)>,
    /// Use fullscreen mode.
    pub use_fullscreen: bool,
    /// Show the mouse cursor.
    pub show_cursor: bool,
    /// Current letterbox amount (0-1).
    pub letterbox_amount: f32,
    /// Target letterbox amount for animation.
    pub letterbox_target: f32,
    pub day_night: DayNightState,
    pub mesh_lod_chains: Vec<MeshLodChain>,
    /// View-local shading derived for the camera the renderer is currently
    /// executing the graph against. Renderer writes this at the start of
    /// each per-camera render. Passes read from this for view-local decisions.
    pub active_view: EffectiveShading,
    /// Monotonic version bumped by the renderer whenever
    /// `render_settings_signature` changes between frames.
    pub settings_version: u64,
    /// Controls whether the focused viewport overrides its
    /// `ViewportUpdateMode` and re-renders every frame.
    pub focus_policy: ViewportFocusPolicy,
    /// Frame-time budget that scales optional post-process sample counts.
    pub adaptive_sampling: AdaptiveSamplingState,
    /// Post-process effects pass settings. The `EffectsPass` reads this each frame.
    pub effects: EffectsState,
    /// The adapter the renderer selected, published once the renderer has a
    /// device. `None` until the first rendered frame.
    pub gpu_profile: Option<GpuProfile>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PerformanceTarget {
    #[default]
    Unbounded,
    Interactive,
    Balanced,
    Quality,
}

impl PerformanceTarget {
    pub fn target_frame_time_ms(self) -> Option<f32> {
        match self {
            PerformanceTarget::Unbounded => None,
            PerformanceTarget::Interactive => Some(1000.0 / 60.0),
            PerformanceTarget::Balanced => Some(1000.0 / 30.0),
            PerformanceTarget::Quality => Some(1000.0 / 15.0),
        }
    }
}

#[derive(Debug, Clone)]
pub struct AdaptiveSamplingState {
    pub target: PerformanceTarget,
    pub frame_time_rolling_ms: f32,
    pub frame_time_sample_count: u32,
    pub ssao_sample_scale: f32,
    pub ssgi_sample_scale: f32,
    pub ssr_step_scale: f32,
}

impl Default for AdaptiveSamplingState {
    fn default() -> Self {
        Self {
            target: PerformanceTarget::Unbounded,
            frame_time_rolling_ms: 16.67,
            frame_time_sample_count: 0,
            ssao_sample_scale: 1.0,
            ssgi_sample_scale: 1.0,
            ssr_step_scale: 1.0,
        }
    }
}

impl AdaptiveSamplingState {
    pub fn record_frame_time(&mut self, frame_time_ms: f32) {
        const WINDOW: u32 = 100;
        if self.frame_time_sample_count == 0 {
            self.frame_time_rolling_ms = frame_time_ms;
            self.frame_time_sample_count = 1;
        } else {
            let alpha = 1.0 / (self.frame_time_sample_count.min(WINDOW) as f32 + 1.0);
            self.frame_time_rolling_ms =
                self.frame_time_rolling_ms * (1.0 - alpha) + frame_time_ms * alpha;
            self.frame_time_sample_count = (self.frame_time_sample_count + 1).min(WINDOW);
        }

        let Some(target_ms) = self.target.target_frame_time_ms() else {
            self.ssao_sample_scale = 1.0;
            self.ssgi_sample_scale = 1.0;
            self.ssr_step_scale = 1.0;
            return;
        };

        let error = self.frame_time_rolling_ms - target_ms;
        let sensitivity = 0.002;
        let max_delta = 0.05;
        let adjustment = (error * sensitivity).clamp(-max_delta, max_delta);

        self.ssao_sample_scale = (self.ssao_sample_scale - adjustment).clamp(0.25, 1.0);
        self.ssgi_sample_scale = (self.ssgi_sample_scale - adjustment).clamp(0.25, 1.0);
        self.ssr_step_scale = (self.ssr_step_scale - adjustment).clamp(0.25, 1.0);
    }
}

/// Hash of every setting that materially affects what a rendered tile
/// looks like, spanning the render/debug/selection buckets. Fields that
/// only affect performance, window chrome, or day/night driver state are
/// excluded. The renderer compares this signature against the previous
/// frame's value and bumps `RendererState::settings_version` when they differ.
pub fn render_settings_signature(
    render: &RenderSettings,
    debug: &DebugDraw,
    selection: &EditorSelection,
) -> u64 {
    use std::hash::{Hash, Hasher};
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    selection
        .bounding_volume_selected_entity
        .map(|entity| (entity.id, entity.generation))
        .hash(&mut hasher);
    for entity in &selection.selected_entities {
        (entity.id, entity.generation).hash(&mut hasher);
    }
    debug.show_grid.hash(&mut hasher);
    debug.show_bounding_volumes.hash(&mut hasher);
    debug.show_normals.hash(&mut hasher);
    debug.normal_line_length.to_bits().hash(&mut hasher);
    for component in debug.normal_line_color {
        component.to_bits().hash(&mut hasher);
    }
    (render.atmosphere as u32).hash(&mut hasher);
    render.show_sky.hash(&mut hasher);
    render.render_layer_world_enabled.hash(&mut hasher);
    render.render_layer_overlay_enabled.hash(&mut hasher);
    render.render_world_to_swapchain.hash(&mut hasher);
    for component in render.clear_color {
        component.to_bits().hash(&mut hasher);
    }
    render.unlit_mode.hash(&mut hasher);
    debug.selection_outline_enabled.hash(&mut hasher);
    for component in debug.selection_outline_color {
        component.to_bits().hash(&mut hasher);
    }
    render.vertex_snap.is_some().hash(&mut hasher);
    if let Some(ref snap) = render.vertex_snap {
        for component in snap.resolution {
            component.to_bits().hash(&mut hasher);
        }
    }
    render.affine_texture_mapping.hash(&mut hasher);
    render.fog.is_some().hash(&mut hasher);
    if let Some(ref fog) = render.fog {
        for component in fog.color {
            component.to_bits().hash(&mut hasher);
        }
        fog.start.to_bits().hash(&mut hasher);
        fog.end.to_bits().hash(&mut hasher);
    }
    let cg = &render.color_grading;
    cg.exposure.to_bits().hash(&mut hasher);
    cg.exposure_compensation_ev.to_bits().hash(&mut hasher);
    cg.auto_exposure.hash(&mut hasher);
    cg.auto_exposure_target.to_bits().hash(&mut hasher);
    cg.auto_exposure_rate.to_bits().hash(&mut hasher);
    cg.auto_exposure_min_ev.to_bits().hash(&mut hasher);
    cg.auto_exposure_max_ev.to_bits().hash(&mut hasher);
    cg.gamma.to_bits().hash(&mut hasher);
    cg.saturation.to_bits().hash(&mut hasher);
    cg.brightness.to_bits().hash(&mut hasher);
    cg.contrast.to_bits().hash(&mut hasher);
    (cg.tonemap_algorithm as u32).hash(&mut hasher);
    render.bloom_enabled.hash(&mut hasher);
    render.bloom_intensity.to_bits().hash(&mut hasher);
    render.bloom_threshold.to_bits().hash(&mut hasher);
    render.bloom_knee.to_bits().hash(&mut hasher);
    render.bloom_filter_radius.to_bits().hash(&mut hasher);
    let dof = &render.depth_of_field;
    dof.enabled.hash(&mut hasher);
    dof.focus_distance.to_bits().hash(&mut hasher);
    dof.focus_range.to_bits().hash(&mut hasher);
    dof.max_blur_radius.to_bits().hash(&mut hasher);
    dof.bokeh_threshold.to_bits().hash(&mut hasher);
    dof.bokeh_intensity.to_bits().hash(&mut hasher);
    (dof.quality as u32).hash(&mut hasher);
    dof.visualize_coc.hash(&mut hasher);
    dof.tilt_shift_enabled.hash(&mut hasher);
    dof.tilt_shift_angle.to_bits().hash(&mut hasher);
    dof.tilt_shift_center.to_bits().hash(&mut hasher);
    dof.tilt_shift_blur_amount.to_bits().hash(&mut hasher);
    dof.visualize_tilt_shift.hash(&mut hasher);
    render.ssao_enabled.hash(&mut hasher);
    render.ssao_radius.to_bits().hash(&mut hasher);
    render.ssao_bias.to_bits().hash(&mut hasher);
    render.ssao_intensity.to_bits().hash(&mut hasher);
    render.ssao_sample_count.hash(&mut hasher);
    debug.ssao_visualization.hash(&mut hasher);
    render.ssao_blur_depth_threshold.to_bits().hash(&mut hasher);
    render.ssao_blur_normal_power.to_bits().hash(&mut hasher);
    render.ssgi_enabled.hash(&mut hasher);
    render.ssgi_radius.to_bits().hash(&mut hasher);
    render.ssgi_intensity.to_bits().hash(&mut hasher);
    render.ssgi_max_steps.hash(&mut hasher);
    render.ssr_enabled.hash(&mut hasher);
    render.ssr_max_steps.hash(&mut hasher);
    render.ssr_thickness.to_bits().hash(&mut hasher);
    render.ssr_max_distance.to_bits().hash(&mut hasher);
    render.ssr_stride.to_bits().hash(&mut hasher);
    render.ssr_fade_start.to_bits().hash(&mut hasher);
    render.ssr_fade_end.to_bits().hash(&mut hasher);
    render.ssr_intensity.to_bits().hash(&mut hasher);
    for component in render.ambient_light {
        component.to_bits().hash(&mut hasher);
    }
    debug.pbr_debug_mode.as_u32().hash(&mut hasher);
    debug.texture_debug_stripes.hash(&mut hasher);
    debug
        .texture_debug_stripes_speed
        .to_bits()
        .hash(&mut hasher);
    render.fxaa_enabled.hash(&mut hasher);
    render.render_scale.to_bits().hash(&mut hasher);
    render.ibl_blend_factor.to_bits().hash(&mut hasher);
    hasher.finish()
}

impl Default for RenderSettings {
    fn default() -> Self {
        Self {
            frame_rate_limit: None,
            atmosphere: Atmosphere::None,
            show_sky: true,
            render_layer_world_enabled: true,
            render_layer_overlay_enabled: true,
            render_world_to_swapchain: true,
            clear_color: [0.0, 0.0, 0.0, 1.0],
            ui_scale: None,
            unlit_mode: false,
            vertex_snap: None,
            affine_texture_mapping: false,
            material_anisotropy_filtering: 16,
            fog: None,
            color_grading: ColorGrading::default(),
            bloom_enabled: true,
            bloom_intensity: 0.5,
            bloom_threshold: 1.0,
            bloom_knee: 0.5,
            bloom_filter_radius: 0.005,
            depth_of_field: DepthOfField::default(),
            ssao_enabled: false,
            ssao_radius: 0.5,
            ssao_bias: 0.025,
            ssao_intensity: 1.0,
            ssao_sample_count: 64,
            ssao_blur_depth_threshold: 0.005,
            ssao_blur_normal_power: 8.0,
            ssgi_enabled: false,
            ssgi_radius: 2.0,
            ssgi_intensity: 1.0,
            ssgi_max_steps: 16,
            ssr_enabled: false,
            ssr_max_steps: 64,
            ssr_thickness: 0.3,
            ssr_max_distance: 50.0,
            ssr_stride: 1.0,
            ssr_fade_start: 0.8,
            ssr_fade_end: 1.0,
            ssr_intensity: 1.0,
            ambient_light: [0.1, 0.1, 0.1, 1.0],
            fxaa_enabled: true,
            vsync_enabled: true,
            render_scale: 1.0,
            ibl_blend_factor: 0.0,
        }
    }
}

impl Default for DebugDraw {
    fn default() -> Self {
        Self {
            show_grid: false,
            show_bounding_volumes: false,
            show_selected_bounding_volume: false,
            show_normals: false,
            normal_line_length: 0.1,
            normal_line_color: [0.0, 1.0, 0.0, 1.0],
            selection_outline_enabled: false,
            selection_outline_color: [1.0, 0.45, 0.0, 1.0],
            pbr_debug_mode: PbrDebugMode::None,
            texture_debug_stripes: false,
            texture_debug_stripes_speed: 100.0,
            ssao_visualization: false,
        }
    }
}

impl Default for RendererState {
    fn default() -> Self {
        Self {
            auto_frame_rate_limit_baseline: None,
            gpu_culling_enabled: true,
            occlusion_culling_enabled: true,
            gpu_batching_enabled: true,
            min_screen_pixel_size: 0.0,
            min_window_size: None,
            use_fullscreen: false,
            show_cursor: true,
            letterbox_amount: 0.0,
            letterbox_target: 0.0,
            day_night: DayNightState::default(),
            mesh_lod_chains: Vec::new(),
            active_view: EffectiveShading::default(),
            settings_version: 0,
            focus_policy: ViewportFocusPolicy::default(),
            adaptive_sampling: AdaptiveSamplingState::default(),
            effects: EffectsState::default(),
            gpu_profile: None,
        }
    }
}
