//! The FIT binary container: file header, definition messages, data messages
//! and the trailing CRC.
//!
//! This is deliberately a small, literal implementation of the FIT protocol
//! rather than a wrapper around a generated SDK. The container is about two
//! hundred lines and every byte of it matters for whether an upload is
//! accepted, so it is worth being able to read all of it.
//!
//! # File layout
//!
//! ```text
//! +--------------------------------+
//! | header (14 bytes)              |  size, protocol, profile, data size,
//! |                                |  ".FIT", header CRC
//! +--------------------------------+
//! | data records (data_size bytes) |  definition + data messages
//! +--------------------------------+
//! | file CRC (2 bytes)             |  over header + data records
//! +--------------------------------+
//! ```

use crate::crc::Crc16;

/// Header length we emit. The 12-byte variant (no header CRC) is legal but the
/// 14-byte form is universally expected.
pub const HEADER_SIZE: u8 = 14;

/// Protocol version 2.0, encoded as `major << 4 | minor`.
pub const PROTOCOL_VERSION: u8 = 0x20;

/// Profile version, `major * 100 + minor`, from FIT SDK 21.
pub const PROFILE_VERSION: u16 = 21_205;

/// The `.FIT` data type signature at bytes 8..12 of the header.
pub const DATA_TYPE: &[u8; 4] = b".FIT";

/// FIT base type identifiers. The high bit marks an endian-sensitive type; the
/// low 5 bits are the type number. Variant names are the FIT type names.
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum BaseType {
    Enum = 0x00,
    Sint8 = 0x01,
    Uint8 = 0x02,
    Sint16 = 0x83,
    Uint16 = 0x84,
    Sint32 = 0x85,
    Uint32 = 0x86,
    String = 0x07,
    Float32 = 0x88,
    Uint8z = 0x0A,
    Uint16z = 0x8B,
    Uint32z = 0x8C,
    Byte = 0x0D,
}

/// One encoded field value, carrying its own base type and width. Variant
/// names mirror the FIT base types they encode to.
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
    Enum(u8),
    Uint8(u8),
    Uint8z(u8),
    Sint8(i8),
    Uint16(u16),
    Uint16z(u16),
    Sint16(i16),
    Uint32(u32),
    Uint32z(u32),
    Sint32(i32),
    Float32(f32),
    /// Null-terminated UTF-8. The encoded size includes the terminator.
    String(String),
}

impl Value {
    /// The FIT base type of this value.
    pub fn base_type(&self) -> BaseType {
        match self {
            Value::Enum(_) => BaseType::Enum,
            Value::Uint8(_) => BaseType::Uint8,
            Value::Uint8z(_) => BaseType::Uint8z,
            Value::Sint8(_) => BaseType::Sint8,
            Value::Uint16(_) => BaseType::Uint16,
            Value::Uint16z(_) => BaseType::Uint16z,
            Value::Sint16(_) => BaseType::Sint16,
            Value::Uint32(_) => BaseType::Uint32,
            Value::Uint32z(_) => BaseType::Uint32z,
            Value::Sint32(_) => BaseType::Sint32,
            Value::Float32(_) => BaseType::Float32,
            Value::String(_) => BaseType::String,
        }
    }

    /// Encoded width in bytes, as it appears in the definition message.
    pub fn size(&self) -> u8 {
        match self {
            Value::Enum(_) | Value::Uint8(_) | Value::Uint8z(_) | Value::Sint8(_) => 1,
            Value::Uint16(_) | Value::Uint16z(_) | Value::Sint16(_) => 2,
            Value::Uint32(_) | Value::Uint32z(_) | Value::Sint32(_) | Value::Float32(_) => 4,
            // UTF-8 bytes plus the null terminator; clamped so a pathological
            // name cannot overflow the single-byte size field.
            Value::String(s) => (s.len().min(u8::MAX as usize - 1) + 1) as u8,
        }
    }

