import os
import logging
from docutils import nodes
from docutils.parsers.rst import directives
import sphinx
from sphinx.locale import _
try:
    from sphinx.errors import NoUri
except ImportError:
    from sphinx.environment import NoUri
from docutils.parsers.rst import Directive
from docutils.parsers.rst.directives.admonitions import BaseAdmonition
from docutils.statemachine import StringList
from docutils.frontend import Values
from sphinx.util.nodes import set_source_info, process_index_entry
from sphinx import addnodes
from ..language import TITLES
from ..ext_helper import info_blocref
[docs]
class blocref_node(nodes.admonition):
    """
    Defines ``blocref`` node.
    """
    pass 
class blocreflist(nodes.General, nodes.Element):
    """
    Defines ``blocreflist`` node.
    """
    pass
[docs]
class BlocRef(BaseAdmonition):
    """
    A ``blocref`` entry, displayed in the form of an admonition.
    It takes the following options:
    * *title*: a title for the block
    * *tag*: a tag to have several categories of blocks
    * *lid* or *label*: a label to refer to
    * *index*: to add an entry to the index (comma separated)
    Example::
        .. blocref::
            :title: example of a blocref
            :tag: example
            :lid: id-you-can-choose
            An example of code::
                print("mignon")
    Which renders as:
    .. blocref::
        :title: example of a blocref
        :tag: dummy_example
        :lid: id-you-can-choose
        An example of code::
            print("mignon")
    All blocks can be displayed in another page by using ``blocreflist``::
        .. blocreflist::
            :tag: dummy_example
            :sort: title
    Only examples tagged as ``dummy_example`` will be inserted here.
    The option ``sort`` sorts items by *title*, *number*, *file*.
    You also link to it by typing ``:ref:'anchor <id-you-can-choose>'`` which gives
    something like :ref:`link_to_blocref <id-you-can-choose>`.
    The link must receive a name.
    .. blocreflist::
        :tag: dummy_example
        :sort: title
    This directive is used to highlight a block about
    anything :class:`sphinx_runpython.blocdefs.sphinx_blocref_extension.BlocRef`,
    a question :class:`sphinx_runpython.blocdefs.sphinx_faqref_extension.FaqRef`,
    an example :class:`sphinx_runpython.blocdefs.sphinx_exref_extension.ExRef`.
    It supports option *index*
    in most of the extensions so that the documentation
    can refer to it.
    """
    node_class = blocref_node
    name_sphinx = "blocref"
    has_content = True
    required_arguments = 0
    optional_arguments = 0
    final_argument_whitespace = False
    option_spec = {
        "class": directives.class_option,
        "title": directives.unchanged,
        "tag": directives.unchanged,
        "lid": directives.unchanged,
        "label": directives.unchanged,
        "index": directives.unchanged,
    }
    def _update_title(self, title, tag, lid):
        """
        Updates the title for the block itself.
        """
        return title
[docs]
    def run(self):
        """
        Builds a node @see cl blocref_node.
        """
        return self.private_run() 
[docs]
    def private_run(self, add_container=False):
        """
        Builds a node @see cl blocref_node.
        :param add_container: add a container node and return as a second result
        :return: list of nodes or list of nodes, container
        """
        name_desc = self.__class__.name_sphinx
        lineno = self.lineno
        settings = self.state.document.settings
        env = settings.env if hasattr(settings, "env") else None
        docname = None if env is None else env.docname
        if docname is not None:
            docname = docname.replace("\\", "/").split("/")[-1]
            legend = f"{docname}:{lineno}"
        else:
            legend = ""
        if not self.options.get("class"):
            self.options["class"] = [f"admonition-{name_desc}"]
        # body
        (blocref,) = super(BlocRef, self).run()  # noqa: UP008
        if isinstance(blocref, nodes.system_message):
            return [blocref]
        # add a label
        lid = self.options.get("lid", self.options.get("label", None))
        if lid:
            container = nodes.container()
            tnl = [f".. _{lid}:", ""]
            content = StringList(tnl)
            self.state.nested_parse(content, self.content_offset, container)
        else:
            container = None
        # mid
        breftag = self.options.get("tag", "").strip()
        if len(breftag) == 0:
            raise ValueError("tag is empty")
        if env is not None:
            mid = int(env.new_serialno(f"index{name_desc}-{breftag}")) + 1
        else:
            mid = -1
        # title
        titleo = self.options.get("title", "").strip()
        if len(titleo) == 0:
            raise ValueError("title is empty")
        title = self._update_title(titleo, breftag, mid)
        # main node
        ttitle = title
        title = nodes.title(text=_(title))
        if container is not None:
            blocref.insert(0, title)
            blocref.insert(0, container)
        else:
            blocref.insert(0, title)
        if add_container:
            ret_container = nodes.container()
            blocref += ret_container
        blocref["breftag"] = breftag
        blocref["brefmid"] = mid
        blocref["breftitle"] = ttitle
        blocref["breftitleo"] = titleo
        blocref["brefline"] = lineno
        blocref["breffile"] = docname
        set_source_info(self, blocref)
        if env is not None:
            targetid = "index%s-%s%s" % (
                name_desc,
                breftag,
                env.new_serialno("index%s%s" % (name_desc, breftag)),
            )
            blocref["breftargetid"] = targetid
            ids = [targetid]
            targetnode = nodes.target(legend, "", ids=ids)
            set_source_info(self, targetnode)
            try:
                self.state.add_target(targetid, "", targetnode, lineno)
            except Exception as e:
                raise RuntimeError(
                    "Issue in \n  File '{0}', line "  # noqa: UP030
                    "{1}\ntitle={2}\ntag={3}\ntargetid={4}".format(
                        docname, lineno, title, breftag, targetid
                    )
                ) from e
            # index node
            index = self.options.get("index", None)
            if index is not None:
                indexnode = addnodes.index()
                indexnode["entries"] = ne = []
                indexnode["inline"] = False
                set_source_info(self, indexnode)
                for entry in index.split(","):
                    ne.extend(process_index_entry(entry, targetid))
            else:
                indexnode = None
        else:
            targetnode = None
            indexnode = None
        res = [a for a in [indexnode, targetnode, blocref] if a is not None]
        if add_container:
            return res, ret_container
        return res 
 
