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 pyo3::class::basic::CompareOp;
use pyo3::class::gc::PyVisit;
use pyo3::exceptions::PyTypeError;
use pyo3::exceptions::PyValueError;
use pyo3::gc::PyTraverseError;
use pyo3::prelude::*;
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::Url;
use super::super::pv::PropertyValue;
use super::super::syn::SynonymScope;
use crate::utils::ClonePy;
use crate::utils::FinalClass;
use crate::utils::AbstractClass;

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

impl IntoPy<fastobo::ast::HeaderClause> for HeaderClause {
    fn into_py(self, py: Python) -> fastobo::ast::HeaderClause {
        (&self).into_py(py)
    }
}

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

/// A header clause, appearing in the OBO header frame.
#[pyclass(subclass, extends=AbstractClause, module="fastobo.header")]
#[derive(Debug, Default, AbstractClass)]
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, FinalClass)]
pub struct FormatVersionClause {
    version: obo::UnquotedString,
}

impl FormatVersionClause {
    pub fn new(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(Box::new(clause.version))
    }
}

impl IntoPy<obo::HeaderClause> for FormatVersionClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

#[pymethods]
impl FormatVersionClause {
    #[new]
    fn __init__(version: String) -> PyClassInitializer<Self> {
        Self::new(obo::UnquotedString::new(version)).into()
    }

    /// `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) {
        self.version = obo::UnquotedString::new(version);
    }
}

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, FinalClass)]
pub struct DataVersionClause {
    version: UnquotedString,
}

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

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

impl IntoPy<obo::HeaderClause> for DataVersionClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

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

#[pymethods]
impl DataVersionClause {
    #[new]
    fn __init__(version: String) -> PyClassInitializer<Self> {
        Self::new(UnquotedString::new(version)).into()
    }

    /// `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) {
        self.version = UnquotedString::new(version);
    }
}

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, FinalClass)]
pub struct DateClause {
    date: obo::NaiveDateTime,
}

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

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

impl IntoPy<obo::HeaderClause> for DateClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

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

#[pymethods]
impl DateClause {
    #[new]
    fn __init__(date: &PyDateTime) -> PyClassInitializer<Self> {
        Self::new(
            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,
            )
        ).into()
    }

    /// `~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) {
        self.date = fastobo::ast::NaiveDateTime::new(
            dt.get_day(),
            dt.get_month(),
            dt.get_year() as u16,
            dt.get_hour(),
            dt.get_minute(),
        );
    }
}

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.extract::<Py<Self>>() {
            let clause = &*clause.as_ref(other.py()).borrow();
            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, FinalClass)]
pub struct SavedByClause {
    name: UnquotedString,
}

impl SavedByClause {
    pub fn new(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(Box::new(clause.name))
    }
}

impl IntoPy<obo::HeaderClause> for SavedByClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

#[pymethods]
impl SavedByClause {
    #[new]
    fn __init__(name: String) -> PyClassInitializer<Self> {
        Self::new(UnquotedString::new(name)).into()
    }

    /// `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) {
        self.name = UnquotedString::new(name);
    }
}

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, FinalClass)]
pub struct AutoGeneratedByClause {
    name: UnquotedString,
}

impl AutoGeneratedByClause {
    pub fn new(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(Box::new(clause.name))
    }
}

impl IntoPy<obo::HeaderClause> for AutoGeneratedByClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

#[pymethods]
impl AutoGeneratedByClause {
    #[new]
    fn __init__(name: String) -> PyClassInitializer<Self> {
        Self::new(UnquotedString::new(name)).into()
    }

    /// `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) {
        self.name = UnquotedString::new(name);
    }
}

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, FinalClass)]
pub struct ImportClause {
    reference: obo::Import, // should be `Import` ?
}

impl ImportClause {
    pub fn new(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(Box::new(clause.reference))
    }
}

impl IntoPy<obo::HeaderClause> for ImportClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

