//! Parse and introspect btf information, from files or loaded objects.
//!
//! To find a specific type you can use one of 3 methods
//!
//! - [Btf::type_by_name]
//! - [Btf::type_by_id]
//! - [Btf::type_by_kind]
//!
//! All of these are generic over `K`, which is any type that can be created from a [`BtfType`],
//! for all of these methods, not finding any type by the passed parameter or finding a type of
//! another [`BtfKind`] will result in a [`None`] being returned (or filtered out in the case of
//! [`Btf::type_by_kind`]). If you want to get a type independently of the kind, just make sure `K`
//! binds to [`BtfType`].

pub mod types;

use std::ffi::CStr;
use std::ffi::CString;
use std::ffi::OsStr;
use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use std::io;
use std::marker::PhantomData;
use std::mem::size_of;
use std::num::NonZeroUsize;
use std::ops::Deref;
use std::os::raw::c_ulong;
use std::os::raw::c_void;
use std::os::unix::prelude::AsRawFd;
use std::os::unix::prelude::FromRawFd;
use std::os::unix::prelude::OsStrExt;
use std::os::unix::prelude::OwnedFd;
use std::path::Path;
use std::ptr;
use std::ptr::NonNull;

use crate::util::parse_ret_i32;
use crate::util::validate_bpf_ret;
use crate::AsRawLibbpf;
use crate::Error;
use crate::ErrorExt as _;
use crate::Result;

use self::types::Composite;

/// The various btf types.
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[repr(u32)]
pub enum BtfKind {
    /// [Void](types::Void)
    Void = 0,
    /// [Int](types::Int)
    Int,
    /// [Ptr](types::Ptr)
    Ptr,
    /// [Array](types::Array)
    Array,
    /// [Struct](types::Struct)
    Struct,
    /// [Union](types::Union)
    Union,
    /// [Enum](types::Enum)
    Enum,
    /// [Fwd](types::Fwd)
    Fwd,
    /// [Typedef](types::Typedef)
    Typedef,
    /// [Volatile](types::Volatile)
    Volatile,
    /// [Const](types::Const)
    Const,
    /// [Restrict](types::Restrict)
    Restrict,
    /// [Func](types::Func)
    Func,
    /// [FuncProto](types::FuncProto)
    FuncProto,
    /// [Var](types::Var)
    Var,
    /// [DataSec](types::DataSec)
    DataSec,
    /// [Float](types::Float)
    Float,
    /// [DeclTag](types::DeclTag)
    DeclTag,
    /// [TypeTag](types::TypeTag)
    TypeTag,
    /// [Enum64](types::Enum64)
    Enum64,
}

impl TryFrom<u32> for BtfKind {
    type Error = u32;

    fn try_from(value: u32) -> Result<Self, Self::Error> {
        use BtfKind::*;

        Ok(match value {
            x if x == Void as u32 => Void,
            x if x == Int as u32 => Int,
            x if x == Ptr as u32 => Ptr,
            x if x == Array as u32 => Array,
            x if x == Struct as u32 => Struct,
            x if x == Union as u32 => Union,
            x if x == Enum as u32 => Enum,
            x if x == Fwd as u32 => Fwd,
            x if x == Typedef as u32 => Typedef,
            x if x == Volatile as u32 => Volatile,
            x if x == Const as u32 => Const,
            x if x == Restrict as u32 => Restrict,
            x if x == Func as u32 => Func,
            x if x == FuncProto as u32 => FuncProto,
            x if x == Var as u32 => Var,
            x if x == DataSec as u32 => DataSec,
            x if x == Float as u32 => Float,
            x if x == DeclTag as u32 => DeclTag,
            x if x == TypeTag as u32 => TypeTag,
            x if x == Enum64 as u32 => Enum64,
            v => return Err(v),
        })
    }
}

/// The id of a btf type.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TypeId(u32);

impl From<u32> for TypeId {
    fn from(s: u32) -> Self {
        Self(s)
    }
}

impl From<TypeId> for u32 {
    fn from(t: TypeId) -> Self {
        t.0
    }
}

impl Display for TypeId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

