use crossbeam_channel::Sender;
use errno::{set_errno, Errno};
use libbpf_sys as bpf;
use std::os::raw::c_void;

use crate::map_common as mc;
use crate::{MapType, XDPError, XDPLoadedObject, XDPResult};

/// Used for working with a perf eBPF map.
pub struct PerfMap<T> {
    map_fd: i32,
    pb: *mut bpf::perf_buffer,
    sender: Option<Sender<PerfEvent<T>>>,
}

/// The event sent from eBPF.
#[derive(Debug)]
pub struct PerfEvent<T> {
    /// The cpu that generated this event.
    pub cpu: i32,
    /// The event type.
    pub event: EventType<T>,
}

/// Event type from eBPF perf event map.
#[derive(Debug)]
pub enum EventType<T> {
    /// The data as generated by the eBPF code.
    Sample(T),
    /// How many events were lost because they weren't read by user-space fast enough.
    Lost(u64),
}

impl<T: Copy> PerfMap<T> {
    /// Get access to the eBPF map `map_name`.
    ///
    /// # Errors
    ///
    /// Returns an error in the following cases:
    /// * The requested key size doesn't match the key size defined in the ELF file.
    /// * The map_type is not `MapType::PerfEventArray`.
    pub fn new(xdp: &XDPLoadedObject, map_name: &str) -> XDPResult<PerfMap<T>> {
        let (map_fd, _vsize, mtype, _max_entries) = mc::validate_map::<i32>(xdp, map_name)?;
        let map_type: MapType = mtype.into();
        if map_type != MapType::PerfEventArray {
            set_errno(Errno(22));
            fail!("Improper map type, must be MapType::PerfEventArray");
        }
        Ok(PerfMap {
            map_fd,
            pb: std::ptr::null_mut(),
            sender: None,
        })
    }

    /// The channel to which every `PerfEvent` will be sent.
    pub fn set_sender(&mut self, sender: Sender<PerfEvent<T>>) {
        self.init_perf_buffer();
        self.sender = Some(sender);
    }

    /// Poll the underlying eBPF map for events, waiting up to `time_ms` milliseconds for an
    /// event. Returns the number of events received. This should be called in a loop.
    pub fn poll(&self, time_ms: i32) -> XDPResult<i32> {
        if self.pb.is_null() || self.sender.is_none() {
            set_errno(Errno(22));
            fail!("No sender set");
        }
        let rc = unsafe { bpf::perf_buffer__poll(self.pb, time_ms) };
        mc::check_rc(rc, rc, "Poll error")
    }

    fn init_perf_buffer(&mut self) {
        let pb_opts = bpf::perf_buffer_opts {
            sample_cb: Some(PerfMap::<T>::sample_event),
            lost_cb: Some(PerfMap::<T>::lost_event),
            ctx: self as *mut _ as *mut c_void,
        };

        self.pb = unsafe { bpf::perf_buffer__new(self.map_fd, 8, &pb_opts) };
    }

    fn send_perf_event(&self, perfevent: PerfEvent<T>) {
        self.sender.as_ref().and_then(|s| s.send(perfevent).ok());
    }

    fn handle_sample_event(&self, cpu: i32, data: *mut c_void, _size: u32) {
        let r: &mut T = unsafe { &mut *(data as *mut T) };
        let perfevent = PerfEvent {
            cpu,
            event: EventType::Sample(*r),
        };
        self.send_perf_event(perfevent);
    }

    fn handle_lost_event(&self, cpu: i32, cnt: u64) {
        let perfevent = PerfEvent {
            cpu,
            event: EventType::Lost(cnt),
        };
        self.send_perf_event(perfevent);
    }

    #[no_mangle]
    unsafe extern "C" fn sample_event(ctx: *mut c_void, cpu: i32, data: *mut c_void, size: u32) {
        let perfmap: &mut PerfMap<T> = &mut *(ctx as *mut PerfMap<T>);
        perfmap.handle_sample_event(cpu, data, size);
    }

    #[no_mangle]
    unsafe extern "C" fn lost_event(ctx: *mut c_void, cpu: i32, cnt: u64) {
        let perfmap: &mut PerfMap<T> = &mut *(ctx as *mut PerfMap<T>);
        perfmap.handle_lost_event(cpu, cnt);
    }
}

impl<T> Drop for PerfMap<T> {
    fn drop(&mut self) {
        unsafe { bpf::perf_buffer__free(self.pb) }
    }
}
