//! Fire2 framing.
//!
//! ```text
//!   [0:4]   u32  payload length
//!   [4:6]   u16  metadata length
//!   [6:8]   u16  component
//!   [8:10]  u16  command          (notification id when msgType == NOTIFICATION)
//!   [10:13] u24  msgNum
//!   [13]    u8   (msgType << 5) | (userIndex & 0x1F)
//!   [14]    u8   options
//!   [15]    u8   reserved
//!
//!   wire = header(16) || metadata || payload
//! ```
//!
//! All fields big-endian. Sixteen bytes, not twelve.
//!
//! # Two wrong layouts this replaces
//!
//! This is the layout in `blaze_responder_v3b.py::fire2` — the code that
//! actually drove a retail FIFA 17 client through login. Two other
//! implementations in this repository disagree and are both wrong for FIFA 17:
//!
//!   * `fifa17-recon/tools/heat2.py::build_fire2_frame` packs
//!     `>IHHHHB3s`, which puts a `u16 msgId` at `[10:12]` and `msgType` at
//!     `[12]`. Its own module docstring still describes that layout. The
//!     responder carries a comment telling callers not to use it.
//!   * `fifa-blaze/crates/blaze-proto/src/frame.rs` implements a *12-byte*
//!     header with a `u16` length, nibble-packed type/options, and a JUMBO
//!     flag. That was a documented guess at FIFA 23's variant, written before
//!     the FIFA 17 recon; nothing has since validated it.
//!
//! There is **no error field** in a Fire2 header — that belongs to Fire v1's
//! 12-byte frame. There is also no jumbo-frame escape: the length is already a
//! full `u32`.

use crate::error::{Error, Result};

/// Header size in bytes.
pub const HEADER_LEN: usize = 16;

/// `msgType`, the top three bits of byte 13.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MsgType {
    Message,
    Reply,
    Notification,
    ErrorReply,
    Ping,
    PingReply,
    /// 6 and 7 are representable in three bits but have never been observed.
    Unknown(u8),
}

impl MsgType {
    pub fn from_bits(bits: u8) -> MsgType {
        match bits & 0x07 {
            0 => MsgType::Message,
            1 => MsgType::Reply,
            2 => MsgType::Notification,
            3 => MsgType::ErrorReply,
            4 => MsgType::Ping,
            5 => MsgType::PingReply,
            other => MsgType::Unknown(other),
        }
    }

    pub fn as_bits(self) -> u8 {
        match self {
            MsgType::Message => 0,
            MsgType::Reply => 1,
            MsgType::Notification => 2,
            MsgType::ErrorReply => 3,
            MsgType::Ping => 4,
            MsgType::PingReply => 5,
            MsgType::Unknown(v) => v & 0x07,
        }
    }

    pub fn name(self) -> &'static str {
        match self {
            MsgType::Message => "MESSAGE",
            MsgType::Reply => "REPLY",
            MsgType::Notification => "NOTIFICATION",
            MsgType::ErrorReply => "ERROR_REPLY",
            MsgType::Ping => "PING",
            MsgType::PingReply => "PING_REPLY",
            MsgType::Unknown(_) => "UNKNOWN",
        }
    }
}

/// A parsed Fire2 header.
///
/// `component`/`command` are plain integers on purpose. Naming them is a
/// game-specific concern: a FIFA 17 adapter owns the tables that turn
/// `0x0009/0x0007` into `Util::preAuth`, and a future FIFA 18 or FIFA 23
/// adapter may map the same numbers differently.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Header {
    pub payload_len: u32,
    pub metadata_len: u16,
    pub component: u16,
    pub command: u16,
    /// 24-bit request correlator.
    pub msg_num: u32,
    pub msg_type: MsgType,
    /// 5-bit local user slot.
    pub user_index: u8,
    pub options: u8,
    pub reserved: u8,
}

impl Header {
    pub fn new(component: u16, command: u16, msg_num: u32, msg_type: MsgType) -> Header {
        Header {
            payload_len: 0,
            metadata_len: 0,
            component,
            command,
            msg_num,
            msg_type,
            user_index: 0,
            options: 0,
            reserved: 0,
        }
    }

    /// Total wire size of this frame: header + metadata + payload.
    pub fn frame_len(&self) -> usize {
        HEADER_LEN + self.metadata_len as usize + self.payload_len as usize
    }

