//! Heat2 encoder.
//!
//! Field layout is `3-byte packed tag || 1 type byte || value`.
//!
//! Two rules are easy to get wrong and both are load-bearing:
//!
//!   1. **Members serialise in ascending packed-tag order.** Not source order,
//!      not alphabetical order of the label — packed-tag order. (They coincide
//!      for equal-length uppercase labels, which is why a bug here hides.)
//!   2. **A nested struct is terminated by `0x00`; the top-level payload is
//!      not.** The top level is delimited by the Fire2 length instead.
//!
//! Encoding is infallible: every `Value` has a wire form.

use super::tag::Tag;
use super::value::{Struct, Value, UNION_UNSET};
use super::varint;

/// Encode a top-level payload body (no trailing terminator).
pub fn encode(s: &Struct) -> Vec<u8> {
    let mut out = Vec::new();
    encode_into(s, &mut out);
    out
}

/// Encode a top-level payload body, appending to `out`.
pub fn encode_into(s: &Struct, out: &mut Vec<u8>) {
    encode_members(s, out);
}

fn encode_members(s: &Struct, out: &mut Vec<u8>) {
    // Stable sort by packed tag: equal tags keep their relative order, so a
    // decoded frame containing duplicates re-encodes identically.
    let mut ordered: Vec<&(Tag, Value)> = s.fields.iter().collect();
    ordered.sort_by_key(|(tag, _)| *tag);

    for (tag, value) in ordered {
        out.extend_from_slice(&tag.as_bytes());
        out.push(value.type_id().as_byte());
        encode_value(value, out);
    }
}

fn encode_value(value: &Value, out: &mut Vec<u8>) {
    match value {
        Value::Int(v) => varint::encode(*v, out),

        Value::String(s) => {
            // Trailing NULs are stripped before framing so the length and the
            // single terminator stay consistent; the length INCLUDES that NUL.
            let raw = s.as_bytes();
            let end = raw.iter().rposition(|&b| b != 0).map_or(0, |i| i + 1);
            let raw = &raw[..end];
            varint::encode(raw.len() as i64 + 1, out);
            out.extend_from_slice(raw);
            out.push(0x00);
        }

        Value::Blob(b) => {
            // Blob length EXCLUDES a terminator — there isn't one.
            varint::encode(b.len() as i64, out);
            out.extend_from_slice(b);
        }

        Value::Struct(s) => {
            encode_members(s, out);
            out.push(0x00);
        }

        Value::List { elem, items } => {
            out.push(elem.as_byte());
            varint::encode(items.len() as i64, out);
            for it in items {
                encode_value(it, out);
            }
        }

        Value::Map { key, val, entries } => {
            out.push(key.as_byte());
            out.push(val.as_byte());
            varint::encode(entries.len() as i64, out);
            for (k, v) in entries {
                encode_value(k, out);
                encode_value(v, out);
            }
        }

        Value::Union { key, member } => {
            out.push(*key);
            if *key != UNION_UNSET {
                if let Some(m) = member {
                    let (tag, val) = m.as_ref();
                    out.extend_from_slice(&tag.as_bytes());
                    out.push(val.type_id().as_byte());
                    encode_value(val, out);
                }
            }
        }

        Value::VarList(items) => {
            varint::encode(items.len() as i64, out);
            for n in items {
                varint::encode(*n, out);
            }
        }

        Value::ObjType { component, ty } => {
            varint::encode(*component, out);
            varint::encode(*ty, out);
        }

        Value::ObjId { component, ty, id } => {
            varint::encode(*component, out);
            varint::encode(*ty, out);
            varint::encode(*id, out);
        }

        Value::Float(f) => out.extend_from_slice(&f.to_be_bytes()),
    }
}

/// Encode a value on its own, without a tag or type byte.
///
/// Useful for a nested struct that a caller frames itself; note this DOES emit
/// the `0x00` terminator for `Value::Struct`.
pub fn encode_value_only(value: &Value) -> Vec<u8> {
    let mut out = Vec::new();
    encode_value(value, &mut out);
    out
}

/// Convenience: the type byte a value will be written with.
pub fn type_byte_of(value: &Value) -> u8 {
    value.type_id().as_byte()
}

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

    #[test]
    fn members_sort_by_packed_tag_not_source_order() {
        let s = Struct::new()
            .with("ZZZZ", Value::Int(3))
            .with("AAAA", Value::Int(1))
            .with("MMMM", Value::Int(2));
        let bytes = encode(&s);
        // Each member is 3 tag + 1 type + 1 varint = 5 bytes; values must come
        // out 1, 2, 3.
        assert_eq!(bytes.len(), 15);
        assert_eq!([bytes[4], bytes[9], bytes[14]], [1, 2, 3]);
    }

    #[test]
    fn top_level_is_unterminated_but_nested_is_terminated() {
        let flat = encode(&Struct::new().with("A", Value::Int(1)));
        assert_eq!(
            flat.last(),
            Some(&1u8),
            "no trailing terminator at top level"
        );

        let nested = encode(&Struct::new().with(
            "OUTR",
            Value::Struct(Struct::new().with("A", Value::Int(1))),
        ));
        assert_eq!(
            nested.last(),
            Some(&0u8),
            "nested struct is 0x00 terminated"
        );
    }

    #[test]
    fn empty_nested_struct_is_a_bare_terminator() {
        let bytes = encode(&Struct::new().with("MTST", Value::Struct(Struct::new())));
        assert_eq!(bytes.len(), 5); // 3 tag + 1 type + 1 terminator
        assert_eq!(bytes[4], 0x00);
    }

    #[test]
    fn string_length_includes_the_nul() {
        let bytes = encode(&Struct::new().with("S", Value::String("ab".into())));
        // tag(3) type(1) len(1)=3 'a' 'b' NUL
        assert_eq!(&bytes[4..], &[0x03, b'a', b'b', 0x00]);
    }

    #[test]
    fn empty_string_is_length_one_plus_nul() {
        let bytes = encode(&Struct::new().with("S", Value::String(String::new())));
        assert_eq!(&bytes[4..], &[0x01, 0x00]);
    }

    #[test]
    fn blob_length_excludes_a_terminator() {
        let bytes = encode(&Struct::new().with("B", Value::Blob(vec![1, 2, 3])));
        assert_eq!(&bytes[4..], &[0x03, 1, 2, 3]);
    }

    #[test]
    fn float_is_big_endian_f32() {
        let bytes = encode(&Struct::new().with("F", Value::Float(1.5)));
        assert_eq!(&bytes[4..], &1.5f32.to_be_bytes());
    }
}
