# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""Generate Docstring."""

# Standard library imports
import re
from collections import OrderedDict

# Third party imports
from qtpy.QtGui import QTextCursor
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QMenu

# Local imports
from spyder.api.widgets.menus import SpyderMenu
from spyder.config.manager import CONF
from spyder.py3compat import to_text_string


def is_start_of_function(text):
    """Return True if text is the beginning of the function definition."""
    if isinstance(text, str):
        function_prefix = ['def', 'async def']
        text = text.lstrip()

        for prefix in function_prefix:
            if text.startswith(prefix):
                return True

    return False


def get_indent(text):
    """Get indent of text.

    https://stackoverflow.com/questions/2268532/grab-a-lines-whitespace-
    indention-with-python
    """
    indent = ''

    ret = re.match(r'(\s*)', text)
    if ret:
        indent = ret.group(1)

    return indent


def is_in_scope_forward(text):
    """Check if the next empty line could be part of the definition."""
    text = text.replace(r"\"", "").replace(r"\'", "")
    scopes = ["'''", '"""', "'", '"']
    indices = [10**6] * 4  # Limits function def length to 10**6
    for i in range(len(scopes)):
        if scopes[i] in text:
            indices[i] = text.index(scopes[i])
    if min(indices) == 10**6:
        return (text.count(")") != text.count("(") or
                text.count("]") != text.count("[") or
                text.count("}") != text.count("{"))
    s = scopes[indices.index(min(indices))]
    p = indices[indices.index(min(indices))]
    ls = len(s)
    if s in text[p + ls:]:
        text = text[:p] + text[p + ls:][text[p + ls:].index(s) + ls:]
        return is_in_scope_forward(text)
    elif ls == 3:
        text = text[:p]
        return (text.count(")") != text.count("(") or
                text.count("]") != text.count("[") or
                text.count("}") != text.count("{"))
    else:
        return False


def is_tuple_brackets(text):
    """Check if the return type is a tuple."""
    scopes = ["(", "[", "{"]
    complements = [")", "]", "}"]
    indices = [10**6] * 4  # Limits return type length to 10**6
    for i in range(len(scopes)):
        if scopes[i] in text:
            indices[i] = text.index(scopes[i])
    if min(indices) == 10**6:
        return "," in text
    s = complements[indices.index(min(indices))]
    p = indices[indices.index(min(indices))]
    if s in text[p + 1:]:
        text = text[:p] + text[p + 1:][text[p + 1:].index(s) + 1:]
        return is_tuple_brackets(text)
    else:
        return False


def is_tuple_strings(text):
    """Check if the return type is a string."""
    text = text.replace(r"\"", "").replace(r"\'", "")
    scopes = ["'''", '"""', "'", '"']
    indices = [10**6] * 4  # Limits return type length to 10**6
    for i in range(len(scopes)):
        if scopes[i] in text:
            indices[i] = text.index(scopes[i])
    if min(indices) == 10**6:
        return is_tuple_brackets(text)
    s = scopes[indices.index(min(indices))]
    p = indices[indices.index(min(indices))]
    ls = len(s)
    if s in text[p + ls:]:
        text = text[:p] + text[p + ls:][text[p + ls:].index(s) + ls:]
        return is_tuple_strings(text)
    else:
        return False


def is_in_scope_backward(text):
    """Check if the next empty line could be part of the definition."""
    return is_in_scope_forward(
        text.replace(r"\"", "").replace(r"\'", "")[::-1])


def remove_comments(text):
    """
    Remove code comments from text while ignoring hash symbols (#) inside
    quotes, which can be part of function arguments.
    """
    return re.sub(pattern=r"""(?<!['"])(#.*)""", repl="", string=text)


