use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use std::fmt::Write;
use std::str::FromStr;
use std::string::ToString;

use fastobo::ast;
use fastobo::ast as obo;
use fastobo::ast::QuotedString;
use fastobo::ast::UnquotedString;
use fastobo::share::Cow;
use fastobo::share::Redeem;
use fastobo::share::Share;

use pyo3::class::basic::CompareOp;
use pyo3::class::gc::PyVisit;
use pyo3::exceptions::IndexError;
use pyo3::exceptions::NotImplementedError;
use pyo3::exceptions::RuntimeError;
use pyo3::exceptions::TypeError;
use pyo3::exceptions::ValueError;
use pyo3::gc::PyTraverseError;
use pyo3::prelude::*;
use pyo3::type_object::PyTypeCreate;
use pyo3::types::PyAny;
use pyo3::types::PyDateAccess;
use pyo3::types::PyDateTime;
use pyo3::types::PyList;
use pyo3::types::PyString;
use pyo3::types::PyTimeAccess;
use pyo3::PyGCProtocol;
use pyo3::PyNativeType;
use pyo3::PyObjectProtocol;
use pyo3::PySequenceProtocol;
use pyo3::PyTypeInfo;

use super::super::abc::AbstractClause;
use super::super::id::BaseIdent;
use super::super::id::Ident;
use super::super::id::IdentPrefix;
use super::super::id::Url;
use super::super::pv::PropertyValue;
use super::super::syn::SynonymScope;
use crate::utils::AsGILRef;
use crate::utils::ClonePy;

// --- Conversion Wrapper ----------------------------------------------------

/// A thin wrapper for a reference to any possible `BaseHeaderClause` subclass.
#[derive(ClonePy, Debug, PartialEq, PyWrapper)]
#[wraps(BaseHeaderClause)]
pub enum HeaderClause {
    FormatVersion(Py<FormatVersionClause>),
    DataVersion(Py<DataVersionClause>),
    Date(Py<DateClause>),
    SavedBy(Py<SavedByClause>),
    AutoGeneratedBy(Py<AutoGeneratedByClause>),
    Import(Py<ImportClause>),
    Subsetdef(Py<SubsetdefClause>),
    SynonymTypedef(Py<SynonymTypedefClause>),
    DefaultNamespace(Py<DefaultNamespaceClause>),
    NamespaceIdRule(Py<NamespaceIdRuleClause>),
    Idspace(Py<IdspaceClause>),
    TreatXrefsAsEquivalent(Py<TreatXrefsAsEquivalentClause>),
    TreatXrefsAsGenusDifferentia(Py<TreatXrefsAsGenusDifferentiaClause>),
    TreatXrefsAsReverseGenusDifferentia(Py<TreatXrefsAsReverseGenusDifferentiaClause>),
    TreatXrefsAsRelationship(Py<TreatXrefsAsRelationshipClause>),
    TreatXrefsAsIsA(Py<TreatXrefsAsIsAClause>),
    TreatXrefsAsHasSubclass(Py<TreatXrefsAsHasSubclassClause>),
    PropertyValue(Py<PropertyValueClause>),
    Remark(Py<RemarkClause>),
    Ontology(Py<OntologyClause>),
    OwlAxioms(Py<OwlAxiomsClause>),
    Unreserved(Py<UnreservedClause>),
}

impl FromPy<fastobo::ast::HeaderClause> for HeaderClause {
    fn from_py(clause: fastobo::ast::HeaderClause, py: Python) -> Self {
        use fastobo::ast::HeaderClause::*;
        match clause {
            FormatVersion(v) => {
                Py::new(py, FormatVersionClause::new(py, v)).map(HeaderClause::FormatVersion)
            }
            DataVersion(v) => {
                Py::new(py, DataVersionClause::new(py, v)).map(HeaderClause::DataVersion)
            }
            Date(dt) => Py::new(py, DateClause::new(py, dt)).map(HeaderClause::Date),
            SavedBy(name) => Py::new(py, SavedByClause::new(py, name)).map(HeaderClause::SavedBy),
            AutoGeneratedBy(name) => {
                Py::new(py, AutoGeneratedByClause::new(py, name)).map(HeaderClause::AutoGeneratedBy)
            }
            Import(i) => Py::new(py, ImportClause::new(py, i)).map(HeaderClause::Import),
            Subsetdef(s, q) => {
                Py::new(py, SubsetdefClause::new(py, s, q)).map(HeaderClause::Subsetdef)
            }
            SynonymTypedef(ty, desc, scope) =>
            //FIXME
            {
                Py::new(py, SynonymTypedefClause::with_scope(py, ty, desc, scope))
                    .map(HeaderClause::SynonymTypedef)
            }
            DefaultNamespace(ns) => {
                Py::new(py, DefaultNamespaceClause::new(py, ns)).map(HeaderClause::DefaultNamespace)
            }
            NamespaceIdRule(r) => {
                Py::new(py, NamespaceIdRuleClause::new(py, r)).map(HeaderClause::NamespaceIdRule)
            }
            Idspace(prefix, url, desc) => {
                Py::new(py, IdspaceClause::with_description(py, prefix, url, desc))
                    .map(HeaderClause::Idspace)
            }
            TreatXrefsAsEquivalent(prefix) => {
                Py::new(py, TreatXrefsAsEquivalentClause::new(py, prefix))
                    .map(HeaderClause::TreatXrefsAsEquivalent)
            }
            TreatXrefsAsGenusDifferentia(p, r, c) => {
                Py::new(py, TreatXrefsAsGenusDifferentiaClause::new(py, p, r, c))
                    .map(HeaderClause::TreatXrefsAsGenusDifferentia)
            }
            TreatXrefsAsReverseGenusDifferentia(p, r, c) => Py::new(
                py,
                TreatXrefsAsReverseGenusDifferentiaClause::new(py, p, r, c),
            )
            .map(HeaderClause::TreatXrefsAsReverseGenusDifferentia),
            TreatXrefsAsRelationship(p, r) => {
                Py::new(py, TreatXrefsAsRelationshipClause::new(py, p, r))
                    .map(HeaderClause::TreatXrefsAsRelationship)
            }
            TreatXrefsAsIsA(p) => {
                Py::new(py, TreatXrefsAsIsAClause::new(py, p)).map(HeaderClause::TreatXrefsAsIsA)
            }
            TreatXrefsAsHasSubclass(p) => Py::new(py, TreatXrefsAsHasSubclassClause::new(py, p))
                .map(HeaderClause::TreatXrefsAsHasSubclass),
            PropertyValue(pv) => {
                Py::new(py, PropertyValueClause::new(py, pv)).map(HeaderClause::PropertyValue)
            }
            Remark(r) => Py::new(py, RemarkClause::new(py, r)).map(HeaderClause::Remark),
            Ontology(ont) => Py::new(py, OntologyClause::new(py, ont)).map(HeaderClause::Ontology),
            OwlAxioms(ax) => Py::new(py, OwlAxiomsClause::new(py, ax)).map(HeaderClause::OwlAxioms),
            Unreserved(tag, value) => {
                Py::new(py, UnreservedClause::new(py, tag, value)).map(HeaderClause::Unreserved)
            }
        }
        .expect("could not allocate memory in Python heap")
    }
}

impl FromPy<HeaderClause> for fastobo::ast::HeaderClause {
    fn from_py(clause: HeaderClause, py: Python) -> Self {
        Self::from_py(&clause, py)
    }
}

