//! Typed representations of every message in the §7.7 surface.
//!
//! For each message:
//! - `MSG_ID` is the MAVLink id used in the 3-byte msgid field.
//! - `CRC_EXTRA` is the message-specific crc-extra byte fed into the X.25 CRC.
//! - `SIZE` is the full payload length (before trailing-zero truncation).
//! - `encode_payload` writes the payload **at its full length**; trailing-zero
//!   truncation is the framer's job.
//! - `decode_payload` accepts the **possibly truncated** payload and zero-pads
//!   it back to `SIZE` before reading.
//!
//! Field ordering inside each payload follows the MAVLink size-sorted rule
//! (descending C primitive size, definition order on ties). This is the
//! invariant the [`CRC_EXTRA`] values were generated against; reordering any
//! field silently breaks wire compatibility with every peer.

use super::MavlinkParseError;

/// The codec-supported §7.7 surface as a single typed enum.
///
/// Adding a new variant requires explicit design-review notes per
/// `architecture.md §7.7`.
#[derive(Debug, Clone, PartialEq)]
pub enum MavlinkMessage {
    Heartbeat(Heartbeat),
    SysStatus(SysStatus),
    SetMode(SetMode),
    Attitude(Attitude),
    GlobalPositionInt(GlobalPositionInt),
    MissionSetCurrent(MissionSetCurrent),
    MissionCurrent(MissionCurrent),
    MissionCount(MissionCount),
    MissionClearAll(MissionClearAll),
    MissionItemReached(MissionItemReached),
    MissionAck(MissionAck),
    MissionRequestInt(MissionRequestInt),
    MissionItemInt(MissionItemInt),
    CommandLong(CommandLong),
    CommandAck(CommandAck),
    ExtendedSysState(ExtendedSysState),
    StatusText(StatusText),
}

impl MavlinkMessage {
    pub fn msg_id(&self) -> u32 {
        match self {
            Self::Heartbeat(_) => Heartbeat::MSG_ID,
            Self::SysStatus(_) => SysStatus::MSG_ID,
            Self::SetMode(_) => SetMode::MSG_ID,
            Self::Attitude(_) => Attitude::MSG_ID,
            Self::GlobalPositionInt(_) => GlobalPositionInt::MSG_ID,
            Self::MissionSetCurrent(_) => MissionSetCurrent::MSG_ID,
            Self::MissionCurrent(_) => MissionCurrent::MSG_ID,
            Self::MissionCount(_) => MissionCount::MSG_ID,
            Self::MissionClearAll(_) => MissionClearAll::MSG_ID,
            Self::MissionItemReached(_) => MissionItemReached::MSG_ID,
            Self::MissionAck(_) => MissionAck::MSG_ID,
            Self::MissionRequestInt(_) => MissionRequestInt::MSG_ID,
            Self::MissionItemInt(_) => MissionItemInt::MSG_ID,
            Self::CommandLong(_) => CommandLong::MSG_ID,
            Self::CommandAck(_) => CommandAck::MSG_ID,
            Self::ExtendedSysState(_) => ExtendedSysState::MSG_ID,
            Self::StatusText(_) => StatusText::MSG_ID,
        }
    }

    pub fn crc_extra(&self) -> u8 {
        match self {
            Self::Heartbeat(_) => Heartbeat::CRC_EXTRA,
            Self::SysStatus(_) => SysStatus::CRC_EXTRA,
            Self::SetMode(_) => SetMode::CRC_EXTRA,
            Self::Attitude(_) => Attitude::CRC_EXTRA,
            Self::GlobalPositionInt(_) => GlobalPositionInt::CRC_EXTRA,
            Self::MissionSetCurrent(_) => MissionSetCurrent::CRC_EXTRA,
            Self::MissionCurrent(_) => MissionCurrent::CRC_EXTRA,
            Self::MissionCount(_) => MissionCount::CRC_EXTRA,
            Self::MissionClearAll(_) => MissionClearAll::CRC_EXTRA,
            Self::MissionItemReached(_) => MissionItemReached::CRC_EXTRA,
            Self::MissionAck(_) => MissionAck::CRC_EXTRA,
            Self::MissionRequestInt(_) => MissionRequestInt::CRC_EXTRA,
            Self::MissionItemInt(_) => MissionItemInt::CRC_EXTRA,
            Self::CommandLong(_) => CommandLong::CRC_EXTRA,
            Self::CommandAck(_) => CommandAck::CRC_EXTRA,
            Self::ExtendedSysState(_) => ExtendedSysState::CRC_EXTRA,
            Self::StatusText(_) => StatusText::CRC_EXTRA,
        }
    }

