// Copyright 2026 California Institute of Technology
//
// You should have received a copy of the licensing terms for this
// software included in the file “LICENSE” located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE.txt

use std::fmt;
use std::time::SystemTime;

/// Backend-agnostic value of a signal.  No EPICS constants or types here.
///
/// EPICS channel access (v3) never delivers signed 8-bit, unsigned 16/32-bit,
/// or 64-bit integers, but other backends may, so the full set of variants is
/// kept.
#[derive(Debug, Clone, PartialEq)]
pub enum SignalData {
    Int8(i8),
    Int16(i16),
    Int32(i32),
    Int64(i64),
    UInt8(u8),
    UInt16(u16),
    UInt32(u32),
    UInt64(u64),
    Float32(f32),
    Float64(f64),
    String(String),
    /// Holds the display text "<int val>, text label".
    Enumeration(String),
}

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

impl SignalData {
    /// The value as a plottable number: numeric variants cast, `Enumeration`
    /// contributes its numeric id (the leading integer of its
    /// `"<int val>, label"` text), `String` has no numeric form.
    pub fn as_f64(&self) -> Option<f64> {
        match self {
            SignalData::Int8(v) => Some(f64::from(*v)),
            SignalData::Int16(v) => Some(f64::from(*v)),
            SignalData::Int32(v) => Some(f64::from(*v)),
            SignalData::Int64(v) => Some(*v as f64),
            SignalData::UInt8(v) => Some(f64::from(*v)),
            SignalData::UInt16(v) => Some(f64::from(*v)),
            SignalData::UInt32(v) => Some(f64::from(*v)),
            SignalData::UInt64(v) => Some(*v as f64),
            SignalData::Float32(v) => Some(f64::from(*v)),
            SignalData::Float64(v) => Some(*v),
            SignalData::String(_) => None,
            SignalData::Enumeration(text) => {
                text.split(',').next().and_then(|id| id.trim().parse().ok())
            }
        }
    }
}

/// The latest known state of a signal.  `data` is `None` while disconnected;
/// stale values are never kept.
#[derive(Debug, Clone)]
pub struct SignalValue {
    pub data: Option<SignalData>,
    /// Not displayed yet; kept for the future history/oscilloscope work.
    pub time: SystemTime,
}

/// Ordered key/value description of a signal as reported by the backend
/// (for EPICS: host, access, native type, request type, element count).
/// Ordered so the display never reshuffles, unlike a hashmap.  The
/// connection state is deliberately not included: the UI derives it from
/// the live value so the two can never disagree.
#[derive(Debug, Clone, PartialEq, Default)]
pub struct SignalInfo(pub Vec<(String, String)>);

/// A channel's slot in the shared store: the latest value plus the cached
/// backend description.  The description is written on every connect and
/// kept through disconnects.
#[derive(Debug, Clone)]
pub struct ChannelEntry {
    pub value: SignalValue,
    pub info: Option<SignalInfo>,
}

impl ChannelEntry {
    /// A fresh entry for a channel that has not delivered data yet.
    pub fn disconnected() -> Self {
        ChannelEntry {
            value: SignalValue {
                data: None,
                time: SystemTime::now(),
            },
            info: None,
        }
    }
}

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

    #[test]
    fn as_f64_covers_every_variant() {
        assert_eq!(SignalData::Int8(-1).as_f64(), Some(-1.0));
        assert_eq!(SignalData::Int16(-2).as_f64(), Some(-2.0));
        assert_eq!(SignalData::Int32(-3).as_f64(), Some(-3.0));
        assert_eq!(SignalData::Int64(-4).as_f64(), Some(-4.0));
        assert_eq!(SignalData::UInt8(1).as_f64(), Some(1.0));
        assert_eq!(SignalData::UInt16(2).as_f64(), Some(2.0));
        assert_eq!(SignalData::UInt32(3).as_f64(), Some(3.0));
        assert_eq!(SignalData::UInt64(4).as_f64(), Some(4.0));
        assert_eq!(SignalData::Float32(1.5).as_f64(), Some(1.5));
        assert_eq!(SignalData::Float64(2.5).as_f64(), Some(2.5));
        assert_eq!(SignalData::String("7".to_string()).as_f64(), None);
    }

    #[test]
    fn as_f64_parses_the_enum_numeric_id() {
        assert_eq!(
            SignalData::Enumeration("2, RAMPING".to_string()).as_f64(),
            Some(2.0)
        );
        // Out-of-range enums are formatted as just the int.
        assert_eq!(SignalData::Enumeration("7".to_string()).as_f64(), Some(7.0));
        assert_eq!(SignalData::Enumeration("junk".to_string()).as_f64(), None);
    }

    #[test]
    fn display_uses_default_formatting() {
        assert_eq!(SignalData::Int32(-42).to_string(), "-42");
        assert_eq!(SignalData::Float64(1.5).to_string(), "1.5");
        assert_eq!(SignalData::String("hi".to_string()).to_string(), "hi");
        assert_eq!(
            SignalData::Enumeration("1, ON".to_string()).to_string(),
            "1, ON"
        );
    }
}