class DocstringWriterExtension(object):
    """Class for insert docstring template automatically."""

    def __init__(self, code_editor):
        """Initialize and Add code_editor to the variable."""
        self.code_editor = code_editor
        self.quote3 = '"""'
        self.quote3_other = "'''"
        self.line_number_cursor = None

    @staticmethod
    def is_beginning_triple_quotes(text):
        """Return True if there are only triple quotes in text."""
        docstring_triggers = ['"""', 'r"""', "'''", "r'''"]
        if text.lstrip() in docstring_triggers:
            return True

        return False

    def is_end_of_function_definition(self, text, line_number):
        """Return True if text is the end of the function definition."""
        text_without_whitespace = "".join(text.split())
        if (
            text_without_whitespace.endswith("):") or
            text_without_whitespace.endswith("]:") or
            (text_without_whitespace.endswith(":") and
             "->" in text_without_whitespace)
        ):
            return True
        elif text_without_whitespace.endswith(":") and line_number > 1:
            complete_text = text_without_whitespace
            document = self.code_editor.document()
            cursor = QTextCursor(
                document.findBlockByNumber(line_number - 2))  # previous line
            for i in range(line_number - 2, -1, -1):
                txt = "".join(str(cursor.block().text()).split())
                if txt.endswith("\\") or is_in_scope_backward(complete_text):
                    if txt.endswith("\\"):
                        txt = txt[:-1]
                    complete_text = txt + complete_text
                else:
                    break
                if i != 0:
                    cursor.movePosition(QTextCursor.PreviousBlock)
            if is_start_of_function(complete_text):
                return (
                    complete_text.endswith("):") or
                    complete_text.endswith("]:") or
                    (complete_text.endswith(":") and
                     "->" in complete_text)
                )
            else:
                return False
        else:
            return False

    def get_function_definition_from_first_line(self):
        """Get func def when the cursor is located on the first def line."""
        document = self.code_editor.document()
        cursor = QTextCursor(
            document.findBlockByNumber(self.line_number_cursor - 1))

        func_text = ''
        func_indent = ''

        is_first_line = True
        line_number = cursor.blockNumber() + 1

        number_of_lines = self.code_editor.blockCount()
        remain_lines = number_of_lines - line_number + 1
        number_of_lines_of_function = 0

        for __ in range(min(remain_lines, 20)):
            cur_text = to_text_string(cursor.block().text()).rstrip()
            cur_text = remove_comments(cur_text)

            if is_first_line:
                if not is_start_of_function(cur_text):
                    return None

                func_indent = get_indent(cur_text)
                is_first_line = False
            else:
                cur_indent = get_indent(cur_text)
                if cur_indent <= func_indent and cur_text.strip() != '':
                    return None
                if is_start_of_function(cur_text):
                    return None
                if (cur_text.strip() == '' and
                        not is_in_scope_forward(func_text)):
                    return None

            if len(cur_text) > 0 and cur_text[-1] == '\\':
                cur_text = cur_text[:-1]

            func_text += cur_text
            number_of_lines_of_function += 1

            if self.is_end_of_function_definition(
                    cur_text, line_number + number_of_lines_of_function - 1):
                return func_text, number_of_lines_of_function

            cursor.movePosition(QTextCursor.NextBlock)

        return None

    def get_function_definition_from_below_last_line(self):
        """Get func def when the cursor is located below the last def line."""
        cursor = self.code_editor.textCursor()
        func_text = ''
        is_first_line = True
        line_number = cursor.blockNumber() + 1
        number_of_lines_of_function = 0

        for __ in range(min(line_number, 20)):
            if cursor.block().blockNumber() == 0:
                return None

            cursor.movePosition(QTextCursor.PreviousBlock)
            prev_text = to_text_string(cursor.block().text()).rstrip()
            prev_text = remove_comments(prev_text)

            if is_first_line:
                if not self.is_end_of_function_definition(
                        prev_text, line_number - 1):
                    return None
                is_first_line = False
            elif self.is_end_of_function_definition(
                    prev_text, line_number - number_of_lines_of_function - 1):
                return None

            if len(prev_text) > 0 and prev_text[-1] == '\\':
                prev_text = prev_text[:-1]

            func_text = prev_text + func_text

            number_of_lines_of_function += 1
            if is_start_of_function(prev_text):
                return func_text, number_of_lines_of_function

        return None

    def get_function_body(self, func_indent):
        """Get the function body text."""
        cursor = self.code_editor.textCursor()
        line_number = cursor.blockNumber() + 1
        number_of_lines = self.code_editor.blockCount()
        body_list = []

        for __ in range(number_of_lines - line_number + 1):
            text = to_text_string(cursor.block().text())
            text_indent = get_indent(text)

            if text.strip() == '':
                pass
            elif len(text_indent) <= len(func_indent):
                break

            body_list.append(text)

            cursor.movePosition(QTextCursor.NextBlock)

        return '\n'.join(body_list)

    def write_docstring(self):
        """Write docstring to editor."""
        line_to_cursor = self.code_editor.get_text('sol', 'cursor')
        if self.is_beginning_triple_quotes(line_to_cursor):
            cursor = self.code_editor.textCursor()
            prev_pos = cursor.position()

            quote = line_to_cursor[-1]
            docstring_type = CONF.get('editor', 'docstring_type')
            docstring = self._generate_docstring(docstring_type, quote)

            if docstring:
                self.code_editor.insert_text(docstring)

                cursor = self.code_editor.textCursor()
                cursor.setPosition(prev_pos, QTextCursor.KeepAnchor)
                cursor.movePosition(QTextCursor.NextBlock)
                cursor.movePosition(QTextCursor.EndOfLine,
                                    QTextCursor.KeepAnchor)
                cursor.clearSelection()
                self.code_editor.setTextCursor(cursor)
                return True

        return False

    def write_docstring_at_first_line_of_function(self):
        """Write docstring to editor at mouse position."""
        result = self.get_function_definition_from_first_line()
        editor = self.code_editor
        if result:
            func_text, number_of_line_func = result
            line_number_function = (self.line_number_cursor +
                                    number_of_line_func - 1)

            cursor = editor.textCursor()
            line_number_cursor = cursor.blockNumber() + 1
            offset = line_number_function - line_number_cursor
            if offset > 0:
                for __ in range(offset):
                    cursor.movePosition(QTextCursor.NextBlock)
            else:
                for __ in range(abs(offset)):
                    cursor.movePosition(QTextCursor.PreviousBlock)
            cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.MoveAnchor)
            editor.setTextCursor(cursor)

            indent = get_indent(func_text)
            editor.insert_text('\n{}{}"""'.format(indent, editor.indent_chars))
            self.write_docstring()

    def write_docstring_for_shortcut(self):
        """Write docstring to editor by shortcut of code editor."""
        # cursor placed below function definition
        result = self.get_function_definition_from_below_last_line()
        if result is not None:
            __, number_of_lines_of_function = result
            cursor = self.code_editor.textCursor()
            for __ in range(number_of_lines_of_function):
                cursor.movePosition(QTextCursor.PreviousBlock)

            self.code_editor.setTextCursor(cursor)

        cursor = self.code_editor.textCursor()
        self.line_number_cursor = cursor.blockNumber() + 1

        self.write_docstring_at_first_line_of_function()

    def _generate_docstring(self, doc_type, quote):
        """Generate docstring."""
        docstring = None

        self.quote3 = quote * 3
        if quote == '"':
            self.quote3_other = "'''"
        else:
            self.quote3_other = '"""'

        result = self.get_function_definition_from_below_last_line()

        if result:
            func_def, __ = result
            func_info = FunctionInfo()
            func_info.parse_def(func_def)

            if func_info.has_info:
                func_body = self.get_function_body(func_info.func_indent)
                if func_body:
                    func_info.parse_body(func_body)

                if doc_type == 'Numpydoc':
                    docstring = self._generate_numpy_doc(func_info)
                elif doc_type == 'Googledoc':
                    docstring = self._generate_google_doc(func_info)
                elif doc_type == "Sphinxdoc":
                    docstring = self._generate_sphinx_doc(func_info)

        return docstring

    def _generate_numpy_doc(self, func_info):
        """Generate a docstring of numpy type."""
        numpy_doc = ''

        arg_names = func_info.arg_name_list
        arg_types = func_info.arg_type_list
        arg_values = func_info.arg_value_list

        if len(arg_names) > 0 and arg_names[0] in ('self', 'cls'):
            del arg_names[0]
            del arg_types[0]
            del arg_values[0]

        indent1 = func_info.func_indent + self.code_editor.indent_chars
        indent2 = func_info.func_indent + self.code_editor.indent_chars * 2

        numpy_doc += '\n{}\n'.format(indent1)

        if len(arg_names) > 0:
            numpy_doc += '\n{}Parameters'.format(indent1)
            numpy_doc += '\n{}----------\n'.format(indent1)

        arg_text = ''
        for arg_name, arg_type, arg_value in zip(arg_names, arg_types,
                                                 arg_values):
            arg_text += '{}{} : '.format(indent1, arg_name)
            if arg_type:
                arg_text += '{}'.format(arg_type)
            else:
                arg_text += 'TYPE'

            if arg_value:
                arg_text += ', optional'

            arg_text += '\n{}DESCRIPTION.'.format(indent2)

            if arg_value:
                arg_value = arg_value.replace(self.quote3, self.quote3_other)
                arg_text += ' The default is {}.'.format(arg_value)

            arg_text += '\n'

        numpy_doc += arg_text

        if func_info.raise_list:
            numpy_doc += '\n{}Raises'.format(indent1)
            numpy_doc += '\n{}------'.format(indent1)
            for raise_type in func_info.raise_list:
                numpy_doc += '\n{}{}'.format(indent1, raise_type)
                numpy_doc += '\n{}DESCRIPTION.'.format(indent2)
            numpy_doc += '\n'

        numpy_doc += '\n'
        if func_info.has_yield:
            header = '{0}Yields\n{0}------\n'.format(indent1)
        else:
            header = '{0}Returns\n{0}-------\n'.format(indent1)

        return_type_annotated = func_info.return_type_annotated
        if return_type_annotated:
            return_section = '{}{}{}'.format(header, indent1,
                                             return_type_annotated)
            return_section += '\n{}DESCRIPTION.'.format(indent2)
        else:
            return_element_type = indent1 + '{return_type}\n' + indent2 + \
                'DESCRIPTION.'
            placeholder = return_element_type.format(return_type='TYPE')
            return_element_name = indent1 + '{return_name} : ' + \
                placeholder.lstrip()

            try:
                return_section = self._generate_docstring_return_section(
                    func_info.return_value_in_body, header,
                    return_element_name, return_element_type, placeholder,
                    indent1)
            except (ValueError, IndexError):
                return_section = '{}{}None.'.format(header, indent1)

        numpy_doc += return_section
        numpy_doc += '\n\n{}{}'.format(indent1, self.quote3)

        return numpy_doc

    def _generate_google_doc(self, func_info):
        """Generate a docstring of google type."""
        google_doc = ''

        arg_names = func_info.arg_name_list
        arg_types = func_info.arg_type_list
        arg_values = func_info.arg_value_list

        if len(arg_names) > 0 and arg_names[0] in ('self', 'cls'):
            del arg_names[0]
            del arg_types[0]
            del arg_values[0]

        indent1 = func_info.func_indent + self.code_editor.indent_chars
        indent2 = func_info.func_indent + self.code_editor.indent_chars * 2

        google_doc += '\n{}\n'.format(indent1)

        if len(arg_names) > 0:
            google_doc += '\n{0}Args:\n'.format(indent1)

        arg_text = ''
        for arg_name, arg_type, arg_value in zip(arg_names, arg_types,
                                                 arg_values):
            arg_text += '{}{} '.format(indent2, arg_name)

            arg_text += '('
            if arg_type:
                arg_text += '{}'.format(arg_type)
            else:
                arg_text += 'TYPE'

            if arg_value:
                arg_text += ', optional'
            arg_text += '):'

            arg_text += ' DESCRIPTION.'

            if arg_value:
                arg_value = arg_value.replace(self.quote3, self.quote3_other)
                arg_text += ' Defaults to {}.\n'.format(arg_value)
            else:
                arg_text += '\n'

        google_doc += arg_text

        if func_info.raise_list:
            google_doc += '\n{0}Raises:'.format(indent1)
            for raise_type in func_info.raise_list:
                google_doc += '\n{}{}'.format(indent2, raise_type)
                google_doc += ': DESCRIPTION.'
            google_doc += '\n'

        google_doc += '\n'
        if func_info.has_yield:
            header = '{}Yields:\n'.format(indent1)
        else:
            header = '{}Returns:\n'.format(indent1)

        return_type_annotated = func_info.return_type_annotated
        if return_type_annotated:
            return_section = '{}{}{}: DESCRIPTION.'.format(
                header, indent2, return_type_annotated)
        else:
            return_element_type = indent2 + '{return_type}: DESCRIPTION.'
            placeholder = return_element_type.format(return_type='TYPE')
            return_element_name = indent2 + '{return_name} ' + \
                '(TYPE): DESCRIPTION.'

            try:
                return_section = self._generate_docstring_return_section(
                    func_info.return_value_in_body, header,
                    return_element_name, return_element_type, placeholder,
                    indent2)
            except (ValueError, IndexError):
                return_section = '{}{}None.'.format(header, indent2)

        google_doc += return_section
        google_doc += '\n\n{}{}'.format(indent1, self.quote3)

        return google_doc

    def _generate_sphinx_doc(self, func_info):
        """Generate a docstring of sphinx type."""
        sphinx_doc = ''

        arg_names = func_info.arg_name_list
        arg_types = func_info.arg_type_list
        arg_values = func_info.arg_value_list

        if len(arg_names) > 0 and arg_names[0] in ('self', 'cls'):
            del arg_names[0]
            del arg_types[0]
            del arg_values[0]

        indent1 = func_info.func_indent + self.code_editor.indent_chars

        sphinx_doc += '\n{}\n'.format(indent1)

        arg_text = ''
        for arg_name, arg_type, arg_value in zip(arg_names, arg_types,
                                                 arg_values):
            arg_text += '{}:param {}: DESCRIPTION'.format(indent1, arg_name)

            if arg_value:
                arg_value = arg_value.replace(self.quote3, self.quote3_other)
                arg_text += ', defaults to {}\n'.format(arg_value)
            else:
                arg_text += '\n'

            arg_text += '{}:type {}: '.format(indent1, arg_name)

            if arg_type:
                arg_text += '{}'.format(arg_type)
            else:
                arg_text += 'TYPE'

            if arg_value:
                arg_text += ', optional'
            arg_text += '\n'

        sphinx_doc += arg_text

        if func_info.raise_list:
            for raise_type in func_info.raise_list:
                sphinx_doc += '{}:raises {}: DESCRIPTION\n'.format(indent1,
                                                                   raise_type)

        if func_info.has_yield:
            header = '{}:yield:'.format(indent1)
        else:
            header = '{}:return:'.format(indent1)

        return_type_annotated = func_info.return_type_annotated
        if return_type_annotated:
            return_section = '{} DESCRIPTION\n'.format(header)
            return_section += '{}:rtype: {}'.format(indent1,
                                                    return_type_annotated)
        else:
            return_section = '{} DESCRIPTION\n'.format(header)
            return_section += '{}:rtype: TYPE'.format(indent1)

        sphinx_doc += return_section
        sphinx_doc += '\n\n{}{}'.format(indent1, self.quote3)

        return sphinx_doc

    @staticmethod
    def find_top_level_bracket_locations(string_toparse):
        """Get the locations of top-level brackets in a string."""
        bracket_stack = []
        replace_args_list = []
        bracket_type = None
        literal_type = ''
        brackets = {'(': ')', '[': ']', '{': '}'}
        for idx, character in enumerate(string_toparse):
            if (not bracket_stack and character in brackets.keys()
                    or character == bracket_type):
                bracket_stack.append(idx)
                bracket_type = character
            elif bracket_type and character == brackets[bracket_type]:
                begin_idx = bracket_stack.pop()
                if not bracket_stack:
                    if not literal_type:
                        if bracket_type == '(':
                            literal_type = '(None)'
                        elif bracket_type == '[':
                            literal_type = '[list]'
                        elif bracket_type == '{':
                            if idx - begin_idx <= 1:
                                literal_type = '{dict}'
                            else:
                                literal_type = '{set}'
                    replace_args_list.append(
                        (string_toparse[begin_idx:idx + 1],
                         literal_type, 1))
                    bracket_type = None
                    literal_type = ''
            elif len(bracket_stack) == 1:
                if bracket_type == '(' and character == ',':
                    literal_type = '(tuple)'
                elif bracket_type == '{' and character == ':':
                    literal_type = '{dict}'
                elif bracket_type == '(' and character == ':':
                    literal_type = '[slice]'

        if bracket_stack:
            raise IndexError('Bracket mismatch')
        for replace_args in replace_args_list:
            string_toparse = string_toparse.replace(*replace_args)
        return string_toparse

    @staticmethod
    def parse_return_elements(return_vals_group, return_element_name,
                              return_element_type, placeholder):
        """Return the appropriate text for a group of return elements."""
        all_eq = (return_vals_group.count(return_vals_group[0])
                  == len(return_vals_group))
        if all([{'[list]', '(tuple)', '{dict}', '{set}'}.issuperset(
                return_vals_group)]) and all_eq:
            return return_element_type.format(
                return_type=return_vals_group[0][1:-1])
        # Output placeholder if special Python chars present in name
        py_chars = {' ', '+', '-', '*', '/', '%', '@', '<', '>', '&', '|', '^',
                    '~', '=', ',', ':', ';', '#', '(', '[', '{', '}', ']',
                    ')', }
        if any([any([py_char in return_val for py_char in py_chars])
                for return_val in return_vals_group]):
            return placeholder
        # Output str type and no name if only string literals
        if all(['"' in return_val or '\'' in return_val
                for return_val in return_vals_group]):
            return return_element_type.format(return_type='str')
        # Output bool type and no name if only bool literals
        if {'True', 'False'}.issuperset(return_vals_group):
            return return_element_type.format(return_type='bool')
        # Output numeric types and no name if only numeric literals
        try:
            [float(return_val) for return_val in return_vals_group]
            num_not_int = 0
            for return_val in return_vals_group:
                try:
                    int(return_val)
                except ValueError:  # If not an integer (EAFP)
                    num_not_int = num_not_int + 1
            if num_not_int == 0:
                return return_element_type.format(return_type='int')
            elif num_not_int == len(return_vals_group):
                return return_element_type.format(return_type='float')
            else:
                return return_element_type.format(return_type='numeric')
        except ValueError:  # Not a numeric if float conversion didn't work
            pass
        # If names are not equal, don't contain "." or are a builtin
        if ({'self', 'cls', 'None'}.isdisjoint(return_vals_group) and all_eq
                and all(['.' not in return_val
                         for return_val in return_vals_group])):
            return return_element_name.format(return_name=return_vals_group[0])
        return placeholder

    def _generate_docstring_return_section(self, return_vals, header,
                                           return_element_name,
                                           return_element_type,
                                           placeholder, indent):
        """Generate the Returns section of a function/method docstring."""
        # If all return values are None, return none
        non_none_vals = [return_val for return_val in return_vals
                         if return_val and return_val != 'None']
        if not non_none_vals:
            return header + indent + 'None.'

        # Get only values with matching brackets that can be cleaned up
        non_none_vals = [return_val.strip(' ()\t\n').rstrip(',')
                         for return_val in non_none_vals]
        non_none_vals = [re.sub('([\"\'])(?:(?=(\\\\?))\\2.)*?\\1',
                                '"string"', return_val)
                         for return_val in non_none_vals]
        unambiguous_vals = []
        for return_val in non_none_vals:
            try:
                cleaned_val = self.find_top_level_bracket_locations(return_val)
            except IndexError:
                continue
            unambiguous_vals.append(cleaned_val)
        if not unambiguous_vals:
            return header + placeholder

        # If remaining are a mix of tuples and not, return single placeholder
        single_vals, tuple_vals = [], []
        for return_val in unambiguous_vals:
            (tuple_vals.append(return_val) if ',' in return_val
             else single_vals.append(return_val))
        if single_vals and tuple_vals:
            return header + placeholder

        # If return values are tuples of different length, return a placeholder
        if tuple_vals:
            num_elements = [return_val.count(',') + 1
                            for return_val in tuple_vals]
            if num_elements.count(num_elements[0]) != len(num_elements):
                return header + placeholder
            num_elements = num_elements[0]
        else:
            num_elements = 1

        # If all have the same len but some ambiguous return that placeholders
        if len(unambiguous_vals) != len(non_none_vals):
            return header + '\n'.join(
                [placeholder for __ in range(num_elements)])

        # Handle tuple (or single) values position by position
        return_vals_grouped = zip(*[
            [return_element.strip() for return_element in
             return_val.split(',')]
            for return_val in unambiguous_vals])
        return_elements_out = []
        for return_vals_group in return_vals_grouped:
            return_elements_out.append(
                self.parse_return_elements(return_vals_group,
                                           return_element_name,
                                           return_element_type,
                                           placeholder))

        return header + '\n'.join(return_elements_out)