    pub fn encode_payload(&self, buf: &mut Vec<u8>) {
        match self {
            Self::Heartbeat(m) => m.encode(buf),
            Self::SysStatus(m) => m.encode(buf),
            Self::SetMode(m) => m.encode(buf),
            Self::Attitude(m) => m.encode(buf),
            Self::GlobalPositionInt(m) => m.encode(buf),
            Self::MissionSetCurrent(m) => m.encode(buf),
            Self::MissionCurrent(m) => m.encode(buf),
            Self::MissionCount(m) => m.encode(buf),
            Self::MissionClearAll(m) => m.encode(buf),
            Self::MissionItemReached(m) => m.encode(buf),
            Self::MissionAck(m) => m.encode(buf),
            Self::MissionRequestInt(m) => m.encode(buf),
            Self::MissionItemInt(m) => m.encode(buf),
            Self::CommandLong(m) => m.encode(buf),
            Self::CommandAck(m) => m.encode(buf),
            Self::ExtendedSysState(m) => m.encode(buf),
            Self::StatusText(m) => m.encode(buf),
        }
    }

    pub fn decode(msg_id: u32, payload: &[u8]) -> Result<Self, MavlinkParseError> {
        match msg_id {
            Heartbeat::MSG_ID => Ok(Self::Heartbeat(Heartbeat::decode(payload)?)),
            SysStatus::MSG_ID => Ok(Self::SysStatus(SysStatus::decode(payload)?)),
            SetMode::MSG_ID => Ok(Self::SetMode(SetMode::decode(payload)?)),
            Attitude::MSG_ID => Ok(Self::Attitude(Attitude::decode(payload)?)),
            GlobalPositionInt::MSG_ID => {
                Ok(Self::GlobalPositionInt(GlobalPositionInt::decode(payload)?))
            }
            MissionSetCurrent::MSG_ID => {
                Ok(Self::MissionSetCurrent(MissionSetCurrent::decode(payload)?))
            }
            MissionCurrent::MSG_ID => Ok(Self::MissionCurrent(MissionCurrent::decode(payload)?)),
            MissionCount::MSG_ID => Ok(Self::MissionCount(MissionCount::decode(payload)?)),
            MissionClearAll::MSG_ID => Ok(Self::MissionClearAll(MissionClearAll::decode(payload)?)),
            MissionItemReached::MSG_ID => Ok(Self::MissionItemReached(MissionItemReached::decode(
                payload,
            )?)),
            MissionAck::MSG_ID => Ok(Self::MissionAck(MissionAck::decode(payload)?)),
            MissionRequestInt::MSG_ID => {
                Ok(Self::MissionRequestInt(MissionRequestInt::decode(payload)?))
            }
            MissionItemInt::MSG_ID => Ok(Self::MissionItemInt(MissionItemInt::decode(payload)?)),
            CommandLong::MSG_ID => Ok(Self::CommandLong(CommandLong::decode(payload)?)),
            CommandAck::MSG_ID => Ok(Self::CommandAck(CommandAck::decode(payload)?)),
            ExtendedSysState::MSG_ID => {
                Ok(Self::ExtendedSysState(ExtendedSysState::decode(payload)?))
            }
            StatusText::MSG_ID => Ok(Self::StatusText(StatusText::decode(payload)?)),
            other => Err(MavlinkParseError::UnknownMessageId(other)),
        }
    }
}

