hdds 1.1.1

High-performance DDS (Data Distribution Service) implementation in pure Rust
// SPDX-License-Identifier: Apache-2.0 OR MIT
// Copyright (c) 2025-2026 naskel.com

// Build script for HDDS
//
// Generates Rust types with CDR2 traits from IDL definitions.
//
// Pipeline: IDL -> Parser -> Codegen -> src/generated/
//
// Phase 7a MVP: Hardcoded Temperature struct for validation.
// Phase 8: Full IDL parser integration via hdds-gen.
//
// This build script generates the Temperature struct used by internal tests.
// Users generate their own types via: hdds_gen rust Temperature.idl -o temperature.rs
// and the generated code will import `use hdds::*` (correct dependency flow).
//
// Output Structure:
//   src/generated/
//   +-- mod.rs           (auto-generated aggregator)
//   +-- temperature.rs   (Temperature struct with CDR2 traits)
//
// Naming Convention:
//   snake_case IDL filename -> module name (temperature.idl -> temperature.rs)

use std::fs;
use std::io::Write;
use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let out_dir = std::env::var("OUT_DIR")?;
    let generated_dir = Path::new(&out_dir).join("generated");

    // Create generated directory in OUT_DIR (cargo publish safe)
    fs::create_dir_all(&generated_dir)?;

    // Phase 7a MVP: Generate hardcoded Temperature struct
    // Phase 8: Parse idl/*.idl files and invoke hdds-gen backend
    generate_temperature_struct(&generated_dir)?;

    // Generate mod.rs aggregator
    generate_mod_rs(&generated_dir)?;

    // Ensure typed cross-lang test stub exists so --all-targets compiles
    // Real file is generated by: scripts/test-sdk-typed.sh (via hdds_gen)
    let typed_test_dir =
        Path::new(&std::env::var("CARGO_MANIFEST_DIR")?).join("../../target/typed-test");
    let typed_test_file = typed_test_dir.join("interop_types.rs");
    if !typed_test_file.exists() {
        fs::create_dir_all(&typed_test_dir)?;
        fs::write(&typed_test_file, generate_interop_stub())?;
    }
    println!("cargo:rerun-if-changed=../../target/typed-test/interop_types.rs");

    // Rebuild if build script changes
    println!("cargo:rerun-if-changed=build.rs");

    // Phase 8: Uncomment when IDL files are added
    // println!("cargo:rerun-if-changed=idl/");

    Ok(())
}

/// Generate a stub for `interop_types.rs` so `--all-targets` compiles
/// even when `hdds_gen` hasn't been run yet.
/// Real file is created by: `scripts/test-sdk-typed.sh`
const fn generate_interop_stub() -> &'static str {
    "\
// Auto-generated stub -- real types created by scripts/test-sdk-typed.sh
// This stub allows cargo check/clippy --all-targets to pass.
// Do NOT edit -- it will be overwritten by hdds_gen.

use hdds::core::ser::{Cdr2Encode, Cdr2Decode, CdrError};

#[derive(Debug, Clone, PartialEq)]
#[repr(u32)]
pub enum SensorKind { TEMPERATURE = 0, PRESSURE = 1, HUMIDITY = 2 }

#[derive(Debug, Clone, PartialEq)]
pub struct GeoPoint { pub latitude: f64, pub longitude: f64 }

#[derive(Debug, Clone, PartialEq)]
pub struct SensorReading {
    pub sensor_id: u32,
    pub kind: SensorKind,
    pub value: f32,
    pub label: String,
    pub timestamp_ns: i64,
    pub history: Vec<f32>,
    pub error_code: Option<i32>,
    pub location: GeoPoint,
}

