# GNU Solfege - free ear training software
# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008  Tom Cato Amundsen
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


from __future__ import absolute_import

import os
import re
import signal
import subprocess
import sys

from solfege 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__ = [
    'OsUtilsException',
    'ExecutableDoesNotExist',
    'ExecutableFormatStringException',
    'BinaryForProgramException',
]


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

class PopenSingleton(object):
    """
    A special version of Popen that will kill the previous program
    started. We use this when running programs using the cmdline lessonfile
    command, and possible other places where we want to run external program
    that should be killed before a new is run.
    """

    class __impl(Popen):
        """ Implementation of the singleton interface """

        def spam(self):
            """ Test method, return singleton id """
            return id(self)

    # storage for the instance reference
    __instance = None

    def __init__(self, *args, **kwargs):
        """ Create singleton instance """
        # Check whether we already have an instance
        if PopenSingleton.__instance is not None:
            PopenSingleton.__instance.kill()
            PopenSingleton.__instance = None
        if PopenSingleton.__instance is None:
            # Create and remember instance
            PopenSingleton.__instance = PopenSingleton.__impl(*args, **kwargs)

        # Store instance reference as the only member in the handle
        self.__dict__['_PopenSingleton__instance'] = PopenSingleton.__instance

    def __getattr__(self, attr):
        """ Delegate access to implementation """
        return getattr(self.__instance, attr)

    def __setattr__(self, attr, value):
        """ Delegate access to implementation """
        return setattr(self.__instance, attr, value)


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

