# -*- coding: utf-8 -*-
"""
    c_annotations.py
    ~~~~~~~~~~~~~~~~

    Supports annotations for C API elements:

    * reference count annotations for C API functions.  Based on
      refcount.py and anno-api.py in the old Python documentation tools.

    * stable API annotations

    Usage:
    * Set the `refcount_file` config value to the path to the reference
    count data file.
    * Set the `stable_abi_file` config value to the path to stable ABI list.

    :copyright: Copyright 2007-2014 by Georg Brandl.
    :license: Python license.
"""

from os import path
import docutils
from docutils import nodes
from docutils.parsers.rst import directives
from docutils.parsers.rst import Directive
from docutils.statemachine import StringList
from sphinx.locale import _ as sphinx_gettext
import csv

from sphinx import addnodes
from sphinx.domains.c import CObject


REST_ROLE_MAP = {
    'function': 'func',
    'var': 'data',
    'type': 'type',
    'macro': 'macro',
    'type': 'type',
    'member': 'member',
}


# Monkeypatch nodes.Node.findall for forwards compatability
# This patch can be dropped when the minimum Sphinx version is 4.4.0
# or the minimum Docutils version is 0.18.1.
if docutils.__version_info__ < (0, 18, 1):
    def findall(self, *args, **kwargs):
        return iter(self.traverse(*args, **kwargs))

    nodes.Node.findall = findall


class RCEntry:
    def __init__(self, name):
        self.name = name
        self.args = []
        self.result_type = ''
        self.result_refs = None


class Annotations:
    def __init__(self, refcount_filename, stable_abi_file):
        self.refcount_data = {}
        with open(refcount_filename, 'r') as fp:
            for line in fp:
                line = line.strip()
                if line[:1] in ("", "#"):
                    # blank lines and comments
                    continue
                parts = line.split(":", 4)
                if len(parts) != 5:
                    raise ValueError("Wrong field count in %r" % line)
                function, type, arg, refcount, comment = parts
                # Get the entry, creating it if needed:
                try:
                    entry = self.refcount_data[function]
                except KeyError:
                    entry = self.refcount_data[function] = RCEntry(function)
                if not refcount or refcount == "null":
                    refcount = None
                else:
                    refcount = int(refcount)
                # Update the entry with the new parameter or the result
                # information.
                if arg:
                    entry.args.append((arg, type, refcount))
                else:
                    entry.result_type = type
                    entry.result_refs = refcount

        self.stable_abi_data = {}
        with open(stable_abi_file, 'r') as fp:
            for record in csv.DictReader(fp):
                role = record['role']
                name = record['name']
                self.stable_abi_data[name] = record

    def add_annotations(self, app, doctree):
        for node in doctree.findall(addnodes.desc_content):
            par = node.parent
            if par['domain'] != 'c':
                continue
            if not par[0].has_key('ids') or not par[0]['ids']:
                continue
            name = par[0]['ids'][0]
            if name.startswith("c."):
                name = name[2:]

            objtype = par['objtype']

            # Stable ABI annotation. These have two forms:
            #   Part of the [Stable ABI](link).
            #   Part of the [Stable ABI](link) since version X.Y.
            # For structs, there's some more info in the message:
            #   Part of the [Limited API](link) (as an opaque struct).
            #   Part of the [Stable ABI](link) (including all members).
            #   Part of the [Limited API](link) (Only some members are part
            #       of the stable ABI.).
            # ... all of which can have "since version X.Y" appended.
            record = self.stable_abi_data.get(name)
            if record:
                if record['role'] != objtype:
                    raise ValueError(
                        f"Object type mismatch in limited API annotation "
                        f"for {name}: {record['role']!r} != {objtype!r}")
                stable_added = record['added']
                message = ' Part of the '
                emph_node = nodes.emphasis(message, message,
                                           classes=['stableabi'])
                ref_node = addnodes.pending_xref(
                    'Stable ABI', refdomain="std", reftarget='stable',
                    reftype='ref', refexplicit="False")
                struct_abi_kind = record['struct_abi_kind']
                if struct_abi_kind in {'opaque', 'members'}:
                    ref_node += nodes.Text('Limited API')
                else:
                    ref_node += nodes.Text('Stable ABI')
                emph_node += ref_node
                if struct_abi_kind == 'opaque':
                    emph_node += nodes.Text(' (as an opaque struct)')
                elif struct_abi_kind == 'full-abi':
                    emph_node += nodes.Text(' (including all members)')
                if record['ifdef_note']:
                    emph_node += nodes.Text(' ' + record['ifdef_note'])
                if stable_added == '3.2':
                    # Stable ABI was introduced in 3.2.
                    pass
                else:
                    emph_node += nodes.Text(f' since version {stable_added}')
                emph_node += nodes.Text('.')
                if struct_abi_kind == 'members':
                    emph_node += nodes.Text(
                        ' (Only some members are part of the stable ABI.)')
                node.insert(0, emph_node)

            # Unstable API annotation.
            if name.startswith('PyUnstable'):
                warn_node = nodes.admonition(
                    classes=['unstable-c-api', 'warning'])
                message = 'This is '
                emph_node = nodes.emphasis(message, message)
                ref_node = addnodes.pending_xref(
                    'Unstable API', refdomain="std",
                    reftarget='unstable-c-api',
                    reftype='ref', refexplicit="False")
                ref_node += nodes.Text('Unstable API')
                emph_node += ref_node
                emph_node += nodes.Text('. It may change without warning in minor releases.')
                warn_node += emph_node
                node.insert(0, warn_node)

            # Return value annotation
            if objtype != 'function':
                continue
            entry = self.refcount_data.get(name)
            if not entry:
                continue
            elif not entry.result_type.endswith("Object*"):
                continue
            if entry.result_refs is None:
                rc = sphinx_gettext('Return value: Always NULL.')
            elif entry.result_refs:
                rc = sphinx_gettext('Return value: New reference.')
            else:
                rc = sphinx_gettext('Return value: Borrowed reference.')
            node.insert(0, nodes.emphasis(rc, rc, classes=['refcount']))


def init_annotations(app):
    annotations = Annotations(
        path.join(app.srcdir, app.config.refcount_file),
        path.join(app.srcdir, app.config.stable_abi_file),
    )
    app.connect('doctree-read', annotations.add_annotations)

    class LimitedAPIList(Directive):

        has_content = False
        required_arguments = 0
        optional_arguments = 0
        final_argument_whitespace = True

        def run(self):
            content = []
            for record in annotations.stable_abi_data.values():
                role = REST_ROLE_MAP[record['role']]
                name = record['name']
                content.append(f'* :c:{role}:`{name}`')

            pnode = nodes.paragraph()
            self.state.nested_parse(StringList(content), 0, pnode)
            return [pnode]

    app.add_directive('limited-api-list', LimitedAPIList)


def setup(app):
    app.add_config_value('refcount_file', '', True)
    app.add_config_value('stable_abi_file', '', True)
    app.connect('builder-inited', init_annotations)

    # monkey-patch C object...
    CObject.option_spec = {
        'noindex': directives.flag,
        'stableabi': directives.flag,
    }
    old_handle_signature = CObject.handle_signature
    def new_handle_signature(self, sig, signode):
        signode.parent['stableabi'] = 'stableabi' in self.options
        return old_handle_signature(self, sig, signode)
    CObject.handle_signature = new_handle_signature
    return {'version': '1.0', 'parallel_read_safe': True}