    /// Parse a header from the first 16 bytes of `buf`.
    pub fn parse(buf: &[u8]) -> Result<Header> {
        if buf.len() < HEADER_LEN {
            return Err(Error::ShortFrame {
                need: HEADER_LEN,
                have: buf.len(),
            });
        }
        Ok(Header {
            payload_len: u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]),
            metadata_len: u16::from_be_bytes([buf[4], buf[5]]),
            component: u16::from_be_bytes([buf[6], buf[7]]),
            command: u16::from_be_bytes([buf[8], buf[9]]),
            msg_num: ((buf[10] as u32) << 16) | ((buf[11] as u32) << 8) | buf[12] as u32,
            msg_type: MsgType::from_bits(buf[13] >> 5),
            user_index: buf[13] & 0x1F,
            options: buf[14],
            reserved: buf[15],
        })
    }

    /// Serialise the 16 header bytes.
    pub fn write(&self, out: &mut Vec<u8>) {
        out.extend_from_slice(&self.payload_len.to_be_bytes());
        out.extend_from_slice(&self.metadata_len.to_be_bytes());
        out.extend_from_slice(&self.component.to_be_bytes());
        out.extend_from_slice(&self.command.to_be_bytes());
        out.push(((self.msg_num >> 16) & 0xFF) as u8);
        out.push(((self.msg_num >> 8) & 0xFF) as u8);
        out.push((self.msg_num & 0xFF) as u8);
        out.push((self.msg_type.as_bits() << 5) | (self.user_index & 0x1F));
        out.push(self.options);
        out.push(self.reserved);
    }

    pub fn to_bytes(&self) -> [u8; HEADER_LEN] {
        let mut v = Vec::with_capacity(HEADER_LEN);
        self.write(&mut v);
        let mut out = [0u8; HEADER_LEN];
        out.copy_from_slice(&v);
        out
    }
}

/// A complete Fire2 frame: header plus its metadata and payload bytes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Frame {
    pub header: Header,
    pub metadata: Vec<u8>,
    /// Raw payload bytes. Undecoded on purpose — the payload is Heat2/TDF, but
    /// framing must stay usable for capture and diagnostics even when the body
    /// does not parse.
    pub payload: Vec<u8>,
}

impl Frame {
    /// Build a frame, deriving the length fields from the buffers.
    pub fn new(
        component: u16,
        command: u16,
        msg_num: u32,
        msg_type: MsgType,
        payload: Vec<u8>,
    ) -> Frame {
        let mut header = Header::new(component, command, msg_num, msg_type);
        header.payload_len = payload.len() as u32;
        Frame {
            header,
            metadata: Vec::new(),
            payload,
        }
    }

    pub fn with_metadata(mut self, metadata: Vec<u8>) -> Frame {
        self.header.metadata_len = metadata.len() as u16;
        self.metadata = metadata;
        self
    }

    pub fn with_user_index(mut self, user_index: u8) -> Frame {
        self.header.user_index = user_index & 0x1F;
        self
    }

    pub fn with_options(mut self, options: u8) -> Frame {
        self.header.options = options;
        self
    }

    /// A reply echoes component, command, msgNum and userIndex verbatim, and
    /// changes only the msgType bits.
    pub fn reply_to(request: &Header, payload: Vec<u8>) -> Frame {
        let mut frame = Frame::new(
            request.component,
            request.command,
            request.msg_num,
            MsgType::Reply,
            payload,
        );
        frame.header.user_index = request.user_index;
        frame
    }

    /// An unsolicited server push. `msgNum` is 0: notifications are not
    /// correlated to any request.
    pub fn notification(component: u16, notify_id: u16, payload: Vec<u8>) -> Frame {
        Frame::new(component, notify_id, 0, MsgType::Notification, payload)
    }

    pub fn encode(&self) -> Vec<u8> {
        let mut out = Vec::with_capacity(self.wire_len());
        self.encode_into(&mut out);
        out
    }

    pub fn encode_into(&self, out: &mut Vec<u8>) {
        // Trust the buffers over any stale length in the header.
        let mut header = self.header;
        header.payload_len = self.payload.len() as u32;
        header.metadata_len = self.metadata.len() as u16;
        header.write(out);
        out.extend_from_slice(&self.metadata);
        out.extend_from_slice(&self.payload);
    }

    pub fn wire_len(&self) -> usize {
        HEADER_LEN + self.metadata.len() + self.payload.len()
    }

    /// Parse one frame from the front of `buf`, returning it and the number of
    /// bytes consumed.
    pub fn parse(buf: &[u8]) -> Result<(Frame, usize)> {
        let header = Header::parse(buf)?;
        let total = header.frame_len();
        if buf.len() < total {
            return Err(Error::ShortFrame {
                need: total,
                have: buf.len(),
            });
        }
        let meta_end = HEADER_LEN + header.metadata_len as usize;
        Ok((
            Frame {
                header,
                metadata: buf[HEADER_LEN..meta_end].to_vec(),
                payload: buf[meta_end..total].to_vec(),
            },
            total,
        ))
    }
}

/// How many bytes a frame starting at `buf` needs in total, if its header is
/// complete. `None` means the header itself has not arrived yet.
///
/// This is what a stream reader needs: Blaze frames arrive coalesced and split
/// across TCP segments, so a reader must size each frame before consuming it.
pub fn frame_size_hint(buf: &[u8]) -> Option<usize> {
    Header::parse(buf).ok().map(|h| h.frame_len())
}

