//! # Python bindings
//!
//! This module contains python bindings for retis events so that they can
//! be inspected in post-processing tools written in python.

use std::{collections::HashMap, ffi::CString, path::PathBuf};

use pyo3::{
    exceptions::{PyKeyError, PyRuntimeError},
    prelude::*,
    types::{IntoPyDict, PyBool, PyList},
};

use super::*;

/// Python representation of an Event.
///
/// This class exposes the event data as well as some helper functions
/// to inspect an event.
///
/// # Accessing event data
///
/// The Event class allows accessing each section by using the __getitem__
/// function. The object returned is of a builtin type whose attributes can
/// be accessed directly.
///
/// In addition, some helpers might be available. One of the helpers that
/// is implemented for all event section types is `to_dict()`, which returns
/// the data as a dictionary.
///
/// Also, sections can be iterated through the `sections()` helper.
///
/// # Displaying events
///
/// The Event class implements the __str__ function which allow to directly use
/// built-in helpers such as `print(event)` or `"{}".format(event)`.
///
/// The Event class also implements the __repr__ function, but in a different
/// way. Here the event is represented as a dictionary. This allows quick
/// investigation of events in the interpreter.
/// ```
#[pyclass(name = "Event")]
pub struct PyEvent(Py<Event>);

// We need this to make it a pyclass.
//
// Safety: no mutable references on internal members is ever given.
unsafe impl Sync for PyEvent {}

impl PyEvent {
    pub(crate) fn new(py: Python<'_>, event: Event) -> PyResult<Self> {
        Ok(Self(Py::new(py, event)?))
    }
}

#[pymethods]
impl PyEvent {
    /// Controls how the PyEvent is represented, eg. what is the output of
    /// `print(repr(e))`.
    fn __repr__<'a>(&'a self, py: Python<'a>) -> String {
        self.0.borrow(py).__repr__(py)
    }

    /// Controls how the PyEvent is represented as a string. This can be used
    /// directly in `print(e)`.
    fn __str__<'a>(&'a self, py: Python<'a>) -> String {
        self.0.borrow(py).__str__(py)
    }

    /// Allows to use the object as a dictionary, e.g. `e['skb']`.
    /// The use of direct attribute access is preferred, e.g: `e.skb`.
    fn __getitem__<'a>(&'a self, py: Python<'a>, attr: &str) -> PyResult<Py<PyAny>> {
        let item = self
            .__getattr__(py, attr)
            .map_err(|_| PyKeyError::new_err(attr.to_string()))?;

        if item.is_none(py) {
            Err(PyKeyError::new_err(attr.to_string()))
        } else {
            Ok(item)
        }
    }

    /// Return a section or None if it's not present.
    fn __getattr__<'a>(&'a self, py: Python<'a>, attr: &str) -> PyResult<Py<PyAny>> {
        // For backwards compatibility reasons, we allow accessing sections in kebab-case.
        self.0.getattr(py, attr.replace("-", "_"))
    }

    /// Allows to check if a section is present in the event, e.g: `'skb' in e`.
    /// Prefer using __getattr__ directly, i.e: `if e.skb is not None`
    fn __contains__<'a>(&'a self, py: Python<'a>, attr: &str) -> PyResult<bool> {
        Ok(self.__getattr__(py, attr).is_ok_and(|x| !x.is_none(py)))
    }

    /// Returns internal data as a dictionary
    ///
    /// Returns a dictionary with all key<>data stored (recursively) in the
    /// event, eg. `e.to_dict()['skb']['dev']`.
    fn to_dict(&self, py: Python<'_>) -> PyObject {
        self.0.borrow(py).to_dict(py)
    }

    /// Returns a list of existing section names.
    pub fn sections(&self, py: Python<'_>) -> PyResult<Py<PyList>> {
        let globals = [("event", self.0.bind(py))].into_py_dict(py)?;

        Ok(py.eval(
            &CString::new("[v for v in dir(event) if getattr(event,v) and not callable(getattr(event,v)) and not v.startswith('__')]")?,
            Some(&globals),
            None,
        )?.downcast_into()?.unbind())
    }
}

