use cpython::exc;
use cpython::ObjectProtocol;
use cpython::PyBytes;
use cpython::PyErr;
use cpython::PyNone;
use cpython::PyObject;
use cpython::PyResult;
use cpython::Python;
use cpython::PythonObject;
use hg::dirstate::DirstateEntry;
use hg::dirstate::EntryState;
use hg::dirstate::TruncatedTimestamp;
use std::cell::Cell;
use std::convert::TryFrom;

py_class!(pub class DirstateItem |py| {
    data entry: Cell<DirstateEntry>;

    def __new__(
        _cls,
        wc_tracked: bool = false,
        p1_tracked: bool = false,
        p2_info: bool = false,
        has_meaningful_data: bool = true,
        has_meaningful_mtime: bool = true,
        parentfiledata: Option<(u32, u32, (u32, u32))> = None,
        fallback_exec: Option<bool> = None,
        fallback_symlink: Option<bool> = None,

    ) -> PyResult<DirstateItem> {
        let mut mode_size_opt = None;
        let mut mtime_opt = None;
        if let Some((mode, size, mtime)) = parentfiledata {
            if has_meaningful_data {
                mode_size_opt = Some((mode, size))
            }
            if has_meaningful_mtime {
                mtime_opt = Some(timestamp(py, mtime)?)
            }
        }
        let entry = DirstateEntry::from_v2_data(
            wc_tracked,
            p1_tracked,
            p2_info,
            mode_size_opt,
            mtime_opt,
            fallback_exec,
            fallback_symlink,
        );
        DirstateItem::create_instance(py, Cell::new(entry))
    }

    @property
    def state(&self) -> PyResult<PyBytes> {
        let state_byte: u8 = self.entry(py).get().state().into();
        Ok(PyBytes::new(py, &[state_byte]))
    }

    @property
    def mode(&self) -> PyResult<i32> {
        Ok(self.entry(py).get().mode())
    }

    @property
    def size(&self) -> PyResult<i32> {
        Ok(self.entry(py).get().size())
    }

    @property
    def mtime(&self) -> PyResult<i32> {
        Ok(self.entry(py).get().mtime())
    }

    @property
    def has_fallback_exec(&self) -> PyResult<bool> {
        match self.entry(py).get().get_fallback_exec() {
            Some(_) => Ok(true),
            None => Ok(false),
        }
    }

    @property
    def fallback_exec(&self) -> PyResult<Option<bool>> {
        match self.entry(py).get().get_fallback_exec() {
            Some(exec) => Ok(Some(exec)),
            None => Ok(None),
        }
    }

    @fallback_exec.setter
    def set_fallback_exec(&self, value: Option<PyObject>) -> PyResult<()> {
        match value {
            None => {self.entry(py).get().set_fallback_exec(None);},
            Some(value) => {
            if value.is_none(py) {
                self.entry(py).get().set_fallback_exec(None);
            } else {
                self.entry(py).get().set_fallback_exec(
                    Some(value.is_true(py)?)
                );
            }},
        }
        Ok(())
    }

    @property
    def has_fallback_symlink(&self) -> PyResult<bool> {
        match self.entry(py).get().get_fallback_symlink() {
            Some(_) => Ok(true),
            None => Ok(false),
        }
    }

    @property
    def fallback_symlink(&self) -> PyResult<Option<bool>> {
        match self.entry(py).get().get_fallback_symlink() {
            Some(symlink) => Ok(Some(symlink)),
            None => Ok(None),
        }
    }

    @fallback_symlink.setter
    def set_fallback_symlink(&self, value: Option<PyObject>) -> PyResult<()> {
        match value {
            None => {self.entry(py).get().set_fallback_symlink(None);},
            Some(value) => {
            if value.is_none(py) {
                self.entry(py).get().set_fallback_symlink(None);
            } else {
                self.entry(py).get().set_fallback_symlink(
                    Some(value.is_true(py)?)
                );
            }},
        }
        Ok(())
    }

    @property
    def tracked(&self) -> PyResult<bool> {
        Ok(self.entry(py).get().tracked())
    }

    @property
    def p1_tracked(&self) -> PyResult<bool> {
        Ok(self.entry(py).get().p1_tracked())
    }

    @property
    def added(&self) -> PyResult<bool> {
        Ok(self.entry(py).get().added())
    }


    @property
    def p2_info(&self) -> PyResult<bool> {
        Ok(self.entry(py).get().p2_info())
    }

    @property
    def removed(&self) -> PyResult<bool> {
        Ok(self.entry(py).get().removed())
    }

    @property
    def maybe_clean(&self) -> PyResult<bool> {
        Ok(self.entry(py).get().maybe_clean())
    }

    @property
    def any_tracked(&self) -> PyResult<bool> {
        Ok(self.entry(py).get().any_tracked())
    }

    def v1_state(&self) -> PyResult<PyBytes> {
        let (state, _mode, _size, _mtime) = self.entry(py).get().v1_data();
        let state_byte: u8 = state.into();
        Ok(PyBytes::new(py, &[state_byte]))
    }

    def v1_mode(&self) -> PyResult<i32> {
        let (_state, mode, _size, _mtime) = self.entry(py).get().v1_data();
        Ok(mode)
    }

    def v1_size(&self) -> PyResult<i32> {
        let (_state, _mode, size, _mtime) = self.entry(py).get().v1_data();
        Ok(size)
    }

    def v1_mtime(&self) -> PyResult<i32> {
        let (_state, _mode, _size, mtime) = self.entry(py).get().v1_data();
        Ok(mtime)
    }

    def need_delay(&self, now: (u32, u32)) -> PyResult<bool> {
        let now = timestamp(py, now)?;
        Ok(self.entry(py).get().need_delay(now))
    }

    def mtime_likely_equal_to(&self, other: (u32, u32)) -> PyResult<bool> {
        if let Some(mtime) = self.entry(py).get().truncated_mtime() {
            Ok(mtime.likely_equal(timestamp(py, other)?))
        } else {
            Ok(false)
        }
    }

    @classmethod
    def from_v1_data(
        _cls,
        state: PyBytes,
        mode: i32,
        size: i32,
        mtime: i32,
    ) -> PyResult<Self> {
        let state = <[u8; 1]>::try_from(state.data(py))
            .ok()
            .and_then(|state| EntryState::try_from(state[0]).ok())
            .ok_or_else(|| PyErr::new::<exc::ValueError, _>(py, "invalid state"))?;
        let entry = DirstateEntry::from_v1_data(state, mode, size, mtime);
        DirstateItem::create_instance(py, Cell::new(entry))
    }

    def drop_merge_data(&self) -> PyResult<PyNone> {
        self.update(py, |entry| entry.drop_merge_data());
        Ok(PyNone)
    }

    def set_clean(
        &self,
        mode: u32,
        size: u32,
        mtime: (u32, u32),
    ) -> PyResult<PyNone> {
        let mtime = timestamp(py, mtime)?;
        self.update(py, |entry| entry.set_clean(mode, size, mtime));
        Ok(PyNone)
    }

    def set_possibly_dirty(&self) -> PyResult<PyNone> {
        self.update(py, |entry| entry.set_possibly_dirty());
        Ok(PyNone)
    }

    def set_tracked(&self) -> PyResult<PyNone> {
        self.update(py, |entry| entry.set_tracked());
        Ok(PyNone)
    }

    def set_untracked(&self) -> PyResult<PyNone> {
        self.update(py, |entry| entry.set_untracked());
        Ok(PyNone)
    }
});

impl DirstateItem {
    pub fn new_as_pyobject(
        py: Python<'_>,
        entry: DirstateEntry,
    ) -> PyResult<PyObject> {
        Ok(DirstateItem::create_instance(py, Cell::new(entry))?.into_object())
    }

    pub fn get_entry(&self, py: Python<'_>) -> DirstateEntry {
        self.entry(py).get()
    }

    // TODO: Use https://doc.rust-lang.org/std/cell/struct.Cell.html#method.update instead when it’s stable
    pub fn update(&self, py: Python<'_>, f: impl FnOnce(&mut DirstateEntry)) {
        let mut entry = self.entry(py).get();
        f(&mut entry);
        self.entry(py).set(entry)
    }
}

pub(crate) fn timestamp(
    py: Python<'_>,
    (s, ns): (u32, u32),
) -> PyResult<TruncatedTimestamp> {
    TruncatedTimestamp::from_already_truncated(s, ns).map_err(|_| {
        PyErr::new::<exc::ValueError, _>(
            py,
            "expected mtime truncated to 31 bits",
        )
    })
}