#[pymethods]
impl ImportClause {
    // FIXME(@althonos): should not be implicit here ?
    #[new]
    pub fn __init__(reference: &str) -> PyResult<PyClassInitializer<Self>> {
        if let Ok(url) = ast::Url::from_str(reference) {
            Ok(Self::new(obo::Import::Url(Box::new(url))).into())
        } else if let Ok(id) = obo::Ident::from_str(reference) {
            Ok(Self::new(obo::Import::Abbreviated(Box::new(id))).into())
        } else {
            Err(PyValueError::new_err(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> {
        impl_repr!(self, SubsetdefClause(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, FinalClass)]
pub struct SubsetdefClause {
    #[pyo3(set)]
    subset: Ident,
    description: QuotedString,
}

impl SubsetdefClause {
    pub fn new(subset: Ident, description: QuotedString) -> Self {
        Self { subset, 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();
        let clause: fastobo::ast::HeaderClause = self.clone_py(py).into_py(py);
        clause.fmt(f)
    }
}

impl IntoPy<obo::HeaderClause> for SubsetdefClause {
    fn into_py(self, py: Python) -> obo::HeaderClause {
        obo::HeaderClause::Subsetdef(
            Box::new(self.subset.into_py(py)),
            Box::new(self.description),
        )
    }
}

#[pymethods]
impl SubsetdefClause {
    #[new]
    fn __init__(subset: Ident, description: String) -> PyClassInitializer<Self> {
        Self::new(subset, QuotedString::new(description)).into()
    }

    /// `~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))
    }

    /// `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) {
        self.description = fastobo::ast::QuotedString::new(description);
    }
}

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

#[pyproto]
impl PyObjectProtocol for SubsetdefClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, SubsetdefClause(self.subset, 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, FinalClass)]
pub struct SynonymTypedefClause {
    #[pyo3(set)]
    typedef: Ident,
    description: QuotedString,
    scope: Option<SynonymScope>,
}

impl SynonymTypedefClause {
    pub fn new(typedef: Ident, description: QuotedString) -> Self {
        Self::with_scope(typedef, description, None)
    }

    pub fn with_scope(typedef: Ident, description: QuotedString, scope: Option<SynonymScope>) -> Self {
        Self { typedef, description, scope }
    }
}

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();
        let clause: fastobo::ast::HeaderClause = self.clone_py(py).into_py(py);
        clause.fmt(f)
    }
}

impl IntoPy<obo::HeaderClause> for SynonymTypedefClause {
    fn into_py(self, py: Python) -> obo::HeaderClause {
        obo::HeaderClause::SynonymTypedef(
            Box::new(self.typedef.into_py(py)),
            Box::new(self.description),
            self.scope.map(|s| Box::new(s.into_py(py))),
        )
    }
}

#[pymethods]
impl SynonymTypedefClause {
    #[new]
    fn __init__(typedef: Ident, description: String, scope: Option<&PyString>) -> PyResult<PyClassInitializer<Self>> {
        let desc = fastobo::ast::QuotedString::new(description);
        // FIXME
        let sc = match scope {
            Some(s) => Some(SynonymScope::from_str(s.to_str()?)?),
            None => None,
        };
        Ok(Self::with_scope(typedef, desc, sc).into())
    }

    /// `~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) {
        self.description = fastobo::ast::QuotedString::new(description);
    }

    /// `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> {
        if let Some(ref scope) = self.scope {
            impl_repr!(self, SynonymTypedefClause(self.typedef, self.description.as_str(), scope))
        } else {
            impl_repr!(self, SynonymTypedefClause(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, FinalClass)]
pub struct DefaultNamespaceClause {
    #[pyo3(set)]
    namespace: Ident,
}

impl DefaultNamespaceClause {
    pub fn new(namespace: Ident) -> Self {
        Self { namespace }
    }
}

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();
        let clause: fastobo::ast::HeaderClause = self.clone_py(py).into_py(py);
        clause.fmt(f)
    }
}

impl IntoPy<obo::HeaderClause> for DefaultNamespaceClause {
    fn into_py(self, py: Python) -> obo::HeaderClause {
        obo::HeaderClause::DefaultNamespace(Box::new(self.namespace.into_py(py)))
    }
}

#[pymethods]
impl DefaultNamespaceClause {
    #[new]
    fn __init__(namespace: &PyAny) -> PyResult<PyClassInitializer<Self>> {
        let gil = Python::acquire_gil();
        let py = gil.python();
        if namespace.is_instance::<BaseIdent>()? {
            Ident::extract(namespace).map(|id| Self::new(id).into())
        } else if namespace.is_instance::<PyString>()? {
            let s: &PyString = FromPyObject::extract(namespace)?;
            let id = ast::Ident::from_str(&s.to_str()?).unwrap(); // FIXME
            Ok(Self::new(id.into_py(py)).into())
        } else {
            let ty = namespace.get_type().name()?;
            let msg = format!("expected str or Ident for 'namespace', found {}", ty);
            Err(PyTypeError::new_err(msg))
        }
    }

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

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

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

    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, FinalClass)]
