#!/usr/bin/env python2

"""
pith tool

A simple command-line tool to execute Python while taking care of the PYTHONPATH.

See https://github.com/weegreenblobbie/pith-tool
"""

__author__ = "Nick Hilton"
__copyright__ = "Copyright 2016 - Present, Nick Hilton et al."
__license__ = "The MIT License (MIT)"
__version__ = "0.3"


try:
    import ConfigParser as configparser
except ImportError:
    import configparser

import glob
import os
import platform
import subprocess
import sys


def main():

    run_dir = os.getcwd()

    #--------------------------------------------------------------------------
    # search for .pithrc

    pith_rc = scan_for_pithrc()

    if pith_rc is None:
        sys.stderr.write(
            "Could not find .pithrc, please create one with contents:\n%s\n" % (
                PITH_RC_TEMPLATE
            )
        )

        raise RuntimeError('Could not find .pithrc')

    root_dir = os.path.dirname(pith_rc)

    os.chdir(root_dir)

    #--------------------------------------------------------------------------
    # parse .pithrc

    # defaults

    echo = True
    path_str = ''
    py_exe = 'python'
    test_prefix = 'test'
    verbose = True
    backtrace = False

    config = read_pithrc(pith_rc)

    try:
        backtrace = config.getboolean('pith', 'backtrace')
    except configparser.NoOptionError:
        pass

    try:
        echo = config.getboolean('pith', 'echo')
    except configparser.NoOptionError:
        pass

    try:
        path_str = config.get('pith', 'pythonpath')
    except configparser.NoOptionError:
        pass

    try:
        py_exe = config.get('pith', 'interpreter')
    except configparser.NoOptionError:
        pass

    try:
        test_prefix = config.get('pith', 'test_prefix').strip()
    except configparser.NoOptionError:
        pass

    try:
        verbose = config.getboolean('pith', 'verbose')
    except configparser.NoOptionError:
        pass

    python_path = parse_python_path(root_dir, path_str)

    if verbose:
        print_settings(root_dir, py_exe, python_path)

    #--------------------------------------------------------------------------
    # handle no arguments, dump settings and exit

    if len(sys.argv) == 1:

        # nothing to do, report settings (if not already reported) and exit

        print("pith-" + __version__)

        if not verbose:
            print_settings(root_dir, py_exe, python_path)

        print("Nothing to do!")

        sys.exit(1)

    #--------------------------------------------------------------------------
    # setup PYTHONPATH in env

    path_sep = ':'

    if 'win32' in platform.system():
        path_sep = ';'

    ppaths = path_sep.join(python_path)

    env = os.environ.copy()
    env['PYTHONPATH'] = ppaths

    #--------------------------------------------------------------------------
    # get command line arguments

    args, paths = make_py_commnad(root_dir, run_dir, test_prefix)

    cmd = [py_exe]
    cmd.extend(args)

    #--------------------------------------------------------------------------
    # python3 doesn't require __init__.py files to be everywhere
    # python2 does.

    ver = subprocess.check_output(
        [py_exe, '--version'], stderr=subprocess.STDOUT)

    ver = ver.decode('ascii')

    is_py2 = 'Python 2.' in ver

    init_files = []

    if is_py2:
        init_files = make_init_py_files(paths, verbose)

    #--------------------------------------------------------------------------
    # finally, execute python

    # show the command unless echo is disabled

    if echo:

        if not verbose:
            print('Entering directory: %s' % root_dir)

        print(" ".join(cmd))

    error = None

    try:
        subprocess.check_call(cmd, env = env)
    except Exception as e:
        error = e

    if is_py2:
        cleanup_init_file(init_files, verbose)

    if error:

        if backtrace:
            raise

        else:
            sys.exit(error.returncode)



def scan_for_pithrc():

    cwd = os.path.abspath(os.getcwd())

    last_cwd = cwd

    while True:

        pith_rc = os.path.join(cwd, '.pithrc')

        if os.path.isfile(pith_rc):
            return pith_rc

        cwd = os.path.dirname(cwd)

        # no more path?

        if cwd == last_cwd:
            break

        last_cwd = cwd

    return None


