'''
To use this extension, follow these instrucitons: https://www.sphinx-doc.org/en/master/development/tutorials/todo.html#using-the-extension
'''
from docutils import nodes
from docutils.parsers.rst import Directive, directives

from sphinx.locale import _
from sphinx.util.docutils import SphinxDirective


'''
Node classes usually don’t have to do anything except inherit from the standard docutils classes defined in docutils.nodes. todo inherits from Admonition because it should be handled like a note or warning, todolist is just a “general” node.

docutils nodes: https://docutils.sourceforge.io/docs/ref/doctree.html
Sphinx nodes: https://www.sphinx-doc.org/en/master/extdev/nodes.html#nodes
'''
class todo(nodes.Admonition, nodes.Element):
    pass


class todolist(nodes.General, nodes.Element):
    pass


def visit_todo_node(self, node):
    self.visit_admonition(node)


def depart_todo_node(self, node):
    self.depart_admonition(node)

'''
A directive class is a class deriving usually from docutils.parsers.rst.Directive. The directive interface is also covered in detail in the docutils documentation; the important thing is that the class should have attributes that configure the allowed markup, and a run method that returns a list of nodes.

Sphinx docs about docutils...Directive: https://www.sphinx-doc.org/en/master/extdev/markupapi.html#docutils.parsers.rst.Directive
docutils.parsers.rst.Directive: http://docutils.sourceforge.net/docs/ref/rst/directives.html

'''

'''
It’s very simple, creating and returning an instance of our todolist node class. The TodolistDirective directive itself has neither content nor arguments that need to be handled.
'''
class TodolistDirective(Directive):

    def run(self):
        return [todolist('')]

'''
Several important things are covered here. First, as you can see, we’re now subclassing the SphinxDirective helper class instead of the usual Directive class. This gives us access to the build environment instance using the self.env property. Without this, we’d have to use the rather convoluted self.state.document.settings.env. Then, to act as a link target (from TodolistDirective), the TodoDirective directive needs to return a target node in addition to the todo node. The target ID (in HTML, this will be the anchor name) is generated by using env.new_serialno which returns a new unique integer on each call and therefore leads to unique target names. The target node is instantiated without any text (the first two arguments).

On creating admonition node, the content body of the directive are parsed using self.state.nested_parse. The first argument gives the content body, and the second one gives content offset. The third argument gives the parent node of parsed result, in our case the todo node. Following this, the todo node is added to the environment. This is needed to be able to create a list of all todo entries throughout the documentation, in the place where the author puts a todolist directive. For this case, the environment attribute todo_all_todos is used (again, the name should be unique, so it is prefixed by the extension name). It does not exist when a new environment is created, so the directive must check and create it if necessary. Various information about the todo entry’s location are stored along with a copy of the node.

In the last line, the nodes that should be put into the doctree are returned: the target node and the admonition node.
'''
class TodoDirective(SphinxDirective):

    # this enables content in the directive
    has_content = True
    option_spec = {
        'woof': directives.unchanged_required,
        'wing': directives.unchanged_required,
        'foo': directives.unchanged_required
    }

    def run(self):
        targetid = 'todo-%d' % self.env.new_serialno('todo')
        targetnode = nodes.target('', '', ids=[targetid])

        todo_node = todo('\n'.join(self.content))
        todo_node += nodes.title(_('Todo'), _('Todo'))
        self.state.nested_parse(self.content, self.content_offset, todo_node)

        if not hasattr(self.env, 'todo_all_todos'):
            self.env.todo_all_todos = []

        self.env.todo_all_todos.append({
            'docname': self.env.docname,
            'lineno': self.lineno,
            'todo': todo_node.deepcopy(),
            'target': targetnode,
            'options': self.options
        })

        return [targetnode, todo_node]

'''
The node structure that the directive returns looks like this:

+--------------------+
| target node        |
+--------------------+
+--------------------+
| todo node          |
+--------------------+
  \__+--------------------+
     | admonition title   |
     +--------------------+
     | paragraph          |
     +--------------------+
     | ...                |
     +--------------------+
'''


'''
Event handlers are one of Sphinx’s most powerful features, providing a way to do hook into any part of the documentation process. There are many events provided by Sphinx itself, as detailed in the API guide, and we’re going to use a subset of them here.

Sphinx event API:https://www.sphinx-doc.org/en/master/extdev/appapi.html#events
'''

'''
Let’s look at the event handlers used in the above example. First, the one for the env-purge-doc event.

Since we store information from source files in the environment, which is persistent, it may become out of date when the source file changes. Therefore, before each source file is read, the environment’s records of it are cleared, and the env-purge-doc event gives extensions a chance to do the same. Here we clear out all todos whose docname matches the given one from the todo_all_todos list. If there are todos left in the document, they will be added again during parsing.

env-purge-doc: https://www.sphinx-doc.org/en/master/extdev/appapi.html#event-env-purge-doc
'''
def purge_todos(app, env, docname):
    if not hasattr(env, 'todo_all_todos'):
        return

    env.todo_all_todos = [todo for todo in env.todo_all_todos
                          if todo['docname'] != docname]

'''
The next handler, for the env-merge-info event, is used during parallel builds. As during parallel builds all threads have their own env, there’s multiple todo_all_todos lists that need to be merged.

env-merge-info: https://www.sphinx-doc.org/en/master/extdev/appapi.html#event-env-merge-info
'''
def merge_todos(app, env, docnames, other):
    if not hasattr(env, 'todo_all_todos'):
        env.todo_all_todos = []
    if hasattr(other, 'todo_all_todos'):
        env.todo_all_todos.extend(other.todo_all_todos)