    /// Append this value to `out` in little-endian order.
    fn write(&self, out: &mut Vec<u8>) {
        match self {
            Value::Enum(v) | Value::Uint8(v) | Value::Uint8z(v) => out.push(*v),
            Value::Sint8(v) => out.push(*v as u8),
            Value::Uint16(v) | Value::Uint16z(v) => out.extend_from_slice(&v.to_le_bytes()),
            Value::Sint16(v) => out.extend_from_slice(&v.to_le_bytes()),
            Value::Uint32(v) | Value::Uint32z(v) => out.extend_from_slice(&v.to_le_bytes()),
            Value::Sint32(v) => out.extend_from_slice(&v.to_le_bytes()),
            Value::Float32(v) => out.extend_from_slice(&v.to_le_bytes()),
            Value::String(s) => {
                let max = usize::from(self.size()) - 1;
                let mut bytes = s.as_bytes();
                if bytes.len() > max {
                    // Never split a UTF-8 sequence.
                    let mut end = max;
                    while end > 0 && (bytes[end] & 0xC0) == 0x80 {
                        end -= 1;
                    }
                    bytes = &bytes[..end];
                }
                out.extend_from_slice(bytes);
                out.resize(out.len() + (max - bytes.len()) + 1, 0);
            }
        }
    }
}

/// A message under construction: an ordered set of (field number, value) pairs.
///
/// Setting the same field twice replaces the value rather than emitting a
/// duplicate, which a definition message may not contain.
#[derive(Debug, Default, Clone)]
pub struct Message {
    fields: Vec<(u8, Value)>,
}

impl Message {
    /// An empty message.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set a field.
    pub fn set(&mut self, field: u8, value: Value) -> &mut Self {
        match self.fields.iter_mut().find(|(n, _)| *n == field) {
            Some(slot) => slot.1 = value,
            None => self.fields.push((field, value)),
        }
        self
    }

    /// Set a field only when the value is present. Absent optional fields are
    /// omitted from the definition entirely rather than written as the base
    /// type's "invalid" sentinel, which keeps files small and stops decoders
    /// from surfacing phantom all-invalid streams.
    pub fn set_opt(&mut self, field: u8, value: Option<Value>) -> &mut Self {
        if let Some(v) = value {
            self.set(field, v);
        }
        self
    }

    /// The fields, in the order they will be written.
    pub fn fields(&self) -> &[(u8, Value)] {
        &self.fields
    }

    /// True when no field has been set.
    pub fn is_empty(&self) -> bool {
        self.fields.is_empty()
    }

    /// The definition-message shape of this message.
    fn shape(&self) -> Shape {
        self.fields
            .iter()
            .map(|(n, v)| (*n, v.size(), v.base_type() as u8))
            .collect()
    }
}

/// A definition-message shape: `(field number, size in bytes, base type)` per
/// field. Two messages sharing a shape can share a definition.
type Shape = Vec<(u8, u8, u8)>;

/// Accumulates data records and emits a complete FIT file.
///
/// Definitions are cached per local message type, so a definition is re-emitted
/// only when a message's shape changes — which is what lets a thousand `record`
/// messages share a single definition.
#[derive(Debug, Default)]
pub struct FitEncoder {
    data: Vec<u8>,
    /// Cached definition shape per local message type (0..16).
    defs: [Option<(u16, Shape)>; 16],
    message_count: usize,
}

impl FitEncoder {
    /// A new, empty encoder.
    pub fn new() -> Self {
        Self::default()
    }

    /// Write `msg` as global message `global` using local message type `local`.
    ///
    /// Emits a definition message first if the shape is not already cached for
    /// this local type. `local` must be 0..=15; anything larger is masked, and
    /// an empty message is skipped (a zero-field definition is legal but
    /// pointless and confuses some parsers).
    pub fn write_message(&mut self, local: u8, global: u16, msg: &Message) {
        if msg.is_empty() {
            return;
        }
        let local = local & 0x0F;
        let shape = msg.shape();

        let cached = self.defs[local as usize]
            .as_ref()
            .is_some_and(|(g, s)| *g == global && *s == shape);

        if !cached {
            self.write_definition(local, global, &shape);
            self.defs[local as usize] = Some((global, shape));
        }

        // Data message header: bit 7 = 0 (normal), bit 6 = 0 (data),
        // bits 0..4 = local message type.
        self.data.push(local);
        for (_, value) in msg.fields() {
            value.write(&mut self.data);
        }
        self.message_count += 1;
    }

