// 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

//! Pure conversions from EPICS DBR structures to the backend-agnostic
//! signal types.  All EPICS constants and layouts stay in this module and
//! in `epics`/`capture`; nothing above them sees DBR types.

use crate::signal::{SignalData, SignalValue};
use epics_rs_sys as sys;
use std::ffi::{c_char, c_void};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

/// Map a channel's native DBF_* field type to the DBR_TIME_* transfer type
/// we subscribe with.  None for field types we do not support.
pub fn time_dbr_for_field(field_type: u32) -> Option<u32> {
    match field_type {
        sys::DBF_STRING => Some(sys::DBR_TIME_STRING),
        sys::DBF_SHORT => Some(sys::DBR_TIME_SHORT),
        sys::DBF_FLOAT => Some(sys::DBR_TIME_FLOAT),
        sys::DBF_ENUM => Some(sys::DBR_TIME_ENUM),
        sys::DBF_CHAR => Some(sys::DBR_TIME_CHAR),
        sys::DBF_LONG => Some(sys::DBR_TIME_LONG),
        sys::DBF_DOUBLE => Some(sys::DBR_TIME_DOUBLE),
        _ => None,
    }
}

pub fn is_enum_field(field_type: u32) -> bool {
    field_type == sys::DBF_ENUM
}

/// The display name of a native DBF_* field type (the supported set only).
pub fn field_type_name(field_type: u32) -> String {
    match field_type {
        sys::DBF_STRING => "DBF_STRING".to_string(),
        sys::DBF_SHORT => "DBF_SHORT".to_string(),
        sys::DBF_FLOAT => "DBF_FLOAT".to_string(),
        sys::DBF_ENUM => "DBF_ENUM".to_string(),
        sys::DBF_CHAR => "DBF_CHAR".to_string(),
        sys::DBF_LONG => "DBF_LONG".to_string(),
        sys::DBF_DOUBLE => "DBF_DOUBLE".to_string(),
        other => format!("DBF_({other})"),
    }
}

/// The display name of a DBR_TIME_* transfer type (the set we subscribe with).
pub fn request_type_name(dbr_type: u32) -> String {
    match dbr_type {
        sys::DBR_TIME_STRING => "DBR_TIME_STRING".to_string(),
        sys::DBR_TIME_SHORT => "DBR_TIME_SHORT".to_string(),
        sys::DBR_TIME_FLOAT => "DBR_TIME_FLOAT".to_string(),
        sys::DBR_TIME_ENUM => "DBR_TIME_ENUM".to_string(),
        sys::DBR_TIME_CHAR => "DBR_TIME_CHAR".to_string(),
        sys::DBR_TIME_LONG => "DBR_TIME_LONG".to_string(),
        sys::DBR_TIME_DOUBLE => "DBR_TIME_DOUBLE".to_string(),
        other => format!("DBR_({other})"),
    }
}

/// Convert an EPICS timestamp (seconds past 1990-01-01) to a SystemTime.
pub fn epics_to_system_time(stamp: &sys::epicsTimeStamp) -> SystemTime {
    let secs = u64::from(stamp.secPastEpoch) + u64::from(sys::POSIX_TIME_AT_EPICS_EPOCH);
    UNIX_EPOCH + Duration::new(secs, stamp.nsec)
}

/// Render an enum value as "<int val>, text label", or just the int when no
/// label is known for it.
pub fn format_enum(value: u16, labels: &[String]) -> String {
    match labels.get(usize::from(value)) {
        Some(label) if !label.is_empty() => format!("{value}, {label}"),
        _ => value.to_string(),
    }
}

/// A fixed-size EPICS string field: bytes up to the first NUL.
fn c_char_array_to_string(chars: &[c_char]) -> String {
    let bytes: Vec<u8> = chars
        .iter()
        .take_while(|&&c| c != 0)
        .map(|&c| c as u8)
        .collect();
    String::from_utf8_lossy(&bytes).into_owned()
}

