#!python
# -*- coding: utf-8 -*-
"""Bootstrapper for Python programs powered by MacroPy3.

**This bootstrapper is moving!**

Starting in `unpythonic` v0.14.1, it is distributed in `imacropy`:

https://github.com/Technologicat/imacropy
https://pypi.org/project/imacropy/

which is its new, permanent home. The bootstrapper will be removed from
`unpythonic` starting in v0.15.0. The reason is this is a general add-on
for MacroPy, not specific to `unpythonic`.
"""

from importlib import import_module
from importlib.util import resolve_name
from types import ModuleType
import os
import sys
import argparse
import warnings

try:  # Python 3.5+
    from importlib.util import module_from_spec as stdlib_module_from_spec
except ImportError:
    stdlib_module_from_spec = None

try:  # Python 3.6+
    MyModuleNotFoundError = ModuleNotFoundError
except NameError:
    MyModuleNotFoundError = ImportError

import macropy.activate

__version__ = '1.4.0'

def module_from_spec(spec):
    """Compatibility wrapper.

    Call ``importlib.util.module_from_spec`` if available (Python 3.5+),
    otherwise approximate it manually (Python 3.4).
    """
    if stdlib_module_from_spec:
        return stdlib_module_from_spec(spec)
    loader = spec.loader
    module = None
    if loader is not None and hasattr(loader, "create_module"):
        module = loader.create_module(spec)
    if module is None:
        module = ModuleType(spec.name)
        module.__loader__ = loader
        module.__spec__ = spec
        module.__file__ = spec.origin
        module.__cached__ = spec.cached
        # module.__package__ and module.__path__ are filled later
    return module

def import_module_as_main(name, script_mode):
    """Import a module, pretending it's __main__.

    Upon success, replaces ``sys.modules["__main__"]`` with the module that
    was imported. Upon failure, propagates any exception raised.

    This is a customized approximation of the standard import semantics, based on:

        https://docs.python.org/3/library/importlib.html#approximating-importlib-import-module
        https://docs.python.org/3/reference/import.html#loading
        https://docs.python.org/3/reference/import.html#import-related-module-attributes
        https://docs.python.org/3/reference/import.html#module-path
        https://docs.python.org/3/reference/import.html#special-considerations-for-main
    """
    # We perform only the user-specified import ourselves; that we must, in order to
    # load it as "__main__". We delegate all the rest to the stdlib import machinery.

    absolute_name = resolve_name(name, package=None)
    # Normally we should return the module from sys.modules if already loaded,
    # but the __main__ in sys.modules is this bootstrapper program, not the user
    # __main__ we're loading. So pretend whatever we're loading isn't loaded yet.
    #
    # Note Python treats __main__ and somemod as distinct modules, even when it's
    # the same file, because __main__ triggers "if __name__ == '__main__':" checks
    # whereas somemod doesn't.
#    try:
#        return sys.modules[absolute_name]
#    except KeyError:
#        pass

    if "" not in sys.path:  # Python 3.6 seems to have removed the special entry "" (the cwd) from sys.path
        # Placing it first overrides installed unpythonic with the local one when running tests
        # (the installed one won't have the "test" submodules).
        sys.path.insert(0, "")

    # path should be folder containing something.py if we are being run as "pydialect something.py" (script_mode=True), and cwd if run as "pydialect -m something"
    path = None
    if '.' in absolute_name:
        parent_name, _, child_name = absolute_name.rpartition('.')
        if not script_mode:
            parent_module = import_module(parent_name)
            path = parent_module.__spec__.submodule_search_locations
        else:  # HACK: try to approximate what "python3 some/path/to/script.py" does
            cwd = os.getcwd()
            path_components = parent_name.split('.')
            path = [os.path.join(*([cwd] + path_components))]
            absolute_name = child_name

    for finder in sys.meta_path:
        if not hasattr(finder, "find_spec"):  # Python 3.6: pkg_resources.extern.VendorImporter has no find_spec
            continue
        spec = finder.find_spec(absolute_name, path)
        if spec is not None:
            break
    else:
        msg = 'No module named {}'.format(absolute_name)
        raise MyModuleNotFoundError(msg, name=absolute_name)

    spec.name = "__main__"
    if spec.loader:
        spec.loader.name = "__main__"  # fool importlib._bootstrap.check_name_wrapper

    # TODO: support old-style loaders that have load_module (no create_module, exec_module)?
    module = module_from_spec(spec)
    try_mainpy = False
    if script_mode:  # including "macropy3 somemod/__init__.py"
        module.__package__ = ""
    elif path:
        module.__package__ = parent_name
    elif spec.origin.endswith("__init__.py"):  # e.g. "macropy3 -m unpythonic"
        module.__package__ = absolute_name
        try_mainpy = True

    # TODO: is this sufficient? Any other cases where we directly handle a package?
    if spec.origin == "namespace":
        module.__path__ = spec.submodule_search_locations

    if try_mainpy:
        # e.g. "import unpythonic" in the above case; it's not the one running as main, so import it normally
        if not script_mode:
            parent_module = import_module(absolute_name)
    elif spec.loader is not None:  # namespace packages have loader=None
        # There's already a __main__ in sys.modules, so the most logical thing
        # to do is to switch it **after** a successful import, not before import
        # as for usual imports (where the ordering prevents infinite recursion
        # and multiple loading).
        spec.loader.exec_module(module)
        sys.modules["__main__"] = module
