// SPDX-License-Identifier: Apache-2.0
// Copyright © 2025 Au-Zone Technologies. All Rights Reserved.

use crate::{
    builtin_interfaces::{Duration, Time},
    std_msgs::{self, Header},
};
use serde_derive::{Deserialize, Serialize};

/// The DmaBuffer message is used to transfer a DMA buffer file descriptor between
/// processes. It is mainly used to transfer the camera image data from the
/// camera driver to the image processing nodes.
///
/// The DmaBuffer message works by publishing the service's pid and the file
/// descriptor of the DMA buffer.  The receiving node can then use the file
/// descriptor to map the DMA buffer into its own memory space using the
/// [pidfd_open(2)](https://man7.org/linux/man-pages/man2/pidfd_open.2.html)
/// system call on Linux.
///
/// The camera frame widht, height, stride, and fourcc code are included in the
/// message to allow the receiving node interpret the image data.  The length
/// field is the size of the DMA buffer in bytes and used to
/// [mmap(3p)](https://man7.org/linux/man-pages/man3/mmap.3p.html) the buffer.
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct DmaBuffer {
    /// Message header containing the timestamp and frame id.
    pub header: std_msgs::Header,
    /// The process id of the service that created the DMA buffer.
    pub pid: u32,
    /// The file descriptor of the DMA buffer.  Used together with the pid to
    /// retrieve the dmabuf into the receiving node's execution context.
    pub fd: i32,
    /// The width of the image in pixels.
    pub width: u32,
    /// The height of the image in pixels.
    pub height: u32,
    /// The stride of the image in bytes.  The stride is used to separate the
    /// rows of the image.  It is typially the same as the width in, accounting
    /// for the pixel size, but may be larger for alignment or cropping reasons.
    pub stride: u32,
    /// The fourcc code of the image.  The fourcc code is a 32-bit integer that
    /// represents the image format.  It is typically a four character code
    /// such as 'YUYV' or 'RGBA'.
    pub fourcc: u32,
    /// The length of the DMA buffer in bytes, used to mmap the buffer.  It can
    /// be larger than the actual image data size.
    pub length: u32,
}

/// The RadarCube interface carries various radar cube reprensentations of the
/// Radar FFT before generally being processed by CFAR into a point cloud.  The
/// cube could provide R, RD, RAD, RA, and so on where R=Range, D=Dopper,
/// and A=Azimuth.  Beyond the common cube dimensions, the cube may also include
/// sequence (chirp), rxchannels, elevation, or undefined dimensions.
///
/// The scales vector is used to convert the cube bins to physical units.  This
/// is used for the range, doppler, azimuth, and elevation dimensions.  For
/// example a range scaling value of 2.5 would mean that each bin in the range
/// dimension is 2.5 meters, so a signal in the 10th bin would be 25 meters away.
///
/// The cube is represented as a 3D array of signed 16-bit integers.  The cube
/// will generally use complex numbers to represent the radar data, the field
/// is_complex is used to indicate if the cube is complex in which case each
/// element is a complex number with the real part followed by the imaginary.
///
/// The header's frame_id field stores the frame id generated by the radar
/// module.  When capturing both the radar cube and point cloud from the module
/// the frame id can be used to correlate the two messages.
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct RadarCube {
    /// Message header containing the timestamp and frame id.
    pub header: std_msgs::Header,
    /// The timestamp of the radar cube data as generated by the radar module.
    pub timestamp: u64,
    /// The layout of the radar cube.  The layout is a vector of dimension labels
    /// that describe the order of the dimensions in the cube.  The labels are
    /// defined in the radar_cube_dimension module.
    pub layout: Vec<u8>,
    /// The shape of the radar cube.
    pub shape: Vec<u16>,
    /// The scales vector is used to convert the cube bins to physical units.
    pub scales: Vec<f32>,
    /// The radar cube data.  The cube is represented as a 3D array of signed
    /// 16-bit integers.  If the cube is complex as indicated by the is_complex
    /// field, then the elements should be interpred as pairs of real and
    /// imaginary values.
    pub cube: Vec<i16>,
    /// The cube uses complex numbers if true.
    pub is_complex: bool,
}