/// Resolve the message-specific `crc_extra` for an inbound msg id; returns
/// `None` for ids outside the §7.7 surface.
pub fn crc_extra_for_id(msg_id: u32) -> Option<u8> {
    Some(match msg_id {
        Heartbeat::MSG_ID => Heartbeat::CRC_EXTRA,
        SysStatus::MSG_ID => SysStatus::CRC_EXTRA,
        SetMode::MSG_ID => SetMode::CRC_EXTRA,
        Attitude::MSG_ID => Attitude::CRC_EXTRA,
        GlobalPositionInt::MSG_ID => GlobalPositionInt::CRC_EXTRA,
        MissionSetCurrent::MSG_ID => MissionSetCurrent::CRC_EXTRA,
        MissionCurrent::MSG_ID => MissionCurrent::CRC_EXTRA,
        MissionCount::MSG_ID => MissionCount::CRC_EXTRA,
        MissionClearAll::MSG_ID => MissionClearAll::CRC_EXTRA,
        MissionItemReached::MSG_ID => MissionItemReached::CRC_EXTRA,
        MissionAck::MSG_ID => MissionAck::CRC_EXTRA,
        MissionRequestInt::MSG_ID => MissionRequestInt::CRC_EXTRA,
        MissionItemInt::MSG_ID => MissionItemInt::CRC_EXTRA,
        CommandLong::MSG_ID => CommandLong::CRC_EXTRA,
        CommandAck::MSG_ID => CommandAck::CRC_EXTRA,
        ExtendedSysState::MSG_ID => ExtendedSysState::CRC_EXTRA,
        StatusText::MSG_ID => StatusText::CRC_EXTRA,
        _ => return None,
    })
}

// ===== helpers =====

#[inline]
fn need(payload: &[u8], required: usize) -> Result<(), MavlinkParseError> {
    if payload.len() < required {
        return Err(MavlinkParseError::TruncatedPayload {
            have: payload.len(),
            need: required,
        });
    }
    Ok(())
}

/// Pad `payload` to `size` bytes with trailing zeros; MAVLink v2 truncates
/// trailing zeros on the wire and the decoder is required to re-extend.
fn padded(payload: &[u8], size: usize) -> [u8; 64] {
    debug_assert!(size <= 64, "max single-message payload in §7.7 fits in 64B");
    let mut buf = [0u8; 64];
    let copy = payload.len().min(size);
    buf[..copy].copy_from_slice(&payload[..copy]);
    buf
}

#[inline]
fn read_u16(b: &[u8], at: usize) -> u16 {
    u16::from_le_bytes([b[at], b[at + 1]])
}

#[inline]
fn read_i16(b: &[u8], at: usize) -> i16 {
    i16::from_le_bytes([b[at], b[at + 1]])
}

#[inline]
fn read_u32(b: &[u8], at: usize) -> u32 {
    u32::from_le_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
}

#[inline]
fn read_i32(b: &[u8], at: usize) -> i32 {
    i32::from_le_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
}

#[inline]
fn read_f32(b: &[u8], at: usize) -> f32 {
    f32::from_le_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
}

// ===== HEARTBEAT (id 0, crc_extra 50, size 9) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Heartbeat {
    pub custom_mode: u32,
    pub mavtype: u8,
    pub autopilot: u8,
    pub base_mode: u8,
    pub system_status: u8,
    pub mavlink_version: u8,
}

impl Heartbeat {
    pub const MSG_ID: u32 = 0;
    pub const CRC_EXTRA: u8 = 50;
    pub const SIZE: usize = 9;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.extend_from_slice(&self.custom_mode.to_le_bytes());
        buf.push(self.mavtype);
        buf.push(self.autopilot);
        buf.push(self.base_mode);
        buf.push(self.system_status);
        buf.push(self.mavlink_version);
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            custom_mode: read_u32(&b, 0),
            mavtype: b[4],
            autopilot: b[5],
            base_mode: b[6],
            system_status: b[7],
            mavlink_version: b[8],
        })
    }
}

// ===== SYS_STATUS (id 1, crc_extra 124, size 31) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SysStatus {
    pub onboard_control_sensors_present: u32,
    pub onboard_control_sensors_enabled: u32,
    pub onboard_control_sensors_health: u32,
    pub load: u16,
    pub voltage_battery: u16,
    pub current_battery: i16,
    pub drop_rate_comm: u16,
    pub errors_comm: u16,
    pub errors_count1: u16,
    pub errors_count2: u16,
    pub errors_count3: u16,
    pub errors_count4: u16,
    pub battery_remaining: i8,
}