def process_blocrefs(app, doctree):
    """
    Collects all blocrefs in the environment
    this is not done in the directive itself because it some transformations
    must have already been run, e.g. substitutions.
    """
    process_blocrefs_generic(app, doctree, bloc_name="blocref", class_node=blocref_node)
def process_blocrefs_generic(app, doctree, bloc_name, class_node):
    """
    Collects all blocrefs in the environment
    this is not done in the directive itself because it some transformations
    must have already been run, e.g. substitutions.
    """
    env = app.builder.env
    attr = f"{bloc_name}_all_{bloc_name}s"
    if not hasattr(env, attr):
        setattr(env, attr, [])
    attr_list = getattr(env, attr)
    for node in doctree.traverse(class_node):
        try:
            targetnode = node.parent[node.parent.index(node) - 1]
            if not isinstance(targetnode, nodes.target):
                raise IndexError
        except IndexError:
            targetnode = None
        newnode = node.deepcopy()
        breftag = newnode["breftag"]
        breftitle = newnode["breftitle"]
        brefmid = newnode["brefmid"]
        brefline = newnode["brefline"]
        breffile = newnode["breffile"]
        del newnode["ids"]
        del newnode["breftag"]
        attr_list.append(
            {
                "docname": env.docname,
                "source": node.source or env.doc2path(env.docname),
                "lineno": node.line,
                "blocref": newnode,
                "target": targetnode,
                "breftag": breftag,
                "breftitle": breftitle,
                "brefmid": brefmid,
                "brefline": brefline,
                "breffile": breffile,
            }
        )
class BlocRefList(Directive):
    """
    A list of all blocref entries, for a specific tag.
    * tag: a tag to filter block having this tag
    * sort: a way to sort the blocks based on the title, file, number, default: *title*
    * contents: add a bullet list with links to added blocks
    Example::
        .. blocreflist::
            :tag: issue
            :contents:
    """
    name_sphinx = "blocreflist"
    node_class = blocreflist
    has_content = False
    required_arguments = 0
    optional_arguments = 0
    final_argument_whitespace = False
    option_spec = {
        "tag": directives.unchanged,
        "sort": directives.unchanged,
        "contents": directives.unchanged,
    }
    def run(self):
        """
        Simply insert an empty blocreflist node which will be replaced later
        when process_blocref_nodes is called.
        """
        name_desc = self.__class__.name_sphinx
        settings = self.state.document.settings
        env = settings.env if hasattr(settings, "env") else None
        docname = None if env is None else env.docname
        tag = self.options.get("tag", "").strip()
        n = self.__class__.node_class("")
        n["breftag"] = tag
        n["brefsort"] = self.options.get("sort", "title").strip()
        n["brefsection"] = self.options.get("section", True) in (
            True,
            "True",
            "true",
            1,
            "1",
        )
        n["brefcontents"] = self.options.get("contents", False) in (
            True,
            "True",
            "true",
            1,
            "1",
            "",
            None,
            "None",
        )
        n["docname"] = docname
        if env is not None:
            targetid = "index%slist-%s" % (
                name_desc,
                env.new_serialno("index%slist" % name_desc),
            )
            targetnode = nodes.target("", "", ids=[targetid])
            return [targetnode, n]
        else:
            return [n]