/// Python representation of an EventSeries.
///
/// When interacting with sorted event files (the ones generated by
/// `retis sort`), the file contains lists of events that form an
/// EventSeries.
///
/// # Accessing events
///
/// The EventSeries binding implements the iterator protocol so it's easy to
/// iterate over the internal events.
///
/// ## Examples
///
/// ```text
/// >>> len(series)
/// 3
/// >>> for event in series:
///     print(event)
/// ```
///
#[pyclass(name = "EventSeries")]
pub(crate) struct PyEventSeries {
    events: Vec<Py<PyEvent>>,
    idx: usize,
}

impl PyEventSeries {
    pub(crate) fn new(py: Python<'_>, mut series: EventSeries) -> PyResult<Self> {
        let mut events = Vec::new();
        series.events.drain(..).try_for_each(|e| -> PyResult<()> {
            events.push(Py::new(py, PyEvent::new(py, e)?)?);
            Ok(())
        })?;
        Ok(Self { events, idx: 0 })
    }
}

#[pymethods]
impl PyEventSeries {
    /// Controls how the PyEventSeries is represented, eg. what is the output of
    /// `print(e)`.
    fn __repr__(&'_ self, py: Python<'_>) -> PyResult<String> {
        let n_events = self.events.len();
        let timestamp = self
            .events
            .first()
            .ok_or_else(|| PyRuntimeError::new_err("Malformed Series with < 1 events"))?
            .try_borrow(py)?
            .0
            .try_borrow(py)?
            .common
            .as_ref()
            .unwrap()
            .timestamp;
        Ok(format!(
            "PyEventSeries(ts = {timestamp}, n_events = {n_events})"
        ))
    }

    // Implementation of the iterator protocol.
    fn __iter__(mut slf: PyRefMut<'_, Self>) -> PyRefMut<'_, Self> {
        slf.idx = 0;
        slf
    }
    pub(crate) fn __next__(&mut self, py: Python<'_>) -> PyResult<Option<Py<PyEvent>>> {
        if self.idx < self.events.len() {
            let event = self.events[self.idx].clone_ref(py);
            self.idx += 1;
            Ok(Some(event))
        } else {
            Ok(None)
        }
    }

    /// Returns the number of events in the series.
    fn __len__(&self) -> usize {
        self.events.len()
    }
}

/// Python event reader
///
/// Objects of this class can read events from unsorted event files.
///
///
/// ## Example
///
/// ```python
/// reader = EventReader("retis.data")
///
/// for event in series:
///     print(event)
/// ```
#[pyclass(name = "EventReader")]
pub(crate) struct PyEventReader {
    pub(crate) factory: file::FileEventsFactory,
}

#[pymethods]
impl PyEventReader {
    #[new]
    pub(crate) fn new(path: PathBuf) -> PyResult<Self> {
        let factory = file::FileEventsFactory::new(path)
            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;

        if matches!(factory.file_type(), file::FileType::Series) {
            return Err(PyRuntimeError::new_err(
                "Cannot create a EventReader from a sorted file. Use an SeriesReader instead",
            ));
        }
        Ok(PyEventReader { factory })
    }

    // Implementation of the iterator protocol.
    pub(crate) fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
        slf
    }

    // Return the next Event.
    pub(crate) fn __next__(
        mut slf: PyRefMut<'_, Self>,
        py: Python<'_>,
    ) -> PyResult<Option<Py<PyAny>>> {
        match slf
            .factory
            .next_event()
            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?
        {
            Some(event) => {
                let pyevent: Bound<'_, PyEvent> = Bound::new(py, PyEvent::new(py, event)?)?;
                Ok(Some(pyevent.into_any().into()))
            }
            None => Ok(None),
        }
    }
}

