//! Documentation attribute related utilities.
use std::borrow::Cow;

use hir::{HasAttrs, db::HirDatabase, resolve_doc_path_on};

/// Holds documentation
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Documentation<'db>(Cow<'db, str>);

impl<'db> Documentation<'db> {
    #[inline]
    pub fn new_owned(s: String) -> Self {
        Documentation(Cow::Owned(s))
    }

    #[inline]
    pub fn new_borrowed(s: &'db str) -> Self {
        Documentation(Cow::Borrowed(s))
    }

    #[inline]
    pub fn into_owned(self) -> Documentation<'static> {
        Documentation::new_owned(self.0.into_owned())
    }

    #[inline]
    pub fn as_str(&self) -> &str {
        &self.0
    }
}

pub trait HasDocs: HasAttrs + Copy {
    fn docs(self, db: &dyn HirDatabase) -> Option<Documentation<'_>> {
        let docs = match self.docs_with_rangemap(db)? {
            Cow::Borrowed(docs) => Documentation::new_borrowed(docs.docs()),
            Cow::Owned(docs) => Documentation::new_owned(docs.into_docs()),
        };
        Some(docs)
    }
    fn docs_with_rangemap(self, db: &dyn HirDatabase) -> Option<Cow<'_, hir::Docs>> {
        self.hir_docs(db).map(Cow::Borrowed)
    }
    fn resolve_doc_path(
        self,
        db: &dyn HirDatabase,
        link: &str,
        ns: Option<hir::Namespace>,
        is_inner_doc: hir::IsInnerDoc,
    ) -> Option<hir::DocLinkDef> {
        resolve_doc_path_on(db, self, link, ns, is_inner_doc)
    }
}

macro_rules! impl_has_docs {
    ($($def:ident,)*) => {$(
        impl HasDocs for hir::$def {}
    )*};
}

impl_has_docs![
    EnumVariant,
    Field,
    Static,
    Const,
    Trait,
    TypeAlias,
    Macro,
    Function,
    Adt,
    Module,
    Impl,
    Crate,
    AssocItem,
    Struct,
    Union,
    Enum,
];

impl HasDocs for hir::ExternCrateDecl {
    fn docs(self, db: &dyn HirDatabase) -> Option<Documentation<'_>> {
        let crate_docs = self.resolved_crate(db)?.hir_docs(db);
        let decl_docs = self.hir_docs(db);
        match (decl_docs, crate_docs) {
            (None, None) => None,
            (Some(docs), None) | (None, Some(docs)) => {
                Some(Documentation::new_borrowed(docs.docs()))
            }
            (Some(decl_docs), Some(crate_docs)) => {
                let mut docs = String::with_capacity(
                    decl_docs.docs().len() + "\n\n".len() + crate_docs.docs().len(),
                );
                docs.push_str(decl_docs.docs());
                docs.push_str("\n\n");
                docs.push_str(crate_docs.docs());
                Some(Documentation::new_owned(docs))
            }
        }
    }

    fn docs_with_rangemap(self, db: &dyn HirDatabase) -> Option<Cow<'_, hir::Docs>> {
        let crate_docs = self.resolved_crate(db)?.hir_docs(db);
        let decl_docs = self.hir_docs(db);
        match (decl_docs, crate_docs) {
            (None, None) => None,
            (Some(docs), None) | (None, Some(docs)) => Some(Cow::Borrowed(docs)),
            (Some(decl_docs), Some(crate_docs)) => {
                let mut docs = decl_docs.clone();
                docs.append_str("\n\n");
                docs.append(crate_docs);
                Some(Cow::Owned(docs))
            }
        }
    }
}