pub struct NamespaceIdRuleClause {
    rule: fastobo::ast::UnquotedString,
}

impl NamespaceIdRuleClause {
    pub fn new(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();
        let clause: fastobo::ast::HeaderClause = self.clone_py(py).into_py(py);
        clause.fmt(f)
    }
}

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

impl IntoPy<obo::HeaderClause> for NamespaceIdRuleClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::NamespaceIdRule(Box::new(self.rule))
    }
}

#[pymethods]
impl NamespaceIdRuleClause {
    #[new]
    fn __init__(rule: String) -> PyClassInitializer<Self> {
        Self::new(fastobo::ast::UnquotedString::new(rule)).into()
    }

    /// `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) {
        self.rule = fastobo::ast::UnquotedString::new(rule);
    }
}

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

#[pyproto]
impl PyObjectProtocol for NamespaceIdRuleClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, NamespaceIdRuleClause(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, FinalClass)]
pub struct IdspaceClause {
    prefix: ast::IdentPrefix,
    /// `~fastobo.id.Url`: the URL used to expand IDs of this IDspace.
    #[pyo3(get, set)]
    url: Py<Url>,
    description: Option<QuotedString>,
}

impl IdspaceClause {
    pub fn new(prefix: ast::IdentPrefix, url: Py<Url>) -> Self {
        Self::with_description(prefix, url, None)
    }

    pub fn with_description(prefix: ast::IdentPrefix, url: Py<Url>, description: Option<QuotedString>) -> Self {
        Self { prefix, url, description }
    }
}

impl ClonePy for IdspaceClause {
    fn clone_py(&self, py: Python) -> Self {
        Self {
            prefix: self.prefix.clone(),
            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();
        let clause: fastobo::ast::HeaderClause = self.clone_py(py).into_py(py);
        clause.fmt(f)
    }
}

impl From<IdspaceClause> for obo::HeaderClause {
    fn from(clause: IdspaceClause) -> Self {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let url = clause.url.as_ref(py).borrow().clone_py(py).into();

        obo::HeaderClause::Idspace(
            Box::new(clause.prefix.clone()),
            Box::new(url),
            clause.description.map(Box::new)
        )
    }
}

impl IntoPy<obo::HeaderClause> for IdspaceClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

#[pymethods]
impl IdspaceClause {
    #[new]
    fn __init__(prefix: String, url: Py<Url>, description: Option<String>) -> PyClassInitializer<Self> {
        let p = ast::IdentPrefix::new(prefix);
        let d = description.map(QuotedString::new);
        Self::with_description(p, url, d).into()
    }

