"""
.. autofunction:: sphinxcontrib.bibtex.style.template.join(\
        sep='', sep2=None, last_sep=None, other=None)

.. autofunction:: sphinxcontrib.bibtex.style.template.sentence(\
        capfirst=False, capitalize=False, add_period=True, \
        sep=', ', sep2=None, last_sep=None, other=None)

.. autofunction:: sphinxcontrib.bibtex.style.template.names(\
        role, sep='', sep2=None, last_sep=None, other=None)

.. autofunction:: sphinxcontrib.bibtex.style.template.entry_label()

.. autofunction:: sphinxcontrib.bibtex.style.template.reference()

.. autofunction:: sphinxcontrib.bibtex.style.template.footnote_reference()
"""

from typing import TYPE_CHECKING, Any, Dict, List, NamedTuple, cast

import docutils.nodes
import pybtex_docutils
from pybtex.richtext import Text
from pybtex.style.template import (
    FieldIsMissing,
    Node,
    _format_list,
    field,
    first_of,
    optional,
    tag,
)
from sphinx.util.nodes import make_refnode

from sphinxcontrib.bibtex.nodes import raw_latex
from sphinxcontrib.bibtex.richtext import BaseReferenceText

if TYPE_CHECKING:
    from pybtex.backends import BaseBackend
    from pybtex.richtext import BaseText
    from pybtex.style import FormattedEntry
    from sphinx.builders import Builder


# extended from pybtex: also copies the docstring into the wrapped object
def node(f):
    n = Node(f.__name__, f)
    n.__doc__ = f.__doc__
    return n


# copied from pybtex join but extended to allow "et al" formatting
@node
def join(children, data, sep="", sep2=None, last_sep=None, other=None):
    """Join text fragments together."""

    if sep2 is None:
        sep2 = sep
    if last_sep is None:
        last_sep = sep
    parts = [part for part in _format_list(children, data) if part]
    if len(parts) <= 1:
        return Text(*parts)
    elif len(parts) == 2:
        return Text(sep2).join(parts)
    elif other is None:
        return Text(last_sep).join([Text(sep).join(parts[:-1]), parts[-1]])
    else:
        return Text(parts[0], other)


@node
def join2(children, data, sep1="", sep2=""):
    """Join text fragments together."""
    if not children:
        return Text()
    else:
        return join(sep=sep1)[children[0], join(sep=sep2)[children[1:]]].format_data(
            data
        )


# copied from pybtex names but using the new join
@node
def sentence(
    children,
    data,
    capfirst=False,
    capitalize=False,
    add_period=True,
    sep=", ",
    sep2=None,
    last_sep=None,
    other=None,
):
    """Join text fragments, capitalize the first letter,
    and add a period to the end.
    """
    text = join(sep=sep, sep2=sep2, last_sep=last_sep, other=other)[
        children
    ].format_data(data)
    if capfirst:
        text = text.capfirst()
    if capitalize:
        text = text.capitalize()
    if add_period:
        text = text.add_period()
    return text


# copied from pybtex names but using the new join allowing "et al" formatting
@node
def names(children, data, role, **kwargs):
    """Return formatted names."""
    assert not children
    try:
        persons = data["entry"].persons[role]
    except KeyError:
        raise FieldIsMissing(role, data["entry"])
    style = data["style"]
    formatted_names = [
        style.person.style_plugin.format(person, style.person.abbreviate)
        for person in persons
    ]
    return join(**kwargs)[formatted_names].format_data(data)


@node
def entry_label(children, data) -> "BaseText":
    """Node for inserting the label of a formatted entry."""
    assert not children
    entry = cast("FormattedEntry", data["formatted_entry"])
    return Text(entry.label)


class SphinxReferenceInfo(NamedTuple):
    """Tuple containing reference info to enable sphinx to resolve a reference
    to a citation.
    """

    builder: "Builder"  #: The Sphinx builder.
    fromdocname: str  #: Document name of the citation reference.
    todocname: str  #: Document name of the bibliography.
    citation_id: str  #: Unique id of the citation within the bibliography.
    title: str  #: Title attribute for reference node.
    pre_text: str  #: Text to come before citation.
    post_text: str  #: Text to come after citation.