    fn write_definition(&mut self, local: u8, global: u16, shape: &[(u8, u8, u8)]) {
        // Definition message header: bit 7 = 0 (normal), bit 6 = 1 (definition).
        self.data.push(0x40 | local);
        self.data.push(0); // reserved
        self.data.push(0); // architecture: 0 = little endian
        self.data.extend_from_slice(&global.to_le_bytes());
        // A definition may describe at most 255 fields.
        self.data.push(shape.len().min(u8::MAX as usize) as u8);
        for &(num, size, base) in shape.iter().take(u8::MAX as usize) {
            self.data.push(num);
            self.data.push(size);
            self.data.push(base);
        }
    }

    /// Number of data messages written so far.
    pub fn message_count(&self) -> usize {
        self.message_count
    }

    /// Byte length of the data-records section written so far.
    pub fn data_len(&self) -> usize {
        self.data.len()
    }

    /// Finish the file: prepend the 14-byte header (with its own CRC) and
    /// append the file CRC over header plus data.
    pub fn finish(self) -> Vec<u8> {
        let mut out = Vec::with_capacity(self.data.len() + 16);
        out.extend_from_slice(&file_header(self.data.len() as u32));
        out.extend_from_slice(&self.data);

        let mut crc = Crc16::new();
        crc.update(&out);
        out.extend_from_slice(&crc.value().to_le_bytes());
        out
    }
}

/// Build the 14-byte FIT file header for a given data-records length.
///
/// `data_size` counts *only* the data records — not the header and not the
/// trailing CRC. Getting that wrong is the second classic way to produce a file
/// that every uploader rejects.
pub fn file_header(data_size: u32) -> [u8; 14] {
    let mut h = [0u8; 14];
    h[0] = HEADER_SIZE;
    h[1] = PROTOCOL_VERSION;
    h[2..4].copy_from_slice(&PROFILE_VERSION.to_le_bytes());
    h[4..8].copy_from_slice(&data_size.to_le_bytes());
    h[8..12].copy_from_slice(DATA_TYPE);

    let mut crc = Crc16::new();
    crc.update(&h[0..12]);
    h[12..14].copy_from_slice(&crc.value().to_le_bytes());
    h
}

/// Structural check on an encoded FIT file: header self-consistency, declared
/// data size against actual length, and both CRCs.
///
/// Exposed because it is exactly the check an uploader performs before deciding
/// whether to look at the contents, and it is cheap enough to run on every file
/// we write.
pub fn verify(bytes: &[u8]) -> Result<(), VerifyError> {
    if bytes.len() < 16 {
        return Err(VerifyError::TooShort(bytes.len()));
    }
    let header_size = bytes[0] as usize;
    if header_size != 12 && header_size != 14 {
        return Err(VerifyError::BadHeaderSize(bytes[0]));
    }
    if &bytes[8..12] != DATA_TYPE {
        return Err(VerifyError::BadSignature([
            bytes[8], bytes[9], bytes[10], bytes[11],
        ]));
    }

    let data_size = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize;
    let expected_len = header_size + data_size + 2;
    if bytes.len() != expected_len {
        return Err(VerifyError::DataSizeMismatch {
            declared: data_size,
            actual: bytes.len().saturating_sub(header_size + 2),
        });
    }

    if header_size == 14 {
        let stored = u16::from_le_bytes([bytes[12], bytes[13]]);
        // A zero header CRC means "not present", which is legal.
        if stored != 0 {
            let computed = crate::crc::crc16(&bytes[0..12]);
            if stored != computed {
                return Err(VerifyError::HeaderCrc { stored, computed });
            }
        }
    }

    let stored = u16::from_le_bytes([bytes[expected_len - 2], bytes[expected_len - 1]]);
    let computed = crate::crc::crc16(&bytes[..expected_len - 2]);
    if stored != computed {
        return Err(VerifyError::FileCrc { stored, computed });
    }
    Ok(())
}

