"""
    sphinx.ext.autodoc.typehints
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    Generating content for autodoc using typehints

    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
    :license: BSD, see LICENSE for details.
"""

import re
from collections import OrderedDict
from typing import Any, Dict, Iterable, Set, cast, get_type_hints

from docutils import nodes
from docutils.nodes import Element

from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.util import inspect, typing


from types import ModuleType
songbird = ModuleType('songbird', 'pyi loaded for typing data')

with open("../songbird/songbird.pyi") as f:
    exec(f.read(), songbird.__dict__)

items = dir(songbird)


def find_item(name: str):
    """Name is expected to be like this ``songbird.sonbird.Config``"""
    end_of_name = name.split(".")[-1]

    if end_of_name in items:
        return getattr(songbird, end_of_name)
    else:
        upper = '.'.join(name.split(".")[:-1])

        if not upper:
            return

        found = find_item(upper)
        if not found:
            return
        return getattr(found, end_of_name)


def record_typehints(app: Sphinx, objtype: str, name: str, obj: Any,
                     options: Dict, args: str, retann: str) -> None:
    """Record type hints to env object."""
    try:
        if callable(obj):
            annotations = app.env.temp_data.setdefault('annotations', {})
            annotation = annotations.setdefault(name, OrderedDict())

            obj_in_pyi = find_item(name)
            if obj_in_pyi:
                obj = obj_in_pyi

            type_hints = get_type_hints(obj)

            for param, hint in type_hints.items():
                if param is not None:
                    annotation[param] = typing.stringify(hint)

    except (TypeError, ValueError) as e:
        pass
    except Exception:
        # I have no idea how this fixes "Optional is undefined" but it does
        pass


def merge_typehints(app: Sphinx, domain: str, objtype: str, contentnode: Element) -> None:
    if domain != 'py':
        return
    if app.config.autodoc_typehints not in ('both', 'description'):
        return

    try:
        signature = cast(addnodes.desc_signature, contentnode.parent[0])
        if signature['module']:
            fullname = '.'.join([signature['module'], signature['fullname']])
        else:
            fullname = signature['fullname']
    except KeyError:
        # signature node does not have valid context info for the target object
        return

    annotations = app.env.temp_data.get('annotations', {})
    if annotations.get(fullname, {}):
        field_lists = [
            n for n in contentnode if isinstance(n, nodes.field_list)]
        if field_lists == []:
            field_list = insert_field_list(contentnode)
            field_lists.append(field_list)

        for field_list in field_lists:
            if app.config.autodoc_typehints_description_target == "all":
                modify_field_list(field_list, annotations[fullname])
            else:
                augment_descriptions_with_types(
                    field_list, annotations[fullname])


def insert_field_list(node: Element) -> nodes.field_list:
    field_list = nodes.field_list()
    desc = [n for n in node if isinstance(n, addnodes.desc)]
    if desc:
        # insert just before sub object descriptions (ex. methods, nested classes, etc.)
        index = node.index(desc[0])
        node.insert(index - 1, [field_list])
    else:
        node += field_list

    return field_list


def modify_field_list(node: nodes.field_list, annotations: Dict[str, str]) -> None:
    arguments: Dict[str, Dict[str, bool]] = {}
    fields = cast(Iterable[nodes.field], node)
    for field in fields:
        field_name = field[0].astext()
        parts = re.split(' +', field_name)
        if parts[0] == 'param':
            if len(parts) == 2:
                # :param xxx:
                arg = arguments.setdefault(parts[1], {})
                arg['param'] = True
            elif len(parts) > 2:
                # :param xxx yyy:
                name = ' '.join(parts[2:])
                arg = arguments.setdefault(name, {})
                arg['param'] = True
                arg['type'] = True
        elif parts[0] == 'type':
            name = ' '.join(parts[1:])
            arg = arguments.setdefault(name, {})
            arg['type'] = True
        elif parts[0] == 'rtype':
            arguments['return'] = {'type': True}

    for name, annotation in annotations.items():
        if name == 'return':
            continue

        arg = arguments.get(name, {})
        if not arg.get('type'):
            field = nodes.field()
            field += nodes.field_name('', 'type ' + name)
            field += nodes.field_body('', nodes.paragraph('', annotation))
            node += field
        if not arg.get('param'):
            field = nodes.field()
            field += nodes.field_name('', 'param ' + name)
            field += nodes.field_body('', nodes.paragraph('', ''))
            node += field

    if 'return' in annotations and 'return' not in arguments:
        field = nodes.field()
        field += nodes.field_name('', 'rtype')
        field += nodes.field_body('', nodes.paragraph('', annotation))
        node += field


def augment_descriptions_with_types(
    node: nodes.field_list,
    annotations: Dict[str, str],
) -> None:
    fields = cast(Iterable[nodes.field], node)
    has_description = set()  # type: Set[str]
    has_type = set()  # type: Set[str]
    for field in fields:
        field_name = field[0].astext()
        parts = re.split(' +', field_name)
        if parts[0] == 'param':
            if len(parts) == 2:
                # :param xxx:
                has_description.add(parts[1])
            elif len(parts) > 2:
                # :param xxx yyy:
                name = ' '.join(parts[2:])
                has_description.add(name)
                has_type.add(name)
        elif parts[0] == 'type':
            name = ' '.join(parts[1:])
            has_type.add(name)
        elif parts[0] in ('return', 'returns'):
            has_description.add('return')
        elif parts[0] == 'rtype':
            has_type.add('return')

    # Add 'type' for parameters with a description but no declared type.
    for name in annotations:
        if name in ('return', 'returns'):
            continue
        if name in has_description and name not in has_type:
            field = nodes.field()
            field += nodes.field_name('', 'type ' + name)
            field += nodes.field_body('',
                                      nodes.paragraph('', annotations[name]))
            node += field

    # Add 'rtype' if 'return' is present and 'rtype' isn't.
    if 'return' in annotations:
        if 'return' in has_description and 'return' not in has_type:
            field = nodes.field()
            field += nodes.field_name('', 'rtype')
            field += nodes.field_body('',
                                      nodes.paragraph('', annotations['return']))
            node += field


def setup(app: Sphinx) -> Dict[str, Any]:
    app.disconnect('autodoc-process-signature')
    app.disconnect('object-description-transform')

    app.connect('autodoc-process-signature', record_typehints)
    app.connect('object-description-transform', merge_typehints)

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