// --- Base ------------------------------------------------------------------

/// A header clause, appearing in the OBO header frame.
#[pyclass(extends=AbstractClause, module="fastobo.header")]
pub struct BaseHeaderClause {}

// --- FormatVersion ---------------------------------------------------------

/// FormatVersionClause(version)
/// --
///
/// A header clause indicating the format version of the OBO document.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct FormatVersionClause {
    version: obo::UnquotedString,
}

impl FormatVersionClause {
    pub fn new(_py: Python, version: obo::UnquotedString) -> Self {
        Self { version }
    }
}

impl Display for FormatVersionClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        obo::HeaderClause::from(self.clone()).fmt(f)
    }
}

impl From<FormatVersionClause> for obo::HeaderClause {
    fn from(clause: FormatVersionClause) -> Self {
        obo::HeaderClause::FormatVersion(clause.version)
    }
}

impl FromPy<FormatVersionClause> for obo::HeaderClause {
    fn from_py(clause: FormatVersionClause, _py: Python) -> Self {
        Self::from(clause)
    }
}

#[pymethods]
impl FormatVersionClause {
    #[new]
    fn __init__(obj: &PyRawObject, version: String) {
        obj.init(Self::new(obj.py(), obo::UnquotedString::new(version)));
    }

    /// `str`: the OBO format version used in document.
    #[getter]
    fn get_version(&self) -> PyResult<&str> {
        Ok(self.version.as_str())
    }

    #[setter]
    fn set_version(&mut self, version: String) -> PyResult<()> {
        self.version = obo::UnquotedString::new(version);
        Ok(())
    }
}

impl_raw_tag!(FormatVersionClause, "format-version");
impl_raw_value!(FormatVersionClause, "{}", self.version);

#[pyproto]
impl PyObjectProtocol for FormatVersionClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, FormatVersionClause(self.version))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.version)
    }
}

// --- DataVersion -----------------------------------------------------------

/// DataVersionClause(version)
/// --
///
/// A header clause indicating the version of the data in the OBO document.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct DataVersionClause {
    version: UnquotedString,
}

impl DataVersionClause {
    pub fn new(_py: Python, version: UnquotedString) -> Self {
        Self { version }
    }
}

impl From<DataVersionClause> for obo::HeaderClause {
    fn from(clause: DataVersionClause) -> obo::HeaderClause {
        obo::HeaderClause::DataVersion(clause.version)
    }
}

impl FromPy<DataVersionClause> for obo::HeaderClause {
    fn from_py(clause: DataVersionClause, _py: Python) -> Self {
        Self::from(clause)
    }
}

impl Display for DataVersionClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        obo::HeaderClause::from(self.clone()).fmt(f)
    }
}

#[pymethods]
impl DataVersionClause {
    #[new]
    fn __init__(obj: &PyRawObject, version: String) {
        obj.init(Self::new(obj.py(), UnquotedString::new(version)));
    }

    /// `str`: the version of the data in the OBO document.
    #[getter]
    fn get_version(&self) -> PyResult<&str> {
        Ok(self.version.as_str())
    }

    #[setter]
    fn set_version(&mut self, version: String) -> PyResult<()> {
        self.version = UnquotedString::new(version);
        Ok(())
    }
}

impl_raw_tag!(DataVersionClause, "data-version");
impl_raw_value!(DataVersionClause, "{}", self.version);

#[pyproto]
impl PyObjectProtocol for DataVersionClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, DataVersionClause(self.version))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.version)
    }
}

// --- Date ------------------------------------------------------------------

/// DateClause(date)
/// --
///
/// A header clause indicating the date the document was last modified.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct DateClause {
    date: obo::NaiveDateTime,
}

impl DateClause {
    pub fn new(_py: Python, date: obo::NaiveDateTime) -> Self {
        Self { date }
    }
}

impl From<DateClause> for obo::HeaderClause {
    fn from(clause: DateClause) -> obo::HeaderClause {
        obo::HeaderClause::Date(clause.date)
    }
}

impl FromPy<DateClause> for obo::HeaderClause {
    fn from_py(clause: DateClause, _py: Python) -> Self {
        Self::from(clause)
    }
}

impl Display for DateClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        obo::HeaderClause::from(self.clone()).fmt(f)
    }
}

#[pymethods]
impl DateClause {
    #[new]
    fn __init__(obj: &PyRawObject, date: &PyDateTime) {
        let dt = fastobo::ast::NaiveDateTime::new(
            date.get_day() as u8,
            date.get_month() as u8,
            date.get_year() as u16,
            date.get_hour() as u8,
            date.get_minute() as u8,
        );
        obj.init(Self::new(obj.py(), dt))
    }

    /// `~datetime.datetime`: the date this document was last modified.
    #[getter]
    fn get_date<'py>(&self, py: Python<'py>) -> PyResult<&'py PyDateTime> {
        PyDateTime::new(
            py,
            self.date.year() as i32,
            self.date.month(),
            self.date.day(),
            self.date.hour(),
            self.date.minute(),
            0,
            0,
            None,
        )
    }

    #[setter]
    fn set_date(&mut self, dt: &PyDateTime) -> PyResult<()> {
        self.date = fastobo::ast::NaiveDateTime::new(
            dt.get_day(),
            dt.get_month(),
            dt.get_year() as u16,
            dt.get_hour(),
            dt.get_minute(),
        );
        Ok(())
    }
}

impl_raw_tag!(DateClause, "date-version");
impl_raw_value!(DateClause, "{}", self.date);

#[pyproto]
impl PyObjectProtocol for DateClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let fmt = PyString::new(py, "DateClause({!r})").to_object(py);
        fmt.call_method1(py, "format", (self.get_date(py)?,))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        if let Ok(ref clause) = other.downcast_ref::<Self>() {
            Ok(match op {
                CompareOp::Eq => self.date == clause.date,
                CompareOp::Ne => self.date != clause.date,
                CompareOp::Lt => self.date < clause.date,
                CompareOp::Le => self.date <= clause.date,
                CompareOp::Gt => self.date > clause.date,
                CompareOp::Ge => self.date >= clause.date,
            }
            .to_object(other.py()))
        } else {
            match op {
                CompareOp::Eq => Ok(false.to_object(other.py())),
                CompareOp::Ne => Ok(true.to_object(other.py())),
                _ => Ok(other.py().NotImplemented()),
            }
        }
    }
}

// --- SavedBy ---------------------------------------------------------------

/// SavedByClause(name)
/// --
///
/// A header clause containing the name of the person who saved the document.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct SavedByClause {
    name: UnquotedString,
}

impl SavedByClause {
    pub fn new(_py: Python, name: UnquotedString) -> Self {
        Self { name }
    }
}

impl Display for SavedByClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        obo::HeaderClause::from(self.clone()).fmt(f)
    }
}

impl From<SavedByClause> for obo::HeaderClause {
    fn from(clause: SavedByClause) -> obo::HeaderClause {
        obo::HeaderClause::SavedBy(clause.name)
    }
}

impl FromPy<SavedByClause> for obo::HeaderClause {
    fn from_py(clause: SavedByClause, _py: Python) -> obo::HeaderClause {
        Self::from(clause)
    }
}

#[pymethods]
impl SavedByClause {
    #[new]
    fn __init__(obj: &PyRawObject, name: String) {
        obj.init(Self::new(obj.py(), UnquotedString::new(name)));
    }