impl Cdr2Encode for SensorReading {
    fn encode_cdr2_le(&self, _buf: &mut [u8]) -> Result<usize, CdrError> {
        panic!(\"stub: run scripts/test-sdk-typed.sh to generate real interop types\")
    }
    fn max_cdr2_size(&self) -> usize { 0 }
}

impl Cdr2Decode for SensorReading {
    fn decode_cdr2_le(_buf: &[u8]) -> Result<(Self, usize), CdrError> {
        panic!(\"stub: run scripts/test-sdk-typed.sh to generate real interop types\")
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct KeyedSample {
    pub id: u32,
    pub active: bool,
    pub kind: SensorKind,
    pub name: String,
    pub origin: GeoPoint,
    pub reading: f32,
}

impl Cdr2Encode for KeyedSample {
    fn encode_cdr2_le(&self, _buf: &mut [u8]) -> Result<usize, CdrError> {
        panic!(\"stub: run scripts/test-sdk-typed.sh to generate real interop types\")
    }
    fn max_cdr2_size(&self) -> usize { 0 }
}

impl Cdr2Decode for KeyedSample {
    fn decode_cdr2_le(_buf: &[u8]) -> Result<(Self, usize), CdrError> {
        panic!(\"stub: run scripts/test-sdk-typed.sh to generate real interop types\")
    }
}
"
}

/// Generate Temperature struct with CDR2 traits (Phase 7a hardcoded MVP)
///
/// Implements:
/// - Struct definition: `struct Temperature { value: f32, timestamp: u32 }`
/// - `Cdr2Encode` trait with alignment and bounds checking
/// - `Cdr2Decode` trait with UTF-8 validation
/// - DDS marker trait
///
/// # Wire Format
///
/// ```text
/// Offset | Field     | Type | Alignment | Size
/// -------|-----------|------|-----------|-----
/// 0      | value     | f32  | 4-byte    | 4
/// 4      | timestamp | u32  | 4-byte    | 4
/// -------|-----------|------|-----------|-----
/// Total: 8 bytes (no padding needed, both fields align=4)
/// ```
#[allow(clippy::too_many_lines, clippy::uninlined_format_args)]
fn generate_temperature_struct(output_dir: &Path) -> std::io::Result<()> {
    let output_path = output_dir.join("temperature.rs");

    // Compute type_id using FNV-1a hash (same algorithm as hdds-codegen)
    let type_name = "Temperature";
    let type_id = compute_fnv1a_hash(type_name);

    // Generate full Temperature module with type_descriptor() impl
    let code = format!("// SPDX-License-Identifier: Apache-2.0 OR MIT
// Copyright (c) 2025-2026 naskel.com
//
// Generated by build.rs (Phase 7a MVP + Task 1.2)
// DO NOT EDIT - Changes will be overwritten on next build

use hdds::core::ser::{{Cdr2Encode, Cdr2Decode, cursor::{{Cursor, CursorMut}}}};

/// Temperature sensor data (Phase 7a test case)
///
/// # Fields
///
/// - `value`: Temperature in degrees Celsius (f32)
/// - `timestamp`: Unix epoch seconds (i32, matches RTI IDL 'long')
///
/// # CDR2 Encoding
///
/// Wire format (8 bytes total, little-endian):
/// ```text
/// [f32 value][u32 timestamp]
/// ```
///
/// Both fields are 4-byte aligned, no padding required.
#[derive(Debug, Clone, PartialEq)]
pub struct Temperature {{
    /// Temperature value in degrees Celsius
    pub value: f32,
    /// Unix epoch timestamp in seconds (i32 to match RTI IDL 'long')
    pub timestamp: i32,
}}

// === DDS trait impl (Task 1.2 - full implementation without proc-macro) ===
impl ::hdds::dds::DDS for Temperature {{
    fn type_descriptor() -> &'static ::hdds::core::types::TypeDescriptor {{
        static DESC: ::hdds::core::types::TypeDescriptor = ::hdds::core::types::TypeDescriptor {{
            type_id: {:#010X},
            type_name: \"Temperature\",  // v168: Simple name to match RTI IDL (no module)
            size_bytes: 8,
            alignment: 4,
            is_variable_size: false,
            fields: &[
                ::hdds::core::types::FieldLayout {{
                    name: \"value\",
                    offset_bytes: 0,
                    field_type: ::hdds::core::types::FieldType::Primitive(::hdds::core::types::PrimitiveKind::F32),
                    alignment: 4,
                    size_bytes: 4,
                    element_type: None,
                }},
                ::hdds::core::types::FieldLayout {{
                    name: \"timestamp\",
                    offset_bytes: 4,
                    field_type: ::hdds::core::types::FieldType::Primitive(::hdds::core::types::PrimitiveKind::I32),  // v64: Match RTI IDL 'long'
                    alignment: 4,
                    size_bytes: 4,
                    element_type: None,
                }},
            ],
        }};
        &DESC
    }}

    fn encode_cdr2(&self, buf: &mut [u8]) -> ::hdds::dds::Result<usize> {{
        use ::hdds::core::ser::Cdr2Encode;
        self.encode_cdr2_le(buf).map_err(Into::into)
    }}

    fn decode_cdr2(buf: &[u8]) -> ::hdds::dds::Result<Self> {{
        use ::hdds::core::ser::Cdr2Decode;
        Self::decode_cdr2_le(buf).map(|(val, _)| val).map_err(Into::into)
    }}

    fn get_type_object() -> Option<::hdds::xtypes::CompleteTypeObject> {{
        Some(::hdds::xtypes::CompleteTypeObject::Struct(
            ::hdds::xtypes::CompleteStructType {{
                struct_flags: ::hdds::xtypes::StructTypeFlag::IS_FINAL,
                header: ::hdds::xtypes::CompleteStructHeader {{
                    base_type: None,
                    detail: ::hdds::xtypes::CompleteTypeDetail::new(\"Temperature\"),  // v168: Simple name
                }},
                member_seq: vec![
                    ::hdds::xtypes::CompleteStructMember {{
                        common: ::hdds::xtypes::CommonStructMember {{
                            member_id: 0,
                            member_flags: ::hdds::xtypes::MemberFlag::empty(),
                            member_type_id: ::hdds::xtypes::TypeIdentifier::TK_FLOAT32,
                        }},
                        detail: ::hdds::xtypes::CompleteMemberDetail::new(\"value\"),
                    }},
                    ::hdds::xtypes::CompleteStructMember {{
                        common: ::hdds::xtypes::CommonStructMember {{
                            member_id: 1,
                            member_flags: ::hdds::xtypes::MemberFlag::empty(),
                            member_type_id: ::hdds::xtypes::TypeIdentifier::TK_INT32,  // v64: Match RTI IDL 'long'
                        }},
                        detail: ::hdds::xtypes::CompleteMemberDetail::new(\"timestamp\"),
                    }},
                ],
            }}
        ))
    }}
}}

// === CDR2 low-level encode/decode impls (manual, Phase 7a) ===
impl Cdr2Encode for Temperature {{
    fn encode_cdr2_le(&self, buf: &mut [u8]) -> Result<usize, hdds::core::ser::CdrError> {{
        use hdds::core::ser::{{CursorMut, CdrError}};

        let mut cursor = CursorMut::new(buf);
        cursor.write_bytes(&self.value.to_le_bytes())
            .map_err(|_| CdrError::BufferTooSmall)?;
        cursor.write_bytes(&self.timestamp.to_le_bytes())
            .map_err(|_| CdrError::BufferTooSmall)?;
        Ok(cursor.offset())
    }}

    fn max_cdr2_size(&self) -> usize {{
        8  // Fixed size: f32 (4) + i32 (4)
    }}
}}

impl Cdr2Decode for Temperature {{
    fn decode_cdr2_le(buf: &[u8]) -> Result<(Self, usize), hdds::core::ser::CdrError> {{
        use hdds::core::ser::{{Cursor, CdrError}};

        let mut cursor = Cursor::new(buf);

        let value_bytes = cursor.read_bytes(4)
            .map_err(|_| CdrError::UnexpectedEof)?;
        let mut value_arr = [0u8; 4];
        value_arr.copy_from_slice(value_bytes);
        let value = f32::from_le_bytes(value_arr);

        let timestamp_bytes = cursor.read_bytes(4)
            .map_err(|_| CdrError::UnexpectedEof)?;
        let mut timestamp_arr = [0u8; 4];
        timestamp_arr.copy_from_slice(timestamp_bytes);
        let timestamp = i32::from_le_bytes(timestamp_arr);  // v64: Match RTI IDL 'long'

        Ok((Self {{ value, timestamp }}, cursor.offset()))
    }}
}}
", type_id);

    let mut file = fs::File::create(&output_path)?;
    file.write_all(code.as_bytes())?;

    println!("cargo:warning=Generated src/generated/temperature.rs");
    Ok(())
}

/// Compute FNV-1a hash (32-bit) for type ID
/// Same algorithm as hdds-codegen and hdds-gen
fn compute_fnv1a_hash(s: &str) -> u32 {
    let mut hash = 2_166_136_261_u32;
    for byte in s.bytes() {
        hash ^= u32::from(byte);
        hash = hash.wrapping_mul(16_777_619);
    }
    hash
}

/// Generate mod.rs aggregator for generated modules
///
/// Creates a module file that:
/// - Warns users not to edit (auto-generated)
/// - Silences linter warnings (generated code)
/// - Declares public submodules for each generated type
///
/// # Output Example
///
/// ```rust
/// // Auto-generated by build.rs
/// #![allow(dead_code, clippy::all)]
///
/// pub mod temperature;
/// ```
fn generate_mod_rs(output_dir: &Path) -> std::io::Result<()> {
    let output_path = output_dir.join("mod.rs");

    let code = "\
// SPDX-License-Identifier: Apache-2.0 OR MIT
// Copyright (c) 2025-2026 naskel.com
//
// Auto-generated by build.rs
// DO NOT EDIT - Changes will be overwritten on next build
//
// This module aggregates all generated types from IDL definitions.
// Phase 7a: temperature (hardcoded MVP)
// Phase 8: Full IDL parser integration

pub mod temperature {
    include!(concat!(env!(\"OUT_DIR\"), \"/generated/temperature.rs\"));
}
";

    let mut file = fs::File::create(&output_path)?;
    file.write_all(code.as_bytes())?;

    println!("cargo:warning=Generated src/generated/mod.rs");
    Ok(())
}