impl SysStatus {
    pub const MSG_ID: u32 = 1;
    pub const CRC_EXTRA: u8 = 124;
    pub const SIZE: usize = 31;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.extend_from_slice(&self.onboard_control_sensors_present.to_le_bytes());
        buf.extend_from_slice(&self.onboard_control_sensors_enabled.to_le_bytes());
        buf.extend_from_slice(&self.onboard_control_sensors_health.to_le_bytes());
        buf.extend_from_slice(&self.load.to_le_bytes());
        buf.extend_from_slice(&self.voltage_battery.to_le_bytes());
        buf.extend_from_slice(&self.current_battery.to_le_bytes());
        buf.extend_from_slice(&self.drop_rate_comm.to_le_bytes());
        buf.extend_from_slice(&self.errors_comm.to_le_bytes());
        buf.extend_from_slice(&self.errors_count1.to_le_bytes());
        buf.extend_from_slice(&self.errors_count2.to_le_bytes());
        buf.extend_from_slice(&self.errors_count3.to_le_bytes());
        buf.extend_from_slice(&self.errors_count4.to_le_bytes());
        buf.push(self.battery_remaining as u8);
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            onboard_control_sensors_present: read_u32(&b, 0),
            onboard_control_sensors_enabled: read_u32(&b, 4),
            onboard_control_sensors_health: read_u32(&b, 8),
            load: read_u16(&b, 12),
            voltage_battery: read_u16(&b, 14),
            current_battery: read_i16(&b, 16),
            drop_rate_comm: read_u16(&b, 18),
            errors_comm: read_u16(&b, 20),
            errors_count1: read_u16(&b, 22),
            errors_count2: read_u16(&b, 24),
            errors_count3: read_u16(&b, 26),
            errors_count4: read_u16(&b, 28),
            battery_remaining: b[30] as i8,
        })
    }
}

// ===== SET_MODE (id 11, crc_extra 89, size 6) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SetMode {
    pub custom_mode: u32,
    pub target_system: u8,
    pub base_mode: u8,
}

impl SetMode {
    pub const MSG_ID: u32 = 11;
    pub const CRC_EXTRA: u8 = 89;
    pub const SIZE: usize = 6;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.extend_from_slice(&self.custom_mode.to_le_bytes());
        buf.push(self.target_system);
        buf.push(self.base_mode);
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            custom_mode: read_u32(&b, 0),
            target_system: b[4],
            base_mode: b[5],
        })
    }
}

// ===== ATTITUDE (id 30, crc_extra 39, size 28) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Attitude {
    pub time_boot_ms: u32,
    pub roll: f32,
    pub pitch: f32,
    pub yaw: f32,
    pub rollspeed: f32,
    pub pitchspeed: f32,
    pub yawspeed: f32,
}

impl Attitude {
    pub const MSG_ID: u32 = 30;
    pub const CRC_EXTRA: u8 = 39;
    pub const SIZE: usize = 28;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.extend_from_slice(&self.time_boot_ms.to_le_bytes());
        buf.extend_from_slice(&self.roll.to_le_bytes());
        buf.extend_from_slice(&self.pitch.to_le_bytes());
        buf.extend_from_slice(&self.yaw.to_le_bytes());
        buf.extend_from_slice(&self.rollspeed.to_le_bytes());
        buf.extend_from_slice(&self.pitchspeed.to_le_bytes());
        buf.extend_from_slice(&self.yawspeed.to_le_bytes());
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            time_boot_ms: read_u32(&b, 0),
            roll: read_f32(&b, 4),
            pitch: read_f32(&b, 8),
            yaw: read_f32(&b, 12),
            rollspeed: read_f32(&b, 16),
            pitchspeed: read_f32(&b, 20),
            yawspeed: read_f32(&b, 24),
        })
    }
}

// ===== GLOBAL_POSITION_INT (id 33, crc_extra 104, size 28) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GlobalPositionInt {
    pub time_boot_ms: u32,
    pub lat_e7: i32,
    pub lon_e7: i32,
    pub alt_mm: i32,
    pub relative_alt_mm: i32,
    pub vx_cmps: i16,
    pub vy_cmps: i16,
    pub vz_cmps: i16,
    pub hdg_cdeg: u16,
}