    /// `str`: the name of the person who saved the document.
    #[getter]
    fn get_name(&self) -> PyResult<&str> {
        Ok(self.name.as_str())
    }

    #[setter]
    fn set_name(&mut self, name: String) -> PyResult<()> {
        self.name = UnquotedString::new(name);
        Ok(())
    }
}

impl_raw_tag!(SavedByClause, "saved-by");
impl_raw_value!(SavedByClause, "{}", self.name);

#[pyproto]
impl PyObjectProtocol for SavedByClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, SavedByClause(self.name))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.name)
    }
}

// --- AutoGeneratedBy -------------------------------------------------------

/// AutoGeneratedByClause(name)
/// --
///
/// A header clause indicating the software that generated the document.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct AutoGeneratedByClause {
    name: UnquotedString,
}

impl AutoGeneratedByClause {
    pub fn new(_py: Python, name: UnquotedString) -> Self {
        Self { name }
    }
}

impl Display for AutoGeneratedByClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        obo::HeaderClause::from(self.clone()).fmt(f)
    }
}

impl From<AutoGeneratedByClause> for obo::HeaderClause {
    fn from(clause: AutoGeneratedByClause) -> obo::HeaderClause {
        obo::HeaderClause::AutoGeneratedBy(clause.name)
    }
}

impl FromPy<AutoGeneratedByClause> for obo::HeaderClause {
    fn from_py(clause: AutoGeneratedByClause, _py: Python) -> obo::HeaderClause {
        Self::from(clause)
    }
}

#[pymethods]
impl AutoGeneratedByClause {
    #[new]
    fn __init__(obj: &PyRawObject, name: String) {
        obj.init(Self::new(obj.py(), UnquotedString::new(name)));
    }

    /// `str`: the name of the software that generated the document.
    #[getter]
    fn get_name(&self) -> PyResult<&str> {
        Ok(self.name.as_ref())
    }

    #[setter]
    fn set_name(&mut self, name: String) -> PyResult<()> {
        self.name = UnquotedString::new(name);
        Ok(())
    }
}

impl_raw_tag!(AutoGeneratedByClause, "auto-generated-by");
impl_raw_value!(AutoGeneratedByClause, "{}", self.name);

#[pyproto]
impl PyObjectProtocol for AutoGeneratedByClause {
    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, AutoGeneratedByClause(self.name))
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.name)
    }
}

// --- Import ----------------------------------------------------------------

/// ImportClause(reference)
/// --
///
/// A clause with a URL or ontology ID referencing another OBO document.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct ImportClause {
    reference: obo::Import, // should be `Import` ?
}

impl ImportClause {
    pub fn new(_py: Python, reference: obo::Import) -> Self {
        Self { reference }
    }
}

impl Display for ImportClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        obo::HeaderClause::from(self.clone()).fmt(f)
    }
}

impl From<ImportClause> for obo::HeaderClause {
    fn from(clause: ImportClause) -> Self {
        obo::HeaderClause::Import(clause.reference)
    }
}

impl FromPy<ImportClause> for obo::HeaderClause {
    fn from_py(clause: ImportClause, _py: Python) -> Self {
        Self::from(clause)
    }
}

#[pymethods]
impl ImportClause {
    // FIXME(@althonos): should not be implicit here ?
    #[new]
    pub fn __init__(obj: &PyRawObject, reference: &str) -> PyResult<()> {
        let py = obj.py();
        if let Ok(url) = url::Url::from_str(reference) {
            Ok(obj.init(Self::new(py, obo::Import::Url(url))))
        } else if let Ok(id) = obo::Ident::from_str(reference) {
            Ok(obj.init(Self::new(py, obo::Import::Abbreviated(id))))
        } else {
            ValueError::into(format!("invalid import: {:?}", reference))
        }
    }

    #[getter]
    /// `str`: a reference to a document to import.
    fn get_reference(&self) -> PyResult<String> {
        Ok(self.reference.to_string()) // FIXME ?
    }
}

impl_raw_tag!(ImportClause, "import");
impl_raw_value!(ImportClause, "{}", self.reference);

#[pyproto]
impl PyObjectProtocol for ImportClause {
    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __repr__(&self) -> PyResult<PyObject> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let fmt = PyString::new(py, "SubsetdefClause({})").to_object(py);
        fmt.call_method1(py, "format", (self.reference.to_string(),))
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.reference)
    }
}

// --- Subsetdef -------------------------------------------------------------

/// SubsetdefClause(subset, description)
/// --
///
/// A header clause declaring a subset in the OBO document.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Debug)]
pub struct SubsetdefClause {
    subset: Ident,
    description: QuotedString,
}

impl SubsetdefClause {
    pub fn new<I>(py: Python, subset: I, description: QuotedString) -> Self
    where
        I: IntoPy<Ident>,
    {
        Self {
            subset: subset.into_py(py),
            description,
        }
    }
}

impl ClonePy for SubsetdefClause {
    fn clone_py(&self, py: Python) -> Self {
        SubsetdefClause {
            subset: self.subset.clone_py(py),
            description: self.description.clone(),
        }
    }
}

impl Display for SubsetdefClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        let gil = Python::acquire_gil();
        let py = gil.python();
        obo::HeaderClause::from_py(self.clone_py(py), py).fmt(f)
    }
}

impl FromPy<SubsetdefClause> for obo::HeaderClause {
    fn from_py(clause: SubsetdefClause, py: Python) -> Self {
        obo::HeaderClause::Subsetdef(
            obo::SubsetIdent::from_py(clause.subset, py),
            clause.description,
        )
    }
}

#[pymethods]
impl SubsetdefClause {
    #[new]
    fn __init__(obj: &PyRawObject, subset: Ident, description: String) -> PyResult<()> {
        let py = obj.py();
        Ok(obj.init(Self::new(py, subset, QuotedString::new(description))))
    }

    /// `~fastobo.id.Ident`: the identifier of the declared subset.
    #[getter]
    fn get_subset<'py>(&self, py: Python<'py>) -> PyResult<PyObject> {
        Ok(self.subset.to_object(py))
    }

    #[setter]
    fn set_subset(&mut self, subset: Ident) -> PyResult<()> {
        self.subset = subset;
        Ok(())
    }

    /// `str`: a description for the declared subset.
    #[getter]
    fn get_description(&self) -> PyResult<&str> {
        Ok(self.description.as_str())
    }

    #[setter]
    fn set_description(&mut self, description: String) -> PyResult<()> {
        self.description = fastobo::ast::QuotedString::new(description);
        Ok(())
    }
}

impl_raw_tag!(SubsetdefClause, "subsetdef");
impl_raw_value!(SubsetdefClause, "{} {}", self.subset, self.description);

#[pyproto]
impl PyObjectProtocol for SubsetdefClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let r = self.subset.to_object(py).call_method0(py, "__repr__")?;
        let fmt = PyString::new(py, "SubsetdefClause({}, {!r})").to_object(py);
        fmt.call_method1(py, "format", (r, self.description.as_str()))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.subset && self.description)
    }
}

// --- SynonymTypedef --------------------------------------------------------

/// SynonymTypedefClause(typedef, description, scope=None)
/// --
///
/// A header clause declaring a synonym type in the OBO document.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Debug)]
pub struct SynonymTypedefClause {
    #[pyo3(set)]
    typedef: Ident,
    description: QuotedString,
    scope: Option<SynonymScope>, // FIXME: Python type
}