def process_blocref_nodes(app, doctree, fromdocname):
    """
    process_blocref_nodes
    """
    process_blocref_nodes_generic(
        app,
        doctree,
        fromdocname,
        class_name="blocref",
        entry_name="brefmes",
        class_node=blocref_node,
        class_node_list=blocreflist,
    )
def process_blocref_nodes_generic(
    app, doctree, fromdocname, class_name, entry_name, class_node, class_node_list
):
    """
    process_blocref_nodes and other kinds of nodes,
    If the configuration file specifies a variable
    ``blocref_include_blocrefs`` equals to False,
    all nodes are removed.
    """
    # logging
    cont = info_blocref(
        app, doctree, fromdocname, class_name, entry_name, class_node, class_node_list
    )
    if not cont:
        return
    # check this is something to process
    env = app.builder.env
    attr_name = f"{class_name}_all_{class_name}s"
    if not hasattr(env, attr_name):
        setattr(env, attr_name, [])
    bloc_list_env = getattr(env, attr_name)
    if len(bloc_list_env) == 0:
        return
    # content
    incconf = f"{class_name}_include_{class_name}s"
    if app.config[incconf] and not app.config[incconf]:
        for node in doctree.traverse(class_node):
            node.parent.remove(node)
    # Replace all blocreflist nodes with a list of the collected blocrefs.
    # Augment each blocref with a backlink to the original location.
    if hasattr(env, "settings"):
        settings = env.settings
        if hasattr(settings, "language_code"):
            lang = env.settings.language_code
        else:
            lang = "en"
    else:
        settings = None
        lang = "en"
    orig_entry = TITLES[lang]["original entry"]
    brefmes = TITLES[lang][entry_name]
    for ilist, node in enumerate(doctree.traverse(class_node_list)):
        if "ids" in node:
            node["ids"] = []
        if not app.config[incconf]:
            node.replace_self([])
            continue
        nbbref = 0
        content = []
        breftag = node["breftag"]
        brefsort = node["brefsort"]
        add_contents = node["brefcontents"]
        brefdocname = node["docname"]
        if add_contents:
            bullets = nodes.enumerated_list()
            content.append(bullets)
        # sorting
        if brefsort == "title":
            double_list = [
                (info.get("breftitle", ""), info)
                for info in bloc_list_env
                if info["breftag"] == breftag
            ]
            double_list.sort(key=lambda x: x[:1])
        elif brefsort == "file":
            double_list = [
                ((info.get("breffile", ""), info.get("brefline", "")), info)
                for info in bloc_list_env
                if info["breftag"] == breftag
            ]
            double_list.sort(key=lambda x: x[:1])
        elif brefsort == "number":
            double_list = [
                (info.get("brefmid", ""), info)
                for info in bloc_list_env
                if info["breftag"] == breftag
            ]
            double_list.sort(key=lambda x: x[:1])
        else:
            raise ValueError("sort option should be file, number, title")
        # printing
        for n, blocref_info_ in enumerate(double_list):
            blocref_info = blocref_info_[1]
            nbbref += 1
            para = nodes.paragraph(classes=[f"{class_name}-source"])
            # Create a target?
            try:
                targ = blocref_info["target"]
            except KeyError as e:
                logger = logging.getLogger("blocref")
                logger.warning(
                    "Unable to find key 'target' in %r (e=%r)", blocref_info, e
                )
                continue
            try:
                targ_refid = blocref_info["target"]["refid"]
            except KeyError as e:
                logger = logging.getLogger("blocref")
                logger.warning("Unable to find key 'refid' in %r (e=%r)", targ, e)
                continue
            int_ids = [f"index{targ_refid}-{env.new_serialno(targ_refid)}"]
            int_targetnode = nodes.target(blocref_info["breftitle"], "", ids=int_ids)
            para += int_targetnode
            # rest of the content
            if app.config[f"{class_name}_link_only"]:
                description = _(f"<<{orig_entry}>>")
            else:
                description = _(brefmes) % (
                    orig_entry,
                    os.path.split(blocref_info["source"])[-1],
                    blocref_info["lineno"],
                )
            desc1 = description[: description.find("<<")]
            desc2 = description[description.find(">>") + 2 :]
            para += nodes.Text(desc1, desc1)
            # Create a reference
            newnode = nodes.reference("", "", internal=True)
            newnode["name"] = _(orig_entry)
            try:
                newnode["refuri"] = app.builder.get_relative_uri(
                    fromdocname, blocref_info["docname"]
                )
                if blocref_info["target"] is None:
                    raise NoUri
                try:
                    newnode["refuri"] += "#" + blocref_info["target"]["refid"]
                except Exception as e:
                    raise KeyError(
                        f"refid in not present in {blocref_info['target']!r}"
                    ) from e
            except NoUri:
                # ignore if no URI can be determined, e.g. for LaTeX output
                pass
            newnode.append(nodes.Text(newnode["name"]))
            # para is duplicate of the content of the block
            para += newnode
            para += nodes.Text(desc2, desc2)
            blocref_entry = blocref_info["blocref"]
            idss = ["index-%s-%d-%d" % (class_name, ilist, n)]
            # Inserts into the blocreflist
            # in the list of links at the beginning of the page.
            if add_contents:
                title = blocref_info["breftitle"]
                item = nodes.list_item()
                p = nodes.paragraph()
                item += p
                newnode = nodes.reference("", title, internal=True)
                try:
                    newnode["refuri"] = app.builder.get_relative_uri(
                        fromdocname, brefdocname
                    )
                    newnode["refuri"] += "#" + idss[0]
                except NoUri:
                    # ignore if no URI can be determined, e.g. for LaTeX output
                    pass
                p += newnode
                bullets += item
            # Adds the content.
            blocref_entry["ids"] = idss
            if not hasattr(blocref_entry, "settings"):
                blocref_entry.settings = Values()
                blocref_entry.settings.env = env
            # If an exception happens here, see blog 2017-05-21 from the
            # documentation.
            env.resolve_references(blocref_entry, blocref_info["docname"], app.builder)
            content.append(blocref_entry)
            content.append(para)
        node.replace_self(content)