class SphinxReferenceText(BaseReferenceText[SphinxReferenceInfo]):
    """Pybtex rich text class generating
    a docutils reference node to a citation
    for use with :class:`SphinxReferenceInfo`.
    """

    def render(self, backend: "BaseBackend") -> List[docutils.nodes.Element]:
        assert isinstance(
            backend, pybtex_docutils.Backend
        ), "SphinxReferenceText only supports the docutils backend"
        info = self.info[0]
        if info.builder.name == "latex":
            key = f"cite.{info.todocname}:{info.citation_id}"
            return (
                [raw_latex(f"\\hyperlink{{{key}}}{{")]
                + super().render(backend)
                + [raw_latex("}")]
            )
        elif info.builder.name == "rinoh":
            children = super().render(backend)
            refid = f"%{info.todocname}#{info.citation_id}"
            refnode = docutils.nodes.citation_reference(
                text=children[0], refid=refid, reftitle=info.title
            )
            refnode.extend(children[1:])
            return [refnode]
        else:
            children = super().render(backend)
            # make_refnode only takes a single child
            refnode2 = make_refnode(
                builder=info.builder,
                fromdocname=info.fromdocname,
                todocname=info.todocname,
                targetid=info.citation_id,
                child=children[0],
                title=info.title,
            )
            refnode2.extend(children[1:])  # type: ignore
            return [refnode2]


@node
def reference(children, data: Dict[str, Any]):
    """Pybtex node for inserting a docutils reference node to a citation.
    The children of the node
    comprise the content of the reference, and any referencing information
    is stored in the *reference_info* key of the *data*.
    The data must also contain a *style* key pointing to the corresponding
    :class:`~sphinxcontrib.bibtex.style.referencing.BaseReferenceStyle`.
    """
    parts = _format_list(children, data)
    info = data["reference_info"]
    assert isinstance(info, SphinxReferenceInfo)
    return SphinxReferenceText(info, *parts)


@node
def pre_text(children, data: Dict[str, Any]):
    assert not children
    info = data["reference_info"]
    assert isinstance(info, SphinxReferenceInfo)
    return Text(info.pre_text)


@node
def post_text(children, data: Dict[str, Any]):
    assert not children
    info = data["reference_info"]
    assert isinstance(info, SphinxReferenceInfo)
    return Text(info.post_text)


class FootReferenceInfo(NamedTuple):
    """Tuple containing reference info to enable sphinx to resolve a footnote
    reference.
    """

    key: str  #: Citation key.
    document: "docutils.nodes.document"  #: Current docutils document.
    refname: str  #: Citation reference name.


class FootReferenceText(BaseReferenceText[FootReferenceInfo]):
    """Pybtex rich text class generating
    a docutils footnote_reference node to a citation
    for use with :class:`FootReferenceInfo`.
    """

    def render(self, backend: "BaseBackend"):
        assert isinstance(
            backend, pybtex_docutils.Backend
        ), "FootReferenceText only supports the docutils backend"
        info = self.info[0]
        # see docutils.parsers.rst.states.Body.footnote_reference()
        refnode = docutils.nodes.footnote_reference(
            "[#%s]_" % info.key, refname=info.refname, auto=1
        )
        info.document.note_autofootnote_ref(refnode)
        info.document.note_footnote_ref(refnode)
        return [refnode]


@node
def footnote_reference(children, data: Dict[str, Any]):
    """Pybtex node for inserting a footnote_reference docutils node.
    Any referencing information
    is stored in the *reference_info* key of the *data*.
    The data must also contain a *style* key pointing to the corresponding
    :class:`~sphinxcontrib.bibtex.style.referencing.BaseReferenceStyle`.
    """
    assert not children
    info = data["reference_info"]
    assert isinstance(info, FootReferenceInfo)
    # we need to give the footnote text some fake content
    # otherwise pybtex richtext engine will mess things up
    return FootReferenceText(info, "#")


@node
def year(children, data: Dict[str, Any]) -> "BaseText":
    assert not children
    return first_of[optional[field("year")], "n.d."].format_data(data)


@node
def author_or_editor_or_title(children, data, **kwargs):
    assert not children
    return first_of[
        optional[names("author", **kwargs)],
        optional[names("editor", **kwargs)],
        tag("em")[field("title")],
    ].format_data(data)
