# Copyright (c) 2023, Riverbank Computing Limited
# All rights reserved.
#
# This copy of SIP is licensed for use under the terms of the SIP License
# Agreement.  See the file LICENSE for more details.
#
# This copy of SIP may also used under the terms of the GNU General Public
# License v2 or v3 as published by the Free Software Foundation which can be
# found in the files LICENSE-GPL2 and LICENSE-GPL3 included in this package.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.


from ...version import SIP_VERSION_STR

from ..specification import (AccessSpecifier, ArgumentType, ArrayArgument,
        EnumBaseType, IfaceFileType, PyQtMethodSpecifier, PySlot)
from ..utils import append_iface_file, find_method

from .formatters import (ArgumentFormatter, ClassFormatter, format_copying,
        format_scoped_py_name)


def output_pyi(spec, pyi_filename):
    """ Output a .pyi file. """

    with open(pyi_filename, 'w') as pf:
        module = spec.module

        # Write the header.
        copying = format_copying(module.copying, '#')

        pf.write(
f'''# The PEP 484 type hints stub file for the {module.py_name} module.
#
# Generated by SIP {SIP_VERSION_STR}
{copying}


''')

        if spec.is_composite:
            _composite_module(pf, spec, module)
        else:
            _module(pf, spec, module)


def _composite_module(pf, spec, module):
    """ Output the type hints for a composite module. """

    for mod in spec.module.all_imports:
        if mod.composite is spec.module:
            pf.write(f'from {mod.fq_py_name.name} import *\n')


def _module(pf, spec, module):
    """ Output the type hints for an ordinary module. """

    # Generate the imports. Note that we assume the super-types are the
    # standard SIP ones.
    if spec.abi_version >= (13, 0):
        pf.write('import enum\n')

    pf.write(
f'''import typing

import {spec.sip_module}
''')

    imports = []

    for mod in module.all_imports:
        parts = mod.fq_py_name.name.split('.')

        if mod.fq_py_name.name == mod.py_name:
            imports.append('import ' + mod.py_name)
        else:
            scope = mod.fq_py_name.name[:-len(mod.py_name) - 1]
            imports.append('from ' + scope + ' import ' + mod.py_name)

    if imports:
        pf.write('\n' + '\n'.join(imports) + '\n')

    # Generate any exported type hint code and any module-specific type hint
    # code.
    _type_hint_code(pf, spec.exported_type_hint_code)
    _type_hint_code(pf, module.type_hint_code)

    # Generate the types - global enums must be first.
    _enums(pf, spec, module)

    # The list of enums and classes that have been defined at any particular
    # point so we know if they can be referenced directly rather than by their
    # names as a string.
    defined = []

    for klass in spec.classes:
        if klass.iface_file.module is not module:
            continue

        if klass.external:
            continue

        if klass.no_type_hint:
            continue

        # Only handle non-nested classes here.
        if klass.scope is not None:
            continue

        # We can't handle extenders.
        if klass.real_class is not None:
            continue

        _class(pf, spec, module, klass, defined)

    for mapped_type in spec.mapped_types:
        if mapped_type.iface_file.module is not module:
            continue

        if mapped_type.py_name is not None:
            _mapped_type(pf, spec, module, mapped_type, defined)

    _variables(pf, spec, module, defined)

    first = True

    for member in module.global_functions:
        if member.py_slot is None:
            first = _separate(pf, first)
            _callable(pf, spec, module, member, module.overloads, False,
                    defined)


def _type_hint_code(pf, type_hint_code, indent=0):
    """ Output handwritten type hint code. """

    s = ''

    for block in type_hint_code:
        s += '\n'

        need_indent = True

        for ch in block.text:
            if need_indent:
                s += _indent(indent)
                need_indent = False

            s += ch

            if ch == '\n':
                need_indent = True

    pf.write(s)