#        # __main__ has no parent module.
#        if path is not None:
#            setattr(parent_module, child_name, module)
    else:  # namespace package
        try_mainpy = True

    if try_mainpy:  # __init__.py (if any) has run; now run __main__.py, like "python3 -m mypackage" does
        has_mainpy = True
        try:
            # __main__.py doesn't need the name "__main__" so we can just import it normally
            import_module("{}.__main__".format(absolute_name))
        except ImportError as e:
            if "No module named" in e.msg:
                has_mainpy = False
            else:
                raise
        if not has_mainpy:
            raise ImportError("No module named {}.__main__; '{}' is a package and cannot be directly executed".format(absolute_name, absolute_name))

    return module

def main():
    """Handle command-line arguments and run the specified main program."""
    parser = argparse.ArgumentParser(description="""Run a Python program with MacroPy3 enabled.""",
                                     formatter_class=argparse.RawDescriptionHelpFormatter)

    parser.add_argument('-v', '--version', action='version', version=('%(prog)s-bootstrapper ' + __version__))
    parser.add_argument(dest='filename', nargs='?', default=None, type=str, metavar='file',
                        help='script to run')
    parser.add_argument('-m', '--module', dest='module', default=None, type=str, metavar='mod',
                        help='run library module as a script (like python3 -m mod)')
    parser.add_argument('-d', '--debug', dest='debug', action="store_true", default=False,
                        help='enable MacroPy logging')
    opts = parser.parse_args()

    if not opts.filename and not opts.module:
        parser.print_help()
        sys.exit(0)
        raise ValueError("Please specify the program to run (either filename or -m module).")
    if opts.filename and opts.module:
        raise ValueError("Please specify just one program to run (either filename or -m module, not both).")

    if opts.debug:
        import macropy.logging

    # Import the module, pretending its name is "__main__".
    #
    # We must import so that macros get expanded, so we can't use
    # runpy.run_module here (which just execs without importing).
    if opts.filename:
        # like "python3 foo/bar.py", we don't initialize any parent packages.
        if not os.path.isfile(opts.filename):
            raise FileNotFoundError("Can't open file '{}'".format(opts.filename))
        # This finds the wrong (standard) loader for macro-enabled scripts...
#        spec = spec_from_file_location("__main__", opts.filename)
#        if not spec:
#            raise ImportError("Not a Python module: '{}'".format(opts.filename))
#        module = module_from_spec(spec)
#        spec.loader.exec_module(module)
        # FIXME: wild guess (see Pyan3 for a better guess?)
        module_name = opts.filename.replace(os.path.sep, '.')
        if module_name.endswith(".__init__.py"):
            module_name = module_name[:-12]
        elif module_name.endswith(".py"):
            module_name = module_name[:-3]
        import_module_as_main(module_name, script_mode=True)
    else:  # opts.module
        # like "python3 -m foo.bar", we initialize parent packages.
        import_module_as_main(opts.module, script_mode=False)

if __name__ == '__main__':
    warnings.warn("The macropy3 bootstrapper has moved to PyPI package 'imacropy', and will be removed from 'unpythonic' in 0.15.0.", FutureWarning)
    main()