    /// `str`: the prefix used in prefixed IDs.
    #[getter]
    fn get_prefix(&self) -> &str {
        self.prefix.as_str()
    }

    /// `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> {
        let gil = Python::acquire_gil();
        let py = gil.python();

        // FIXME: self.prefix may need to be escaped
        let url = &*self.url.as_ref(py).borrow();
        if let Some(desc) = &self.description {
            Ok(format!("{} {} {}", self.prefix, url, desc))
        } else {
            Ok(format!("{} {}", self.prefix, 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> {
        if let Some(ref desc) = self.description {
            impl_repr!(self, IdspaceClause(self.prefix.as_str(), self.url, desc.as_str()))
        } else {
            impl_repr!(self, IdspaceClause(self.prefix.as_str(), self.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, FinalClass)]
pub struct TreatXrefsAsEquivalentClause {
    idspace: ast::IdentPrefix,
}

impl TreatXrefsAsEquivalentClause {
    pub fn new(idspace: ast::IdentPrefix) -> Self {
        Self { idspace }
    }
}

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(Box::new(clause.idspace.clone()))
    }
}

impl IntoPy<obo::HeaderClause> for TreatXrefsAsEquivalentClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

#[pymethods]
impl TreatXrefsAsEquivalentClause {
    #[new]
    fn __init__(prefix: String) -> PyClassInitializer<Self> {
        Self::new(ast::IdentPrefix::new(prefix)).into()
    }

    /// `str`: the ID prefix to select some Xrefs with.
    #[getter]
    fn get_idspace(&self) -> &str {
        self.idspace.as_str()
    }
}

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.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.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, FinalClass)]
pub struct TreatXrefsAsGenusDifferentiaClause {
    idspace: ast::IdentPrefix,
    relation: Ident,
    filler: Ident,
}

impl TreatXrefsAsGenusDifferentiaClause {
    pub fn new(idspace: ast::IdentPrefix, relation: Ident, filler: Ident) -> Self {
        Self { idspace, relation, filler }
    }
}

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();
        let clause: fastobo::ast::HeaderClause = self.clone_py(py).into_py(py);
        clause.fmt(f)
    }
}

impl IntoPy<obo::HeaderClause> for TreatXrefsAsGenusDifferentiaClause {
    fn into_py(self, py: Python) -> obo::HeaderClause {
        obo::HeaderClause::TreatXrefsAsGenusDifferentia(
            Box::new(self.idspace.into()),
            Box::new(self.relation.into_py(py)),
            Box::new(self.filler.into_py(py)),
        )
    }
}

#[pymethods]
impl TreatXrefsAsGenusDifferentiaClause {
    #[new]
    fn __init__(prefix: String, relation: Ident, filler: Ident) -> PyClassInitializer<Self> {
        Self::new(ast::IdentPrefix::new(prefix), relation, filler).into()
    }

    /// `str`: the ID prefix to select some Xrefs with.
    #[getter]
    fn get_idspace(&self) -> &str {
        self.idspace.as_str()
    }
}

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.as_str(), 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, FinalClass)]
pub struct TreatXrefsAsReverseGenusDifferentiaClause {
    idspace: ast::IdentPrefix,
    relation: Ident,      // Should be `RelationId`
    filler: Ident,        // Should be `ClassId`
}

impl TreatXrefsAsReverseGenusDifferentiaClause {
    pub fn new(idspace: ast::IdentPrefix, relation: Ident, filler: Ident) -> Self {
        Self { idspace, relation, filler }
    }
}

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();
        let clause: fastobo::ast::HeaderClause = self.clone_py(py).into_py(py);
        clause.fmt(f)
    }
}

impl IntoPy<obo::HeaderClause> for TreatXrefsAsReverseGenusDifferentiaClause {
    fn into_py(self, py: Python) -> obo::HeaderClause {
        obo::HeaderClause::TreatXrefsAsReverseGenusDifferentia(
            Box::new(self.idspace.clone()),
            Box::new(self.relation.into_py(py)),
            Box::new(self.filler.into_py(py)),
        )
    }
}