#[derive(Debug)]
enum DropPolicy {
    Nothing,
    SelfPtrOnly,
    ObjPtr(*mut libbpf_sys::bpf_object),
}

/// The btf information of a bpf object.
///
/// The lifetime bound protects against this object outliving its source. This can happen when it
/// was derived from an [`Object`](super::Object), which owns the data this structs points too. When
/// instead the [`Btf::from_path`] method is used, the lifetime will be `'static` since it doesn't
/// borrow from anything.
pub struct Btf<'source> {
    ptr: NonNull<libbpf_sys::btf>,
    drop_policy: DropPolicy,
    _marker: PhantomData<&'source ()>,
}

impl Btf<'static> {
    /// Load the btf information from specified path.
    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
        fn inner(path: &Path) -> Result<Btf<'static>> {
            let path = CString::new(path.as_os_str().as_bytes()).map_err(|_| {
                Error::with_invalid_data(format!("invalid path {path:?}, has null bytes"))
            })?;
            let ptr = unsafe { libbpf_sys::btf__parse(path.as_ptr(), ptr::null_mut()) };
            let ptr = validate_bpf_ret(ptr).context("failed to parse BTF information")?;
            Ok(Btf {
                ptr,
                drop_policy: DropPolicy::SelfPtrOnly,
                _marker: PhantomData,
            })
        }
        inner(path.as_ref())
    }

    /// Load the vmlinux btf information from few well-known locations.
    pub fn from_vmlinux() -> Result<Self> {
        let ptr = unsafe { libbpf_sys::btf__load_vmlinux_btf() };
        let ptr = validate_bpf_ret(ptr).context("failed to load BTF from vmlinux")?;

        Ok(Btf {
            ptr,
            drop_policy: DropPolicy::SelfPtrOnly,
            _marker: PhantomData,
        })
    }

    /// Load the btf information of an bpf object from a program id.
    pub fn from_prog_id(id: u32) -> Result<Self> {
        let fd = parse_ret_i32(unsafe { libbpf_sys::bpf_prog_get_fd_by_id(id) })?;
        let fd = unsafe {
            // SAFETY: parse_ret_i32 will check that this fd is above -1
            OwnedFd::from_raw_fd(fd)
        };
        let mut info = libbpf_sys::bpf_prog_info::default();
        parse_ret_i32(unsafe {
            libbpf_sys::bpf_obj_get_info_by_fd(
                fd.as_raw_fd(),
                (&mut info as *mut libbpf_sys::bpf_prog_info).cast::<c_void>(),
                &mut (size_of::<libbpf_sys::bpf_prog_info>() as u32),
            )
        })?;

        let ptr = unsafe { libbpf_sys::btf__load_from_kernel_by_id(info.btf_id) };
        let ptr = validate_bpf_ret(ptr).context("failed to load BTF from kernel")?;

        Ok(Self {
            ptr,
            drop_policy: DropPolicy::SelfPtrOnly,
            _marker: PhantomData,
        })
    }
}

impl<'btf> Btf<'btf> {
    /// Create a new `Btf` instance from the given [`libbpf_sys::bpf_object`].
    pub fn from_bpf_object(obj: &'btf libbpf_sys::bpf_object) -> Result<Option<Self>> {
        Self::from_bpf_object_raw(obj)
    }

    fn from_bpf_object_raw(obj: *const libbpf_sys::bpf_object) -> Result<Option<Self>> {
        let ptr = unsafe {
            // SAFETY: the obj pointer is valid since it's behind a reference.
            libbpf_sys::bpf_object__btf(obj)
        };
        // Contrary to general `libbpf` contract, `bpf_object__btf` may
        // return `NULL` without setting `errno`.
        if ptr.is_null() {
            return Ok(None)
        }
        let ptr = validate_bpf_ret(ptr).context("failed to create BTF from BPF object")?;
        let slf = Self {
            ptr,
            drop_policy: DropPolicy::Nothing,
            _marker: PhantomData,
        };
        Ok(Some(slf))
    }