impl GlobalPositionInt {
    pub const MSG_ID: u32 = 33;
    pub const CRC_EXTRA: u8 = 104;
    pub const SIZE: usize = 28;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.extend_from_slice(&self.time_boot_ms.to_le_bytes());
        buf.extend_from_slice(&self.lat_e7.to_le_bytes());
        buf.extend_from_slice(&self.lon_e7.to_le_bytes());
        buf.extend_from_slice(&self.alt_mm.to_le_bytes());
        buf.extend_from_slice(&self.relative_alt_mm.to_le_bytes());
        buf.extend_from_slice(&self.vx_cmps.to_le_bytes());
        buf.extend_from_slice(&self.vy_cmps.to_le_bytes());
        buf.extend_from_slice(&self.vz_cmps.to_le_bytes());
        buf.extend_from_slice(&self.hdg_cdeg.to_le_bytes());
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            time_boot_ms: read_u32(&b, 0),
            lat_e7: read_i32(&b, 4),
            lon_e7: read_i32(&b, 8),
            alt_mm: read_i32(&b, 12),
            relative_alt_mm: read_i32(&b, 16),
            vx_cmps: read_i16(&b, 20),
            vy_cmps: read_i16(&b, 22),
            vz_cmps: read_i16(&b, 24),
            hdg_cdeg: read_u16(&b, 26),
        })
    }
}

// ===== MISSION_SET_CURRENT (id 41, crc_extra 28, size 4) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MissionSetCurrent {
    pub seq: u16,
    pub target_system: u8,
    pub target_component: u8,
}

impl MissionSetCurrent {
    pub const MSG_ID: u32 = 41;
    pub const CRC_EXTRA: u8 = 28;
    pub const SIZE: usize = 4;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.extend_from_slice(&self.seq.to_le_bytes());
        buf.push(self.target_system);
        buf.push(self.target_component);
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            seq: read_u16(&b, 0),
            target_system: b[2],
            target_component: b[3],
        })
    }
}

// ===== MISSION_CURRENT (id 42, crc_extra 28, size 2) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MissionCurrent {
    pub seq: u16,
}

impl MissionCurrent {
    pub const MSG_ID: u32 = 42;
    pub const CRC_EXTRA: u8 = 28;
    pub const SIZE: usize = 2;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.extend_from_slice(&self.seq.to_le_bytes());
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            seq: read_u16(&b, 0),
        })
    }
}

// ===== MISSION_COUNT (id 44, crc_extra 221, size 4) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MissionCount {
    pub count: u16,
    pub target_system: u8,
    pub target_component: u8,
}

impl MissionCount {
    pub const MSG_ID: u32 = 44;
    pub const CRC_EXTRA: u8 = 221;
    pub const SIZE: usize = 4;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.extend_from_slice(&self.count.to_le_bytes());
        buf.push(self.target_system);
        buf.push(self.target_component);
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            count: read_u16(&b, 0),
            target_system: b[2],
            target_component: b[3],
        })
    }
}

// ===== MISSION_CLEAR_ALL (id 45, crc_extra 232, size 2) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MissionClearAll {
    pub target_system: u8,
    pub target_component: u8,
}

impl MissionClearAll {
    pub const MSG_ID: u32 = 45;
    pub const CRC_EXTRA: u8 = 232;
    pub const SIZE: usize = 2;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.push(self.target_system);
        buf.push(self.target_component);
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            target_system: b[0],
            target_component: b[1],
        })
    }
}

// ===== MISSION_ITEM_REACHED (id 46, crc_extra 11, size 2) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MissionItemReached {
    pub seq: u16,
}

impl MissionItemReached {
    pub const MSG_ID: u32 = 46;
    pub const CRC_EXTRA: u8 = 11;
    pub const SIZE: usize = 2;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.extend_from_slice(&self.seq.to_le_bytes());
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            seq: read_u16(&b, 0),
        })
    }
}

// ===== MISSION_ACK (id 47, crc_extra 153, size 3) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MissionAck {
    pub target_system: u8,
    pub target_component: u8,
    pub mission_result: u8,
}

impl MissionAck {
    pub const MSG_ID: u32 = 47;
    pub const CRC_EXTRA: u8 = 153;
    pub const SIZE: usize = 3;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.push(self.target_system);
        buf.push(self.target_component);
        buf.push(self.mission_result);
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            target_system: b[0],
            target_component: b[1],
            mission_result: b[2],
        })
    }
}

// ===== MISSION_REQUEST_INT (id 51, crc_extra 38, size 4) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MissionRequestInt {
    pub seq: u16,
    pub target_system: u8,
    pub target_component: u8,
}

impl MissionRequestInt {
    pub const MSG_ID: u32 = 51;
    pub const CRC_EXTRA: u8 = 38;
    pub const SIZE: usize = 4;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.extend_from_slice(&self.seq.to_le_bytes());
        buf.push(self.target_system);
        buf.push(self.target_component);
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            seq: read_u16(&b, 0),
            target_system: b[2],
            target_component: b[3],
        })
    }
}