impl SynonymTypedefClause {
    pub fn new<T, D>(py: Python, typedef: T, description: D) -> Self
    where
        T: IntoPy<Ident>,
        D: Into<QuotedString>,
    {
        Self {
            typedef: typedef.into_py(py),
            description: description.into(),
            scope: None,
        }
    }

    pub fn with_scope<T, D, S>(py: Python, typedef: T, description: D, scope: Option<S>) -> Self
    where
        T: IntoPy<Ident>,
        D: Into<QuotedString>,
        S: IntoPy<SynonymScope>,
    {
        Self {
            typedef: typedef.into_py(py),
            description: description.into(),
            scope: scope.map(|s| s.into_py(py)),
        }
    }
}

impl ClonePy for SynonymTypedefClause {
    fn clone_py(&self, py: Python) -> Self {
        SynonymTypedefClause {
            typedef: self.typedef.clone_py(py),
            description: self.description.clone(),
            scope: self.scope.clone(),
        }
    }
}

impl Display for SynonymTypedefClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        let gil = Python::acquire_gil();
        let py = gil.python();
        obo::HeaderClause::from_py(self.clone_py(py), py).fmt(f)
    }
}

impl FromPy<SynonymTypedefClause> for obo::HeaderClause {
    fn from_py(clause: SynonymTypedefClause, py: Python) -> Self {
        obo::HeaderClause::SynonymTypedef(
            clause.typedef.into_py(py),
            clause.description,
            clause.scope.map(|s| s.into_py(py)),
        )
    }
}

#[pymethods]
impl SynonymTypedefClause {
    #[new]
    fn __init__(obj: &PyRawObject, typedef: Ident, description: String, scope: Option<String>) {
        let desc = fastobo::ast::QuotedString::new(description);
        let sc = scope.map(|s| fastobo::ast::SynonymScope::from_str(&s).unwrap()); // FIXME
        obj.init(Self::with_scope(obj.py(), typedef, desc, sc));
    }

    /// `~fastobo.id.Ident`: the identifier of the declared synonym type.
    #[getter]
    fn get_typedef(&self) -> PyResult<&Ident> {
        Ok(&self.typedef)
    }

    /// `str`: the description of the declared synonym type.
    #[getter]
    fn get_description(&self) -> PyResult<&str> {
        Ok(self.description.as_str())
    }

    #[setter]
    fn set_description(&mut self, description: String) -> PyResult<()> {
        self.description = fastobo::ast::QuotedString::new(description);
        Ok(())
    }

    /// `str`, optional: the scope of the synonym type, or `None`.
    #[getter]
    fn get_scope(&self) -> PyResult<Option<String>> {
        Ok(self.scope.as_ref().map(ToString::to_string))
    }

    #[setter]
    fn set_scope(&mut self, scope: Option<&str>) -> PyResult<()> {
        self.scope = scope.map(SynonymScope::from_str).transpose()?;
        Ok(())
    }

    pub fn raw_value(&self) -> PyResult<String> {
        if let Some(scope) = &self.scope {
            Ok(format!("{} {} {}", self.typedef, self.description, scope))
        } else {
            Ok(format!("{} {}", self.typedef, self.description))
        }
    }
}

impl_raw_tag!(SynonymTypedefClause, "synonymtypedef");

#[pyproto]
impl PyObjectProtocol for SynonymTypedefClause {
    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __repr__(&self) -> PyResult<PyObject> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        if let Some(ref scope) = self.scope {
            let fmt = PyString::new(py, "SynonymTypedefClause({!r}, {!r}, {!r})").to_object(py);
            fmt.call_method1(
                py,
                "format",
                (
                    &self.typedef,
                    self.description.as_str(),
                    scope.to_object(py),
                ),
            )
        } else {
            let fmt = PyString::new(py, "SynonymTypedefClause({!r}, {!r})").to_object(py);
            fmt.call_method1(py, "format", (&self.typedef, self.description.as_str()))
        }
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(
            self,
            other,
            op,
            self.typedef && self.description && self.scope
        )
    }
}

// --- DefaultNamespace ------------------------------------------------------

/// DefaultNamespaceClause(namespace)
/// --
///
/// A clause declaring the default namespace for the rest of the document.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Debug)]
pub struct DefaultNamespaceClause {
    namespace: Ident,
}

impl DefaultNamespaceClause {
    pub fn new<I>(py: Python, namespace: I) -> Self
    where
        I: IntoPy<Ident>,
    {
        Self {
            namespace: namespace.into_py(py),
        }
    }
}

impl ClonePy for DefaultNamespaceClause {
    fn clone_py(&self, py: Python) -> Self {
        Self {
            namespace: self.namespace.clone_py(py),
        }
    }
}

impl Display for DefaultNamespaceClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        let gil = Python::acquire_gil();
        let py = gil.python();
        obo::HeaderClause::from_py(self.clone_py(py), py).fmt(f)
    }
}

impl FromPy<DefaultNamespaceClause> for obo::HeaderClause {
    fn from_py(clause: DefaultNamespaceClause, py: Python) -> Self {
        obo::HeaderClause::DefaultNamespace(clause.namespace.into_py(py))
    }
}

#[pymethods]
impl DefaultNamespaceClause {
    #[new]
    fn __init__(obj: &PyRawObject, namespace: &PyAny) -> PyResult<()> {
        let py = obj.py();
        let ident = if py.is_instance::<BaseIdent, PyAny>(namespace)? {
            Ident::extract(namespace)?
        } else if py.is_instance::<PyString, PyAny>(namespace)? {
            let s: &PyString = FromPyObject::extract(namespace)?;
            let id = ast::Ident::from_str(&s.to_string()?).unwrap(); // FIXME
            Ident::from_py(id, py)
        } else {
            return TypeError::into("expected str or Ident for 'namespace'");
        };
        Ok(obj.init(Self::new(py, ident)))
    }

    /// `~fastobo.id.Ident`: the default namespace for this ontology.
    #[getter]
    fn get_namespace(&self) -> PyResult<&Ident> {
        Ok(&self.namespace)
    }

    #[setter]
    fn set_namespace(&mut self, namespace: Ident) -> PyResult<()> {
        self.namespace = namespace;
        Ok(())
    }
}

impl_raw_tag!(DefaultNamespaceClause, "default-namespace");
impl_raw_value!(DefaultNamespaceClause, "{}", self.namespace);

#[pyproto]
impl PyObjectProtocol for DefaultNamespaceClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let ns = self.namespace.to_object(py);
        let nsref = ns.as_ref(py);
        let fmt = PyString::new(py, "DefaultNamespaceClause({})").to_object(py);
        fmt.call_method1(py, "format", (nsref.repr()?,))
    }

    fn __str__(&self) -> PyResult<String> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        Ok(self.clone_py(py).to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.namespace)
    }
}

// --- NamespaceIdRuleClause -------------------------------------------------

/// NamespaceIdRuleClause(rule)
/// --
///
/// A clause to describe the rule for Namespace ID generation in this document.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct NamespaceIdRuleClause {
    rule: fastobo::ast::UnquotedString,
}

impl NamespaceIdRuleClause {
    pub fn new(_py: Python, rule: fastobo::ast::UnquotedString) -> Self {
        Self { rule }
    }
}