/// Split a buffer into as many whole frames as it contains.
///
/// Returns the frames plus the number of bytes consumed; any trailing partial
/// frame is left for the caller to retry once more bytes arrive.
pub fn parse_all(buf: &[u8]) -> Result<(Vec<Frame>, usize)> {
    let mut frames = Vec::new();
    let mut off = 0;
    while off < buf.len() {
        match Frame::parse(&buf[off..]) {
            Ok((frame, used)) => {
                off += used;
                frames.push(frame);
            }
            Err(Error::ShortFrame { .. }) => break,
            Err(e) => return Err(e),
        }
    }
    Ok((frames, off))
}

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

    #[test]
    fn header_is_sixteen_bytes() {
        let h = Header::new(0x0009, 0x0007, 1, MsgType::Reply);
        assert_eq!(h.to_bytes().len(), 16);
    }

    #[test]
    fn field_offsets_match_the_proven_layout() {
        let mut h = Header::new(0x0009, 0x0007, 0x123456, MsgType::Reply);
        h.payload_len = 0x11223344;
        h.metadata_len = 0x5566;
        h.user_index = 3;
        h.options = 0x42;
        let b = h.to_bytes();
        assert_eq!(&b[0..4], &[0x11, 0x22, 0x33, 0x44]); // payload len u32
        assert_eq!(&b[4..6], &[0x55, 0x66]); // metadata len u16
        assert_eq!(&b[6..8], &[0x00, 0x09]); // component
        assert_eq!(&b[8..10], &[0x00, 0x07]); // command
        assert_eq!(&b[10..13], &[0x12, 0x34, 0x56]); // msgNum u24
        assert_eq!(b[13], (1 << 5) | 3); // msgType | userIndex
        assert_eq!(b[14], 0x42);
        assert_eq!(b[15], 0x00);
    }

    #[test]
    fn reply_bit_pattern_is_0x20_and_notification_is_0x40() {
        let reply = Frame::new(1, 0x0A, 7, MsgType::Reply, vec![]).encode();
        assert_eq!(reply[13], 0x20);
        let notify = Frame::notification(0x7802, 0x0008, vec![]).encode();
        assert_eq!(notify[13], 0x40);
    }

    #[test]
    fn payload_length_exceeds_sixteen_bits_without_a_jumbo_flag() {
        // The stale 12-byte implementation would have needed an escape here.
        let frame = Frame::new(1, 1, 1, MsgType::Reply, vec![0x5A; 70_000]);
        let bytes = frame.encode();
        let parsed = Header::parse(&bytes).unwrap();
        assert_eq!(parsed.payload_len, 70_000);
        assert_eq!(bytes.len(), 16 + 70_000);
    }

    #[test]
    fn round_trips_with_metadata() {
        let frame = Frame::new(0x0009, 0x0007, 5, MsgType::Message, b"payload".to_vec())
            .with_metadata(vec![0xde, 0xad, 0xbe, 0xef]);
        let bytes = frame.encode();
        let (back, used) = Frame::parse(&bytes).unwrap();
        assert_eq!(used, bytes.len());
        assert_eq!(back.metadata, vec![0xde, 0xad, 0xbe, 0xef]);
        assert_eq!(back.payload, b"payload");
        assert_eq!(back.encode(), bytes);
    }

    #[test]
    fn reply_echoes_routing_fields() {
        let req = Header {
            user_index: 5,
            ..Header::new(0x0001, 0x000A, 0x2222, MsgType::Message)
        };
        let reply = Frame::reply_to(&req, vec![1, 2, 3]);
        assert_eq!(reply.header.component, 0x0001);
        assert_eq!(reply.header.command, 0x000A);
        assert_eq!(reply.header.msg_num, 0x2222);
        assert_eq!(reply.header.user_index, 5);
        assert_eq!(reply.header.msg_type, MsgType::Reply);
    }

    #[test]
    fn msg_num_is_twenty_four_bits() {
        let frame = Frame::new(1, 1, 0xFFFFFF, MsgType::Reply, vec![]);
        let back = Header::parse(&frame.encode()).unwrap();
        assert_eq!(back.msg_num, 0xFFFFFF);
    }

    #[test]
    fn splits_coalesced_frames_and_leaves_a_partial_tail() {
        let a = Frame::new(9, 2, 1, MsgType::Reply, vec![1]).encode();
        let b = Frame::new(1, 10, 2, MsgType::Reply, vec![2, 3]).encode();
        let mut stream = a.clone();
        stream.extend_from_slice(&b);
        stream.extend_from_slice(&[0x00, 0x00]); // partial third header

        let (frames, used) = parse_all(&stream).unwrap();
        assert_eq!(frames.len(), 2);
        assert_eq!(used, a.len() + b.len());
        assert_eq!(frames[1].payload, vec![2, 3]);
    }

    #[test]
    fn short_buffer_reports_what_it_needs() {
        assert_eq!(
            Header::parse(&[0u8; 8]),
            Err(Error::ShortFrame { need: 16, have: 8 })
        );
        assert_eq!(frame_size_hint(&[0u8; 8]), None);
    }
}