#[pymethods]
impl TreatXrefsAsReverseGenusDifferentiaClause {
    #[new]
    fn __init__(prefix: String, relation: Ident, filler: Ident) -> PyClassInitializer<Self> {
        Self::new(ast::IdentPrefix::new(prefix), relation, filler).into()
    }

    /// `str`: the ID prefix to select some Xrefs with.
    #[getter]
    fn get_idspace(&self) -> &str {
        self.idspace.as_str()
    }
}

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.as_str(), 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, FinalClass)]
pub struct TreatXrefsAsRelationshipClause {
    idspace: ast::IdentPrefix,
    relation: Ident,
}

impl TreatXrefsAsRelationshipClause {
    pub fn new(idspace: ast::IdentPrefix, relation: Ident) -> Self {
        Self { idspace, relation }
    }
}

impl ClonePy for TreatXrefsAsRelationshipClause {
    fn clone_py(&self, py: Python) -> Self {
        Self {
            idspace: self.idspace.clone(),
            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();
        let clause: fastobo::ast::HeaderClause = self.clone_py(py).into_py(py);
        clause.fmt(f)
    }
}

impl IntoPy<obo::HeaderClause> for TreatXrefsAsRelationshipClause {
    fn into_py(self, py: Python) -> obo::HeaderClause {
        obo::HeaderClause::TreatXrefsAsRelationship(
            Box::new(self.idspace.clone()),
            Box::new(self.relation.into_py(py)),
        )
    }
}

#[pymethods]
impl TreatXrefsAsRelationshipClause {
    #[new]
    fn __init__(prefix: String, relation: Ident) -> PyClassInitializer<Self> {
        Self::new(ast::IdentPrefix::new(prefix), relation).into()
    }

    /// `str`: the ID prefix to select some Xrefs with.
    #[getter]
    fn get_idspace(&self) -> &str {
        self.idspace.as_str()
    }
}

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.as_str(), 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, FinalClass)]
pub struct TreatXrefsAsIsAClause {
    idspace: ast::IdentPrefix,
}

impl TreatXrefsAsIsAClause {
    pub fn new(idspace: ast::IdentPrefix) -> Self {
        Self { idspace }
    }
}

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

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

impl IntoPy<obo::HeaderClause> for TreatXrefsAsIsAClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

#[pymethods]
impl TreatXrefsAsIsAClause {
    #[new]
    fn __init__(prefix: String) -> PyClassInitializer<Self> {
        Self::new(ast::IdentPrefix::new(prefix)).into()
    }

    /// `str`: the ID prefix to select some Xrefs with.
    #[getter]
    fn get_idspace(&self) -> &str {
        self.idspace.as_str()
    }
}

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.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.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, FinalClass)]
pub struct TreatXrefsAsHasSubclassClause {
    idspace: ast::IdentPrefix,
}

impl TreatXrefsAsHasSubclassClause {
    pub fn new(idspace: ast::IdentPrefix) -> Self {
        Self { idspace }
    }
}

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(Box::new(clause.idspace.clone()))
    }
}

impl IntoPy<obo::HeaderClause> for TreatXrefsAsHasSubclassClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

#[pymethods]
impl TreatXrefsAsHasSubclassClause {
    #[new]
    fn __init__(prefix: String) -> PyClassInitializer<Self> {
        Self::new(ast::IdentPrefix::new(prefix)).into()
    }

    /// `str`: the ID prefix to select some Xrefs with.
    #[getter]
    fn get_idspace(&self) -> &str {
        self.idspace.as_str()
    }

    pub fn raw_value(&self) -> String {
        self.idspace.to_string()
    }
}

impl_raw_tag!(TreatXrefsAsHasSubclassClause, "treat-xrefs-as-has-subclass");

