/// Time resolution for `DType::Timestamp`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TimeUnit {
    Second,
    Millisecond,
    Microsecond,
    Nanosecond,
}

/// A named, typed field — used in `DType::Struct` and as array attributes.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Field {
    pub name: String,
    pub dtype: DType,
}

/// Cell data types supported by arrDb.
///
/// This is our own enum (not Arrow's) so that `arrdb-core` stays dependency-free.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DType {
    Bool,
    Int8,
    Int16,
    Int32,
    Int64,
    UInt8,
    UInt16,
    UInt32,
    UInt64,
    Float32,
    Float64,
    Timestamp(TimeUnit),
    /// Composite cell type (e.g., RGB = Struct([r: UInt8, g: UInt8, b: UInt8])).
    Struct(Vec<Field>),
}

impl DType {
    /// Returns the size in bytes of a single scalar value of this type.
    ///
    /// Returns `None` for `Struct` (variable, depends on fields).
    pub fn byte_size(&self) -> Option<usize> {
        match self {
            DType::Bool => Some(1),
            DType::Int8 | DType::UInt8 => Some(1),
            DType::Int16 | DType::UInt16 => Some(2),
            DType::Int32 | DType::UInt32 | DType::Float32 => Some(4),
            DType::Int64 | DType::UInt64 | DType::Float64 => Some(8),
            DType::Timestamp(_) => Some(8), // i64 under the hood
            DType::Struct(fields) => {
                let mut total = 0usize;
                for f in fields {
                    total += f.dtype.byte_size()?;
                }
                Some(total)
            }
        }
    }

    /// Returns `true` if this is a numeric type (integer or float).
    pub fn is_numeric(&self) -> bool {
        matches!(
            self,
            DType::Int8
                | DType::Int16
                | DType::Int32
                | DType::Int64
                | DType::UInt8
                | DType::UInt16
                | DType::UInt32
                | DType::UInt64
                | DType::Float32
                | DType::Float64
        )
    }

    /// Returns `true` if this is a floating-point type.
    pub fn is_float(&self) -> bool {
        matches!(self, DType::Float32 | DType::Float64)
    }

    /// Returns `true` if this is an integer type (signed or unsigned).
    pub fn is_integer(&self) -> bool {
        matches!(
            self,
            DType::Int8
                | DType::Int16
                | DType::Int32
                | DType::Int64
                | DType::UInt8
                | DType::UInt16
                | DType::UInt32
                | DType::UInt64
        )
    }
}

impl std::fmt::Display for DType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DType::Bool => write!(f, "BOOL"),
            DType::Int8 => write!(f, "INT8"),
            DType::Int16 => write!(f, "INT16"),
            DType::Int32 => write!(f, "INT32"),
            DType::Int64 => write!(f, "INT64"),
            DType::UInt8 => write!(f, "UINT8"),
            DType::UInt16 => write!(f, "UINT16"),
            DType::UInt32 => write!(f, "UINT32"),
            DType::UInt64 => write!(f, "UINT64"),
            DType::Float32 => write!(f, "FLOAT32"),
            DType::Float64 => write!(f, "FLOAT64"),
            DType::Timestamp(unit) => write!(f, "TIMESTAMP({unit})"),
            DType::Struct(fields) => {
                write!(f, "STRUCT(")?;
                for (i, field) in fields.iter().enumerate() {
                    if i > 0 {
                        write!(f, ", ")?;
                    }
                    write!(f, "{}: {}", field.name, field.dtype)?;
                }
                write!(f, ")")
            }
        }
    }
}

impl std::fmt::Display for TimeUnit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TimeUnit::Second => write!(f, "SECOND"),
            TimeUnit::Millisecond => write!(f, "MILLISECOND"),
            TimeUnit::Microsecond => write!(f, "MICROSECOND"),
            TimeUnit::Nanosecond => write!(f, "NANOSECOND"),
        }
    }
}

/// A typed scalar value — used for literals, fill values, and expression constants.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ScalarValue {
    Bool(bool),
    Int8(i8),
    Int16(i16),
    Int32(i32),
    Int64(i64),
    UInt8(u8),
    UInt16(u16),
    UInt32(u32),
    UInt64(u64),
    Float32(f32),
    Float64(f64),
    Timestamp(TimeUnit, i64),
    Null(DType),
}

