//! Type descriptor size computation.
//!
//! Go's `abi.Type.DescriptorSize()` computes the total size of a concrete type
//! descriptor including the base `abi.Type`, the concrete-type extra fields,
//! an optional `UncommonType`, variable-length data (parameters, fields, methods),
//! and the method array.
//!
//! Source: `src/internal/abi/type.go:750-799`

use crate::structures::{
    abitype::AbiType,
    arraytype::ArrayTypeExtra,
    chantype::ChanTypeExtra,
    elemtype::ElemTypeExtra,
    functype::FuncTypeExtra,
    interfacetype::InterfaceTypeExtra,
    kind,
    maptype::MapTypeExtra,
    method::{GoImethod, GoMethod},
    structtype::{GoStructField, StructTypeExtra},
    uncommon::UncommonType,
    util::align_up,
};

/// Compute the total descriptor size for a type, equivalent to Go's
/// `abi.Type.DescriptorSize()` from `src/internal/abi/type.go:750-799`.
///
/// The total is: concrete_type_size + uncommon_type_size + variable_data + methods.
pub fn descriptor_size(type_data: &[u8], abi_type: &AbiType, ps: u8) -> Option<usize> {
    let p = ps as usize;
    let base_sz = AbiType::size(ps);

    let (concrete_sz, var_sz) = match abi_type.kind() {
        kind::ARRAY => (base_sz.checked_add(ArrayTypeExtra::size(ps))?, 0usize),
        kind::CHAN => (base_sz.checked_add(ChanTypeExtra::size(ps))?, 0),
        kind::FUNC => {
            let extra_off = base_sz;
            let extra = FuncTypeExtra::parse(type_data.get(extra_off..)?)?;
            let param_count = (extra.in_count as usize).checked_add(extra.num_out() as usize)?;
            // Params follow FuncTypeExtra after ptr-aligned padding.
            let after_extra = base_sz.checked_add(FuncTypeExtra::SIZE)?;
            let params_off = align_up(after_extra, p)?;
            let pad = params_off.checked_sub(after_extra)?;
            let extra_total = FuncTypeExtra::SIZE.checked_add(pad)?;
            (
                base_sz.checked_add(extra_total)?,
                param_count.checked_mul(p)?,
            )
        }
        kind::INTERFACE => {
            let extra_off = base_sz;
            let extra = InterfaceTypeExtra::parse(type_data.get(extra_off..)?, ps)?;
            (
                base_sz.checked_add(InterfaceTypeExtra::size(ps))?,
                (extra.methods.len as usize).checked_mul(GoImethod::SIZE)?,
            )
        }
        kind::MAP => (base_sz.checked_add(MapTypeExtra::size(ps))?, 0),
        kind::POINTER => (base_sz.checked_add(ElemTypeExtra::size(ps))?, 0),
        kind::SLICE => (base_sz.checked_add(ElemTypeExtra::size(ps))?, 0),
        kind::STRUCT => {
            let extra_off = base_sz;
            let extra = StructTypeExtra::parse(type_data.get(extra_off..)?, ps)?;
            (
                base_sz.checked_add(StructTypeExtra::size(ps))?,
                (extra.fields.len as usize).checked_mul(GoStructField::size(ps))?,
            )
        }
        kind::BOOL
        | kind::INT
        | kind::INT8
        | kind::INT16
        | kind::INT32
        | kind::INT64
        | kind::UINT
        | kind::UINT8
        | kind::UINT16
        | kind::UINT32
        | kind::UINT64
        | kind::UINTPTR
        | kind::FLOAT32
        | kind::FLOAT64
        | kind::COMPLEX64
        | kind::COMPLEX128
        | kind::STRING
        | kind::UNSAFE_POINTER => (base_sz, 0),
        _ => return None,
    };

    let mut total = concrete_sz;

    let mcount = if abi_type.has_uncommon() {
        let ut_off = concrete_sz;
        let ut = UncommonType::parse(type_data.get(ut_off..)?)?;
        total = total.checked_add(UncommonType::SIZE)?;
        ut.mcount as usize
    } else {
        0
    };

    total = total.checked_add(var_sz)?;
    total = total.checked_add(mcount.checked_mul(GoMethod::SIZE)?)?;

    Some(total)
}

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

    fn make_abitype_buf(ps: u8, kind: u8, tflag: u8) -> Vec<u8> {
        let size = AbiType::size(ps);
        let mut buf = vec![0u8; size];
        let p = ps as usize;
        // Kind_ is at offset 2*ps + 7
        buf[2 * p + 7] = kind;
        // TFlag is at offset 2*ps + 4
        buf[2 * p + 4] = tflag;
        buf
    }

    #[test]
    fn scalar_type_size() {
        let buf = make_abitype_buf(8, kind::INT, 0);
        let abi = AbiType::parse(&buf, 8).unwrap();
        let sz = descriptor_size(&buf, &abi, 8).unwrap();
        assert_eq!(sz, AbiType::size(8));
    }

    #[test]
    fn pointer_type_size() {
        let mut buf = make_abitype_buf(8, kind::POINTER, 0);
        buf.extend(vec![0u8; ElemTypeExtra::size(8)]);
        let abi = AbiType::parse(&buf, 8).unwrap();
        let sz = descriptor_size(&buf, &abi, 8).unwrap();
        assert_eq!(sz, AbiType::size(8) + ElemTypeExtra::size(8));
    }

    #[test]
    fn unknown_kind_returns_none() {
        let buf = make_abitype_buf(8, 0xFF, 0);
        let abi = AbiType::parse(&buf, 8).unwrap();
        assert!(descriptor_size(&buf, &abi, 8).is_none());
    }
}