#[pyproto]
impl PyObjectProtocol for TreatXrefsAsHasSubclassClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, TreatXrefsAsHasSubclassClause(self.idspace.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.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, FinalClass)]
pub struct PropertyValueClause {
    #[pyo3(set)]
    inner: PropertyValue,
}

impl PropertyValueClause {
    fn new(property_value: PropertyValue) -> Self {
        Self {
            inner: property_value
        }
    }
}

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();
        let clause: fastobo::ast::HeaderClause = self.clone_py(py).into_py(py);
        clause.fmt(f)
    }
}

impl IntoPy<ast::HeaderClause> for PropertyValueClause {
    fn into_py(self, py: Python) -> ast::HeaderClause {
        ast::HeaderClause::PropertyValue(Box::new(self.inner.into_py(py)))
    }
}

#[pymethods]
impl PropertyValueClause {
    #[new]
    fn __init__(property_value: PropertyValue) -> PyClassInitializer<Self> {
        Self::new(property_value).into()
    }

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

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, FinalClass)]
pub struct RemarkClause {
    remark: UnquotedString,
}

impl RemarkClause {
    pub fn new(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(Box::new(clause.remark))
    }
}

impl IntoPy<obo::HeaderClause> for RemarkClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

#[pymethods]
impl RemarkClause {
    #[new]
    fn __init__(remark: String) -> PyClassInitializer<Self> {
        Self::new(UnquotedString::new(remark)).into()
    }

    /// `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) {
        self.remark = fastobo::ast::UnquotedString::new(remark);
    }
}

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

#[pyproto]
impl PyObjectProtocol for RemarkClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, RemarkClause(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, FinalClass)]
pub struct OntologyClause {
    ontology: UnquotedString,
}

impl OntologyClause {
    pub fn new(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(Box::new(clause.ontology))
    }
}

impl IntoPy<obo::HeaderClause> for OntologyClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

#[pymethods]
impl OntologyClause {
    #[new]
    fn __init__(ontology: String) -> PyClassInitializer<Self> {
        Self::new(UnquotedString::new(ontology)).into()
    }

    /// `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) {
        self.ontology = fastobo::ast::UnquotedString::new(ontology);
    }
}

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

#[pyproto]
impl PyObjectProtocol for OntologyClause {
    fn __repr__(&self) -> PyResult<PyObject> {
        impl_repr!(self, OntologyClause(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, FinalClass)]
pub struct OwlAxiomsClause {
    axioms: UnquotedString,
}

impl OwlAxiomsClause {
    pub fn new(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(Box::new(clause.axioms))
    }
}

impl IntoPy<obo::HeaderClause> for OwlAxiomsClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

#[pymethods]
impl OwlAxiomsClause {
    #[new]
    fn __init__(axioms: String) -> PyClassInitializer<Self> {
        // FIXME: validate syntax using `horned-functional`
        Self::new(UnquotedString::new(axioms)).into()
    }

    /// `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) {
        self.axioms = fastobo::ast::UnquotedString::new(axioms);
    }
}

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> {
        impl_repr!(self, OwlAxiomsClause(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, FinalClass)]
pub struct UnreservedClause {
    tag: UnquotedString,
    value: UnquotedString,
}

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

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

impl IntoPy<obo::HeaderClause> for UnreservedClause {
    fn into_py(self, _py: Python) -> obo::HeaderClause {
        obo::HeaderClause::from(self)
    }
}

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

#[pymethods]
impl UnreservedClause {
    #[new]
    fn __init__(tag: String, value: String) -> PyClassInitializer<Self> {
        Self::new(UnquotedString::new(tag), UnquotedString::new(value)).into()
    }

    #[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) {
        self.tag = UnquotedString::new(tag);
    }

    #[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) {
        self.value = UnquotedString::new(value);
    }

    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> {
        impl_repr!(self, UnreservedClause(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)
    }
}