    /// From raw bytes coming from an object file.
    pub fn from_raw(name: &'btf str, object_file: &'btf [u8]) -> Result<Option<Self>> {
        let cname = CString::new(name)
            .map_err(|_| Error::with_invalid_data(format!("invalid path {name:?}, has null bytes")))
            .unwrap();

        let obj_opts = libbpf_sys::bpf_object_open_opts {
            sz: size_of::<libbpf_sys::bpf_object_open_opts>() as libbpf_sys::size_t,
            object_name: cname.as_ptr(),
            ..Default::default()
        };

        let ptr = unsafe {
            libbpf_sys::bpf_object__open_mem(
                object_file.as_ptr() as *const c_void,
                object_file.len() as c_ulong,
                &obj_opts,
            )
        };

        let mut bpf_obj = validate_bpf_ret(ptr).context("failed to open BPF object from memory")?;
        // SAFETY: The pointer has been validated.
        let bpf_obj = unsafe { bpf_obj.as_mut() };
        match Self::from_bpf_object_raw(bpf_obj) {
            Ok(Some(this)) => Ok(Some(Self {
                drop_policy: DropPolicy::ObjPtr(bpf_obj),
                ..this
            })),
            x => {
                // SAFETY: The obj pointer is valid because we checked
                //         its validity.
                unsafe {
                    // We free it here, otherwise it will be a memory
                    // leak as this codepath (Ok(None) | Err(e)) does
                    // not reference it anymore and as such it can be
                    // dropped.
                    libbpf_sys::bpf_object__close(bpf_obj)
                };
                x
            }
        }
    }

    /// Gets a string at a given offset.
    ///
    /// Returns [`None`] when the offset is out of bounds or if the name is empty.
    fn name_at(&self, offset: u32) -> Option<&OsStr> {
        let name = unsafe {
            // SAFETY:
            // Assuming that btf is a valid pointer, this is always okay to call.
            libbpf_sys::btf__name_by_offset(self.ptr.as_ptr(), offset)
        };
        NonNull::new(name as *mut _)
            .map(|p| unsafe {
                // SAFETY: a non-null pointer coming from libbpf is always valid
                OsStr::from_bytes(CStr::from_ptr(p.as_ptr()).to_bytes())
            })
            .filter(|s| !s.is_empty()) // treat empty strings as none
    }

    /// Whether this btf instance has no types.
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }

    /// The number of [BtfType]s in this object.
    pub fn len(&self) -> usize {
        unsafe {
            // SAFETY: the btf pointer is valid.
            libbpf_sys::btf__type_cnt(self.ptr.as_ptr()) as usize
        }
    }

    /// The btf pointer size.
    pub fn ptr_size(&self) -> Result<NonZeroUsize> {
        let sz = unsafe { libbpf_sys::btf__pointer_size(self.ptr.as_ptr()) as usize };
        NonZeroUsize::new(sz).ok_or_else(|| {
            Error::with_io_error(io::ErrorKind::Other, "could not determine pointer size")
        })
    }

    /// Find a btf type by name
    ///
    /// # Panics
    /// If `name` has null bytes.
    pub fn type_by_name<'s, K>(&'s self, name: &str) -> Option<K>
    where
        K: TryFrom<BtfType<'s>>,
    {
        let c_string = CString::new(name)
            .map_err(|_| Error::with_invalid_data(format!("{name:?} contains null bytes")))
            .unwrap();
        let ty = unsafe {
            // SAFETY: the btf pointer is valid and the c_string pointer was created from safe code
            // therefore it's also valid.
            libbpf_sys::btf__find_by_name(self.ptr.as_ptr(), c_string.as_ptr())
        };
        if ty < 0 {
            None
        } else {
            self.type_by_id(TypeId(ty as _))
        }
    }

    /// Find a type by it's [TypeId].
    pub fn type_by_id<'s, K>(&'s self, type_id: TypeId) -> Option<K>
    where
        K: TryFrom<BtfType<'s>>,
    {
        let btf_type = unsafe {
            // SAFETY: the btf pointer is valid.
            libbpf_sys::btf__type_by_id(self.ptr.as_ptr(), type_id.0)
        };

        let btf_type = NonNull::new(btf_type as *mut libbpf_sys::btf_type)?;

        let ty = unsafe {
            // SAFETY: if it is non-null then it points to a valid type.
            btf_type.as_ref()
        };

        let name = self.name_at(ty.name_off);

        BtfType {
            type_id,
            name,
            source: self,
            ty,
        }
        .try_into()
        .ok()
    }

    /// Find all types of a specific type kind.
    pub fn type_by_kind<'s, K>(&'s self) -> impl Iterator<Item = K> + 's
    where
        K: TryFrom<BtfType<'s>>,
    {
        (1..self.len() as u32)
            .map(TypeId::from)
            .filter_map(|id| self.type_by_id(id))
            .filter_map(|t| K::try_from(t).ok())
    }
}