// ===== MISSION_ITEM_INT (id 73, crc_extra 38, size 37) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MissionItemInt {
    pub param1: f32,
    pub param2: f32,
    pub param3: f32,
    pub param4: f32,
    pub x: i32,
    pub y: i32,
    pub z: f32,
    pub seq: u16,
    pub command: u16,
    pub target_system: u8,
    pub target_component: u8,
    pub frame: u8,
    pub current: u8,
    pub autocontinue: u8,
}

impl MissionItemInt {
    pub const MSG_ID: u32 = 73;
    pub const CRC_EXTRA: u8 = 38;
    pub const SIZE: usize = 37;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.extend_from_slice(&self.param1.to_le_bytes());
        buf.extend_from_slice(&self.param2.to_le_bytes());
        buf.extend_from_slice(&self.param3.to_le_bytes());
        buf.extend_from_slice(&self.param4.to_le_bytes());
        buf.extend_from_slice(&self.x.to_le_bytes());
        buf.extend_from_slice(&self.y.to_le_bytes());
        buf.extend_from_slice(&self.z.to_le_bytes());
        buf.extend_from_slice(&self.seq.to_le_bytes());
        buf.extend_from_slice(&self.command.to_le_bytes());
        buf.push(self.target_system);
        buf.push(self.target_component);
        buf.push(self.frame);
        buf.push(self.current);
        buf.push(self.autocontinue);
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            param1: read_f32(&b, 0),
            param2: read_f32(&b, 4),
            param3: read_f32(&b, 8),
            param4: read_f32(&b, 12),
            x: read_i32(&b, 16),
            y: read_i32(&b, 20),
            z: read_f32(&b, 24),
            seq: read_u16(&b, 28),
            command: read_u16(&b, 30),
            target_system: b[32],
            target_component: b[33],
            frame: b[34],
            current: b[35],
            autocontinue: b[36],
        })
    }
}

// ===== COMMAND_LONG (id 76, crc_extra 152, size 33) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CommandLong {
    pub param1: f32,
    pub param2: f32,
    pub param3: f32,
    pub param4: f32,
    pub param5: f32,
    pub param6: f32,
    pub param7: f32,
    pub command: u16,
    pub target_system: u8,
    pub target_component: u8,
    pub confirmation: u8,
}

impl CommandLong {
    pub const MSG_ID: u32 = 76;
    pub const CRC_EXTRA: u8 = 152;
    pub const SIZE: usize = 33;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.extend_from_slice(&self.param1.to_le_bytes());
        buf.extend_from_slice(&self.param2.to_le_bytes());
        buf.extend_from_slice(&self.param3.to_le_bytes());
        buf.extend_from_slice(&self.param4.to_le_bytes());
        buf.extend_from_slice(&self.param5.to_le_bytes());
        buf.extend_from_slice(&self.param6.to_le_bytes());
        buf.extend_from_slice(&self.param7.to_le_bytes());
        buf.extend_from_slice(&self.command.to_le_bytes());
        buf.push(self.target_system);
        buf.push(self.target_component);
        buf.push(self.confirmation);
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            param1: read_f32(&b, 0),
            param2: read_f32(&b, 4),
            param3: read_f32(&b, 8),
            param4: read_f32(&b, 12),
            param5: read_f32(&b, 16),
            param6: read_f32(&b, 20),
            param7: read_f32(&b, 24),
            command: read_u16(&b, 28),
            target_system: b[30],
            target_component: b[31],
            confirmation: b[32],
        })
    }
}

// ===== COMMAND_ACK (id 77, crc_extra 143, size 3) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CommandAck {
    pub command: u16,
    pub result: u8,
}

impl CommandAck {
    pub const MSG_ID: u32 = 77;
    pub const CRC_EXTRA: u8 = 143;
    pub const SIZE: usize = 3;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.extend_from_slice(&self.command.to_le_bytes());
        buf.push(self.result);
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            command: read_u16(&b, 0),
            result: b[2],
        })
    }
}

// ===== EXTENDED_SYS_STATE (id 245, crc_extra 130, size 2) =====

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ExtendedSysState {
    pub vtol_state: u8,
    pub landed_state: u8,
}

