"""
Funtions:
    1. Update `sys.path`.
    2. Locate and check out target module.
    3. Passing args and kwargs to target function.

Placeholders:
    PROJ_LIB_DIR        dir
    ADD_PYWIN32_SUPPORT bool[False]
    MODULE_PATHS        list[dir]
    *DEFAULT_CONF*      file[*.pkl]
        TARGET_DIR      dir
        TARGET_PKG      str
        TARGET_MOD      str
        TARGET_FUNC     str
        TARGET_ARGS     list[val]
        TARGET_KWARGS   dict[var, val]
"""
import os
import sys
import traceback

from os.path import abspath

# add current dir to `sys.path` (this is necessary for embed python)
os.chdir(os.path.dirname(__file__))
sys.path.insert(0, abspath('.'))

# add project lib to environment
sys.path.insert(1, abspath('{PROJ_LIB_DIR}'))

module_paths = {MODULE_PATHS}
if module_paths:
    sys.path.extend(map(abspath, module_paths))

add_pywin32_support = {ADD_PYWIN32_SUPPORT}
if add_pywin32_support:
    try:
        # ref: https://stackoverflow.com/questions/58612306/how-to-fix
        #      -importerror-dll-load-failed-while-importing-win32api
        #      (see comment from @ocroquette)
        import pywin32_system32  # noqa
        _pywin32_dir = pywin32_system32.__path__._path[0]  # noqa
        # -> ~/lib/site-packages/pywin32_system32
        os.add_dll_directory(_pywin32_dir)
        #   FIXME: this function is introduced since python 3.8.

        _site_packages_dir = os.path.dirname(_pywin32_dir)
        sys.path.extend((
            _site_packages_dir + '/pythonwin',
            _site_packages_dir + '/win32',
            _site_packages_dir + '/win32/lib',
        ))
    except ImportError:
        print('Adding pywin32 support failed')


# ------------------------------------------------------------------------------

def _parse_target_conf(args):
    if len(args) == 0:
        raise Exception('sys.argv cannot be empty!')
    elif len(args) == 1:  # auto repair
        # note: if we launch directly from `python pylauncher.py`, the
        #   pylauncher config file is omitted. we will auto repair it.
        conf_file = '.pylauncher_conf/__main__.pkl'
    else:
        if args[1].startswith('.pylauncher_conf/'):
            conf_file = args.pop(1)
        else:
            print('[pylauncher][warning]',
                  'pylauncher config file not provided, will use default '
                  'config path')
            conf_file = '.pylauncher_conf/__main__.pkl'

    assert conf_file.endswith(('.json', '.pkl')), (
        'Unknown file type to load config!', conf_file
    )

    if conf_file.endswith('.json'):
        from json import load
        with open(conf_file, 'r', encoding='utf-8') as f:
            return load(f)
    else:
        from pickle import load
        with open(conf_file, 'rb') as f:
            return load(f)


def launch(conf):
    try:
        # check in target dir to make sure all sequent relative paths
        # references are based on the target dir.
        os.chdir(conf['TARGET_DIR'])
        sys.path.append(abspath('.'))

        from importlib import import_module
        # target name is a filename without suffix
        module = import_module(conf['TARGET_NAME'], '')
        main = getattr(module, conf['TARGET_FUNC'], None)
        if main is not None:
            main(*conf['TARGET_ARGS'], **conf['TARGET_KWARGS'])

    except Exception:
        _show_error_info(traceback.format_exc())
        input('Press enter to leave...')
    sys.exit()


def _show_error_info(err_msg, title='Runtime Exception', terminal='console'):
    """
    Args:
        err_msg: suggest passing `traceback.format_exc()`.
        title:
        terminal:

    Rerferences:
        https://stackoverflow.com/questions/1278705/when-i-catch-an-exception
            -how-do-i-get-the-type-file-and-line-number
        https://stackoverflow.com/questions/17280637/tkinter-messagebox-without
            -window
        https://www.cnblogs.com/freeweb/p/5048833.html
    """
    if terminal == 'console':
        print(title + ':', err_msg)
    elif terminal == 'tkinter':
        from tkinter import Tk, messagebox
        root = Tk()
        root.withdraw()
        messagebox.showerror(title=title, message=err_msg)
    elif terminal == 'vbsbox':
        return os.popen(
            'echo msgbox "{{msg}}", 64, "{{title}}" > '
            'alert.vbs && start '
            'alert.vbs && ping -n 2 127.1 > '
            'nul && del alert.vbs'.format(title=title, msg=err_msg)
        ).read()


if __name__ == '__main__':
    try:
        conf = _parse_target_conf(sys.argv)
        # print(':l', conf)
        launch(conf)
    except Exception:
        _show_error_info(traceback.format_exc())
        input('Press enter to leave...')
