use std::{
    cmp,
    ffi::CStr,
    io,
    mem::{self, MaybeUninit},
    slice,
};

use libc::{c_long, c_uint, ENOENT};

use crate::{
    bpf_map_def,
    generated::{bpf_attach_type, bpf_attr, bpf_cmd, bpf_insn},
    programs::VerifierLog,
    syscalls::SysResult,
    Pod, RawFd, BPF_OBJ_NAME_LEN,
};

use super::{syscall, Syscall};

pub(crate) fn bpf_create_map(name: &CStr, def: &bpf_map_def) -> SysResult {
    let mut attr = unsafe { mem::zeroed::<bpf_attr>() };

    let u = unsafe { &mut attr.__bindgen_anon_1 };
    u.map_type = def.map_type;
    u.key_size = def.key_size;
    u.value_size = def.value_size;
    u.max_entries = def.max_entries;
    u.map_flags = def.map_flags;

    // u.map_name is 16 bytes max and must be NULL terminated
    let name_len = cmp::min(name.to_bytes().len(), BPF_OBJ_NAME_LEN - 1);
    u.map_name[..name_len]
        .copy_from_slice(unsafe { slice::from_raw_parts(name.as_ptr(), name_len) });

    sys_bpf(bpf_cmd::BPF_MAP_CREATE, &attr)
}

pub(crate) fn bpf_load_program(
    ty: c_uint,
    insns: &[bpf_insn],
    license: &CStr,
    kernel_version: u32,
    log: &mut VerifierLog,
) -> SysResult {
    let mut attr = unsafe { mem::zeroed::<bpf_attr>() };

    let u = unsafe { &mut attr.__bindgen_anon_3 };
    u.prog_type = ty;
    u.expected_attach_type = 0;
    u.insns = insns.as_ptr() as u64;
    u.insn_cnt = insns.len() as u32;
    u.license = license.as_ptr() as u64;
    u.kern_version = kernel_version;
    let log_buf = log.buf();
    if log_buf.capacity() > 0 {
        u.log_level = 7;
        u.log_buf = log_buf.as_mut_ptr() as u64;
        u.log_size = log_buf.capacity() as u32;
    }

    sys_bpf(bpf_cmd::BPF_PROG_LOAD, &attr)
}

fn lookup<K: Pod, V: Pod>(
    fd: RawFd,
    key: &K,
    flags: u64,
    cmd: bpf_cmd::Type,
) -> Result<Option<V>, (c_long, io::Error)> {
    let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
    let mut value = MaybeUninit::uninit();

    let u = unsafe { &mut attr.__bindgen_anon_2 };
    u.map_fd = fd as u32;
    u.key = key as *const _ as u64;
    u.__bindgen_anon_1.value = &mut value as *mut _ as u64;
    u.flags = flags;

    match sys_bpf(cmd, &attr) {
        Ok(_) => Ok(Some(unsafe { value.assume_init() })),
        Err((_, io_error)) if io_error.raw_os_error() == Some(ENOENT) => Ok(None),
        Err(e) => Err(e),
    }
}

pub(crate) fn bpf_map_lookup_elem<K: Pod, V: Pod>(
    fd: RawFd,
    key: &K,
    flags: u64,
) -> Result<Option<V>, (c_long, io::Error)> {
    lookup(fd, key, flags, bpf_cmd::BPF_MAP_LOOKUP_ELEM)
}

pub(crate) fn bpf_map_lookup_and_delete_elem<K: Pod, V: Pod>(
    fd: RawFd,
    key: &K,
) -> Result<Option<V>, (c_long, io::Error)> {
    lookup(fd, key, 0, bpf_cmd::BPF_MAP_LOOKUP_AND_DELETE_ELEM)
}

pub(crate) fn bpf_map_update_elem<K, V>(fd: RawFd, key: &K, value: &V, flags: u64) -> SysResult {
    let mut attr = unsafe { mem::zeroed::<bpf_attr>() };

    let u = unsafe { &mut attr.__bindgen_anon_2 };
    u.map_fd = fd as u32;
    u.key = key as *const _ as u64;
    u.__bindgen_anon_1.value = value as *const _ as u64;
    u.flags = flags;

    sys_bpf(bpf_cmd::BPF_MAP_UPDATE_ELEM, &attr)
}

pub(crate) fn bpf_map_update_elem_ptr<K, V>(
    fd: RawFd,
    key: *const K,
    value: *const V,
    flags: u64,
) -> SysResult {
    let mut attr = unsafe { mem::zeroed::<bpf_attr>() };

    let u = unsafe { &mut attr.__bindgen_anon_2 };
    u.map_fd = fd as u32;
    u.key = key as u64;
    u.__bindgen_anon_1.value = value as u64;
    u.flags = flags;

    sys_bpf(bpf_cmd::BPF_MAP_UPDATE_ELEM, &attr)
}

pub(crate) fn bpf_map_delete_elem<K>(fd: RawFd, key: &K) -> SysResult {
    let mut attr = unsafe { mem::zeroed::<bpf_attr>() };

    let u = unsafe { &mut attr.__bindgen_anon_2 };
    u.map_fd = fd as u32;
    u.key = key as *const _ as u64;

    sys_bpf(bpf_cmd::BPF_MAP_DELETE_ELEM, &attr)
}

pub(crate) fn bpf_map_get_next_key<K>(
    fd: RawFd,
    key: Option<&K>,
) -> Result<Option<K>, (c_long, io::Error)> {
    let mut attr = unsafe { mem::zeroed::<bpf_attr>() };
    let mut next_key = MaybeUninit::uninit();

    let u = unsafe { &mut attr.__bindgen_anon_2 };
    u.map_fd = fd as u32;
    if let Some(key) = key {
        u.key = key as *const _ as u64;
    }
    u.__bindgen_anon_1.next_key = &mut next_key as *mut _ as u64;

    match sys_bpf(bpf_cmd::BPF_MAP_GET_NEXT_KEY, &attr) {
        Ok(_) => Ok(Some(unsafe { next_key.assume_init() })),
        Err((_, io_error)) if io_error.raw_os_error() == Some(ENOENT) => Ok(None),
        Err(e) => Err(e),
    }
}

// since kernel 5.7
pub(crate) fn bpf_link_create(
    prog_fd: RawFd,
    target_fd: RawFd,
    attach_type: bpf_attach_type::Type,
    flags: u32,
) -> SysResult {
    let mut attr = unsafe { mem::zeroed::<bpf_attr>() };

    attr.link_create.prog_fd = prog_fd as u32;
    attr.link_create.__bindgen_anon_1.target_fd = target_fd as u32;
    attr.link_create.attach_type = attach_type;
    attr.link_create.flags = flags;

    sys_bpf(bpf_cmd::BPF_LINK_CREATE, &attr)
}

fn sys_bpf<'a>(cmd: bpf_cmd::Type, attr: &'a bpf_attr) -> SysResult {
    syscall(Syscall::Bpf { cmd, attr })
}