/// Dimensional labels are used to describe the radar cube layout. Not all cubes
/// include every label.  Undefined is used for dimensions not covered by this
/// list.  For example the Raivin radar cube is in the sequence, range,
/// rxchannel, and doppler dimensions.  These labels can be used so applications
/// can interpret the radar cube data genericly and portably between different
/// radar modules.
pub mod radar_cube_dimension {
    pub const UNDEFINED: u8 = 0;
    pub const RANGE: u8 = 1;
    pub const DOPPLER: u8 = 2;
    pub const AZIMUTH: u8 = 3;
    pub const ELEVATION: u8 = 4;
    pub const RXCHANNEL: u8 = 5;
    pub const SEQUENCE: u8 = 6;
}

/// This interface carries Radar Information related to the device configuration.
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct RadarInfo {
    /// Message header containing the timestamp and frame id.
    pub header: std_msgs::Header,
    /// Radar center frequency band.
    pub center_frequency: String,
    /// The frequency sweep controls the detection range of the radar.
    pub frequency_sweep: String,
    /// The range-toggle mode allows the radar to alternate between various
    /// frequency sweep configurations.  Applications must handle range
    /// toggling as targets will not be consistent between messages as the
    /// frequency alternates between sweeps.
    pub range_toggle: String,
    /// The detection sensitivity controls the sensitivity to recognize a target.
    pub detection_sensitivity: String,
    /// True if the radar is configured to output radar cubes.
    pub cube: bool,
}

#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct Detect {
    pub header: std_msgs::Header,
    pub input_timestamp: Time,
    pub model_time: Time,
    pub output_time: Time,
    pub boxes: Vec<Box>,
}

#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct Box {
    pub center_x: f32,
    pub center_y: f32,
    pub width: f32,
    pub height: f32,
    pub label: String,
    pub score: f32,
    pub distance: f32,
    pub speed: f32,
    pub track: Track,
}

#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct Track {
    pub id: String,
    pub lifetime: i32,
    pub created: Time,
}

#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct Mask {
    /// The height of the mask, 0 if this dimension is unused.
    pub height: u32,
    /// The width of the mask, 0 if this dimension is unused.
    pub width: u32,
    /// The length of the mask, 0 if this dimension is unused.  The length would
    /// be used in 3D masks to represent the depth.  It could also be used for 2D
    /// bird's eye view masks along with width instead of height (elevation).
    pub length: u32,
    /// The optional encoding for the mask ("" for no compression, or "zstd" for zstd compression).
    pub encoding: String,
    /// The segmentation mask data.  The array should be reshaped according to the
    /// height, width, and length dimensions.  The dimension order is row-major.
    pub mask: Vec<u8>,
    /// If the mask is to be matched with a box for instance segmentation
    pub boxed: bool,
}

#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct Model {
    /// Message header containing the timestamp and frame id.
    pub header: Header,
    /// Duration to load inputs into the model.
    pub input_time: Duration,
    /// Duration to run the model, not including input/output/decoding.
    pub model_time: Duration,
    /// Duration to read the outputs from the model.
    pub output_time: Duration,
    /// Duration to decode the outputs from the model, including nms and tracking.
    pub decode_time: Duration,
    /// Box detections from the model.  Empty if none detected or if model does
    /// not support detection.
    pub boxes: Vec<Box>,
    /// Segmentation masks from the model.  Empty array if model does not generate
    /// masks.  Generally models will only generate a single mask if they do.
    pub masks: Vec<Mask>,
}

pub mod model_info {
    pub const RAW: u8 = 0;
    pub const INT8: u8 = 1;
    pub const UINT8: u8 = 2;
    pub const INT16: u8 = 3;
    pub const UINT16: u8 = 4;
    pub const FLOAT16: u8 = 5;
    pub const INT32: u8 = 6;
    pub const UINT32: u8 = 7;
    pub const FLOAT32: u8 = 8;
    pub const INT64: u8 = 9;
    pub const UINT64: u8 = 10;
    pub const FLOAT64: u8 = 11;
    pub const STRING: u8 = 12;
}

#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct ModelInfo {
    pub header: Header,
    pub input_shape: Vec<u32>,
    pub input_type: u8,
    pub output_shape: Vec<u32>,
    pub output_type: u8,
    pub labels: Vec<String>,
    pub model_type: String,
    pub model_format: String,
    pub model_name: String,
}