class FunctionInfo(object):
    """Parse function definition text."""

    def __init__(self):
        """."""
        self.has_info = False
        self.func_text = ''
        self.args_text = ''
        self.func_indent = ''
        self.arg_name_list = []
        self.arg_type_list = []
        self.arg_value_list = []
        self.return_type_annotated = None
        self.return_value_in_body = []
        self.raise_list = None
        self.has_yield = False

    @staticmethod
    def is_char_in_pairs(pos_char, pairs):
        """Return True if the character is in pairs of brackets or quotes."""
        for pos_left, pos_right in pairs.items():
            if pos_left < pos_char < pos_right:
                return True

        return False

    @staticmethod
    def _find_quote_position(text):
        """Return the start and end position of pairs of quotes."""
        pos = {}
        is_found_left_quote = False

        for idx, character in enumerate(text):
            if is_found_left_quote is False:
                if character == "'" or character == '"':
                    is_found_left_quote = True
                    quote = character
                    left_pos = idx
            else:
                if character == quote and text[idx - 1] != '\\':
                    pos[left_pos] = idx
                    is_found_left_quote = False

        if is_found_left_quote:
            raise IndexError("No matching close quote at: " + str(left_pos))

        return pos

    def _find_bracket_position(self, text, bracket_left, bracket_right,
                               pos_quote):
        """Return the start and end position of pairs of brackets.

        https://stackoverflow.com/questions/29991917/
        indices-of-matching-parentheses-in-python
        """
        pos = {}
        pstack = []

        for idx, character in enumerate(text):
            if character == bracket_left and \
                    not self.is_char_in_pairs(idx, pos_quote):
                pstack.append(idx)
            elif character == bracket_right and \
                    not self.is_char_in_pairs(idx, pos_quote):
                if len(pstack) == 0:
                    raise IndexError(
                        "No matching closing parens at: " + str(idx))
                pos[pstack.pop()] = idx

        if len(pstack) > 0:
            raise IndexError(
                "No matching opening parens at: " + str(pstack.pop()))

        return pos

    def split_arg_to_name_type_value(self, args_list):
        """Split argument text to name, type, value."""
        for arg in args_list:
            arg_type = None
            arg_value = None

            has_type = False
            has_value = False

            pos_colon = arg.find(':')
            pos_equal = arg.find('=')

            if pos_equal > -1:
                has_value = True

            if pos_colon > -1:
                if not has_value:
                    has_type = True
                elif pos_equal > pos_colon:  # exception for def foo(arg1=":")
                    has_type = True

            if has_value and has_type:
                arg_name = arg[0:pos_colon].strip()
                arg_type = arg[pos_colon + 1:pos_equal].strip()
                arg_value = arg[pos_equal + 1:].strip()
            elif not has_value and has_type:
                arg_name = arg[0:pos_colon].strip()
                arg_type = arg[pos_colon + 1:].strip()
            elif has_value and not has_type:
                arg_name = arg[0:pos_equal].strip()
                arg_value = arg[pos_equal + 1:].strip()
            else:
                arg_name = arg.strip()

            self.arg_name_list.append(arg_name)
            self.arg_type_list.append(arg_type)
            self.arg_value_list.append(arg_value)

    def split_args_text_to_list(self, args_text):
        """Split the text including multiple arguments to list.

        This function uses a comma to separate arguments and ignores a comma in
        brackets and quotes.
        """
        args_list = []
        idx_find_start = 0
        idx_arg_start = 0

        try:
            pos_quote = self._find_quote_position(args_text)
            pos_round = self._find_bracket_position(args_text, '(', ')',
                                                    pos_quote)
            pos_curly = self._find_bracket_position(args_text, '{', '}',
                                                    pos_quote)
            pos_square = self._find_bracket_position(args_text, '[', ']',
                                                     pos_quote)
        except IndexError:
            return None

        while True:
            pos_comma = args_text.find(',', idx_find_start)

            if pos_comma == -1:
                break

            idx_find_start = pos_comma + 1

            if self.is_char_in_pairs(pos_comma, pos_round) or \
                    self.is_char_in_pairs(pos_comma, pos_curly) or \
                    self.is_char_in_pairs(pos_comma, pos_square) or \
                    self.is_char_in_pairs(pos_comma, pos_quote):
                continue

            args_list.append(args_text[idx_arg_start:pos_comma])
            idx_arg_start = pos_comma + 1

        if idx_arg_start < len(args_text):
            args_list.append(args_text[idx_arg_start:])

        return args_list

    def parse_def(self, text):
        """Parse the function definition text."""
        self.__init__()

        if not is_start_of_function(text):
            return

        self.func_indent = get_indent(text)

        text = text.strip()

        return_type_re = re.search(
            r'->[ ]*([\"\'a-zA-Z0-9_,()\[\] ]*):$', text)
        if return_type_re:
            self.return_type_annotated = return_type_re.group(1).strip(" ()\\")
            if is_tuple_strings(self.return_type_annotated):
                self.return_type_annotated = (
                    "(" + self.return_type_annotated + ")"
                )
            text_end = text.rfind(return_type_re.group(0))
        else:
            self.return_type_annotated = None
            text_end = len(text)

        pos_args_start = text.find('(') + 1
        pos_args_end = text.rfind(')', pos_args_start, text_end)

        self.args_text = text[pos_args_start:pos_args_end]

        args_list = self.split_args_text_to_list(self.args_text)
        if args_list is not None:
            self.has_info = True
            self.split_arg_to_name_type_value(args_list)

    def parse_body(self, text):
        """Parse the function body text."""
        re_raise = re.findall(r'[ \t]raise ([a-zA-Z0-9_]*)', text)
        if len(re_raise) > 0:
            self.raise_list = [x.strip() for x in re_raise]
            # remove duplicates from list while keeping it in the order
            # in python 2.7
            # stackoverflow.com/questions/7961363/removing-duplicates-in-lists
            self.raise_list = list(OrderedDict.fromkeys(self.raise_list))

        re_yield = re.search(r'[ \t]yield ', text)
        if re_yield:
            self.has_yield = True

        # get return value
        pattern_return = r'return |yield '
        line_list = text.split('\n')
        is_found_return = False
        line_return_tmp = ''

        for line in line_list:
            line = line.strip()

            if is_found_return is False:
                if re.match(pattern_return, line):
                    is_found_return = True

            if is_found_return:
                line_return_tmp += line
                # check the integrity of line
                try:
                    pos_quote = self._find_quote_position(line_return_tmp)

                    if line_return_tmp[-1] == '\\':
                        line_return_tmp = line_return_tmp[:-1]
                        continue

                    self._find_bracket_position(line_return_tmp, '(', ')',
                                                pos_quote)
                    self._find_bracket_position(line_return_tmp, '{', '}',
                                                pos_quote)
                    self._find_bracket_position(line_return_tmp, '[', ']',
                                                pos_quote)
                except IndexError:
                    continue

                return_value = re.sub(pattern_return, '', line_return_tmp)
                self.return_value_in_body.append(return_value)

                is_found_return = False
                line_return_tmp = ''


class QMenuOnlyForEnter(SpyderMenu):
    """The class executes the selected action when "enter key" is input.

    If a input of keyboard is not the "enter key", the menu is closed and
    the input is inserted to code editor.
    """

    def __init__(self, code_editor):
        """Init SpyderMenu."""
        super(QMenuOnlyForEnter, self).__init__(code_editor)
        self.code_editor = code_editor

    def keyPressEvent(self, event):
        """Close the instance if key is not enter key."""
        key = event.key()
        if key not in (Qt.Key_Enter, Qt.Key_Return):
            self.code_editor.keyPressEvent(event)
            self.close()
        else:
            super(QMenuOnlyForEnter, self).keyPressEvent(event)