/// Decode a DBR_TIME_* structure into a SignalValue.  Arrays are not
/// supported: only element 0 (the `value` field) is read.  Returns None for
/// transfer types we do not handle.
///
/// # Safety
/// `dbr` must point to a valid, fully-initialized DBR structure of the kind
/// named by `dbr_type`, as delivered by a channel access callback.
pub unsafe fn decode_time_dbr(
    dbr_type: u32,
    dbr: *const c_void,
    labels: &[String],
) -> Option<SignalValue> {
    if dbr.is_null() {
        return None;
    }
    let (data, stamp) = match dbr_type {
        sys::DBR_TIME_STRING => {
            // SAFETY: the caller guarantees `dbr` matches `dbr_type`.
            let p = unsafe { &*(dbr as *const sys::dbr_time_string) };
            (
                SignalData::String(c_char_array_to_string(&p.value)),
                p.stamp,
            )
        }
        sys::DBR_TIME_SHORT => {
            // SAFETY: the caller guarantees `dbr` matches `dbr_type`.
            let p = unsafe { &*(dbr as *const sys::dbr_time_short) };
            (SignalData::Int16(p.value), p.stamp)
        }
        sys::DBR_TIME_FLOAT => {
            // SAFETY: the caller guarantees `dbr` matches `dbr_type`.
            let p = unsafe { &*(dbr as *const sys::dbr_time_float) };
            (SignalData::Float32(p.value), p.stamp)
        }
        sys::DBR_TIME_ENUM => {
            // SAFETY: the caller guarantees `dbr` matches `dbr_type`.
            let p = unsafe { &*(dbr as *const sys::dbr_time_enum) };
            (
                SignalData::Enumeration(format_enum(p.value, labels)),
                p.stamp,
            )
        }
        sys::DBR_TIME_CHAR => {
            // SAFETY: the caller guarantees `dbr` matches `dbr_type`.
            let p = unsafe { &*(dbr as *const sys::dbr_time_char) };
            // dbr_char_t is epicsUInt8.
            (SignalData::UInt8(p.value), p.stamp)
        }
        sys::DBR_TIME_LONG => {
            // SAFETY: the caller guarantees `dbr` matches `dbr_type`.
            let p = unsafe { &*(dbr as *const sys::dbr_time_long) };
            (SignalData::Int32(p.value), p.stamp)
        }
        sys::DBR_TIME_DOUBLE => {
            // SAFETY: the caller guarantees `dbr` matches `dbr_type`.
            let p = unsafe { &*(dbr as *const sys::dbr_time_double) };
            (SignalData::Float64(p.value), p.stamp)
        }
        _ => return None,
    };
    Some(SignalValue {
        data: Some(data),
        time: epics_to_system_time(&stamp),
    })
}