/// The local time interface publishes the current time on the device.  It is
/// mainly intended to allow synchronization of multiple MCAP files by the
/// EdgeFirst Publisher.  The idea is to calculate the offset from the timestamp
/// in the header with the actual local time, then when multiple MCAP files have
/// the local time topic recorded the relative offsets can then be calculated.
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct LocalTime {
    /// Message header containing the timestamp and frame id.
    pub header: Header,
    /// The base date from which the local time is calculated.  This could be an
    /// epoch such as the standard UNIX 1 January 1970 or it could be the current
    /// date.  To calculate the real local time both the date, time, and timezone
    /// are combined into a valid date and time.
    pub date: Date,
    /// The time offset from the date.  If the date is the current day then the
    /// time is the normal time of day.  If the date is an epoch than many days
    /// will be represented in the time.
    pub time: Time,
    /// The timezone offset in minutes from UTC of the time value.  The timezone
    /// would be +/- 720 (+/- 12 hours).  Minutes are used to allow for partial
    /// offsets such as Newfoundland in Canada which is UTC-210 (UTC-3h30).
    pub timezone: i16,
}

#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct Date {
    pub year: u16,
    pub month: u8,
    pub day: u8,
}

/// Check if a type name is supported by this module.
pub fn is_type_supported(type_name: &str) -> bool {
    matches!(
        type_name,
        "Box"
            | "Date"
            | "Detect"
            | "DmaBuffer"
            | "LocalTime"
            | "Mask"
            | "Model"
            | "ModelInfo"
            | "RadarCube"
            | "RadarInfo"
            | "Track"
    )
}

/// List all type schema names in this module.
pub fn list_types() -> &'static [&'static str] {
    &[
        "edgefirst_msgs/msg/Box",
        "edgefirst_msgs/msg/Date",
        "edgefirst_msgs/msg/Detect",
        "edgefirst_msgs/msg/DmaBuffer",
        "edgefirst_msgs/msg/LocalTime",
        "edgefirst_msgs/msg/Mask",
        "edgefirst_msgs/msg/Model",
        "edgefirst_msgs/msg/ModelInfo",
        "edgefirst_msgs/msg/RadarCube",
        "edgefirst_msgs/msg/RadarInfo",
        "edgefirst_msgs/msg/Track",
    ]
}

// SchemaType implementations
use crate::schema_registry::SchemaType;

impl SchemaType for Box {
    const SCHEMA_NAME: &'static str = "edgefirst_msgs/msg/Box";
}

impl SchemaType for Date {
    const SCHEMA_NAME: &'static str = "edgefirst_msgs/msg/Date";
}

impl SchemaType for Detect {
    const SCHEMA_NAME: &'static str = "edgefirst_msgs/msg/Detect";
}

impl SchemaType for DmaBuffer {
    const SCHEMA_NAME: &'static str = "edgefirst_msgs/msg/DmaBuffer";
}

impl SchemaType for LocalTime {
    const SCHEMA_NAME: &'static str = "edgefirst_msgs/msg/LocalTime";
}

impl SchemaType for Mask {
    const SCHEMA_NAME: &'static str = "edgefirst_msgs/msg/Mask";
}

impl SchemaType for Model {
    const SCHEMA_NAME: &'static str = "edgefirst_msgs/msg/Model";
}

impl SchemaType for ModelInfo {
    const SCHEMA_NAME: &'static str = "edgefirst_msgs/msg/ModelInfo";
}

impl SchemaType for RadarCube {
    const SCHEMA_NAME: &'static str = "edgefirst_msgs/msg/RadarCube";
}

impl SchemaType for RadarInfo {
    const SCHEMA_NAME: &'static str = "edgefirst_msgs/msg/RadarInfo";
}

impl SchemaType for Track {
    const SCHEMA_NAME: &'static str = "edgefirst_msgs/msg/Track";
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::builtin_interfaces::Time;
    use crate::serde_cdr::{deserialize, serialize};

    #[test]
    fn detect_roundtrip() {
        // Empty detections (common case)
        let empty = Detect {
            header: Header {
                stamp: Time::new(0, 0),
                frame_id: String::new(),
            },
            input_timestamp: Time::new(0, 0),
            model_time: Time::new(0, 0),
            output_time: Time::new(0, 0),
            boxes: vec![],
        };
        let bytes = serialize(&empty).unwrap();
        assert_eq!(empty, deserialize::<Detect>(&bytes).unwrap());

        // With detections
        let detect = Detect {
            header: Header {
                stamp: Time::new(100, 500_000_000),
                frame_id: "camera".to_string(),
            },
            input_timestamp: Time::new(100, 400_000_000),
            model_time: Time::new(0, 50_000_000),
            output_time: Time::new(100, 500_000_000),
            boxes: vec![Box {
                center_x: 0.5,
                center_y: 0.5,
                width: 0.1,
                height: 0.2,
                label: "car".to_string(),
                score: 0.98,
                distance: 10.0,
                speed: 5.0,
                track: Track {
                    id: "t1".to_string(),
                    lifetime: 5,
                    created: Time::new(95, 0),
                },
            }],
        };
        let bytes = serialize(&detect).unwrap();
        assert_eq!(detect, deserialize::<Detect>(&bytes).unwrap());
    }

