# SPDX-FileCopyrightText: 2009-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later

# Copyright (c) 2009 Fernando Perez, https://www.stani.be

# Original copyright (see doc-string):
# ****************************************************************************
#       Copyright (C) 2001-2006 Fernando Perez <fperez@colorado.edu>
#
#  Distributed under the terms of the BSD License.  The full license is in
#  the file COPYING, distributed as part of this software.
# ****************************************************************************

"""Completer for import statements

Original code was from IPython/Extensions/ipy_completers.py. The following
changes have been made:
- ported to python3
- pep8 polishing
- limit list of modules to prefix in case of "from w"
- sorted modules
- added sphinx documentation
- complete() returns a blank list of the module isn't found
"""


import os
import sys

TIMEOUT_STORAGE = 3  # Time in secs after which the root-modules will be stored
TIMEOUT_GIVEUP = 20  # Time in secs after which we give up

ROOT_MODULES = None


def get_root_modules():
    """
    Returns a list containing the names of all the modules available in the
    folders of the python-path.

    :returns: modules
    :rtype: list[ModuleType]
    """
    global ROOT_MODULES
    modules = []
    if not (ROOT_MODULES is None):
        return ROOT_MODULES
    from time import time
    t = time()
    store = False
    for path in sys.path:
        modules += module_list(path)
        if time() - t >= TIMEOUT_STORAGE and not store:
            # Caching the list of root modules, please wait!
            store = True
        if time() - t > TIMEOUT_GIVEUP:
            # This is taking too long, we give up.
            ROOT_MODULES = []
            return []

    modules += sys.builtin_module_names

    # needed for modules defined in C
    modules += sys.modules.keys()

    modules = set(modules)
    modules.discard("__init__")
    modules = sorted(list(modules))
    if store:
        ROOT_MODULES = modules
    return modules


def module_list(path):
    """
    Return the list containing the names of the modules available in
    the given folder.

    :arg path: folder path
    :type path: str
    :returns: modules
    :rtype: list[ModuleType]
    """

    if os.path.isdir(path):
        folder_list = os.listdir(path)
    elif path.endswith('.egg'):
        from zipimport import zipimporter
        try:
            folder_list = [f for f in zipimporter(path)._files]
        except:
            folder_list = []
    else:
        folder_list = []
    # folder_list = glob.glob(os.path.join(path,'*'))
    folder_list = [
        p for p in folder_list
        if (os.path.exists(os.path.join(path, p, '__init__.py')) or
            p[-3:] in {'.py', '.so'} or
            p[-4:] in {'.pyc', '.pyo', '.pyd'})]

    folder_list = [os.path.basename(p).split('.')[0] for p in folder_list]
    return folder_list


def complete(line):
    """
    Returns a list containing the completion possibilities for an import line.

    :arg line:

        incomplete line which contains an import statement::

            import xml.d
            from xml.dom import

    :type line: str
    :returns: list of completion possibilities
    :rtype: list[str]

    >>> complete('import weak')
    ['weakref']
    >>> complete('from weakref import C')
    ['CallableProxyType']
    """
    import inspect

    def try_import(mod, *, only_modules=False):

        def is_importable(module, attr):
            if only_modules:
                return inspect.ismodule(getattr(module, attr))
            else:
                return not (attr[:2] == '__' and attr[-2:] == '__')

        try:
            m = __import__(mod)
        except:
            return []
        mods = mod.split('.')
        for module in mods[1:]:
            m = getattr(m, module)
        if (not hasattr(m, '__file__')) or (not only_modules) or\
           (hasattr(m, '__file__') and '__init__' in m.__file__):
            completion_list = [attr for attr in dir(m)
                               if is_importable(m, attr)]
        else:
            completion_list = []
        completion_list.extend(getattr(m, '__all__', []))
        if hasattr(m, '__file__') and '__init__' in m.__file__:
            completion_list.extend(module_list(os.path.dirname(m.__file__)))
        completion_list = list(set(completion_list))
        if '__init__' in completion_list:
            completion_list.remove('__init__')
        return completion_list

    def filter_prefix(names, prefix):
        return [name for name in names if name.startswith(prefix)]

    words = line.split(' ')
    if len(words) == 3 and words[0] == 'from':
        return ['import ']
    if len(words) < 3 and (words[0] in {'import', 'from'}):
        if len(words) == 1:
            return get_root_modules()
        mod = words[1].split('.')
        if len(mod) < 2:
            return filter_prefix(get_root_modules(), words[-1])
        completion_list = try_import('.'.join(mod[:-1]), only_modules=True)
        completion_list = ['.'.join(mod[:-1] + [el]) for el in completion_list]
        return filter_prefix(completion_list, words[-1])
    if len(words) >= 3 and words[0] == 'from':
        mod = words[1]
        return filter_prefix(try_import(mod), words[-1])

    # get here if the import is not found
    # import invalid_module
    #                       ^, in this case return nothing
    return []