def _class(pf, spec, module, klass, defined, indent=0):
    """ Output the type hints for a class. """

    nr_overloads = 0

    if not klass.is_hidden_namespace:
        _separate(pf, True, indent)

        s = _indent(indent)

        s += f'class {klass.py_name.name}('

        if klass.superclasses:
            s += ', '.join(
                    [ClassFormatter(spec, sc).as_type_hint(module, defined)
                            for sc in klass.superclasses])

        elif klass.supertype is not None:
            # In ABI v12 the default supertype does not contain the fully
            # qualified name of the sip module so we fix it here.
            if spec.abi_version[0] == 12 and klass.supertype.name.startswith('sip.'):
                s += spec.sip_module + klass.supertype.name[4:]
            else:
                s += klass.supertype.name

        else:
            simple = 'simple' if klass.iface_file.type is IfaceFileType.NAMESPACE else ''

            s += f'{spec.sip_module}.{simple}wrapper'

        # See if there is anything in the class body.
        for ctor in klass.ctors:
            if ctor.access_specifier is AccessSpecifier.PRIVATE:
                continue

            if ctor.no_type_hint:
                continue

            nr_overloads += 1

        no_body = (klass.type_hint_code is None and nr_overloads == 0)

        if no_body:
            for overload in klass.overloads:
                if overload.access_specifier is AccessSpecifier.PRIVATE:
                    continue

                if overload.no_type_hint:
                    continue

                no_body = False
                break

        if no_body:
            for enum in spec.enums:
                if enum.scope is klass and not enum.no_type_hint:
                    no_body = False
                    break

        if no_body:
            for nested in spec.classes:
                if nested.scope is klass and not nested.no_type_hint:
                    no_body = False
                    break

        if no_body:
            for variable in spec.variables:
                if variable.scope is klass and not variable.no_type_hint:
                    no_body = False
                    break

        suffix = ' ...' if no_body else ''

        s += f'):{suffix}\n'

        pf.write(s)

        indent += 1

        if klass.type_hint_code is not None:
            _type_hint_code(pf, [klass.type_hint_code], indent)

    _enums(pf, spec, module, defined=defined, scope=klass, indent=indent)

    # Handle any nested classes.
    for nested in spec.classes:
        if nested.scope is klass and not nested.no_type_hint:
            _class(pf, spec, module, nested, defined, indent)

    _variables(pf, spec, module, defined, scope=klass, indent=indent)

    first = True

    for ctor in klass.ctors:
        if ctor.access_specifier is AccessSpecifier.PRIVATE:
            continue

        if ctor.no_type_hint:
            continue

        first = _separate(pf, first, indent)

        _ctor(pf, spec, module, ctor, nr_overloads > 1, defined, indent)

    first = True

    for member in klass.members:
        first = _separate(pf, first, indent)

        _callable(pf, spec, module, member, klass.overloads,
                not klass.is_hidden_namespace, defined, indent)

    for prop in klass.properties:
        first = _separate(pf, first, indent)

        getter = find_method(klass, prop.getter)
        if getter is not None:
            _property(pf, spec, module, prop, False, getter, klass.overloads,
                    defined, indent)

            if prop.setter is not None:
                setter = find_method(klass, prop.setter)
                if setter is not None:
                    _property(pf, spec, module, prop, True, setter,
                            klass.overloads, defined, indent)

    if not klass.is_hidden_namespace:
        # Keep track of what has been defined so that forward references are no
        # longer required.
        append_iface_file(defined, klass.iface_file)

def _mapped_type(pf, spec, module, mapped_type, defined, indent=0):
    """ Output the type hints for a mapped type. """

    # See if there is anything in the mapped type body.
    no_body = (len(mapped_type.members) == 0)

    if no_body:
        for enum in spec.enums:
            if enum.scope is mapped_type and not enum.no_type_hint:
                no_body = FALSE
                break

    if not no_body:
        _separate(pf, True, indent)

        s = _indent(indent)
        s += f'class {mapped_type.py_name.name}({spec.sip_module}.wrapper):\n'
        pf.write(s)

        indent += 1

        _enums(pf, spec, module, defined=defined, scope=mapped_type,
                indent=indent)

        first = True

        for member in mapped_type.members:
            first = _separate(pf, first, indent)

            _callable(pf, spec, module, member, member.overloads, True,
                    defined, indent)

    # Keep track of what has been defined so that forward references are no
    # longer required.
    append_iface_file(defined, mapped_type.iface_file)


def _ctor(pf, spec, module, ctor, overloaded, defined, indent):
    """ Output a ctor type hint. """

    if overloaded:
        s = _indent(indent)
        s += '@typing.overload\n'
        pf.write(s)

    s = _indent(indent)
    s += 'def __init__('

    args = ['self']

    for arg_nr, arg in enumerate(ctor.py_signature.args):
        as_str = _argument(spec, module, arg, defined, arg_nr=arg_nr)
        if as_str:
            args.append(as_str)

    s += ', '.join(args) + ') -> None: ...\n'

    pf.write(s)


