use crate::dtype::DType;

/// A single axis of a multi-dimensional array.
///
/// Dimensions are integer-indexed internally. Real-valued coordinate mapping
/// (e.g., lon/lat to cell index) is handled by a separate transform layer.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Dimension {
    /// Axis name (e.g., "x", "y", "time").
    pub name: String,
    /// Coordinate type for this axis.
    pub dtype: DType,
    /// Lower bound of the extent (inclusive).
    pub lo: i64,
    /// Upper bound of the extent (inclusive).
    pub hi: i64,
    /// Number of cells per chunk along this axis.
    pub chunk_size: u64,
}

impl Dimension {
    /// Creates a new dimension.
    ///
    /// # Panics
    ///
    /// Panics if `hi < lo` or `chunk_size == 0`.
    pub fn new(name: impl Into<String>, dtype: DType, lo: i64, hi: i64, chunk_size: u64) -> Self {
        assert!(hi >= lo, "dimension hi ({hi}) must be >= lo ({lo})");
        assert!(chunk_size > 0, "chunk_size must be > 0");
        Self {
            name: name.into(),
            dtype,
            lo,
            hi,
            chunk_size,
        }
    }

    /// Total number of cells along this dimension.
    pub fn extent(&self) -> u64 {
        (self.hi - self.lo + 1) as u64
    }

    /// Number of chunks along this dimension (ceiling division).
    pub fn num_chunks(&self) -> u64 {
        self.extent().div_ceil(self.chunk_size)
    }
}

impl std::fmt::Display for Dimension {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{} {} DIMENSION [{} : {} : {}]",
            self.name, self.dtype, self.lo, self.hi, self.chunk_size
        )
    }
}

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

    #[cfg(feature = "serde")]
    #[test]
    fn dimension_serde_roundtrip() {
        let dim = Dimension::new("x", DType::Float64, 0, 9999, 256);
        let json = serde_json::to_string(&dim).unwrap();
        let back: Dimension = serde_json::from_str(&json).unwrap();
        assert_eq!(back, dim);
    }

    #[test]
    fn dimension_extent_and_chunks() {
        let dim = Dimension::new("x", DType::Float64, 0, 9999, 256);
        assert_eq!(dim.extent(), 10000);
        // ceil(10000 / 256) = 40
        assert_eq!(dim.num_chunks(), 40);
    }

    #[test]
    fn dimension_single_chunk() {
        let dim = Dimension::new("band", DType::UInt8, 0, 2, 8);
        assert_eq!(dim.extent(), 3);
        assert_eq!(dim.num_chunks(), 1);
    }

    #[test]
    fn dimension_exact_fit() {
        let dim = Dimension::new("y", DType::Int32, 0, 255, 256);
        assert_eq!(dim.extent(), 256);
        assert_eq!(dim.num_chunks(), 1);
    }

    #[test]
    fn dimension_display() {
        let dim = Dimension::new(
            "time",
            DType::Timestamp(crate::dtype::TimeUnit::Second),
            0,
            1460,
            30,
        );
        assert_eq!(
            dim.to_string(),
            "time TIMESTAMP(SECOND) DIMENSION [0 : 1460 : 30]"
        );
    }

    #[test]
    #[should_panic(expected = "hi (0) must be >= lo (10)")]
    fn dimension_invalid_extent() {
        Dimension::new("bad", DType::Int32, 10, 0, 1);
    }

    #[test]
    #[should_panic(expected = "chunk_size must be > 0")]
    fn dimension_zero_chunk_size() {
        Dimension::new("bad", DType::Int32, 0, 10, 0);
    }
}
