use std::sync::LazyLock;

use serde::Deserialize;

use crate::{noise::simplex::OctaveSimplexNoiseSampler, random::legacy_rand::LegacyRand};

/// Global noise sampler for biome temperature variation.
pub static TEMPERATURE_NOISE: LazyLock<OctaveSimplexNoiseSampler> = LazyLock::new(|| {
    let mut rand = LegacyRand::from_seed(1234);
    OctaveSimplexNoiseSampler::new(&mut rand, &[0])
});

/// Global noise sampler for frozen ocean biome adjustments.
pub static FROZEN_OCEAN_NOISE: LazyLock<OctaveSimplexNoiseSampler> = LazyLock::new(|| {
    let mut rand = LegacyRand::from_seed(3456);
    OctaveSimplexNoiseSampler::new(&mut rand, &[-2, -1, 0])
});

/// Global noise sampler for foliage-based temperature adjustments.
pub static FOLIAGE_NOISE: LazyLock<OctaveSimplexNoiseSampler> = LazyLock::new(|| {
    let mut rand = LegacyRand::from_seed(2345);
    OctaveSimplexNoiseSampler::new(&mut rand, &[0])
});

/// Modifiers that adjust the base biome temperature.
#[derive(Clone, Deserialize, Copy, Hash, PartialEq, Eq, Debug)]
#[serde(rename_all = "UPPERCASE")]
pub enum TemperatureModifier {
    /// No temperature modification.
    None,
    /// Frozen biome adjustment, typically used for frozen oceans and snowy areas.
    Frozen,
}

impl TemperatureModifier {
    /// Applies the temperature modifier to a given position and base temperature.
    ///
    /// # Parameters
    /// - `x`: X coordinate in the world.
    /// - `z`: Z coordinate in the world.
    /// - `temperature`: The base biome temperature.
    ///
    /// # Returns
    /// - The modified temperature as an `f32`.
    pub fn convert_temperature(&self, x: f64, z: f64, temperature: f32) -> f32 {
        match self {
            Self::None => temperature,
            Self::Frozen => {
                let frozen_ocean_sample =
                    FROZEN_OCEAN_NOISE.sample(x * 0.05, z * 0.05, false) * 7.0;
                let foliage_sample = FOLIAGE_NOISE.sample(x * 0.2, z * 0.2, false);

                let threshold = frozen_ocean_sample + foliage_sample;
                if threshold < 0.3 {
                    let foliage_sample = FOLIAGE_NOISE.sample(x * 0.09, z * 0.09, false);
                    if foliage_sample < 0.8 {
                        return 0.2f32;
                    }
                }

                temperature
            }
        }
    }
}

/// Represents weather information for a biome, including temperature and precipitation.
#[derive(Clone, Debug)]
pub struct Weather {
    has_precipitation: bool,
    /// Base temperature of the biome.
    temperature: f32,
    /// Modifier affecting the base temperature.
    temperature_modifier: TemperatureModifier,
    /// Rate of rainfall or snowfall in the biome.
    #[expect(dead_code)]
    downfall: f32,
}

impl Weather {
    /// Creates a new `Weather` instance.
    ///
    /// # Parameters
    /// - `has_precipitation`: Whether the biome has precipitation.
    /// - `temperature`: Base temperature of the biome.
    /// - `temperature_modifier`: Modifier affecting the temperature.
    /// - `downfall`: Amount of rainfall or snowfall.
    #[must_use]
    pub const fn new(
        has_precipitation: bool,
        temperature: f32,
        temperature_modifier: TemperatureModifier,
        downfall: f32,
    ) -> Self {
        Self {
            has_precipitation,
            temperature,
            temperature_modifier,
            downfall,
        }
    }

    /// Returns the raw base temperature of the biome, without any height or modifier adjustment.
    #[must_use]
    pub const fn base_temperature(&self) -> f32 {
        self.temperature
    }

    /// Computes the effective temperature at a given position.
    ///
    /// # Parameters
    /// - `x`, `z`: World coordinates.
    /// - `y`: Y-level of the position.
    /// - `sea_level`: Sea level in the world.
    ///
    /// # Returns
    /// - The temperature at the position as `f32`.
    ///
    /// # Notes
    /// - This function is computationally expensive and should be cached.
    /// - Temperature is influenced by `TemperatureModifier` and noise samplers.
    pub fn compute_temperature(&self, x: f64, y: i32, z: f64, sea_level: i32) -> f32 {
        let modified_temperature =
            self.temperature_modifier
                .convert_temperature(x, z, self.temperature);
        let offset_sea_level = sea_level + 17;

        if y > offset_sea_level {
            let temperature_noise =
                (TEMPERATURE_NOISE.sample(x / 8.0, z / 8.0, false) * 8.0) as f32;

            modified_temperature
                - (temperature_noise + y as f32 - offset_sea_level as f32) * 0.05f32 / 40.0f32
        } else {
            modified_temperature
        }
    }

    #[must_use]
    pub fn warm_enough_to_rain(&self, x: i32, y: i32, z: i32, sea_level: i32) -> bool {
        self.compute_temperature(f64::from(x), y, f64::from(z), sea_level) >= 0.15
    }

    #[must_use]
    pub fn is_rain_at(&self, x: i32, y: i32, z: i32, sea_level: i32) -> bool {
        self.has_precipitation && self.warm_enough_to_rain(x, y, z, sea_level)
    }
}

#[cfg(test)]
mod tests {
    use super::{TemperatureModifier, Weather};

    #[test]
    fn precipitation_controls_rain() {
        let weather = Weather::new(false, 1.0, TemperatureModifier::None, 0.0);
        assert!(!weather.is_rain_at(0, 64, 0, 63));
    }

    #[test]
    fn warm_precipitation_is_rain() {
        let weather = Weather::new(true, 1.0, TemperatureModifier::None, 0.0);
        assert!(weather.is_rain_at(0, 64, 0, 63));
    }

    #[test]
    fn cold_precipitation_is_snow() {
        let weather = Weather::new(true, 0.0, TemperatureModifier::None, 0.0);
        assert!(!weather.is_rain_at(0, 64, 0, 63));
    }
}