impl AsRawLibbpf for Btf<'_> {
    type LibbpfType = libbpf_sys::btf;

    /// Retrieve the underlying [`libbpf_sys::btf`] object.
    fn as_libbpf_object(&self) -> NonNull<Self::LibbpfType> {
        self.ptr
    }
}

impl Debug for Btf<'_> {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        struct BtfDumper<'btf>(&'btf Btf<'btf>);

        impl Debug for BtfDumper<'_> {
            fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
                f.debug_list()
                    .entries(
                        (1..self.0.len())
                            .map(|i| TypeId::from(i as u32))
                            // SANITY: A type with this ID should always exist
                            //         given that BTF IDs are fully populated up
                            //         to `len`. Conversion to `BtfType` is
                            //         always infallible.
                            .map(|id| self.0.type_by_id::<BtfType<'_>>(id).unwrap()),
                    )
                    .finish()
            }
        }

        f.debug_tuple("Btf<'_>").field(&BtfDumper(self)).finish()
    }
}

impl Drop for Btf<'_> {
    fn drop(&mut self) {
        match self.drop_policy {
            DropPolicy::Nothing => {}
            DropPolicy::SelfPtrOnly => {
                unsafe {
                    // SAFETY: the btf pointer is valid.
                    libbpf_sys::btf__free(self.ptr.as_ptr())
                }
            }
            DropPolicy::ObjPtr(obj) => {
                unsafe {
                    // SAFETY: the bpf obj pointer is valid.
                    // closing the obj automatically frees the associated btf object.
                    libbpf_sys::bpf_object__close(obj)
                }
            }
        }
    }
}

/// An undiscriminated btf type
///
/// The [`btf_type_match`](crate::btf_type_match) can be used to match on the variants of this type
/// as if it was a rust enum.
///
/// You can also use the [`TryFrom`] trait to convert to any of the possible [`types`].
#[derive(Clone, Copy)]
pub struct BtfType<'btf> {
    type_id: TypeId,
    name: Option<&'btf OsStr>,
    source: &'btf Btf<'btf>,
    ///  the __bindgen_anon_1 field is a union defined as
    ///  ```no_run
    ///  union btf_type__bindgen_ty_1 {
    ///      size_: u32,
    ///      type_: u32,
    ///  }
    ///  ```
    ty: &'btf libbpf_sys::btf_type,
}

impl Debug for BtfType<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("BtfType")
            .field("type_id", &self.type_id)
            .field("name", &self.name())
            .field("source", &self.source.as_libbpf_object())
            .field("ty", &(self.ty as *const _))
            .finish()
    }
}

impl<'btf> BtfType<'btf> {
    /// This type's type id.
    #[inline]
    pub fn type_id(&self) -> TypeId {
        self.type_id
    }

