# Copyright 2012-2014 MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Motor specific extensions to Sphinx."""

import inspect

from docutils.nodes import field, list_item, paragraph, title_reference, literal
from docutils.nodes import field_list, field_body, bullet_list, Text, field_name
from sphinx.addnodes import (desc, desc_content, versionmodified,
                             desc_signature, seealso, pending_xref)
from sphinx.util.inspect import safe_getattr

import motor
import motor.core


# This is a place to store info while parsing, to be used before generating.
motor_info = {}


def find_by_path(root, classes):
    if not classes:
        return [root]

    _class = classes[0]
    rv = []
    for child in root.children:
        if isinstance(child, _class):
            rv.extend(find_by_path(child, classes[1:]))

    return rv


def get_parameter_names(parameters_node):
    parameter_names = []
    for list_item_node in find_by_path(parameters_node, [list_item]):
        title_ref_nodes = find_by_path(
            list_item_node, [paragraph, (title_reference, pending_xref)])

        parameter_names.append(title_ref_nodes[0].astext())

    return parameter_names


def insert_callback(parameters_node):
    # We need to know what params are here already
    parameter_names = get_parameter_names(parameters_node)

    if 'callback' not in parameter_names:
        if '*args' in parameter_names:
            args_pos = parameter_names.index('*args')
        else:
            args_pos = len(parameter_names)

        if '**kwargs' in parameter_names:
            kwargs_pos = parameter_names.index('**kwargs')
        else:
            kwargs_pos = len(parameter_names)

        doc = (
            " (optional): function taking (result, error), executed when"
            " operation completes")

        new_item = list_item(
            '', paragraph(
                '', '',
                literal('', 'callback'),
                # literal(text='callback'),
                Text(doc)))

        # Insert "callback" before *args and **kwargs
        parameters_node.insert(min(args_pos, kwargs_pos), new_item)


def process_motor_nodes(app, doctree):
    # Search doctree for Motor's methods and attributes whose docstrings were
    # copied from PyMongo, and fix them up for Motor:
    #   1. Add a 'callback' param (sometimes optional, sometimes required) to
    #      all async methods. If the PyMongo method took no params, we create
    #      a parameter-list from scratch, otherwise we edit PyMongo's list.
    #   2. Remove all version annotations like "New in version 2.0" since
    #      PyMongo's version numbers are meaningless in Motor's docs.
    #   3. Remove "seealso" directives that reference PyMongo's docs.
    #
    # We do this here, rather than by registering a callback to Sphinx's
    # 'autodoc-process-signature' event, because it's way easier to handle the
    # parsed doctree before it's turned into HTML than it is to update the RST.
    for objnode in doctree.traverse(desc):
        if objnode['objtype'] in ('method', 'attribute'):
            signature_node = find_by_path(objnode, [desc_signature])[0]
            name = '.'.join([
                signature_node['module'], signature_node['fullname']])

            assert name.startswith('motor.')
            obj_motor_info = motor_info.get(name)
            if obj_motor_info:
                desc_content_node = find_by_path(objnode, [desc_content])[0]
                if obj_motor_info.get('is_async_method'):
                    try:
                        # Find the parameter list, a bullet_list instance
                        parameters_node = find_by_path(
                            desc_content_node,
                            [field_list, field, field_body, bullet_list])[0]
                    except IndexError:
                        # PyMongo method has no parameters, create an empty
                        # params list
                        parameters_node = bullet_list()
                        parameters_field_list_node = field_list(
                            '',
                            field(
                                '',
                                field_name('', 'Parameters '),
                                field_body('', parameters_node)))

                        desc_content_node.append(parameters_field_list_node)

                    insert_callback(parameters_node)

                    callback_future_text = (
                        "If a callback is passed, returns None, else returns a"
                        " Future.")

                    desc_content_node.append(
                        paragraph('', Text(callback_future_text)))

                if obj_motor_info['is_pymongo_docstring']:
                    # Remove all "versionadded", "versionchanged" and
                    # "deprecated" directives from the docs we imported from
                    # PyMongo
                    version_nodes = find_by_path(
                        desc_content_node, [versionmodified])

                    for version_node in version_nodes:
                        version_node.parent.remove(version_node)

                    # Remove all "seealso" directives that contain :doc:
                    # references from PyMongo's docs
                    seealso_nodes = find_by_path(desc_content_node, [seealso])

                    for seealso_node in seealso_nodes:
                        if 'reftype="doc"' in str(seealso_node):
                            seealso_node.parent.remove(seealso_node)


def get_motor_attr(motor_class, name, *defargs):
    """If any Motor attributes can't be accessed, grab the equivalent PyMongo
    attribute. While we're at it, store some info about each attribute
    in the global motor_info dict.
    """
    attr = safe_getattr(motor_class, name)
    method_class = safe_getattr(attr, 'im_class', None)
    from_pymongo = not safe_getattr(
        method_class, '__module__', '').startswith('motor')

    # Store some info for process_motor_nodes()
    full_name = '%s.%s.%s' % (
        motor_class.__module__, motor_class.__name__, name)

    # TODO: hack! All docstrings should be updated with fully-qualified refs.
    full_name_legacy = 'motor.motor_tornado.%s.%s' % (
        motor_class.__name__, name)

    is_async_method = getattr(attr, 'is_async_method', False)
    is_cursor_method = getattr(attr, 'is_motorcursor_chaining_method', False)
    if is_async_method or is_cursor_method:
        pymongo_method = getattr(
            motor_class.__delegate_class__, attr.pymongo_method_name)
    else:
        pymongo_method = None

    # attr.doc is set by statement like 'error = AsyncRead(doc="OBSOLETE")'.
    is_pymongo_doc = ((from_pymongo or is_async_method or is_cursor_method)
                      and not getattr(attr, 'doc', None))

    motor_info[full_name] = motor_info[full_name_legacy] = {
        # These sub-attributes are set in motor.asynchronize()
        'is_async_method': is_async_method,
        'is_pymongo_docstring': is_pymongo_doc,
        'pymongo_method': pymongo_method,
    }

    return attr


def get_motor_argspec(pymongo_method, is_async_method):
    args, varargs, kwargs, defaults = inspect.getargspec(pymongo_method)

    # This part is copied from Sphinx's autodoc.py
    if args and args[0] in ('cls', 'self'):
        del args[0]

    defaults = list(defaults) if defaults else []

    if is_async_method:
        # Add 'callback=None' argument
        args.append('callback')
        defaults.append(None)

    return args, varargs, kwargs, defaults


# Adapted from MethodDocumenter.format_args
def format_motor_args(pymongo_method, is_async_method):
    argspec = get_motor_argspec(pymongo_method, is_async_method)
    formatted_argspec = inspect.formatargspec(*argspec)
    # escape backslashes for reST
    return formatted_argspec.replace('\\', '\\\\')


def process_motor_signature(
        app, what, name, obj, options, signature, return_annotation):
    if name in motor_info and motor_info[name].get('pymongo_method'):
        # Real sig obscured by decorator, reconstruct it
        pymongo_method = motor_info[name]['pymongo_method']
        is_async_method = motor_info[name]['is_async_method']
        args = format_motor_args(pymongo_method, is_async_method)
        return args, return_annotation


def setup(app):
    app.add_autodoc_attrgetter(type(motor.core.AgnosticBase), get_motor_attr)
    app.connect('autodoc-process-signature', process_motor_signature)
    app.connect("doctree-read", process_motor_nodes)