/// Extract the label strings from a DBR_GR_ENUM structure.
///
/// # Safety
/// `dbr` must point to a valid dbr_gr_enum as delivered by a channel access
/// callback for a DBR_GR_ENUM get.
pub unsafe fn decode_gr_enum_labels(dbr: *const c_void) -> Vec<String> {
    if dbr.is_null() {
        return Vec::new();
    }
    // SAFETY: the caller guarantees `dbr` points to a dbr_gr_enum.
    let p = unsafe { &*(dbr as *const sys::dbr_gr_enum) };
    let count = usize::from(p.no_str.clamp(0, sys::MAX_ENUM_STATES as i16) as u16);
    p.strs[..count]
        .iter()
        .map(|s| c_char_array_to_string(s))
        .collect()
}

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

    const EPOCH_OFFSET: u64 = 631_152_000; // 1970 -> 1990 in seconds

    fn stamp(sec: u32, nsec: u32) -> sys::epicsTimeStamp {
        sys::epicsTimeStamp {
            secPastEpoch: sec,
            nsec,
        }
    }

    fn unix_time(secs: u64, nsec: u32) -> SystemTime {
        UNIX_EPOCH + Duration::new(secs, nsec)
    }

    fn fill_c_string(dest: &mut [c_char], s: &str) {
        for (d, b) in dest.iter_mut().zip(s.bytes()) {
            *d = b as c_char;
        }
    }

    #[test]
    fn timestamp_conversion() {
        // The EPICS epoch itself is 1990-01-01, i.e. 631152000 Unix.
        assert_eq!(
            epics_to_system_time(&stamp(0, 0)),
            unix_time(EPOCH_OFFSET, 0)
        );
        assert_eq!(
            epics_to_system_time(&stamp(1000, 42)),
            unix_time(EPOCH_OFFSET + 1000, 42)
        );
    }

    #[test]
    fn enum_formatting() {
        let labels = vec!["OFF".to_string(), "ON".to_string(), String::new()];
        assert_eq!(format_enum(0, &labels), "0, OFF");
        assert_eq!(format_enum(1, &labels), "1, ON");
        // Empty label and out-of-range fall back to the bare int.
        assert_eq!(format_enum(2, &labels), "2");
        assert_eq!(format_enum(7, &labels), "7");
        assert_eq!(format_enum(3, &[]), "3");
    }

    #[test]
    fn decode_double() {
        let dbr = sys::dbr_time_double {
            status: 0,
            severity: 0,
            stamp: stamp(1000, 500),
            RISC_pad: 0,
            value: 3.5,
        };
        // SAFETY: `dbr` is a properly initialized dbr_time_double.
        let v =
            unsafe { decode_time_dbr(sys::DBR_TIME_DOUBLE, &raw const dbr as *const c_void, &[]) }
                .expect("decodes");
        assert_eq!(v.data, Some(SignalData::Float64(3.5)));
        assert_eq!(v.time, unix_time(EPOCH_OFFSET + 1000, 500));
    }

    #[test]
    fn decode_enum_with_labels() {
        let dbr = sys::dbr_time_enum {
            status: 0,
            severity: 0,
            stamp: stamp(5, 0),
            RISC_pad: 0,
            value: 1,
        };
        let labels = vec!["OFF".to_string(), "ON".to_string()];
        // SAFETY: `dbr` is a properly initialized dbr_time_enum.
        let v = unsafe {
            decode_time_dbr(sys::DBR_TIME_ENUM, &raw const dbr as *const c_void, &labels)
        }
        .expect("decodes");
        assert_eq!(v.data, Some(SignalData::Enumeration("1, ON".to_string())));
    }

    #[test]
    fn decode_string() {
        let mut dbr = sys::dbr_time_string {
            status: 0,
            severity: 0,
            stamp: stamp(0, 0),
            value: [0; 60],
        };
        fill_c_string(&mut dbr.value, "hello");
        // SAFETY: `dbr` is a properly initialized dbr_time_string.
        let v =
            unsafe { decode_time_dbr(sys::DBR_TIME_STRING, &raw const dbr as *const c_void, &[]) }
                .expect("decodes");
        assert_eq!(v.data, Some(SignalData::String("hello".to_string())));
    }

    #[test]
    fn decode_rejects_unknown_and_null() {
        let dbr = sys::dbr_time_enum {
            status: 0,
            severity: 0,
            stamp: stamp(0, 0),
            RISC_pad: 0,
            value: 0,
        };
        // SAFETY: pointer is valid; the type is simply one we don't handle.
        let unknown =
            unsafe { decode_time_dbr(sys::DBR_GR_ENUM, &raw const dbr as *const c_void, &[]) };
        assert!(unknown.is_none());
        // SAFETY: a null pointer is checked before any dereference.
        let null = unsafe { decode_time_dbr(sys::DBR_TIME_DOUBLE, std::ptr::null(), &[]) };
        assert!(null.is_none());
    }

    #[test]
    fn gr_enum_labels() {
        let mut dbr = sys::dbr_gr_enum {
            status: 0,
            severity: 0,
            no_str: 2,
            strs: [[0; 26]; 16],
            value: 0,
        };
        fill_c_string(&mut dbr.strs[0], "OFF");
        fill_c_string(&mut dbr.strs[1], "ON");
        // SAFETY: `dbr` is a properly initialized dbr_gr_enum.
        let labels = unsafe { decode_gr_enum_labels(&raw const dbr as *const c_void) };
        assert_eq!(labels, vec!["OFF".to_string(), "ON".to_string()]);
        // SAFETY: a null pointer is checked before any dereference.
        assert!(unsafe { decode_gr_enum_labels(std::ptr::null()) }.is_empty());
    }

    #[test]
    fn field_type_mapping() {
        assert_eq!(
            time_dbr_for_field(sys::DBF_DOUBLE),
            Some(sys::DBR_TIME_DOUBLE)
        );
        assert_eq!(
            time_dbr_for_field(sys::DBF_STRING),
            Some(sys::DBR_TIME_STRING)
        );
        assert_eq!(time_dbr_for_field(sys::DBF_ENUM), Some(sys::DBR_TIME_ENUM));
        assert_eq!(time_dbr_for_field(999), None);
        assert!(is_enum_field(sys::DBF_ENUM));
        assert!(!is_enum_field(sys::DBF_LONG));
    }

    #[test]
    fn type_names_cover_the_supported_set() {
        assert_eq!(field_type_name(sys::DBF_DOUBLE), "DBF_DOUBLE");
        assert_eq!(field_type_name(sys::DBF_ENUM), "DBF_ENUM");
        assert_eq!(field_type_name(999), "DBF_(999)");
        assert_eq!(request_type_name(sys::DBR_TIME_DOUBLE), "DBR_TIME_DOUBLE");
        assert_eq!(request_type_name(sys::DBR_TIME_STRING), "DBR_TIME_STRING");
        assert_eq!(request_type_name(999), "DBR_(999)");
    }
}