'''
The doctree-resolved event is emitted at the end of phase 3 (resolving) and allows custom resolving to be done. The handler we have written for this event is a bit more involved. If the todo_include_todos config value (which we’ll describe shortly) is false, all todo and todolist nodes are removed from the documents. If not, todo nodes just stay where and how they are. todolist nodes are replaced by a list of todo entries, complete with backlinks to the location where they come from. The list items are composed of the nodes from the todo entry and docutils nodes created on the fly: a paragraph for each entry, containing text that gives the location, and a link (reference node containing an italic node) with the backreference. The reference URI is built by sphinx.builders.Builder.get_relative_uri() which creates a suitable URI depending on the used builder, and appending the todo node’s (the target’s) ID as the anchor name.

doctree-resolved: https://www.sphinx-doc.org/en/master/extdev/appapi.html#event-doctree-resolved
phase 3 (resolving): https://www.sphinx-doc.org/en/master/extdev/index.html#build-phases
get_relative_uri(): https://www.sphinx-doc.org/en/master/extdev/builderapi.html#sphinx.builders.Builder.get_relative_uri
'''
def process_todo_nodes(app, doctree, fromdocname):

    # if extention turned off, remove all todo nodes from doctree
    if not app.config.todo_include_todos:
        for node in doctree.traverse(todo):
            node.parent.remove(node)

    # Replace all todolist nodes with a list of the collected todos.
    # Augment each todo with a backlink to the original location.
    env = app.builder.env

    if not hasattr(env, 'todo_all_todos'):
        env.todo_all_todos = []

    for node in doctree.traverse(todolist):
        if not app.config.todo_include_todos:
            node.replace_self([])
            continue

        content = []

        for todo_info in env.todo_all_todos:
            # if todo_info['options']:
                # print('OPTIONS FOUND IN {0}'.format(todo_info['docname']))
                # print(todo_info['options'])

            # for todo in app.env.todo_all_todos:
            #     for key in todo['options']:
            #         print('key: {0}\tvalue: {1}'.format(key, todo['options'][key]))

            para = nodes.paragraph()
            filename = env.doc2path(todo_info['docname'], base=None)
            description = (
                _('(The original entry is located in %s, line %d and can be found ') %
                (filename, todo_info['lineno']))
            para += nodes.Text(description, description)

            # Create a reference
            newnode = nodes.reference('', '')
            innernode = nodes.emphasis(_('here'), _('here'))
            newnode['refdocname'] = todo_info['docname']
            newnode['refuri'] = app.builder.get_relative_uri(
                fromdocname, todo_info['docname'])
            newnode['refuri'] += '#' + todo_info['target']['refid']
            newnode.append(innernode)
            para += newnode
            para += nodes.Text('.)', '.)')

            # Insert into the todolist
            content.append(todo_info['todo'])
            content.append(para)

        node.replace_self(content)

def process_build_finished(app, exception):
    if exception:
        print('Exception hit!')
        print(dir(exception))

'''
The setup function is a requirement and is used to plug directives into Sphinx. However, we also use it to hook up the other parts of our extension. Let’s look at our setup function:

- add_config_value() lets Sphinx know that it should recognize the new config value todo_include_todos, whose default value should be False (this also tells Sphinx that it is a boolean value). If the third argument was 'html', HTML documents would be full rebuild if the config value changed its value. This is needed for config values that influence reading (build phase 1 (reading)).
    - add_config_value(): https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx.application.Sphinx.add_config_value
    - phase 1 (reading): https://www.sphinx-doc.org/en/master/extdev/index.html#build-phases

- add_node() adds a new node class to the build system. It also can specify visitor functions for each supported output format. These visitor functions are needed when the new nodes stay until phase 4 (writing). Since the todolist node is always replaced in phase 3 (resolving), it doesn’t need any.
    - add_node(): https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx.application.Sphinx.add_node
    - phase 3 (resolving): https://www.sphinx-doc.org/en/master/extdev/index.html#build-phases
    - phase 4 (writing): https://www.sphinx-doc.org/en/master/extdev/index.html#build-phases

- add_directive() adds a new directive, given by name and class.
    - add_directive(): https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx.application.Sphinx.add_directive

- Finally, connect() adds an event handler to the event whose name is given by the first argument. The event handler function is called with several arguments which are documented with the event.
    - connect(): https://www.sphinx-doc.org/en/master/extdev/appapi.html#sphinx.application.Sphinx.connect
'''
def setup(app):
    app.add_config_value('todo_include_todos', False, 'html')

    app.add_node(todolist)
    app.add_node(todo,
                 html=(visit_todo_node, depart_todo_node),
                 latex=(visit_todo_node, depart_todo_node),
                 text=(visit_todo_node, depart_todo_node))

    app.add_directive('todo', TodoDirective)
    app.add_directive('todolist', TodolistDirective)
    app.connect('doctree-resolved', process_todo_nodes)
    app.connect('env-purge-doc', purge_todos)
    app.connect('env-merge-info', merge_todos)
    app.connect('build-finished', process_build_finished)

    return {
        'version': '0.1',
        'parallel_read_safe': True,
        'parallel_write_safe': True,
    }