def purge_blocrefs(app, env, docname):
    """
    purge_blocrefs
    """
    if not hasattr(env, "blocref_all_blocrefs"):
        return
    env.blocref_all_blocrefs = [
        blocref for blocref in env.blocref_all_blocrefs if blocref["docname"] != docname
    ]
def merge_blocref(app, env, docnames, other):
    """
    merge_blocref
    """
    if not hasattr(other, "blocref_all_blocrefs"):
        return
    if not hasattr(env, "blocref_all_blocrefs"):
        env.blocref_all_blocrefs = []
    env.blocref_all_blocrefs.extend(other.blocref_all_blocrefs)
def visit_blocref_node(self, node):
    """
    visit_blocref_node
    """
    self.visit_admonition(node)
def depart_blocref_node(self, node):
    """
    depart_blocref_node,
    see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
    """
    self.depart_admonition(node)
def visit_blocreflist_node(self, node):
    """
    visit_blocreflist_node
    see https://github.com/sphinx-doc/sphinx/blob/master/sphinx/writers/html.py
    """
    self.visit_admonition(node)
def depart_blocreflist_node(self, node):
    """
    depart_blocref_node
    """
    self.depart_admonition(node)
def setup(app):
    """
    setup for ``blocref`` (sphinx)
    """
    if hasattr(app, "add_mapping"):
        app.add_mapping("blocref", blocref_node)
        app.add_mapping("blocreflist", blocreflist)
    app.add_config_value("blocref_include_blocrefs", True, "html")
    app.add_config_value("blocref_link_only", False, "html")
    app.add_node(
        blocreflist,
        html=(visit_blocreflist_node, depart_blocreflist_node),
        epub=(visit_blocreflist_node, depart_blocreflist_node),
        latex=(visit_blocreflist_node, depart_blocreflist_node),
        elatex=(visit_blocreflist_node, depart_blocreflist_node),
        text=(visit_blocreflist_node, depart_blocreflist_node),
        md=(visit_blocreflist_node, depart_blocreflist_node),
        rst=(visit_blocreflist_node, depart_blocreflist_node),
    )
    app.add_node(
        blocref_node,
        html=(visit_blocref_node, depart_blocref_node),
        epub=(visit_blocref_node, depart_blocref_node),
        elatex=(visit_blocref_node, depart_blocref_node),
        latex=(visit_blocref_node, depart_blocref_node),
        text=(visit_blocref_node, depart_blocref_node),
        md=(visit_blocref_node, depart_blocref_node),
        rst=(visit_blocref_node, depart_blocref_node),
    )
    app.add_directive("blocref", BlocRef)
    app.add_directive("blocreflist", BlocRefList)
    app.connect("doctree-read", process_blocrefs)
    app.connect("doctree-resolved", process_blocref_nodes)
    app.connect("env-purge-doc", purge_blocrefs)
    app.connect("env-merge-info", merge_blocref)
    return {"version": sphinx.__display_version__, "parallel_read_safe": True}