impl Display for NamespaceIdRuleClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        let gil = Python::acquire_gil();
        let py = gil.python();
        obo::HeaderClause::from_py(self.clone_py(py), py).fmt(f)
    }
}

impl FromPy<NamespaceIdRuleClause> for obo::HeaderClause {
    fn from_py(clause: NamespaceIdRuleClause, _py: Python) -> Self {
        obo::HeaderClause::NamespaceIdRule(clause.rule)
    }
}

impl From<NamespaceIdRuleClause> for obo::HeaderClause {
    fn from(clause: NamespaceIdRuleClause) -> Self {
        obo::HeaderClause::NamespaceIdRule(clause.rule)
    }
}

#[pymethods]
impl NamespaceIdRuleClause {
    #[new]
    fn __init__(obj: &PyRawObject, rule: String) -> PyResult<()> {
        Ok(obj.init(Self::new(
            obj.py(),
            fastobo::ast::UnquotedString::new(rule),
        )))
    }

    /// `str`: the default namespace for this ontology.
    #[getter]
    fn get_rule(&self) -> PyResult<&str> {
        Ok(self.rule.as_str())
    }

    #[setter]
    fn set_rule(&mut self, rule: String) -> PyResult<()> {
        self.rule = fastobo::ast::UnquotedString::new(rule);
        Ok(())
    }
}

impl_raw_tag!(NamespaceIdRuleClause, "namespace-id-rule");
impl_raw_value!(NamespaceIdRuleClause, "{}", self.rule);

#[pyproto]
impl PyObjectProtocol for NamespaceIdRuleClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let fmt = PyString::new(py, "NamespaceIdRuleClause({})").to_object(py);
        fmt.call_method1(py, "format", (self.rule.as_str(),))
    }

    fn __str__(&self) -> PyResult<String> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        Ok(self.clone_py(py).to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.rule)
    }
}

// --- IdspaceClause ---------------------------------------------------------

/// IdspaceClause(prefix, url, description=None)
/// --
///
/// A clause giving the mapping between a "local" and a "global" ID space.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Debug)]
pub struct IdspaceClause {
    prefix: IdentPrefix,
    url: Url,
    description: Option<QuotedString>,
}

impl IdspaceClause {
    pub fn new<I, U>(_py: Python, prefix: I, url: U) -> Self
    where
        I: Into<IdentPrefix>,
        U: Into<Url>,
    {
        Self {
            prefix: prefix.into(),
            url: url.into(),
            description: None,
        }
    }

    pub fn with_description<I, U, D>(_py: Python, prefix: I, url: U, description: D) -> Self
    where
        I: Into<IdentPrefix>,
        U: Into<Url>,
        D: Into<Option<QuotedString>>,
    {
        Self {
            prefix: prefix.into(),
            url: url.into(),
            description: description.into(),
        }
    }
}

impl ClonePy for IdspaceClause {
    fn clone_py(&self, py: Python) -> Self {
        Self {
            prefix: self.prefix.clone_py(py),
            url: self.url.clone(),
            description: self.description.clone(),
        }
    }
}

impl Display for IdspaceClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        let gil = Python::acquire_gil();
        let py = gil.python();
        fastobo::ast::HeaderClause::from_py(self.clone_py(py), py).fmt(f)
    }
}

impl From<IdspaceClause> for obo::HeaderClause {
    fn from(clause: IdspaceClause) -> Self {
        obo::HeaderClause::Idspace(clause.prefix.into(), clause.url.into(), clause.description)
    }
}

impl FromPy<IdspaceClause> for obo::HeaderClause {
    fn from_py(clause: IdspaceClause, _py: Python) -> Self {
        obo::HeaderClause::from(clause)
    }
}

#[pymethods]
impl IdspaceClause {
    /// `~fastobo.id.IdentPrefix`: the prefix used in prefixed IDs.
    #[getter]
    fn get_prefix(&self) -> PyResult<IdentPrefix> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        Ok(self.prefix.clone_py(py))
    }

    /// `~fastobo.id.Url`: the URL used to expand IDs of this IDspace.
    #[getter]
    fn get_url<'py>(&self, py: Python<'py>) -> PyResult<Url> {
        Ok(self.url.clone_py(py))
    }

    /// `str`, optional: a description of this IDspace.
    #[getter]
    fn get_description(&self) -> PyResult<Option<&str>> {
        match &self.description {
            Some(ref s) => Ok(Some(s.as_str())),
            None => Ok(None),
        }
    }

    pub fn raw_value(&self) -> PyResult<String> {
        if let Some(desc) = &self.description {
            Ok(format!("{} {} {}", self.prefix, self.url, desc))
        } else {
            Ok(format!("{} {}", self.prefix, self.url))
        }
    }
}

impl_raw_tag!(IdspaceClause, "idspace");

#[pyproto]
impl PyObjectProtocol for IdspaceClause {
    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __repr__(&self) -> PyResult<PyObject> {
        let gil = Python::acquire_gil();
        let py = gil.python();

        let pref = self.prefix.as_gil_ref(py).as_str();
        let url = self.url.__repr__()?;

        if let Some(ref desc) = self.description {
            let fmt = PyString::new(py, "IdspaceClause({!r}, {}, {!r})").to_object(py);
            fmt.call_method1(py, "format", (pref, url, desc.as_str()))
        } else {
            let fmt = PyString::new(py, "IdspaceClause({!r}, {})").to_object(py);
            fmt.call_method1(py, "format", (pref, url))
        }
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.prefix && self.url && self.description)
    }
}

// --- TreatXrefsAsEquivalentClause ------------------------------------------

/// TreatXrefsAsEquivalentClause(idspace)
/// --
///
/// A macro to treats xrefs coming from an ID space as equivalence statements.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct TreatXrefsAsEquivalentClause {
    idspace: IdentPrefix,
}

impl TreatXrefsAsEquivalentClause {
    pub fn new<I>(_py: Python, idspace: I) -> Self
    where
        I: Into<IdentPrefix>,
    {
        Self {
            idspace: idspace.into(),
        }
    }
}

impl Display for TreatXrefsAsEquivalentClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        obo::HeaderClause::from(self.clone()).fmt(f)
    }
}

impl From<TreatXrefsAsEquivalentClause> for obo::HeaderClause {
    fn from(clause: TreatXrefsAsEquivalentClause) -> Self {
        obo::HeaderClause::TreatXrefsAsEquivalent(clause.idspace.into())
    }
}

impl FromPy<TreatXrefsAsEquivalentClause> for obo::HeaderClause {
    fn from_py(clause: TreatXrefsAsEquivalentClause, _py: Python) -> Self {
        Self::from(clause)
    }
}

#[pymethods]
impl TreatXrefsAsEquivalentClause {
    /// `~fastobo.id.IdentPrefix`: the ID prefix to select some Xrefs with.
    #[getter]
    fn get_idspace<'py>(&self, py: Python<'py>) -> PyResult<IdentPrefix> {
        Ok(self.idspace.clone_py(py))
    }
}

impl_raw_tag!(TreatXrefsAsEquivalentClause, "treat-xrefs-as-equivalent");
impl_raw_value!(TreatXrefsAsEquivalentClause, "{}", self.idspace);

#[pyproto]
impl PyObjectProtocol for TreatXrefsAsEquivalentClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, TreatXrefsAsEquivalentClause(self.idspace))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.idspace)
    }
}