impl ExtendedSysState {
    pub const MSG_ID: u32 = 245;
    pub const CRC_EXTRA: u8 = 130;
    pub const SIZE: usize = 2;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.push(self.vtol_state);
        buf.push(self.landed_state);
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        let b = padded(payload, Self::SIZE);
        Ok(Self {
            vtol_state: b[0],
            landed_state: b[1],
        })
    }
}

// ===== STATUSTEXT (id 253, crc_extra 83, size 51) =====

#[derive(Debug, Clone, PartialEq)]
pub struct StatusText {
    pub severity: u8,
    /// 50-byte null-padded ASCII; values beyond the first NUL are ignored on
    /// decode. Encoding zero-pads to 50 bytes.
    pub text: [u8; 50],
}

impl StatusText {
    pub const MSG_ID: u32 = 253;
    pub const CRC_EXTRA: u8 = 83;
    pub const SIZE: usize = 51;

    pub fn encode(&self, buf: &mut Vec<u8>) {
        buf.push(self.severity);
        buf.extend_from_slice(&self.text);
    }

    pub fn decode(payload: &[u8]) -> Result<Self, MavlinkParseError> {
        need(payload, 1)?;
        let mut text = [0u8; 50];
        let body_len = payload.len() - 1;
        let copy = body_len.min(50);
        text[..copy].copy_from_slice(&payload[1..1 + copy]);
        Ok(Self {
            severity: payload[0],
            text,
        })
    }

    /// Build a `StatusText` from a UTF-8 string, truncating to 50 bytes and
    /// zero-padding the rest.
    pub fn from_str(severity: u8, msg: &str) -> Self {
        let bytes = msg.as_bytes();
        let mut text = [0u8; 50];
        let copy = bytes.len().min(50);
        text[..copy].copy_from_slice(&bytes[..copy]);
        Self { severity, text }
    }
}

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

    #[test]
    fn heartbeat_round_trips() {
        // Arrange
        let m = Heartbeat {
            custom_mode: 0xDEADBEEF,
            mavtype: 2,
            autopilot: 3,
            base_mode: 0x81,
            system_status: 4,
            mavlink_version: 3,
        };

        // Act
        let mut buf = Vec::new();
        m.encode(&mut buf);
        let decoded = Heartbeat::decode(&buf).unwrap();

        // Assert
        assert_eq!(buf.len(), Heartbeat::SIZE);
        assert_eq!(decoded, m);
    }

    #[test]
    fn command_long_round_trips() {
        // Arrange
        let m = CommandLong {
            param1: 1.5,
            param2: 2.25,
            param3: -3.0,
            param4: f32::NAN,
            param5: 50.123,
            param6: -42.42,
            param7: 100.0,
            command: 20, // MAV_CMD_NAV_RETURN_TO_LAUNCH
            target_system: 1,
            target_component: 1,
            confirmation: 0,
        };

        // Act
        let mut buf = Vec::new();
        m.encode(&mut buf);
        let decoded = CommandLong::decode(&buf).unwrap();

        // Assert: NaN compares unequal so compare bit pattern manually
        assert_eq!(buf.len(), CommandLong::SIZE);
        assert_eq!(decoded.param1, m.param1);
        assert!(decoded.param4.is_nan());
        assert_eq!(decoded.command, m.command);
        assert_eq!(decoded.confirmation, m.confirmation);
    }

    #[test]
    fn statustext_truncates_long_string() {
        // Arrange
        let long = "x".repeat(100);

        // Act
        let m = StatusText::from_str(6, &long);

        // Assert
        assert_eq!(m.text[..50], [b'x'; 50][..]);
    }

    #[test]
    fn decode_truncated_heartbeat_zero_pads() {
        // Arrange: a HEARTBEAT payload trimmed of its trailing mavlink_version byte
        let mut buf = Vec::new();
        Heartbeat {
            custom_mode: 7,
            mavtype: 2,
            autopilot: 3,
            base_mode: 0,
            system_status: 4,
            mavlink_version: 3,
        }
        .encode(&mut buf);
        let truncated = &buf[..Heartbeat::SIZE - 1];

        // Act
        let decoded = Heartbeat::decode(truncated).unwrap();

        // Assert: the trimmed byte is read as zero (MAVLink v2 trailing-zero rule).
        assert_eq!(decoded.mavlink_version, 0);
    }
}