def _enums(pf, spec, module, defined=None, scope=None, indent=0):
    """ Output the type hints for all the enums in a scope. """

    for enum in spec.enums:
        if enum.module is not module:
            continue

        if enum.scope is not scope:
            continue

        if enum.no_type_hint:
            continue

        _separate(pf, True, indent)

        if enum.py_name is not None:
            enum_type = format_scoped_py_name(enum.scope, enum.py_name.name)

            superclass = 'int'

            if spec.abi_version >= (13, 0):
                if enum.base_type is EnumBaseType.ENUM:
                    superclass = 'enum.Enum'
                elif enum.base_type is EnumBaseType.FLAG:
                    superclass = 'enum.Flag'
                elif enum.base_type in (EnumBaseType.INT_ENUM, EnumBaseType.UNSIGNED_INT_ENUM):
                    superclass = 'enum.IntEnum'
                elif enum.base_type is EnumBaseType.INT_FLAG:
                    superclass = 'enum.IntFlag'

            # Handle an enum with no members.
            for member in enum.members:
                if not member.no_type_hint:
                    trivial = ''
                    break
            else:
                trivial = ' ...'

            s = _indent(indent)
            s+= f'class {enum.py_name.name}({superclass}):{trivial}\n'
            pf.write(s)

            indent += 1
        else:
            enum_type = 'int'

        for member in enum.members:
            if not member.no_type_hint:
                s = _indent(indent)
                s += f'{member.py_name.name} = ... # type: {enum_type}\n'
                pf.write(s)

        if enum.py_name is not None:
            indent -= 1


def _variables(pf, spec, module, defined, scope=None, indent=0):
    """ Output the type hints for all the variables in a scope. """

    first = True

    for variable in spec.variables:
        if variable.module is not module:
            continue

        if variable.scope is not scope:
            continue

        if variable.no_type_hint:
            continue

        py_type = _type(spec, module, variable.type, defined)

        first = _separate(pf, first, indent)

        s = _indent(indent)
        s += f'{variable.py_name.name} = ... # type: {py_type}\n'
        pf.write(s)


def _callable(pf, spec, module, member, overloads, is_method, defined,
        indent=0):
    """ Output the type hints for a callable. """

    # Count the number of overloads.
    nr_overloads = 0

    for overload in overloads:
        if overload.access_specifier is AccessSpecifier.PRIVATE:
            continue

        if overload.common is not member:
            continue

        if overload.no_type_hint:
            continue

        # Signals can have the same name as ordinary methods however
        # 'typing.overload' cannot be used with ClassVar.  We choose to
        # generate a type hint for the signal rather than any method.
        if overload.pyqt_method_specifier is PyQtMethodSpecifier.SIGNAL:
            scope = '' if module.py_name == 'QtCore' else 'QtCore.'

            s = _indent(indent)
            s += f'{overload.common.py_name.name}: typing.ClassVar[{scope}pyqtSignal]\n'
            pf.write(s)

            return

        nr_overloads += 1

    # Handle each overload.
    overload_nr = 0

    for overload in overloads:
        if overload.access_specifier is AccessSpecifier.PRIVATE:
            continue

        if overload.common is not member:
            continue

        if overload.no_type_hint:
            continue

        _overload(pf, spec, module, overload, nr_overloads > 1, overload_nr,
                is_method, defined, indent)

        overload_nr += 1


def _property(pf, spec, module, prop, is_setter, member, overloads, defined,
        indent):
    """ Output the type hints for a property. """

    for overload in overloads:
        if overload.access_specifier is AccessSpecifier.PRIVATE:
            continue

        if overload.common is not member:
            continue

        if overload.no_type_hint:
            continue

        s = _indent(indent)

        if is_setter:
            s += f'@{prop.name.name}.setter\n'
        else:
            s += '@property\n'

        pf.write(s)

        signature = _python_signature(spec, module, overload.py_signature,
                defined)

        s = _indent(indent)
        s += f'def {prop.name.name}{signature}: ...\n'
        pf.write(s)

        break