// --- TreatXrefsAsGenusDifferentiaClause ------------------------------------

/// TreatXrefsAsGenusDifferentiaClause(idspace, relation, filler)
/// --
///
/// A macro to treats xrefs from an ID space as genus-differentia definitions.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Debug)]
pub struct TreatXrefsAsGenusDifferentiaClause {
    idspace: IdentPrefix,
    relation: Ident,
    filler: Ident,
}

impl TreatXrefsAsGenusDifferentiaClause {
    pub fn new<I, R, F>(py: Python, idspace: I, relation: R, filler: F) -> Self
    where
        I: Into<IdentPrefix>,
        R: IntoPy<Ident>,
        F: IntoPy<Ident>,
    {
        Self {
            idspace: idspace.into(),
            relation: relation.into_py(py),
            filler: filler.into_py(py),
        }
    }
}

impl ClonePy for TreatXrefsAsGenusDifferentiaClause {
    fn clone_py(&self, py: Python) -> Self {
        Self {
            idspace: self.idspace.clone(),
            relation: self.relation.clone_py(py),
            filler: self.filler.clone_py(py),
        }
    }
}

impl Display for TreatXrefsAsGenusDifferentiaClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        let gil = Python::acquire_gil();
        let py = gil.python();
        obo::HeaderClause::from_py(self.clone_py(py), py).fmt(f)
    }
}

impl FromPy<TreatXrefsAsGenusDifferentiaClause> for obo::HeaderClause {
    fn from_py(clause: TreatXrefsAsGenusDifferentiaClause, py: Python) -> Self {
        obo::HeaderClause::TreatXrefsAsGenusDifferentia(
            clause.idspace.into(),
            clause.relation.into_py(py),
            clause.filler.into_py(py),
        )
    }
}

#[pymethods]
impl TreatXrefsAsGenusDifferentiaClause {
    /// `~fastobo.id.IdentPrefix`: the ID prefix to select some Xrefs with.
    #[getter]
    fn get_idspace<'py>(&self, py: Python<'py>) -> PyResult<IdentPrefix> {
        Ok(self.idspace.clone_py(py))
    }
}

impl_raw_tag!(
    TreatXrefsAsGenusDifferentiaClause,
    "treat-xrefs-as-genus-differentia"
);
impl_raw_value!(
    TreatXrefsAsGenusDifferentiaClause,
    "{} {} {}",
    self.idspace,
    self.relation,
    self.filler
);

#[pyproto]
impl PyObjectProtocol for TreatXrefsAsGenusDifferentiaClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, TreatXrefsAsGenusDifferentiaClause(self.idspace, self.relation, self.filler))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.idspace && self.relation && self.filler)
    }
}

// --- TreatXrefsAsReverseGenusDifferentiaClause -----------------------------

/// TreatXrefsAsReverseGenusDifferentiaClause(idspace, relation, filler)
/// --
///
/// A macro to treats xrefs from an ID space as reverse genus-differentia definitions.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Debug)]
pub struct TreatXrefsAsReverseGenusDifferentiaClause {
    idspace: IdentPrefix,
    relation: Ident,      // Should be `RelationId`
    filler: Ident,        // Should be `ClassId`
}

impl TreatXrefsAsReverseGenusDifferentiaClause {
    pub fn new<I, R, F>(py: Python, idspace: I, relation: R, filler: F) -> Self
    where
        I: Into<IdentPrefix>,
        R: IntoPy<Ident>,
        F: IntoPy<Ident>,
    {
        Self {
            idspace: idspace.into(),
            relation: relation.into_py(py),
            filler: filler.into_py(py),
        }
    }
}

impl ClonePy for TreatXrefsAsReverseGenusDifferentiaClause {
    fn clone_py(&self, py: Python) -> Self {
        Self {
            idspace: self.idspace.clone(),
            relation: self.relation.clone_py(py),
            filler: self.filler.clone_py(py),
        }
    }
}

impl Display for TreatXrefsAsReverseGenusDifferentiaClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        let gil = Python::acquire_gil();
        let py = gil.python();
        obo::HeaderClause::from_py(self.clone_py(py), py).fmt(f)
    }
}

impl FromPy<TreatXrefsAsReverseGenusDifferentiaClause> for obo::HeaderClause {
    fn from_py(clause: TreatXrefsAsReverseGenusDifferentiaClause, py: Python) -> Self {
        obo::HeaderClause::TreatXrefsAsReverseGenusDifferentia(
            clause.idspace.into(),
            clause.relation.into_py(py),
            clause.filler.into_py(py),
        )
    }
}

#[pymethods]
impl TreatXrefsAsReverseGenusDifferentiaClause {
    /// `~fastobo.id.IdentPrefix`: the ID prefix to select some Xrefs with.
    #[getter]
    fn get_idspace<'py>(&self, py: Python<'py>) -> PyResult<IdentPrefix> {
        Ok(self.idspace.clone_py(py))
    }
}

impl_raw_tag!(
    TreatXrefsAsReverseGenusDifferentiaClause,
    "treat-xrefs-as-reverse-genus-differentia"
);
impl_raw_value!(
    TreatXrefsAsReverseGenusDifferentiaClause,
    "{} {} {}",
    self.idspace,
    self.relation,
    self.filler
);

#[pyproto]
impl PyObjectProtocol for TreatXrefsAsReverseGenusDifferentiaClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, TreatXrefsAsReverseGenusDifferentiaClause(self.idspace, self.relation, self.filler))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.idspace && self.relation && self.filler)
    }
}

// --- TreatXrefsAsRelationshipClause ----------------------------------------

/// TreatXrefsAsRelationshipClause(idspace, relation)
/// --
///
/// A macro to treats xrefs from an ID space as being relationships.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Debug)]
pub struct TreatXrefsAsRelationshipClause {
    idspace: IdentPrefix,
    relation: Ident,
}

impl TreatXrefsAsRelationshipClause {
    pub fn new<I, R>(py: Python, idspace: I, relation: R) -> Self
    where
        I: Into<IdentPrefix>,
        R: IntoPy<Ident>,
    {
        Self {
            idspace: idspace.into(),
            relation: relation.into_py(py),
        }
    }
}

impl ClonePy for TreatXrefsAsRelationshipClause {
    fn clone_py(&self, py: Python) -> Self {
        Self {
            idspace: self.idspace.clone_py(py),
            relation: self.relation.clone_py(py),
        }
    }
}

impl Display for TreatXrefsAsRelationshipClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        let gil = Python::acquire_gil();
        let py = gil.python();
        obo::HeaderClause::from_py(self.clone_py(py), py).fmt(f)
    }
}

impl FromPy<TreatXrefsAsRelationshipClause> for obo::HeaderClause {
    fn from_py(clause: TreatXrefsAsRelationshipClause, py: Python) -> Self {
        obo::HeaderClause::TreatXrefsAsRelationship(
            clause.idspace.into(),
            clause.relation.into_py(py),
        )
    }
}

#[pymethods]
impl TreatXrefsAsRelationshipClause {
    /// `~fastobo.id.IdentPrefix`: the ID prefix to select some Xrefs with.
    #[getter]
    fn get_idspace<'py>(&self, py: Python<'py>) -> PyResult<IdentPrefix> {
        Ok(self.idspace.clone_py(py))
    }
}