impl ScalarValue {
    /// Returns the [`DType`] of this scalar value.
    pub fn dtype(&self) -> DType {
        match self {
            ScalarValue::Bool(_) => DType::Bool,
            ScalarValue::Int8(_) => DType::Int8,
            ScalarValue::Int16(_) => DType::Int16,
            ScalarValue::Int32(_) => DType::Int32,
            ScalarValue::Int64(_) => DType::Int64,
            ScalarValue::UInt8(_) => DType::UInt8,
            ScalarValue::UInt16(_) => DType::UInt16,
            ScalarValue::UInt32(_) => DType::UInt32,
            ScalarValue::UInt64(_) => DType::UInt64,
            ScalarValue::Float32(_) => DType::Float32,
            ScalarValue::Float64(_) => DType::Float64,
            ScalarValue::Timestamp(unit, _) => DType::Timestamp(*unit),
            ScalarValue::Null(dt) => dt.clone(),
        }
    }
}

impl std::fmt::Display for ScalarValue {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ScalarValue::Bool(v) => write!(f, "{v}"),
            ScalarValue::Int8(v) => write!(f, "{v}"),
            ScalarValue::Int16(v) => write!(f, "{v}"),
            ScalarValue::Int32(v) => write!(f, "{v}"),
            ScalarValue::Int64(v) => write!(f, "{v}"),
            ScalarValue::UInt8(v) => write!(f, "{v}"),
            ScalarValue::UInt16(v) => write!(f, "{v}"),
            ScalarValue::UInt32(v) => write!(f, "{v}"),
            ScalarValue::UInt64(v) => write!(f, "{v}"),
            ScalarValue::Float32(v) => write!(f, "{v}"),
            ScalarValue::Float64(v) => write!(f, "{v}"),
            ScalarValue::Timestamp(_, v) => write!(f, "{v}"),
            ScalarValue::Null(_) => write!(f, "NULL"),
        }
    }
}

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

    #[cfg(feature = "serde")]
    mod serde_tests {
        use super::*;

        #[test]
        fn dtype_serde_roundtrip() {
            let types = vec![
                DType::Bool,
                DType::Int32,
                DType::Float64,
                DType::Timestamp(TimeUnit::Nanosecond),
                DType::Struct(vec![
                    Field {
                        name: "r".into(),
                        dtype: DType::UInt8,
                    },
                    Field {
                        name: "g".into(),
                        dtype: DType::UInt8,
                    },
                ]),
            ];
            for dt in &types {
                let json = serde_json::to_string(dt).unwrap();
                let back: DType = serde_json::from_str(&json).unwrap();
                assert_eq!(&back, dt);
            }
        }

        #[test]
        fn scalar_value_serde_roundtrip() {
            let values = vec![
                ScalarValue::Bool(true),
                ScalarValue::Int32(42),
                ScalarValue::Float64(3.125),
                ScalarValue::Null(DType::Float32),
                ScalarValue::Timestamp(TimeUnit::Second, 1000),
            ];
            for v in &values {
                let json = serde_json::to_string(v).unwrap();
                let back: ScalarValue = serde_json::from_str(&json).unwrap();
                assert_eq!(&back, v);
            }
        }
    }

    #[test]
    fn dtype_byte_sizes() {
        assert_eq!(DType::Bool.byte_size(), Some(1));
        assert_eq!(DType::Int8.byte_size(), Some(1));
        assert_eq!(DType::UInt16.byte_size(), Some(2));
        assert_eq!(DType::Float32.byte_size(), Some(4));
        assert_eq!(DType::Int64.byte_size(), Some(8));
        assert_eq!(DType::Float64.byte_size(), Some(8));
        assert_eq!(DType::Timestamp(TimeUnit::Nanosecond).byte_size(), Some(8));
    }

    #[test]
    fn struct_byte_size() {
        let rgb = DType::Struct(vec![
            Field {
                name: "r".into(),
                dtype: DType::UInt8,
            },
            Field {
                name: "g".into(),
                dtype: DType::UInt8,
            },
            Field {
                name: "b".into(),
                dtype: DType::UInt8,
            },
        ]);
        assert_eq!(rgb.byte_size(), Some(3));
    }

    #[test]
    fn dtype_classification() {
        assert!(DType::Float32.is_float());
        assert!(DType::Float64.is_float());
        assert!(!DType::Int32.is_float());

        assert!(DType::Int32.is_integer());
        assert!(DType::UInt64.is_integer());
        assert!(!DType::Float32.is_integer());

        assert!(DType::Float32.is_numeric());
        assert!(DType::Int16.is_numeric());
        assert!(!DType::Bool.is_numeric());
    }

    #[test]
    fn dtype_display() {
        assert_eq!(DType::Float32.to_string(), "FLOAT32");
        assert_eq!(
            DType::Timestamp(TimeUnit::Microsecond).to_string(),
            "TIMESTAMP(MICROSECOND)"
        );
    }

    #[test]
    fn scalar_value_dtype_roundtrip() {
        let v = ScalarValue::Float64(2.78);
        assert_eq!(v.dtype(), DType::Float64);

        let v = ScalarValue::Null(DType::Int32);
        assert_eq!(v.dtype(), DType::Int32);
    }
}
