use std::mem::size_of;
use std::os::unix::io::AsRawFd;
use std::os::unix::io::BorrowedFd;

use bitflags::bitflags;

use crate::util;
use crate::Result;

bitflags! {
    /// Flags to configure the `XDP` operations
    pub struct XdpFlags: u32 {
        /// No flags.
        const NONE              = 0;
        /// See [`libbpf_sys::XDP_FLAGS_UPDATE_IF_NOEXIST`].
        const UPDATE_IF_NOEXIST = libbpf_sys::XDP_FLAGS_UPDATE_IF_NOEXIST;
        /// See [`libbpf_sys::XDP_FLAGS_SKB_MODE`].
        const SKB_MODE          = libbpf_sys::XDP_FLAGS_SKB_MODE;
        /// See [`libbpf_sys::XDP_FLAGS_DRV_MODE`].
        const DRV_MODE          = libbpf_sys::XDP_FLAGS_DRV_MODE;
        /// See [`libbpf_sys::XDP_FLAGS_HW_MODE`].
        const HW_MODE           = libbpf_sys::XDP_FLAGS_HW_MODE;
        /// See [`libbpf_sys::XDP_FLAGS_REPLACE`].
        const REPLACE           = libbpf_sys::XDP_FLAGS_REPLACE;
        /// See [`libbpf_sys::XDP_FLAGS_MODES`].
        const MODES             = libbpf_sys::XDP_FLAGS_MODES;
        /// See [`libbpf_sys::XDP_FLAGS_MASK`].
        const MASK              = libbpf_sys::XDP_FLAGS_MASK;
    }

}

/// Represents a XDP program.
///
/// This struct exposes operations to attach, detach and query a XDP program
#[derive(Debug)]
pub struct Xdp<'fd> {
    fd: BorrowedFd<'fd>,
    attach_opts: libbpf_sys::bpf_xdp_attach_opts,
    query_opts: libbpf_sys::bpf_xdp_query_opts,
}

impl<'fd> Xdp<'fd> {
    /// Create a new XDP instance with the given file descriptor of the
    /// `SEC("xdp")` [`Program`][crate::Program].
    pub fn new(fd: BorrowedFd<'fd>) -> Self {
        let mut xdp = Xdp {
            fd,
            attach_opts: libbpf_sys::bpf_xdp_attach_opts::default(),
            query_opts: libbpf_sys::bpf_xdp_query_opts::default(),
        };
        xdp.attach_opts.sz = size_of::<libbpf_sys::bpf_xdp_attach_opts>() as libbpf_sys::size_t;
        xdp.query_opts.sz = size_of::<libbpf_sys::bpf_xdp_query_opts>() as libbpf_sys::size_t;
        xdp
    }

    /// Attach the XDP program to the given interface to start processing the
    /// packets
    ///
    /// # Notes
    /// Once a program is attached, it will outlive the userspace program. Make
    /// sure to detach the program if its not desired.
    pub fn attach(&self, ifindex: i32, flags: XdpFlags) -> Result<()> {
        let ret = unsafe {
            libbpf_sys::bpf_xdp_attach(
                ifindex,
                self.fd.as_raw_fd(),
                flags.bits(),
                &self.attach_opts,
            )
        };
        util::parse_ret(ret)
    }

    /// Detach the XDP program from the interface
    pub fn detach(&self, ifindex: i32, flags: XdpFlags) -> Result<()> {
        let ret = unsafe { libbpf_sys::bpf_xdp_detach(ifindex, flags.bits(), &self.attach_opts) };
        util::parse_ret(ret)
    }

    /// Query to inspect the program
    pub fn query(&self, ifindex: i32, flags: XdpFlags) -> Result<libbpf_sys::bpf_xdp_query_opts> {
        let mut opts = self.query_opts;
        let err = unsafe { libbpf_sys::bpf_xdp_query(ifindex, flags.bits() as i32, &mut opts) };
        util::parse_ret(err).map(|()| opts)
    }

    /// Query to inspect the program identifier (`prog_id`)
    pub fn query_id(&self, ifindex: i32, flags: XdpFlags) -> Result<u32> {
        let mut prog_id = 0;
        let err =
            unsafe { libbpf_sys::bpf_xdp_query_id(ifindex, flags.bits() as i32, &mut prog_id) };
        util::parse_ret(err).map(|()| prog_id)
    }

    /// Replace an existing xdp program (identified by `old_prog_fd`) with this xdp program
    pub fn replace(&self, ifindex: i32, old_prog_fd: BorrowedFd<'_>) -> Result<()> {
        let mut opts = self.attach_opts;
        opts.old_prog_fd = old_prog_fd.as_raw_fd();
        let ret = unsafe {
            libbpf_sys::bpf_xdp_attach(
                ifindex,
                self.fd.as_raw_fd(),
                XdpFlags::REPLACE.bits(),
                &opts,
            )
        };
        util::parse_ret(ret)
    }
}