impl_raw_tag!(
    TreatXrefsAsRelationshipClause,
    "treat-xrefs-as-relationship"
);
impl_raw_value!(
    TreatXrefsAsRelationshipClause,
    "{} {}",
    self.idspace,
    self.relation
);

#[pyproto]
impl PyObjectProtocol for TreatXrefsAsRelationshipClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, TreatXrefsAsRelationshipClause(self.idspace, self.relation))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.idspace && self.relation)
    }
}

// --- TreatXrefsAsIsA -------------------------------------------------------

/// TreatXrefsAsIsAClause(idspace)
/// --
///
/// A macro to treats xrefs from an ID space as being subclassing relations.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct TreatXrefsAsIsAClause {
    idspace: IdentPrefix,
}

impl TreatXrefsAsIsAClause {
    pub fn new<I>(_py: Python, idspace: I) -> Self
    where
        I: Into<IdentPrefix>,
    {
        Self {
            idspace: idspace.into(),
        }
    }
}

impl Display for TreatXrefsAsIsAClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        let gil = Python::acquire_gil();
        let py = gil.python();
        obo::HeaderClause::from_py(self.clone_py(py), py).fmt(f)
    }
}

impl From<TreatXrefsAsIsAClause> for obo::HeaderClause {
    fn from(clause: TreatXrefsAsIsAClause) -> obo::HeaderClause {
        obo::HeaderClause::TreatXrefsAsIsA(clause.idspace.into())
    }
}

impl FromPy<TreatXrefsAsIsAClause> for obo::HeaderClause {
    fn from_py(clause: TreatXrefsAsIsAClause, _py: Python) -> obo::HeaderClause {
        Self::from(clause)
    }
}

#[pymethods]
impl TreatXrefsAsIsAClause {
    /// `~fastobo.id.IdentPrefix`: the ID prefix to select some Xrefs with.
    #[getter]
    fn get_idspace<'py>(&self, py: Python<'py>) -> PyResult<IdentPrefix> {
        Ok(self.idspace.clone_py(py))
    }
}

impl_raw_tag!(TreatXrefsAsIsAClause, "treat-xrefs-as-is_a");
impl_raw_value!(TreatXrefsAsIsAClause, "{}", self.idspace);

#[pyproto]
impl PyObjectProtocol for TreatXrefsAsIsAClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, TreatXrefsAsIsAClause(self.idspace))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.idspace)
    }
}

// --- TreatXrefsAsHasSubclassClause -----------------------------------------

/// TreatXrefsAsHasSubclassClause(idspace)
/// --
///
/// A macro to treats xrefs from an ID space as being superclassing relations.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct TreatXrefsAsHasSubclassClause {
    idspace: IdentPrefix,
}

impl TreatXrefsAsHasSubclassClause {
    pub fn new<I>(_py: Python, idspace: I) -> Self
    where
        I: Into<IdentPrefix>,
    {
        Self {
            idspace: idspace.into(),
        }
    }
}

impl Display for TreatXrefsAsHasSubclassClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        fastobo::ast::HeaderClause::from(self.clone()).fmt(f)
    }
}

impl From<TreatXrefsAsHasSubclassClause> for obo::HeaderClause {
    fn from(clause: TreatXrefsAsHasSubclassClause) -> Self {
        obo::HeaderClause::TreatXrefsAsHasSubclass(clause.idspace.into())
    }
}

impl FromPy<TreatXrefsAsHasSubclassClause> for obo::HeaderClause {
    fn from_py(clause: TreatXrefsAsHasSubclassClause, _py: Python) -> Self {
        Self::from(clause)
    }
}

#[pymethods]
impl TreatXrefsAsHasSubclassClause {
    /// `~fastobo.id.IdentPrefix`: the ID prefix to select some Xrefs with.
    #[getter]
    fn get_idspace<'py>(&self, py: Python<'py>) -> PyResult<IdentPrefix> {
        Ok(self.idspace.clone_py(py))
    }
}

impl_raw_tag!(TreatXrefsAsHasSubclassClause, "treat-xrefs-as-has-subclass");
impl_raw_value!(TreatXrefsAsHasSubclassClause, "{}", self.idspace);

#[pyproto]
impl PyObjectProtocol for TreatXrefsAsHasSubclassClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, TreatXrefsAsHasSubclassClause(self.idspace))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.idspace)
    }
}

// --- PropertyValue ---------------------------------------------------------

/// PropertyValueClause(property_value)
/// --
///
/// A clause that binds a property to a value in the OBO document.
///
/// Arguments:
///     property_value (~fastobo.pv.AbstractPropertyValue): the property value
///         to annotate the current OBO document with.
///
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Debug)]
pub struct PropertyValueClause {
    inner: PropertyValue,
}

impl PropertyValueClause {
    fn new<P>(py: Python, property_value: P) -> Self
    where
        P: IntoPy<PropertyValue>,
    {
        Self {
            inner: property_value.into_py(py),
        }
    }
}

impl ClonePy for PropertyValueClause {
    fn clone_py(&self, py: Python) -> Self {
        Self {
            inner: self.inner.clone_py(py),
        }
    }
}

impl Display for PropertyValueClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        let gil = Python::acquire_gil();
        let py = gil.python();
        fastobo::ast::HeaderClause::from_py(self.clone_py(py), py).fmt(f)
    }
}

impl FromPy<PropertyValueClause> for ast::HeaderClause {
    fn from_py(clause: PropertyValueClause, py: Python) -> ast::HeaderClause {
        ast::HeaderClause::PropertyValue(clause.inner.into_py(py))
    }
}

#[pymethods]
impl PropertyValueClause {
    #[new]
    fn __init__(obj: &PyRawObject, property_value: PropertyValue) -> PyResult<()> {
        obj.init(Self::new(obj.py(), property_value));
        Ok(())
    }

    #[getter]
    /// `~fastobo.pv.AbstractPropertyValue`: an annotation of the document.
    fn get_property_value(&self) -> PyResult<&PropertyValue> {
        Ok(&self.inner)
    }

    #[setter]
    fn set_property_value(&mut self, property: PropertyValue) -> PyResult<()> {
        self.inner = property;
        Ok(())
    }
}

impl_raw_tag!(PropertyValueClause, "property_value");
impl_raw_value!(PropertyValueClause, "{}", self.inner);

#[pyproto]
impl PyObjectProtocol for PropertyValueClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, PropertyValueClause(self.inner))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.inner)
    }
}

// --- Remark ----------------------------------------------------------------

/// RemarkClause(remark)
/// --
///
/// A header clause storing general comments for the current OBO file.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct RemarkClause {
    remark: UnquotedString,
}

impl RemarkClause {
    pub fn new(_py: Python, remark: ast::UnquotedString) -> Self {
        Self { remark }
    }
}

impl Display for RemarkClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        let clause: obo::HeaderClause = self.clone().into();
        clause.fmt(f)
    }
}

impl From<RemarkClause> for obo::HeaderClause {
    fn from(clause: RemarkClause) -> Self {
        obo::HeaderClause::Remark(clause.remark)
    }
}

impl FromPy<RemarkClause> for obo::HeaderClause {
    fn from_py(clause: RemarkClause, _py: Python) -> Self {
        Self::from(clause)
    }
}

#[pymethods]
impl RemarkClause {
    #[new]
    fn __init__(obj: &PyRawObject, remark: String) -> PyResult<()> {
        let py = obj.py();
        Ok(obj.init(Self::new(py, UnquotedString::new(remark))))
    }

