"""Utilities for mypy.stubgen, mypy.stubgenc, and mypy.stubdoc modules."""

import sys
import os.path
import inspect
import json
import pkgutil
import importlib
import subprocess
from types import ModuleType
from contextlib import contextmanager

from typing import Optional, Tuple, List, IO, Iterator


class CantImport(Exception):
    def __init__(self, module: str, message: str):
        self.module = module
        self.message = message


def is_c_module(module: ModuleType) -> bool:
    return ('__file__' not in module.__dict__ or
            os.path.splitext(module.__dict__['__file__'])[-1] in ['.so', '.pyd'])


def write_header(file: IO[str], module_name: Optional[str] = None,
                 pyversion: Tuple[int, int] = (3, 5)) -> None:
    """Write a header to file indicating this file is auto-generated by stubgen."""
    if module_name:
        file.write('# Stubs for %s (Python %s)\n' % (module_name, pyversion[0]))
    file.write(
        '#\n'
        '# NOTE: This dynamically typed stub was automatically generated by stubgen.\n\n')


def default_py2_interpreter() -> str:
    """Find a system Python 2 interpreter.

    Return full path or exit if failed.
    """
    # TODO: Make this do something reasonable in Windows.
    for candidate in ('/usr/bin/python2', '/usr/bin/python'):
        if not os.path.exists(candidate):
            continue
        output = subprocess.check_output([candidate, '--version'],
                                         stderr=subprocess.STDOUT).strip()
        if b'Python 2' in output:
            return candidate
    raise SystemExit("Can't find a Python 2 interpreter -- "
                     "please use the --python-executable option")


def walk_packages(packages: List[str]) -> Iterator[str]:
    """Iterates through all packages and sub-packages in the given list.

    This uses runtime imports to find both Python and C modules. For Python packages
    we simply pass the __path__ attribute to pkgutil.walk_packages() to get the content
    of the package (all subpackages and modules).  However, packages in C extensions
    do not have this attribute, so we have to roll out our own logic: recursively find
    all modules imported in the package that have matching names.
    """
    for package_name in packages:
        try:
            package = importlib.import_module(package_name)
        except Exception:
            report_missing(package_name)
            continue
        yield package.__name__
        # get the path of the object (needed by pkgutil)
        path = getattr(package, '__path__', None)
        if path is None:
            # Object has no path; this means it's either a module inside a package
            # (and thus no sub-packages), or it could be a C extension package.
            if is_c_module(package):
                # This is a C extension module, now get the list of all sub-packages
                # using the inspect module
                subpackages = [package.__name__ + "." + name
                               for name, val in inspect.getmembers(package)
                               if inspect.ismodule(val)
                               and val.__name__ == package.__name__ + "." + name]
                # Recursively iterate through the subpackages
                for submodule in walk_packages(subpackages):
                    yield submodule
            # It's a module inside a package.  There's nothing else to walk/yield.
        else:
            all_packages = pkgutil.walk_packages(path, prefix=package.__name__ + ".",
                                                 onerror=lambda r: None)
            for importer, qualified_name, ispkg in all_packages:
                yield qualified_name


def find_module_path_and_all_py2(module: str,
                                 interpreter: str) -> Optional[Tuple[str,
                                                                     Optional[List[str]]]]:
    """Return tuple (module path, module __all__) for a Python 2 module.

    The path refers to the .py/.py[co] file. The second tuple item is
    None if the module doesn't define __all__.

    Raise CantImport if the module can't be imported, or exit if it's a C extension module.
    """
    cmd_template = '{interpreter} -c "%s"'.format(interpreter=interpreter)
    code = ("import importlib, json; mod = importlib.import_module('%s'); "
            "print(mod.__file__); print(json.dumps(getattr(mod, '__all__', None)))") % module
    try:
        output_bytes = subprocess.check_output(cmd_template % code, shell=True)
    except subprocess.CalledProcessError as e:
        raise CantImport(module, str(e))
    output = output_bytes.decode('ascii').strip().splitlines()
    module_path = output[0]
    if not module_path.endswith(('.py', '.pyc', '.pyo')):
        raise SystemExit('%s looks like a C module; they are not supported for Python 2' %
                         module)
    if module_path.endswith(('.pyc', '.pyo')):
        module_path = module_path[:-1]
    module_all = json.loads(output[1])
    return module_path, module_all


def find_module_path_and_all_py3(module: str) -> Optional[Tuple[str, Optional[List[str]]]]:
    """Find module and determine __all__ for a Python 3 module.

    Return None if the module is a C module. Return (module_path, __all__) if
    it is a Python module. Raise CantImport if import failed.
    """
    # TODO: Support custom interpreters.
    try:
        mod = importlib.import_module(module)
    except Exception as e:
        raise CantImport(module, str(e))
    if is_c_module(mod):
        return None
    module_all = getattr(mod, '__all__', None)
    if module_all is not None:
        module_all = list(module_all)
    return mod.__file__, module_all


@contextmanager
def generate_guarded(mod: str, target: str,
                     ignore_errors: bool = True, quiet: bool = False) -> Iterator[None]:
    """Ignore or report errors during stub generation.

    Optionally report success.
    """
    try:
        yield
    except Exception as e:
        if not ignore_errors:
            raise e
        else:
            # --ignore-errors was passed
            print("Stub generation failed for", mod, file=sys.stderr)
    else:
        if not quiet:
            print('Created %s' % target)


def report_missing(mod: str, message: Optional[str] = '') -> None:
    if message:
        message = ' with error: ' + message
    print('Failed to import {}{}; skipping it'.format(mod, message))


def fail_missing(mod: str) -> None:
    raise SystemExit("Can't find module '{}' (consider using --search-path)".format(mod))