    #[test]
    fn mask_roundtrip() {
        // Uncompressed mask
        let mask = Mask {
            height: 480,
            width: 640,
            length: 0,
            encoding: String::new(),
            mask: vec![0u8; 480 * 640],
            boxed: false,
        };
        let bytes = serialize(&mask).unwrap();
        assert_eq!(mask, deserialize::<Mask>(&bytes).unwrap());

        // Compressed mask
        let compressed = Mask {
            height: 1080,
            width: 1920,
            length: 5,
            encoding: "zstd".to_string(),
            mask: vec![1, 2, 3, 4, 5],
            boxed: true,
        };
        let bytes = serialize(&compressed).unwrap();
        assert_eq!(compressed, deserialize::<Mask>(&bytes).unwrap());
    }

    #[test]
    fn dmabuf_roundtrip() {
        let dmabuf = DmaBuffer {
            header: Header {
                stamp: Time::new(100, 0),
                frame_id: "camera".to_string(),
            },
            pid: 12345,
            height: 1080,
            width: 1920,
            stride: 1920 * 3,
            fourcc: 0x34325247, // RG24
            fd: 42,
            length: 1920 * 1080 * 3,
        };
        let bytes = serialize(&dmabuf).unwrap();
        assert_eq!(dmabuf, deserialize::<DmaBuffer>(&bytes).unwrap());
    }

    #[test]
    fn model_info_roundtrip() {
        let model = ModelInfo {
            header: Header {
                stamp: Time::new(0, 0),
                frame_id: String::new(),
            },
            input_shape: vec![1, 3, 640, 640],
            input_type: 8, // FLOAT32
            output_shape: vec![1, 25200, 85],
            output_type: 8, // FLOAT32
            labels: vec!["person".to_string(), "car".to_string()],
            model_type: "yolov8".to_string(),
            model_format: "onnx".to_string(),
            model_name: "yolov8n".to_string(),
        };
        let bytes = serialize(&model).unwrap();
        assert_eq!(model, deserialize::<ModelInfo>(&bytes).unwrap());
    }

    #[test]
    fn radar_cube_roundtrip() {
        // Small real-valued cube
        let cube = RadarCube {
            header: Header {
                stamp: Time::new(1234567890, 123456789),
                frame_id: "radar".to_string(),
            },
            timestamp: 1234567890123456,
            layout: vec![6, 1, 5, 2], // SEQUENCE, RANGE, RXCHANNEL, DOPPLER
            shape: vec![16, 256, 4, 64],
            scales: vec![1.0, 2.5, 1.0, 0.5],
            cube: (0..16 * 256 * 4 * 64).map(|i| i as i16).collect(),
            is_complex: false,
        };
        let bytes = serialize(&cube).unwrap();
        assert_eq!(cube, deserialize::<RadarCube>(&bytes).unwrap());

        // Complex cube
        let complex_cube = RadarCube {
            header: Header {
                stamp: Time::new(0, 0),
                frame_id: String::new(),
            },
            timestamp: 0,
            layout: vec![1, 2], // RANGE, DOPPLER
            shape: vec![64, 32],
            scales: vec![1.0, 0.1],
            cube: vec![100, 200, -100, -200],
            is_complex: true,
        };
        let bytes = serialize(&complex_cube).unwrap();
        assert_eq!(complex_cube, deserialize::<RadarCube>(&bytes).unwrap());
    }

    #[test]
    fn local_time_roundtrip() {
        let lt = LocalTime {
            header: Header {
                stamp: Time::new(0, 0),
                frame_id: "clock".to_string(),
            },
            date: Date {
                year: 2025,
                month: 1,
                day: 27,
            },
            time: Time::new(43200, 0), // noon
            timezone: -300,            // EST (UTC-5)
        };
        let bytes = serialize(&lt).unwrap();
        assert_eq!(lt, deserialize::<LocalTime>(&bytes).unwrap());
    }
}