    /// `str`: a remark about the ontology.
    #[getter]
    fn get_remark(&self) -> PyResult<&str> {
        Ok(self.remark.as_str())
    }

    #[setter]
    fn set_remark(&mut self, remark: String) -> PyResult<()> {
        self.remark = fastobo::ast::UnquotedString::new(remark);
        Ok(())
    }
}

impl_raw_tag!(RemarkClause, "remark");
impl_raw_value!(RemarkClause, "{}", self.remark);

#[pyproto]
impl PyObjectProtocol for RemarkClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let fmt = PyString::new(py, "RemarkClause({!r})").to_object(py);
        fmt.call_method1(py, "format", (self.remark.as_str(),))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.remark)
    }
}

// --- Ontology --------------------------------------------------------------

/// OntologyClause(ontology)
/// --
///
/// The ontology ID of the current OBO file.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct OntologyClause {
    ontology: UnquotedString,
}

impl OntologyClause {
    pub fn new(_py: Python, ontology: UnquotedString) -> Self {
        Self { ontology }
    }
}

impl Display for OntologyClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        obo::HeaderClause::from(self.clone()).fmt(f)
    }
}

impl From<OntologyClause> for obo::HeaderClause {
    fn from(clause: OntologyClause) -> Self {
        obo::HeaderClause::Ontology(clause.ontology)
    }
}

impl FromPy<OntologyClause> for obo::HeaderClause {
    fn from_py(clause: OntologyClause, _py: Python) -> Self {
        Self::from(clause)
    }
}

#[pymethods]
impl OntologyClause {
    #[new]
    fn __init__(obj: &PyRawObject, ontology: String) -> PyResult<()> {
        let py = obj.py();
        Ok(obj.init(Self::new(py, UnquotedString::new(ontology))))
    }

    /// `str`: the ID of the ontology described in the OBO document.
    #[getter]
    fn get_ontology(&self) -> PyResult<&str> {
        Ok(self.ontology.as_str())
    }

    #[setter]
    fn set_ontology(&mut self, ontology: String) -> PyResult<()> {
        self.ontology = fastobo::ast::UnquotedString::new(ontology);
        Ok(())
    }
}

impl_raw_tag!(OntologyClause, "ontology");
impl_raw_value!(OntologyClause, "{}", self.ontology);

#[pyproto]
impl PyObjectProtocol for OntologyClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let fmt = PyString::new(py, "OntologyClause({!r})").to_object(py);
        fmt.call_method1(py, "format", (self.ontology.as_str(),))
    }

    fn __str__(&self) -> PyResult<String> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.ontology)
    }
}

// --- OwlAxioms -------------------------------------------------------------

/// OwlAxiomsClause(axioms)
/// --
///
/// A header clause containing untranslatable OWL axioms.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct OwlAxiomsClause {
    axioms: UnquotedString,
}

impl OwlAxiomsClause {
    pub fn new(_py: Python, axioms: UnquotedString) -> Self {
        Self { axioms }
    }
}

impl Display for OwlAxiomsClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        fastobo::ast::HeaderClause::from(self.clone()).fmt(f)
    }
}

impl From<OwlAxiomsClause> for obo::HeaderClause {
    fn from(clause: OwlAxiomsClause) -> Self {
        obo::HeaderClause::OwlAxioms(clause.axioms)
    }
}

impl FromPy<OwlAxiomsClause> for obo::HeaderClause {
    fn from_py(clause: OwlAxiomsClause, _py: Python) -> Self {
        Self::from(clause)
    }
}

#[pymethods]
impl OwlAxiomsClause {
    #[new]
    fn __init__(obj: &PyRawObject, axioms: String) -> PyResult<()> {
        let py = obj.py();
        Ok(obj.init(Self::new(py, UnquotedString::new(axioms))))
    }

    /// `str`: raw OWL axioms that have no equivalent in the OBO language.
    #[getter]
    fn get_axioms(&self) -> PyResult<&str> {
        Ok(self.axioms.as_str())
    }

    #[setter]
    fn set_axioms(&mut self, axioms: String) -> PyResult<()> {
        self.axioms = fastobo::ast::UnquotedString::new(axioms);
        Ok(())
    }
}

impl_raw_tag!(OwlAxiomsClause, "owl-axioms");
impl_raw_value!(OwlAxiomsClause, "{}", self.axioms);

#[pyproto]
impl PyObjectProtocol for OwlAxiomsClause {
    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __repr__(&self) -> PyResult<PyObject> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let fmt = PyString::new(py, "OwlAxiomsClause({!r})").to_object(py);
        fmt.call_method1(py, "format", (self.axioms.as_str(),))
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.axioms)
    }
}

// --- UnreservedClause ------------------------------------------------------

/// UnreservedClause(tag, value)
/// --
///
/// A tag/value pair not reserved in the OBO specification.
#[pyclass(extends=BaseHeaderClause, module="fastobo.header")]
#[derive(Clone, ClonePy, Debug)]
pub struct UnreservedClause {
    tag: UnquotedString,
    value: UnquotedString,
}

impl UnreservedClause {
    pub fn new(_py: Python, tag: UnquotedString, value: UnquotedString) -> Self {
        Self { tag, value }
    }
}

impl From<UnreservedClause> for obo::HeaderClause {
    fn from(clause: UnreservedClause) -> obo::HeaderClause {
        obo::HeaderClause::Unreserved(clause.tag, clause.value)
    }
}

impl FromPy<UnreservedClause> for obo::HeaderClause {
    fn from_py(clause: UnreservedClause, _py: Python) -> Self {
        Self::from(clause)
    }
}

impl Display for UnreservedClause {
    fn fmt(&self, f: &mut Formatter) -> FmtResult {
        obo::HeaderClause::from(self.clone()).fmt(f)
    }
}

#[pymethods]
impl UnreservedClause {
    #[new]
    fn __init__(obj: &PyRawObject, tag: String, value: String) {
        let py = obj.py();
        obj.init(Self::new(
            py,
            UnquotedString::new(tag),
            UnquotedString::new(value),
        ))
    }

    #[getter]
    /// `str`: The tag of the clause.
    fn get_tag(&self) -> PyResult<&str> {
        Ok(self.tag.as_str())
    }

    #[setter]
    fn set_tag(&mut self, tag: String) -> PyResult<()> {
        self.tag = UnquotedString::new(tag);
        Ok(())
    }

    #[getter]
    /// `str`: The value of the clause.
    fn get_value(&self) -> PyResult<&str> {
        Ok(self.tag.as_str())
    }

    #[setter]
    fn set_value(&mut self, value: String) -> PyResult<()> {
        self.value = UnquotedString::new(value);
        Ok(())
    }

    pub fn raw_tag(&self) -> PyResult<&str> {
        Ok(self.tag.as_str())
    }
}

impl_raw_value!(UnreservedClause, "{}", self.value);

#[pyproto]
impl PyObjectProtocol for UnreservedClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let fmt = PyString::new(py, "UnreservedClause({!r}, {!r})").to_object(py);
        fmt.call_method1(py, "format", (self.tag.as_str(), self.value.as_str()))
    }

    fn __str__(&self) -> PyResult<String> {
        Ok(self.to_string())
    }

    fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyResult<PyObject> {
        impl_richmp!(self, other, op, self.tag && self.value)
    }
}