/// Python series reader
///
/// Objects of this class can read events from unsorted event files.
///
///
/// ## Example
///
/// ```python
/// reader = EventReader("retis.data")
///
/// for event in series:
///     print(event)
/// ```
#[pyclass(name = "SeriesReader")]
pub(crate) struct PySeriesReader {
    factory: file::FileEventsFactory,
}

#[pymethods]
impl PySeriesReader {
    #[new]
    pub(crate) fn new(path: PathBuf) -> PyResult<Self> {
        let factory = file::FileEventsFactory::new(path)
            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;

        if matches!(factory.file_type(), file::FileType::Event) {
            return Err(PyRuntimeError::new_err(
                "Cannot create a SeriesReader from an unsorted file. Use an EventReader instead",
            ));
        }
        Ok(PySeriesReader { factory })
    }

    // Implementation of the iterator protocol.
    pub(crate) fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
        slf
    }

    // Return the next EventSeries.
    pub(crate) fn __next__(
        mut slf: PyRefMut<'_, Self>,
        py: Python<'_>,
    ) -> PyResult<Option<Py<PyAny>>> {
        match slf
            .factory
            .next_series()
            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?
        {
            Some(series) => {
                let pyseries: Bound<'_, PyEventSeries> =
                    Bound::new(py, PyEventSeries::new(py, series)?)?;
                Ok(Some(pyseries.into_any().into()))
            }
            None => Ok(None),
        }
    }
}

/// Python event file
///
/// Objects of this class can read files generated by retis and create
/// EventReader and SeriesReader instances to iterate over their content.
///
/// ## Example
///
/// ```python
/// event_file = EventFile("retis.data")
/// if event_file.sorted():
///     for series in event_file.series():
///         for event in series:
///             print(event)
///
/// else:
///     for event in event_file.events():
///         print(event)
/// ```
#[pyclass(name = "EventFile")]
pub(crate) struct PyEventFile {
    path: PathBuf,
    ftype: file::FileType,
}

#[pymethods]
impl PyEventFile {
    #[new]
    pub(crate) fn new(path: PathBuf) -> PyResult<Self> {
        let temp = file::FileEventsFactory::new(&path)
            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
        let ftype = temp.file_type();
        Ok(PyEventFile {
            path,
            ftype: ftype.clone(),
        })
    }

    // Returns whether the file is sorted.
    pub(crate) fn sorted(&self) -> PyResult<bool> {
        match self.ftype {
            file::FileType::Series => Ok(true),
            file::FileType::Event => Ok(false),
        }
    }

    pub(crate) fn events(&self) -> PyResult<PyEventReader> {
        PyEventReader::new(self.path.clone())
    }

    pub(crate) fn series(&self) -> PyResult<PySeriesReader> {
        PySeriesReader::new(self.path.clone())
    }
}

/// Converts a serde_json::Value to a PyObject.
pub(crate) fn to_pyobject(val: &serde_json::Value, py: Python<'_>) -> PyObject {
    use serde_json::Value;
    match val {
        Value::Null => py.None(),
        Value::Bool(b) => <pyo3::Bound<'_, PyBool> as Clone>::clone(&b.into_pyobject(py).unwrap())
            .into_any()
            .unbind(),
        Value::Number(n) => n
            .as_i64()
            .map(|x| x.into_pyobject(py).unwrap().into_any().unbind())
            .or(n
                .as_u64()
                .map(|x| x.into_pyobject(py).unwrap().into_any().unbind()))
            .or(n
                .as_f64()
                .map(|x| x.into_pyobject(py).unwrap().into_any().unbind()))
            .expect("Cannot convert number to Python object"),
        Value::String(s) => s.into_pyobject(py).unwrap().into_any().unbind(),
        Value::Array(a) => {
            let vec: Vec<_> = a.iter().map(|x| to_pyobject(x, py)).collect();
            vec.into_pyobject(py).unwrap().into_any().unbind()
        }
        Value::Object(o) => {
            let map: HashMap<_, _> = o.iter().map(|(k, v)| (k, to_pyobject(v, py))).collect();
            map.into_pyobject(py).unwrap().into_any().unbind()
        }
    }
}