def read_pithrc(pith_rc):

    config = configparser.SafeConfigParser()
    config.read(pith_rc)

    return config


def parse_python_path(root_dir, path_str):
    """
    The incoming python string should be a string seperated by newlines:

        "\n../nsound/nsound_git_ws\n../nsound/nsound_git_ws2"

    This should should become a list of strings of absolute paths.
    """

    paths = path_str.strip().split('\n')

    python_path = [root_dir]

    for p in paths:
        p = p.strip()

        # p may be a relative path, convert to abs

        abs_p = os.path.abspath(p)

        if not os.path.isdir(abs_p):
            raise RuntimeError(
                "While parsing .pithrc pythonpath, could not find path: %s" % p
            )

        python_path.append(abs_p)

    return python_path


def print_settings(root_dir, py_exe, python_path):
    print('Entering directory: %s' % root_dir)
    print('Python interpreter: %s' % py_exe)

    if len(python_path) > 0:
        print('PYTHONPATH:')

    for p in python_path:
        print('    %s' % p)


def make_py_commnad(root_dir, run_dir, test_prefix):
    """
    gobble up sys.argv and transform into propery python module command.
    """

    # run_dir must start with root dir

    if not run_dir.startswith(root_dir):
        raise RuntimeError("oops, what's going on:\n%s\n%s\n" % (
            root_dir,
            run_dir)
        )

    os.chdir(run_dir) # so relative path lookup works

    args = []
    paths = []

#~    print('-' * 80)

    for arg in sys.argv[1:]:

#~        print "arg = ", arg

        # convert any file argument into an absolute path

        is_path = os.path.exists(arg)
        is_dot_py = False
        is_test = False
        is_test = os.path.basename(arg).startswith(test_prefix)

        if is_path or is_test:
            arg = os.path.abspath(arg)

            # is regular .py file?
            is_dot_py = arg.endswith('.py')

        #---------------------------------------------------------------------
        # check if this is a unit test

#~        print "arg = ", arg
#~        print "is_path = ", is_path
#~        print "is_dot_py = ", is_dot_py
#~        print "is_test = ", is_test

        # strip off root dir if possible

        if arg.startswith(root_dir):
            arg = arg[len(root_dir) + 1 :]

#~        print "arg = ", arg

        if is_dot_py or is_test:

            # pull off .py
            if is_dot_py:
                [arg, _] = os.path.splitext(arg)

            mod = arg.replace(os.path.sep, '.')

#~            print "mod = ", mod

            if is_test:
                temp = ['-m', 'unittest']
                temp.extend(args)
                temp.append(mod)
                args = temp
            else:
                args.extend(['-m', mod])

            # save the paths to this script so for python2, temporary
            # __init__.py files can be created

            dirname = os.path.dirname(arg)

            while len(dirname) > 0:
                paths.append(dirname)
                dirname = os.path.dirname(dirname)

        else:

#~            print('not py file or unittest: %s' % arg)

            args.append(arg)

    os.chdir(root_dir)

    return args, paths


def make_init_py_files(paths, verbose):
    """
    For each path, if __init__.py doesn't exist write out a temporary one.
    """

    filelist = []

    for p in paths:

        init = os.path.join(p, '__init__.py')

        if not os.path.isfile(init):

            filelist.append(init)

            if verbose:
                print('Touching %s' % init)

            with open(init, 'w') as fd:
                fd.write('')

    return filelist


def cleanup_init_file(init_files, verbose):

    for init in init_files:

        filelist = glob.glob(init + '*') # pick up .pyc and .pyo extensions

        for f in filelist:

            if verbose:
                print('Cleaning up %s' % f)

            os.remove(f)


#------------------------------------------------------------------------------
# globals

PITH_RC_TEMPLATE = """\
[pithrc]

interpreter = python

pythonpath =

verbose = true

"""



if __name__ == "__main__":
    main()