/// Why [`verify`] rejected a file.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum VerifyError {
    /// Shorter than the smallest legal file (header plus CRC).
    #[error("file is {0} bytes, too short to be a FIT file")]
    TooShort(
        /// Actual file length.
        usize,
    ),
    /// Byte 0 is neither 12 nor 14.
    #[error("header size {0} is neither 12 nor 14")]
    BadHeaderSize(
        /// The declared header size.
        u8,
    ),
    /// Bytes 8..12 are not `.FIT`.
    #[error("data type signature is {0:?}, expected \".FIT\"")]
    BadSignature(
        /// The bytes found where the signature should be.
        [u8; 4],
    ),
    /// The header's data size does not match the bytes actually present.
    #[error("header declares {declared} data bytes but the file carries {actual}")]
    DataSizeMismatch {
        /// Data size from the header.
        declared: usize,
        /// Data bytes actually present.
        actual: usize,
    },
    /// The header CRC does not check out.
    #[error("header CRC is {stored:#06x}, computed {computed:#06x}")]
    HeaderCrc {
        /// CRC read from the file.
        stored: u16,
        /// CRC computed over bytes 0..12.
        computed: u16,
    },
    /// The trailing file CRC does not check out.
    #[error("file CRC is {stored:#06x}, computed {computed:#06x}")]
    FileCrc {
        /// CRC read from the end of the file.
        stored: u16,
        /// CRC computed over header plus data.
        computed: u16,
    },
}

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

    #[test]
    fn header_layout_is_byte_exact() {
        let h = file_header(0x1234);
        assert_eq!(h[0], 14, "header size");
        assert_eq!(h[1], 0x20, "protocol version 2.0");
        assert_eq!(&h[2..4], &PROFILE_VERSION.to_le_bytes(), "profile version");
        assert_eq!(&h[4..8], &[0x34, 0x12, 0x00, 0x00], "data size, little endian");
        assert_eq!(&h[8..12], b".FIT", "data type signature");

        let crc = u16::from_le_bytes([h[12], h[13]]);
        assert_eq!(crc, crate::crc::crc16(&h[0..12]));
        assert_ne!(crc, 0, "a real header CRC, not the 'absent' sentinel");
    }

    #[test]
    fn empty_file_is_header_plus_crc() {
        let bytes = FitEncoder::new().finish();
        assert_eq!(bytes.len(), 16);
        assert_eq!(&bytes[4..8], &[0, 0, 0, 0]);
        assert!(verify(&bytes).is_ok());
    }

    #[test]
    fn definition_and_data_bytes_are_exact() {
        let mut enc = FitEncoder::new();
        let mut msg = Message::new();
        msg.set(0, Value::Enum(4));
        msg.set(1, Value::Uint16(255));
        enc.write_message(0, 0, &msg);
        let bytes = enc.finish();

        let data = &bytes[14..bytes.len() - 2];
        #[rustfmt::skip]
        let expected: &[u8] = &[
            // definition message for global 0, local 0, two fields
            0x40,             // header: normal, definition, local 0
            0x00,             // reserved
            0x00,             // little endian
            0x00, 0x00,       // global message number 0 (file_id)
            0x02,             // two fields
            0x00, 0x01, 0x00, // field 0, 1 byte, enum
            0x01, 0x02, 0x84, // field 1, 2 bytes, uint16
            // data message
            0x00,             // header: normal, data, local 0
            0x04,             // type = activity
            0xFF, 0x00,       // manufacturer = 255, little endian
        ];
        assert_eq!(data, expected);
        assert_eq!(
            u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize,
            expected.len()
        );
        assert!(verify(&bytes).is_ok());
    }

    #[test]
    fn definition_is_reused_for_identical_shapes() {
        let mut enc = FitEncoder::new();
        let mut msg = Message::new();
        msg.set(253, Value::Uint32(1));
        for i in 0..5u32 {
            msg.set(253, Value::Uint32(i));
            enc.write_message(3, mesg_record(), &msg);
        }
        // One 3+3+1-byte definition (6 header bytes + 3 per field) plus five
        // 5-byte data messages.
        assert_eq!(enc.data_len(), (6 + 3) + 5 * (1 + 4));
        assert_eq!(enc.message_count(), 5);
    }

    #[test]
    fn definition_is_re_emitted_when_the_shape_changes() {
        let mut enc = FitEncoder::new();
        let mut a = Message::new();
        a.set(253, Value::Uint32(1));
        enc.write_message(3, mesg_record(), &a);
        let after_first = enc.data_len();

        let mut b = Message::new();
        b.set(253, Value::Uint32(2));
        b.set(7, Value::Uint16(250));
        enc.write_message(3, mesg_record(), &b);
        // Second write costs a new 12-byte definition plus a 7-byte data message.
        assert_eq!(enc.data_len() - after_first, (6 + 6) + (1 + 4 + 2));
    }

    fn mesg_record() -> u16 {
        crate::profile::mesg::RECORD
    }

    #[test]
    fn strings_are_null_terminated_and_sized_with_the_terminator() {
        let v = Value::String("BikeControl".into());
        assert_eq!(v.size(), 12);
        let mut out = Vec::new();
        v.write(&mut out);
        assert_eq!(out, b"BikeControl\0");
    }

    #[test]
    fn empty_string_is_a_single_null() {
        let v = Value::String(String::new());
        assert_eq!(v.size(), 1);
        let mut out = Vec::new();
        v.write(&mut out);
        assert_eq!(out, b"\0");
    }

    #[test]
    fn overlong_strings_are_truncated_on_a_char_boundary() {
        let v = Value::String("é".repeat(200));
        let size = usize::from(v.size());
        let mut out = Vec::new();
        v.write(&mut out);
        assert_eq!(out.len(), size);
        assert_eq!(*out.last().unwrap(), 0);
        // Truncation must not leave a partial UTF-8 sequence.
        assert!(std::str::from_utf8(&out[..out.len() - 1]).is_ok());
    }

    #[test]
    fn setting_a_field_twice_replaces_rather_than_duplicates() {
        let mut msg = Message::new();
        msg.set(7, Value::Uint16(100));
        msg.set(7, Value::Uint16(200));
        assert_eq!(msg.fields().len(), 1);
        assert_eq!(msg.fields()[0].1, Value::Uint16(200));
    }

    #[test]
    fn set_opt_skips_none() {
        let mut msg = Message::new();
        msg.set_opt(3, None);
        msg.set_opt(4, Some(Value::Uint8(90)));
        assert_eq!(msg.fields().len(), 1);
        assert_eq!(msg.fields()[0].0, 4);
    }

    #[test]
    fn signed_values_use_twos_complement_little_endian() {
        let mut out = Vec::new();
        Value::Sint16(-100).write(&mut out);
        assert_eq!(out, vec![0x9C, 0xFF]);
        out.clear();
        Value::Sint8(-1).write(&mut out);
        assert_eq!(out, vec![0xFF]);
        out.clear();
        Value::Sint32(-2).write(&mut out);
        assert_eq!(out, vec![0xFE, 0xFF, 0xFF, 0xFF]);
    }

    #[test]
    fn verify_rejects_a_corrupted_file_crc() {
        let mut bytes = FitEncoder::new().finish();
        let last = bytes.len() - 1;
        bytes[last] ^= 0xFF;
        assert!(matches!(verify(&bytes), Err(VerifyError::FileCrc { .. })));
    }

    #[test]
    fn verify_rejects_a_corrupted_header_crc() {
        let mut bytes = FitEncoder::new().finish();
        bytes[12] ^= 0xFF;
        assert!(matches!(verify(&bytes), Err(VerifyError::HeaderCrc { .. })));
    }

    #[test]
    fn verify_rejects_a_wrong_data_size() {
        let mut bytes = FitEncoder::new().finish();
        bytes[4] = 99;
        assert!(matches!(
            verify(&bytes),
            Err(VerifyError::DataSizeMismatch { .. })
        ));
    }

    #[test]
    fn verify_rejects_a_bad_signature() {
        let mut bytes = FitEncoder::new().finish();
        bytes[8] = b'X';
        assert!(matches!(verify(&bytes), Err(VerifyError::BadSignature(_))));
    }

    #[test]
    fn verify_rejects_truncation() {
        let bytes = FitEncoder::new().finish();
        assert!(matches!(
            verify(&bytes[..10]),
            Err(VerifyError::TooShort(10))
        ));
    }

    #[test]
    fn every_single_byte_corruption_is_caught() {
        // The strongest statement we can make about the checksums without an
        // uploader: no one-byte change to the file survives verification.
        let mut enc = FitEncoder::new();
        let mut msg = Message::new();
        msg.set(253, Value::Uint32(1_000_000_000));
        msg.set(7, Value::Uint16(250));
        enc.write_message(3, crate::profile::mesg::RECORD, &msg);
        let good = enc.finish();

        for i in 0..good.len() {
            let mut bad = good.clone();
            bad[i] ^= 0x01;
            assert!(
                verify(&bad).is_err(),
                "flipping a bit in byte {i} was not detected"
            );
        }
    }
}
