
import os
import sys
import subprocess
import signal
import re
import filesystem

if sys.platform == 'win32':
    import ctypes


def get_drives():
    """
    Windows only function.
    ----------------------
    Return available drives.
    """
    # First we do this with maxlen = 1 to figure out how long string we need.
    i = ctypes.c_int(0)
    v = ctypes.create_string_buffer('\000')
    retval = ctypes.windll.kernel32.GetLogicalDriveStringsW(i, v)
    if retval == 0:
        raise Exception("get_drives failed. Retval==0")
    # GetLogicalDriveStringsW will return the number of chars needed.
    # So now we get the real data.
    maxlen = retval
    i = ctypes.c_int(maxlen - 1)
    v = ctypes.create_string_buffer('\000' * maxlen)
    retval = ctypes.windll.kernel32.GetLogicalDriveStringsA(i, v)
    return [x for x in v.raw.split("\000") if x]


class OsUtilsException(Exception):
    pass

class RunningExecutableFailed(OsUtilsException):
    def __init__(self, cmd):
        OsUtilsException.__init__(self,
          _("Running the command %s failed.") % cmd)

class ExecutableDoesNotExist(OsUtilsException):
    """
    We should raise this exception if os.system fails and returns
    anything else than 0.
    """
    def __init__(self, cmd):
        OsUtilsException.__init__(self,
          _("The executable '%s' does not exist.") % cmd)

class ExecutableFormatStringException(OsUtilsException):
    def __init__(self):
        OsUtilsException.__init__(self,
          _("The executable string should contain zero or one '%s'."))

class BinaryBaseException(OsUtilsException):
    def __init__(self, binary, exception):
        OsUtilsException.__init__(self)
        self.msg2 = _("Tried `%(bin)s`:\n\n\t%(exception)s\n\n"
            "Please check that the program is installed. If you did not supply"
            "the full path to the program in the preferences window (Ctrl-12),"
            "you must make sure the program is on your PATH.") % {
             'bin': str(binary).decode(sys.getfilesystemencoding(), 'replace'),
             'exception': str(exception).decode(
                 sys.getfilesystemencoding(), 'replace')}

class BinaryForProgramException(BinaryBaseException):
    def __init__(self, program_name, binary, exception):
        BinaryBaseException.__init__(self, binary, exception)
        self.msg1 = _("Failed to execute a binary for %s") % program_name

class BinaryForMediaPlayerException(BinaryBaseException):
    def __init__(self, typeid, binary, exception):
        BinaryBaseException.__init__(self, binary, exception)
        self.msg1 = _("Failed to execute media player for %s files") % typeid.upper()

class BinaryForMediaConvertorException(BinaryBaseException):
    r = re.compile("app/(?P<from>[a-z0-9]+)_to_(?P<to>[a-z0-9]+)_cmd")
    def __init__(self, varname, binary, exception):
        BinaryBaseException.__init__(self, binary, exception)
        m = self.r.match(varname)
        self.msg1 = _("Failed to execute binary to convert from %(from)s to %(to)s") % {
            'to': m.group('to').upper(),
            'from': m.group('from').upper()
        }


__all__ = ['run_external_program',
    'OsUtilsException',
    'ExecutableDoesNotExist',
    'ExecutableFormatStringException',
    'BinaryForProgramException',
]
__child_pid = None

def kill_external_program():
    """
    Kill the previous ran program if it is still running.
    Do nothing if there is nothing to kill.
    """
    global __child_pid
    if sys.platform != 'win32':
        if __child_pid:
            try:
                os.kill(__child_pid, signal.SIGKILL)
            except OSError:
                pass
            os.wait()
            __child_pid = None

def run_external_program(cmdstring, wdir, argument):
    global __child_pid
    if not cmdstring:
        raise ExecutableDoesNotExist(cmdstring)
    if "%s" not in cmdstring:
        cmdline = "%s %s" % (cmdstring, argument)
    else:
        try:
            cmdline = cmdstring % argument
        except TypeError:
            raise ExecutableFormatStringException()
    wdir = os.path.normpath(wdir)
    cur_dir = os.getcwdu()
    v = cmdline.split()
    v = v[:1] + v
    if not os.path.exists(os.path.join(wdir, v[0])):
        raise ExecutableDoesNotExist(v[0])
    if sys.platform == 'win32':
        os.chdir(wdir)
        os.system(cmdline)
        os.chdir(cur_dir)
        return
    kill_external_program()
    pid = os.fork()
    if pid == 0:
        os.chdir(wdir)
        try:
            os.execlp(*v)
        except OSError, x:
            print >> sys.stderr, "OS Error:", x
            os._exit(-1)
            os.chdir(cur_dir)
        os.chdir(cur_dir)
    else:
        __child_pid = pid

if sys.version_info < (2, 6):
    # PY26
    class Popen(subprocess.Popen):
        """
        We define this class because Popen.kill was added in Python 2.6,
        and we want to run with 2.5 too.
        """
        def __init__(self, *args, **kwargs):
            subprocess.Popen.__init__(self, *args, **kwargs)
        def kill(self):
            if sys.platform == 'win32':
                # http://code.activestate.com/recipes/347462/
                PROCESS_TERMINATE = 1
                handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, False, self.pid)
                ctypes.windll.kernel32.TerminateProcess(handle, -1)
                ctypes.windll.kernel32.CloseHandle(handle)
            else:
                os.kill(self.pid, signal.SIGKILL)
                self.wait()
else:
    Popen = subprocess.Popen

def find_progs(execs):
    """
    Return a list of full path names to the executables named in execs
    if they are found on the path. Avoid duplicates caused by the same
    directory listed more than once in the PATH variable.

    execs - a tuple of possible executable names
    """
    assert isinstance(execs, (list, tuple,))
    retval = set()
    for p in os.environ['PATH'].split(os.pathsep):
        for e in execs:
            ex = os.path.join(p, e)
            if os.path.isfile(ex):
                retval.add(ex)
    return retval


def find_mma_executables(ignore_drives):
    """
    Return a set of command lines that we think will run MMA.
    ignore_drives is only used on win32. It is ignored on other operating
    systems.
    """
    if sys.platform != 'win32':
        return find_progs(("mma",))
    else:
        retval = set()
        for drive in get_drives():
            if drive.upper() in ignore_drives:
                continue
            try:
                for dirname in os.listdir(drive):
                    if dirname.lower().startswith('mma'):
                        dir = os.path.join(drive, dirname)
                        mma_py = os.path.join(drive, dirname, "mma.py")
                        if os.path.exists(mma_py):
                            retval.add(mma_py)
                        mma_bat = os.path.join(drive, dirname, "mma.bat")
                        if os.path.exists(mma_bat):
                            retval.add(mma_bat)
            except WindowsError:
                # People have had this exception raised:
                # WindowsError: [Error 21] The device is not ready: 'X:\\*.*'
                # So we choose to continue with the next drive if anything
                # unexcepted happens. 
                continue
    return retval

def find_csound_executables():
    """
    Return a list of possible csound executables.
    Currently we return the name of the csound binary found as
    %PROGRAMFILES%\csound\bin\csound.exe
    """
    retval = find_progs(('csound.exe', 'csound'))
    if sys.platform == 'win32':
        fn = os.path.join(filesystem.win32_program_files_folder(),
            "csound", "bin", "csound.exe")
        if os.path.exists(fn):
            retval.add(fn)
    return retval