def _overload(pf, spec, module, overload, overloaded, overload_nr, is_method,
        defined, indent):
    """ Output the type hints for a single overload. """

    # mypy recommends using 'object' as the argument type.
    is_eq_slot = (overload.common.py_slot in (PySlot.EQ, PySlot.NE))

    # The recommendation means any subsequent overloads are pointless.
    if is_eq_slot:
        if overload_nr > 0:
            return
    elif overloaded:
        pf.write(_indent(indent) + '@typing.overload\n')

    if is_method and overload.is_static:
        pf.write(_indent(indent) + '@staticmethod\n')

    s = _indent(indent)

    if is_eq_slot:
        signature = '(self, other: object)'
    else:
        need_self = (is_method and not overload.is_static)

        signature = _python_signature(spec, module, overload.py_signature,
                defined, need_self=need_self)

    s += f'def {overload.common.py_name.name}{signature}: ...\n'

    pf.write(s)


def _python_signature(spec, module, signature, defined, need_self=True):
    """ Return a Python signature. """

    # Handle the input values.
    in_args = []

    if need_self:
        in_args.append('self')

    nr_out = 0

    for arg_nr, arg in enumerate(signature.args):
        if arg.is_out:
            nr_out += 1

        if arg.is_in:
            as_str = _argument(spec, module, arg, defined, arg_nr=arg_nr)
            if as_str:
                in_args.append(as_str)

    in_args = ', '.join(in_args)

    # Handle the output values.
    result = signature.result

    if result.type is ArgumentType.VOID and len(result.derefs) == 0:
        is_result = False
    else:
        type_hints = result.type_hints

        # An empty type hint specifies a void return.
        if type_hints is not None and type_hints.hint_out is not None and type_hints.hint_out == '':
            is_result = False
        else:
            is_result = True

    if is_result or nr_out > 0:
        results_s = ''

        needs_tuple = ((is_result and nr_out > 0) or nr_out > 1)

        if needs_tuple:
            results_s += 'typing.Tuple['

        out_args = []

        if is_result:
            as_str = _argument(spec, module, result, defined)
            if as_str:
                out_args.append(as_str)

        for arg in signature.args:
            if arg.is_out:
                as_str = _argument(spec, module, arg, defined)
                if as_str:
                    out_args.append(as_str)

        results_s += ', '.join(out_args)

        if needs_tuple:
            results_s += ']'
    else:
        results_s = 'None'

    return f'({in_args}) -> {results_s}'


def _argument(spec, module, arg, defined, arg_nr=-1):
    """ Return a Python argument. """

    if arg.array is ArrayArgument.ARRAY_SIZE:
        return None

    optional = (arg_nr >= 0 and arg.default_value is not None)
    use_optional = False

    s = ''

    if arg_nr >= 0 and arg.type is not ArgumentType.ELLIPSIS:
        if arg.name is None:
            s += f'a{arg_nr}: '
        else:
            name = _fix_py_keyword(arg.name.name)
            s += f'{name}: '

    if optional:
        # Assume pointers can be None unless specified otherwise.
        if arg.allow_none or (not arg.disallow_none and arg.derefs):
            s += 'typing.Optional['
            use_optional = True

    if arg.array is ArrayArgument.ARRAY:
        s += spec.sip_module + '.array['

    s += _type(spec, module, arg, defined, out=(arg_nr < 0))

    if arg_nr >= 0 and arg.type is ArgumentType.ELLIPSIS:
        if arg.name is None:
            s += f'a{arg_nr}'
        else:
            s += _fix_py_keyword(arg.name.name)

    if arg.array is ArrayArgument.ARRAY:
        s += ']'

    if optional:
        if use_optional:
            s += ']'

        s += ' = ...'

    return s


def _type(spec, module, arg, defined, out=False):
    """ Return the type hint of a type. """

    return ArgumentFormatter(spec, arg).as_type_hint(module, out, defined)


def _indent(indent):
    """ Return the required indentation. """

    return ' ' * (4 * indent)


def _separate(pf, first, indent=0):
    """ Output a newline if not already done. """

    if first:
        pf.write('\n' if indent else '\n\n')

    return False


_PYTHON_KEYWORDS = (
    'False', 'None', 'True', 'and', 'as', 'assert', 'break', 'class',
    'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for',
    'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not',
    'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield',
    'exec', 'print',
)

def _fix_py_keyword(word):
    """ Return a fixed word if it is a Python keyword (or has been at any
    time).
    """

    if word in _PYTHON_KEYWORDS:
        word += '_'

    return word
