//! The Heat2 value model.
//!
//! Deliberately a *generic* Blaze value tree: it knows the eleven wire types
//! and nothing about any game. No FIFA 17 command IDs, field names, or response
//! schemas appear here or anywhere else in this crate — those belong to a game
//! adapter one layer up.

use super::tag::Tag;

/// The eleven Heat2 type bytes.
///
/// `Int` through `Struct` are validated against captured FIFA 17 traffic. The
/// rest are marked UNVERIFIED in the oracle (`heat2.py`): they are absent from
/// every capture we hold, and their layouts are consistent-with rather than
/// proven-against EA. `TypeId::is_verified` carries that distinction into code
/// so it cannot be lost by a reader who skips the comments.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum TypeId {
    Int = 0x00,
    String = 0x01,
    Blob = 0x02,
    Struct = 0x03,
    List = 0x04,
    Map = 0x05,
    Union = 0x06,
    VarList = 0x07,
    ObjType = 0x08,
    ObjId = 0x09,
    Float = 0x0A,
}

/// Union discriminator meaning "no member set". UNVERIFIED.
pub const UNION_UNSET: u8 = 0x7F;

impl TypeId {
    pub fn from_byte(b: u8) -> Option<TypeId> {
        Some(match b {
            0x00 => TypeId::Int,
            0x01 => TypeId::String,
            0x02 => TypeId::Blob,
            0x03 => TypeId::Struct,
            0x04 => TypeId::List,
            0x05 => TypeId::Map,
            0x06 => TypeId::Union,
            0x07 => TypeId::VarList,
            0x08 => TypeId::ObjType,
            0x09 => TypeId::ObjId,
            0x0A => TypeId::Float,
            _ => return None,
        })
    }

    pub fn as_byte(self) -> u8 {
        self as u8
    }

    pub fn name(self) -> &'static str {
        match self {
            TypeId::Int => "int",
            TypeId::String => "string",
            TypeId::Blob => "blob",
            TypeId::Struct => "struct",
            TypeId::List => "list",
            TypeId::Map => "map",
            TypeId::Union => "union",
            TypeId::VarList => "varlist",
            TypeId::ObjType => "objtype",
            TypeId::ObjId => "objid",
            TypeId::Float => "float",
        }
    }

    pub fn from_name(name: &str) -> Option<TypeId> {
        Some(match name {
            "int" => TypeId::Int,
            "string" => TypeId::String,
            "blob" => TypeId::Blob,
            "struct" => TypeId::Struct,
            "list" => TypeId::List,
            "map" => TypeId::Map,
            "union" => TypeId::Union,
            "varlist" => TypeId::VarList,
            "objtype" => TypeId::ObjType,
            "objid" => TypeId::ObjId,
            "float" => TypeId::Float,
            _ => return None,
        })
    }

    /// True when the layout is proven against captured FIFA 17 traffic.
    ///
    /// A `false` here is a standing warning: the codec will round-trip such a
    /// value against itself and against the Python oracle, and that still says
    /// nothing about what a real Blaze server emits.
    pub fn is_verified(self) -> bool {
        matches!(
            self,
            TypeId::Int | TypeId::String | TypeId::Blob | TypeId::Struct
        )
    }
}

/// A decoded Heat2 value.
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
    Int(i64),
    String(String),
    Blob(Vec<u8>),
    Struct(Struct),
    List {
        elem: TypeId,
        items: Vec<Value>,
    },
    Map {
        key: TypeId,
        val: TypeId,
        entries: Vec<(Value, Value)>,
    },
    Union {
        key: u8,
        member: Option<Box<(Tag, Value)>>,
    },
    VarList(Vec<i64>),
    ObjType {
        component: i64,
        ty: i64,
    },
    ObjId {
        component: i64,
        ty: i64,
        id: i64,
    },
    Float(f32),
}

impl Value {
    pub fn type_id(&self) -> TypeId {
        match self {
            Value::Int(_) => TypeId::Int,
            Value::String(_) => TypeId::String,
            Value::Blob(_) => TypeId::Blob,
            Value::Struct(_) => TypeId::Struct,
            Value::List { .. } => TypeId::List,
            Value::Map { .. } => TypeId::Map,
            Value::Union { .. } => TypeId::Union,
            Value::VarList(_) => TypeId::VarList,
            Value::ObjType { .. } => TypeId::ObjType,
            Value::ObjId { .. } => TypeId::ObjId,
            Value::Float(_) => TypeId::Float,
        }
    }

    pub fn as_int(&self) -> Option<i64> {
        match self {
            Value::Int(v) => Some(*v),
            _ => None,
        }
    }

    pub fn as_str(&self) -> Option<&str> {
        match self {
            Value::String(s) => Some(s),
            _ => None,
        }
    }

    pub fn as_struct(&self) -> Option<&Struct> {
        match self {
            Value::Struct(s) => Some(s),
            _ => None,
        }
    }
}

/// An ordered set of tagged members.
///
/// A `Vec`, not a map: the wire format is a sequence, duplicate tags are
/// physically representable, and encoding has to sort by packed tag anyway.
/// Keeping the sequence means a decoded frame can be re-encoded byte-for-byte
/// even when it contains something a map would silently drop.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Struct {
    pub fields: Vec<(Tag, Value)>,
}

impl Struct {
    pub fn new() -> Struct {
        Struct { fields: Vec::new() }
    }

    pub fn with(mut self, tag: &str, value: Value) -> Struct {
        self.fields.push((Tag::from_label(tag), value));
        self
    }

    pub fn push(&mut self, tag: &str, value: Value) {
        self.fields.push((Tag::from_label(tag), value));
    }

    /// First member with this tag, if any.
    pub fn get(&self, tag: &str) -> Option<&Value> {
        let t = Tag::from_label(tag);
        self.fields.iter().find(|(k, _)| *k == t).map(|(_, v)| v)
    }

    pub fn len(&self) -> usize {
        self.fields.len()
    }

    pub fn is_empty(&self) -> bool {
        self.fields.is_empty()
    }

    pub fn iter(&self) -> impl Iterator<Item = &(Tag, Value)> {
        self.fields.iter()
    }
}

impl FromIterator<(Tag, Value)> for Struct {
    fn from_iter<I: IntoIterator<Item = (Tag, Value)>>(iter: I) -> Struct {
        Struct {
            fields: iter.into_iter().collect(),
        }
    }
}

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

    #[test]
    fn type_bytes_round_trip() {
        for b in 0x00u8..=0x0A {
            let t = TypeId::from_byte(b).expect("known type");
            assert_eq!(t.as_byte(), b);
            assert_eq!(TypeId::from_name(t.name()), Some(t));
        }
        assert_eq!(TypeId::from_byte(0x0B), None);
    }

    #[test]
    fn only_capture_backed_types_claim_verification() {
        assert!(TypeId::Int.is_verified());
        assert!(TypeId::Struct.is_verified());
        assert!(!TypeId::List.is_verified());
        assert!(!TypeId::Float.is_verified());
    }

    #[test]
    fn struct_lookup_finds_members() {
        let s = Struct::new()
            .with("PID", Value::Int(33068179))
            .with("NAME", Value::String("x".into()));
        assert_eq!(s.get("PID").and_then(Value::as_int), Some(33068179));
        assert_eq!(s.get("NOPE"), None);
    }
}