    /// This type's name.
    #[inline]
    pub fn name(&'_ self) -> Option<&'btf OsStr> {
        self.name
    }

    /// This type's kind.
    #[inline]
    pub fn kind(&self) -> BtfKind {
        ((self.ty.info >> 24) & 0x1f).try_into().unwrap()
    }

    #[inline]
    fn vlen(&self) -> u32 {
        self.ty.info & 0xffff
    }

    #[inline]
    fn kind_flag(&self) -> bool {
        (self.ty.info >> 31) == 1
    }

    /// Whether this represent's a modifier.
    #[inline]
    pub fn is_mod(&self) -> bool {
        matches!(
            self.kind(),
            BtfKind::Volatile | BtfKind::Const | BtfKind::Restrict | BtfKind::TypeTag
        )
    }

    /// Whether this represents any kind of enum.
    #[inline]
    pub fn is_any_enum(&self) -> bool {
        matches!(self.kind(), BtfKind::Enum | BtfKind::Enum64)
    }

    /// Whether this btf type is core compatible to `other`.
    #[inline]
    pub fn is_core_compat(&self, other: &Self) -> bool {
        self.kind() == other.kind() || (self.is_any_enum() && other.is_any_enum())
    }

    /// Whether this type represents a composite type (struct/union).
    #[inline]
    pub fn is_composite(&self) -> bool {
        matches!(self.kind(), BtfKind::Struct | BtfKind::Union)
    }

    /// The size of the described type.
    ///
    /// # Safety
    ///
    /// This function can only be called when the [`Self::kind`] returns one of:
    ///   - [`BtfKind::Int`],
    ///   - [`BtfKind::Float`],
    ///   - [`BtfKind::Enum`],
    ///   - [`BtfKind::Struct`],
    ///   - [`BtfKind::Union`],
    ///   - [`BtfKind::DataSec`],
    ///   - [`BtfKind::Enum64`],
    #[inline]
    unsafe fn size_unchecked(&self) -> u32 {
        unsafe { self.ty.__bindgen_anon_1.size }
    }

    /// The [`TypeId`] of the referenced type.
    ///
    /// # Safety
    /// This function can only be called when the [`Self::kind`] returns one of:
    ///     - [`BtfKind::Ptr`],
    ///     - [`BtfKind::Typedef`],
    ///     - [`BtfKind::Volatile`],
    ///     - [`BtfKind::Const`],
    ///     - [`BtfKind::Restrict`],
    ///     - [`BtfKind::Func`],
    ///     - [`BtfKind::FuncProto`],
    ///     - [`BtfKind::Var`],
    ///     - [`BtfKind::DeclTag`],
    ///     - [`BtfKind::TypeTag`],
    #[inline]
    unsafe fn referenced_type_id_unchecked(&self) -> TypeId {
        unsafe { self.ty.__bindgen_anon_1.type_ }.into()
    }

    /// If this type implements [`ReferencesType`], returns the type it references.
    pub fn next_type(&self) -> Option<Self> {
        match self.kind() {
            BtfKind::Ptr
            | BtfKind::Typedef
            | BtfKind::Volatile
            | BtfKind::Const
            | BtfKind::Restrict
            | BtfKind::Func
            | BtfKind::FuncProto
            | BtfKind::Var
            | BtfKind::DeclTag
            | BtfKind::TypeTag => {
                let tid = unsafe {
                    // SAFETY: we checked the kind
                    self.referenced_type_id_unchecked()
                };
                self.source.type_by_id(tid)
            }

            BtfKind::Void
            | BtfKind::Int
            | BtfKind::Array
            | BtfKind::Struct
            | BtfKind::Union
            | BtfKind::Enum
            | BtfKind::Fwd
            | BtfKind::DataSec
            | BtfKind::Float
            | BtfKind::Enum64 => None,
        }
    }

    /// Given a type, follows the refering type ids until it finds a type that isn't a modifier or
    /// a [`BtfKind::Typedef`].
    ///
    /// See [is_mod](Self::is_mod).
    pub fn skip_mods_and_typedefs(&self) -> Self {
        let mut ty = *self;
        loop {
            if ty.is_mod() || ty.kind() == BtfKind::Typedef {
                ty = ty.next_type().unwrap();
            } else {
                return ty;
            }
        }
    }

    /// Returns the alignment of this type, if this type points to some modifier or typedef, those
    /// will be skipped until the underlying type (with an alignment) is found.
    ///
    /// See [skip_mods_and_typedefs](Self::skip_mods_and_typedefs).
    pub fn alignment(&self) -> Result<NonZeroUsize> {
        let skipped = self.skip_mods_and_typedefs();
        match skipped.kind() {
            BtfKind::Int => {
                let ptr_size = skipped.source.ptr_size()?;
                let int = types::Int::try_from(skipped).unwrap();
                Ok(Ord::min(
                    ptr_size,
                    NonZeroUsize::new(((int.bits + 7) / 8).into()).unwrap(),
                ))
            }
            BtfKind::Ptr => skipped.source.ptr_size(),
            BtfKind::Array => types::Array::try_from(skipped)
                .unwrap()
                .contained_type()
                .alignment(),
            BtfKind::Struct | BtfKind::Union => {
                let c = Composite::try_from(skipped).unwrap();
                let mut align = NonZeroUsize::new(1usize).unwrap();
                for m in c.iter() {
                    align = Ord::max(
                        align,
                        skipped
                            .source
                            .type_by_id::<Self>(m.ty)
                            .unwrap()
                            .alignment()?,
                    );
                }

                Ok(align)
            }
            BtfKind::Enum | BtfKind::Enum64 | BtfKind::Float => {
                Ok(Ord::min(skipped.source.ptr_size()?, unsafe {
                    // SAFETY: We checked the type.
                    // Unwrap: Enums in C have always size >= 1
                    NonZeroUsize::new_unchecked(skipped.size_unchecked() as usize)
                }))
            }
            BtfKind::Var => {
                let var = types::Var::try_from(skipped).unwrap();
                var.source
                    .type_by_id::<Self>(var.referenced_type_id())
                    .unwrap()
                    .alignment()
            }
            BtfKind::DataSec => unsafe {
                // SAFETY: We checked the type.
                NonZeroUsize::new(skipped.size_unchecked() as usize)
            }
            .ok_or_else(|| Error::with_invalid_data("DataSec with size of 0")),
            BtfKind::Void
            | BtfKind::Volatile
            | BtfKind::Const
            | BtfKind::Restrict
            | BtfKind::Typedef
            | BtfKind::FuncProto
            | BtfKind::Fwd
            | BtfKind::Func
            | BtfKind::DeclTag
            | BtfKind::TypeTag => Err(Error::with_invalid_data(format!(
                "Cannot get alignment of type with kind {:?}. TypeId is {}",
                skipped.kind(),
                skipped.type_id(),
            ))),
        }
    }
}

/// Some btf types have a size field, describing their size.
///
/// # Safety
///
/// It's only safe to implement this for types where the underlying btf_type has a .size set.
///
/// See the [docs](https://www.kernel.org/doc/html/latest/bpf/btf.html) for a reference of which
/// [`BtfKind`] can implement this trait.
pub unsafe trait HasSize<'btf>: Deref<Target = BtfType<'btf>> + sealed::Sealed {
    /// The size of the described type.
    #[inline]
    fn size(&self) -> usize {
        unsafe { self.size_unchecked() as usize }
    }
}

/// Some btf types refer to other types by their type id.
///
/// # Safety
///
/// It's only safe to implement this for types where the underlying btf_type has a .type set.
///
/// See the [docs](https://www.kernel.org/doc/html/latest/bpf/btf.html) for a reference of which
/// [`BtfKind`] can implement this trait.
pub unsafe trait ReferencesType<'btf>:
    Deref<Target = BtfType<'btf>> + sealed::Sealed
{
    /// The referenced type's id.
    #[inline]
    fn referenced_type_id(&self) -> TypeId {
        unsafe { self.referenced_type_id_unchecked() }
    }

    /// The referenced type.
    #[inline]
    fn referenced_type(&self) -> BtfType<'btf> {
        self.source.type_by_id(self.referenced_type_id()).unwrap()
    }
}

mod sealed {
    pub trait Sealed {}
}

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

    use std::mem::discriminant;

    #[test]
    fn from_vmlinux() {
        assert!(Btf::from_vmlinux().is_ok());
    }

    #[test]
    fn btf_kind() {
        use BtfKind::*;

        for t in [
            Void, Int, Ptr, Array, Struct, Union, Enum, Fwd, Typedef, Volatile, Const, Restrict,
            Func, FuncProto, Var, DataSec, Float, DeclTag, TypeTag, Enum64,
        ] {
            // check if discriminants match after a roundtrip conversion
            assert_eq!(
                discriminant(&t),
                discriminant(&BtfKind::try_from(t as u32).unwrap())
            );
        }
    }
}
