use bazaar::filters::ContentFilter;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use std::fs::Permissions;
use std::io::Error;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;

#[pyclass]
struct HashCache {
    hashcache: Box<bazaar::hashcache::HashCache>,
}

pub struct PyContentFilter {
    content_filter: Py<PyAny>,
}

#[pyclass]
struct PyChunkIterator {
    input: Box<dyn Iterator<Item = Result<Vec<u8>, Error>> + Send + Sync>,
}

#[pymethods]
impl PyChunkIterator {
    fn __next__<'py>(&mut self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyBytes>>> {
        match self.input.next() {
            Some(Ok(item)) => Ok(Some(PyBytes::new(py, &item))),
            Some(Err(e)) => Err(e.into()),
            None => Ok(None),
        }
    }
}

fn map_py_err_to_io_err(e: PyErr) -> Error {
    Error::new(std::io::ErrorKind::Other, e.to_string())
}

fn map_py_err_to_iter_io_err(
    e: PyErr,
) -> Box<dyn Iterator<Item = Result<Vec<u8>, Error>> + Send + Sync> {
    Box::new(std::iter::once(Err(map_py_err_to_io_err(e))))
}

impl PyContentFilter {
    fn _impl(
        &self,
        input: Box<dyn Iterator<Item = Result<Vec<u8>, Error>> + Send + Sync>,
        worker: &str,
    ) -> Box<dyn Iterator<Item = Result<Vec<u8>, Error>> + Send + Sync> {
        Python::attach(|py| {
            let worker = self.content_filter.getattr(py, worker);
            let py_input = PyChunkIterator { input };
            let py_output = worker.unwrap().call1(py, (py_input,));
            if let Err(e) = py_output {
                return map_py_err_to_iter_io_err(e);
            }
            let py_output = py_output.unwrap();
            let next = move || {
                Python::attach(|py| {
                    let item = py_output.call_method0(py, "__next__");
                    match item {
                        Err(e) => Some(Err(map_py_err_to_io_err(e))),
                        Ok(item) => {
                            if item.is_none(py) {
                                None
                            } else {
                                Some(Ok(item.extract(py).map_err(map_py_err_to_io_err).unwrap()))
                            }
                        }
                    }
                })
            };
            Box::new(std::iter::from_fn(next))
        })
    }
}

impl ContentFilter for PyContentFilter {
    fn reader(
        &self,
        input: Box<dyn Iterator<Item = Result<Vec<u8>, Error>> + Send + Sync>,
    ) -> Box<dyn Iterator<Item = Result<Vec<u8>, Error>> + Send + Sync> {
        self._impl(input, "reader")
    }

    fn writer(
        &self,
        input: Box<dyn Iterator<Item = Result<Vec<u8>, Error>> + Send + Sync>,
    ) -> Box<dyn Iterator<Item = Result<Vec<u8>, Error>> + Send + Sync> {
        self._impl(input, "worker")
    }
}

fn content_filter_to_fn(
    content_filter_provider: Py<PyAny>,
) -> Box<dyn Fn(&Path, u64) -> Box<dyn ContentFilter> + Send + Sync> {
    Box::new(move |path, ctime| {
        Python::attach(|py| {
            Box::new(PyContentFilter {
                content_filter: content_filter_provider.call1(py, (path, ctime)).unwrap(),
            })
        })
    })
}

fn extract_fs_time(obj: &Bound<PyAny>) -> Result<i64, PyErr> {
    if let Ok(val) = obj.extract::<i64>() {
        Ok(val)
    } else if let Ok(val) = obj.extract::<f64>() {
        Ok(val as i64)
    } else {
        Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
            "Expected int or float",
        ))
    }
}

#[pymethods]
impl HashCache {
    #[new]
    #[pyo3(signature = (
        root,
        cache_file_name,
        mode = None,
        content_filter_provider = None
    ))]
    fn new(
        root: &str,
        cache_file_name: &str,
        mode: Option<u32>,
        content_filter_provider: Option<Py<PyAny>>,
    ) -> Self {
        Self {
            hashcache: Box::new(bazaar::hashcache::HashCache::new(
                Path::new(root),
                Path::new(cache_file_name),
                {
                    #[cfg(unix)]
                    { mode.map(Permissions::from_mode) }
                    #[cfg(not(unix))]
                    { let _ = mode; None }
                },
                content_filter_provider.map(content_filter_to_fn),
            )),
        }
    }

    fn cache_file_name(&self) -> &str {
        self.hashcache.cache_file_name().to_str().unwrap()
    }

    fn clear(&mut self) {
        self.hashcache.clear();
    }

    fn scan(&mut self) {
        self.hashcache.scan();
    }

    #[pyo3(signature = (path, stat_value = None))]
    fn get_sha1<'a>(
        &mut self,
        py: Python<'a>,
        path: &str,
        stat_value: Option<Bound<PyAny>>,
    ) -> PyResult<Bound<'a, PyAny>> {
        let sha1;
        if let Some(stat_value) = stat_value {
            let fp = bazaar::hashcache::Fingerprint {
                size: stat_value.getattr("st_size")?.extract()?,
                mtime: extract_fs_time(&stat_value.getattr("st_mtime")?)?,
                ctime: extract_fs_time(&stat_value.getattr("st_ctime")?)?,
                ino: stat_value.getattr("st_ino")?.extract()?,
                dev: stat_value.getattr("st_dev")?.extract()?,
                mode: stat_value.getattr("st_mode")?.extract()?,
            };
            sha1 = self
                .hashcache
                .get_sha1_by_fingerprint(Path::new(path), &fp)?;
        } else {
            let ret = self.hashcache.get_sha1(Path::new(path), None)?;
            if let Some(s) = ret {
                sha1 = s;
            } else {
                return Ok(py.None().into_bound(py));
            }
        }
        Ok(PyBytes::new(py, sha1.as_bytes()).into_any())
    }

    fn write(&mut self) -> PyResult<()> {
        self.hashcache.write().map_err(|e| e.into())
    }

    fn read(&mut self) -> PyResult<()> {
        self.hashcache.read().map_err(|e| e.into())
    }

    fn cutoff_time(&self) -> i64 {
        self.hashcache.cutoff_time()
    }

    fn set_cutoff_offset(&mut self, offset: i64) {
        self.hashcache.set_cutoff_offset(offset);
    }

    #[getter]
    fn miss_count(&self) -> u32 {
        self.hashcache.miss_count()
    }

    #[getter]
    fn hit_count(&self) -> u32 {
        self.hashcache.hit_count()
    }

    #[getter]
    fn needs_write(&self) -> bool {
        self.hashcache.needs_write()
    }

    fn fingerprint(&self, abspath: &str) -> Option<(u64, i64, i64, u64, u64, u32)> {
        let fp = self.hashcache.fingerprint(Path::new(abspath), None);
        fp.map(|fp| (fp.size, fp.mtime, fp.ctime, fp.ino, fp.dev, fp.mode))
    }
}

pub(crate) fn hashcache(m: &Bound<PyModule>) -> PyResult<()> {
    m.add_class::<HashCache>()?;
    Ok(())
}
