import os, sys, struct, re
from distutils import sysconfig, util
from distutils.ccompiler import new_compiler, show_compilers
from distutils.core import Command
from distutils.dep_util import newer, newer_group

from Structures import Script, ExecutableScript, Executable

STUB_MAIN_SOURCE = os.path.join(os.path.dirname(__file__), 'stubmain.c')

SHELL_SCRIPT_HEAD = """#!%(env_executable)s%(env_args)s%(py_executable)s

# Force Python to ignore the current directory when importing the
# module(s) used in the script body.
import sys, imp
module_info = imp.find_module('Ft', filter(None, sys.path))
imp.load_module('Ft', *module_info)

### SCRIPT BODY ###
"""

COMMAND_LINE_APP_BODY = """# script generated in %s
%%(import_stmt)s
%%(classname)s().run()
""" % __name__

class BuildScripts(Command):

    command_name = 'build_scripts'

    description = "\"build\" scripts"

    user_options = [
        ('build-dir=', 'd', "directory to \"build\" (copy) to"),
        ('build-temp=', 't',
         "directory for temporary files (build by-products)"),
        ('force', 'f', "forcibly build everything (ignore file timestamps"),
        ('debug', 'g', "compile/link with debugging information"),
        ('compiler=', 'c', "specify the compiler type"),
        ]

    help_options = [
        ('help-compiler', None,
         "list available compilers", show_compilers),
        ]

    boolean_options = ['force', 'debug']

    stub_name = 'stubmain'

    def initialize_options(self):
        self.build_dir = None
        self.build_temp = None
        self.force = None
        self.debug = None
        self.compiler = None
        return

    def finalize_options(self):
        undefined_temp = self.build_temp is None

        self.set_undefined_options('build',
                                   ('build_scripts', 'build_dir'),
                                   ('build_temp', 'build_temp'),
                                   ('compiler', 'compiler'),
                                   ('debug', 'debug'),
                                   ('force', 'force'))

        if undefined_temp:
            self.build_temp = os.path.join(self.build_temp, 'scripts')

        self.scripts = self.distribution.scripts or []

        # Get the linker arguments for building executables
        if os.name == 'posix':
            args = sysconfig.get_config_vars('LDFLAGS', 'LINKFORSHARED')
            self.link_preargs = ' '.join(args).split()

            args = sysconfig.get_config_vars('LIBS', 'MODLIBS', 'SYSLIBS',
                                             'LDLAST')
            self.link_postargs = ' '.join(args).split()
        else:
            self.link_preargs = []
            self.link_postargs = []

        # Get the extension for executables (only if needed for the OS)
        if os.name == 'nt' or sys.platform.startswith('darwin'):
            self.exe_extension = sysconfig.get_config_var('EXE') or ''
        else:
            self.exe_extension = ''

        if os.name == 'nt':
            if self.debug:
                self.exe_extension = '_d' + self.exe_extension
            stub_exe = self.stub_name + self.exe_extension
            self.win32_stub_filename = os.path.join(self.build_temp, stub_exe)
        else:
            self.win32_stub_filename = None
        return

    def run(self):
        """
        Create the proper script for the current platform.
        """
        if not self.scripts:
            return

        # Ensure the destination directory exists
        self.mkpath(self.build_dir)

        # Break out the various types of scripts
        scripts = [ x for x in self.scripts
                        if isinstance(x, Script) ]
        executables = [ x for x in self.scripts
                        if isinstance(x, Executable) ]
        exec_scripts = [ x for x in self.scripts
                         if isinstance(x, ExecutableScript) ]

        # Create the compiler for any Windows script, executables or
        # executable scripts
        if (os.name == 'nt' and (scripts or exec_scripts)) or executables:
            self._prep_compiler()

        # Create the executable stub for any Windows script or executable
        # scripts
        if os.name == 'nt' and (scripts or exec_scripts):
            self._prep_win32_stub()

        for script in scripts:
            self.build_script(script)

        for executable in executables:
            self.build_executable(executable)

        for exec_script in exec_scripts:
            self.build_executable_script(exec_script)

        return

    # -- worker functions ---------------------------------------------

    def build_script(self, script):
        """
        Builds a CommandLineApp script.
        On POSIX systems, this is a generated shell script.  For Windows,
        it is a compiled executable with the generated file appended to the
        end of the stub.
        """
        # Create the script body
        import_stmt = 'import ' + script.identifier
        if script.module:
            import_stmt = 'from ' + script.module + ' ' + import_stmt

        repl = {'classname' : script.identifier,
                'import_stmt' : import_stmt,
                }

        self.write_script(script, COMMAND_LINE_APP_BODY % repl)
        return

    def build_executable_script(self, exec_script):
        """
        Builds a hand-written shell script.
        On POSIX systems, this is nothing more than a file copy.  For
        Windows, the executable stub has the contents of the script file
        appended to the end of the stub.
        """

        # Read in the script body
        source = util.convert_path(exec_script.source)
        f = open(source, 'r')
        script_body = f.read()
        f.close()

        self.write_script(exec_script, script_body, [source])
        return

    def build_executable(self, executable):
        """
        Builds a compiled executable.
        For all systems, the executable is created in the same fashion as
        the Python interpreter executable.
        """
        outfile = self.get_script_filename(executable)

        all_sources = self._prep_build(script)
        sources = []
        for source, includes in all_sources:
            sources.append(source)
            sources.extend(includes)

        if not (self.force or newer_group(sources, outfile, 'newer')):
            self.announce("skipping '%s' executable (up-to-date)" %
                          executable.name)
            return
        else:
            self.announce("building '%s' executable" % executable.name)

        output_dir = os.path.join(self.build_temp, executable.name)

        macros = executable.define_macros[:]
        for undef in executable.undef_macros:
            macros.append((undef,))

        objects = []
        for source, includes in all_sources:
            if not self.force:
                # Recompile if the includes or source are newer than the
                # resulting object files.
                objs = self.compiler.object_filenames([source], 1, output_dir)

                # Recompile if any of the inputs are newer than the object
                inputs = [source] + includes
                force = 0
                for filename in objs:
                    force = force or newer_group(inputs, filename, 'newer')
                self.compiler.force = force

            objs = self.compiler.compile(
                [source],
                output_dir=output_dir,
                macros=macros,
                include_dirs=executable.include_dirs,
                debug=self.debug,
                extra_postargs=executable.extra_compile_args)
            objects.extend(objs)

        # Reset the force flag on the compiler
        self.compiler.force = self.force

        # Now link the object files together into a "shared object" --
        # of course, first we have to figure out all the other things
        # that go into the mix.
        if os.name == 'nt' and self.debug:
            executable = executable.name + '_d'
        else:
            executable = executable.name

        if executable.extra_objects:
            objects.extend(executable.extra_objects)

        # On Windows, non-MSVC compilers need some help finding python
        # libs. This logic comes from distutils/command/build_ext.py.
        libraries = executable.libraries
        if sys.platform == "win32":
            from distutils.msvccompiler import MSVCCompiler
            if not isinstance(self.compiler, MSVCCompiler):
                template = "python%d%d"
                if self.debug:
                    template = template + "_d"
                pythonlib = (template % (sys.hexversion >> 24,
                                         (sys.hexversion >> 16) & 0xff))
                libraries += [pythonlib]

        self.compiler.link_executable(
            objects, executable,
            libraries=libraries,
            library_dirs=executable.library_dirs,
            runtime_library_dirs=executable.runtime_library_dirs,
            extra_preargs=self.link_preargs,
            extra_postargs=self.link_postargs + executable.extra_link_args,
            debug=self.debug,
            build_temp=self.build_temp)

        return

    # -- utility functions --------------------------------------------

    def write_script(self, script, script_body, extra_sources=None):
        # Get the destination filename
        outfile = self.get_script_filename(script)

        # Get the list of source dependencies for this script
        sources = [__file__]
        if os.name == 'nt':
            sources.append(self.win32_stub_filename)
        if extra_sources:
            sources.extend(extra_sources)

        # Determine if the script needs to be built
        if not (self.force or newer_group(sources, outfile, 'newer')):
            self.announce("skipping '%s' script (up-to-date)" % script.name)
            return
        else:
            self.announce("building '%s' script" % script.name)

        if self.dry_run:
            # Don't actually create the script
            return

        # prepare args for the bang path at the top of the script
        ENV_BIN = '/usr/bin/env'
        if os.name == 'posix':
            # Some Solaris platforms may not have an 'env' binary.
            # If /usr/bin/env exists, use '#!/usr/bin/env python'
            # otherwise, use '#!' + sys.executable
            env_exec = os.path.isfile(ENV_BIN) and \
                os.access(ENV_BIN, os.X_OK) and ENV_BIN or ''
            env_args = ''
            py_exec = env_exec and 'python' or sys.executable
        else:
            # shouldn't matter on non-POSIX; we'll just use defaults
            env_exec = ENV_BIN
            env_args = ''
            py_exec = 'python'

        head_args = { 'env_executable': env_exec,
                      'env_args': env_exec and (' %s' % env_args) or '',
                      'py_executable': py_exec }

        script = SHELL_SCRIPT_HEAD % head_args + script_body

        if os.name == 'posix':
            # Create the file with execute permissions set
            fd = os.open(outfile, os.O_WRONLY|os.O_CREAT|os.O_TRUNC, 0755)
            os.write(fd, script)
            os.close(fd)
        elif os.name == 'nt':
            # Get the stub executable data
            f = open(self.win32_stub_filename, 'rb')
            stub_data = f.read()
            f.close()

            # append the data to the stub executable
            f = open(outfile, 'wb')

            # Write the executable
            f.write(stub_data)

            # Write the script_data struct
            # struct script_data {
            #   int tag;
            #   int length;
            # }
            script_data = struct.pack("ii", 0x1234567A, len(script))
            f.write(script_data)

            # Write the script string
            f.write(script)

            # Write the script_info struct
            # struct script_info {
            #   int tag;
            #   int offset;
            # }
            script_info = struct.pack("ii", 0x1234567A, len(stub_data))
            f.write(script_info)

            f.close()
        else:
            raise DistutilsPlatformError, \
                "don't know how to build scripts on platform %r" % os.name
        return

    def get_script_filename(self, script):
        """
        Convert the name of a script into the name of the file which it
        will be run from.
        """
        # All Windows scripts are executables
        if os.name == 'nt' or isinstance(script, Executable):
            script_name = script.name + self.exe_extension
        else:
            script_name = script.name
        return os.path.join(self.build_dir, script_name)

    # -- helper functions ---------------------------------------------

    def _prep_compiler(self):
        # Setup the CCompiler object that we'll use to do all the
        # compiling and linking
        self.compiler = new_compiler(compiler=self.compiler,
                                     verbose=self.verbose,
                                     dry_run=self.dry_run,
                                     force=self.force)
        sysconfig.customize_compiler(self.compiler)

        # Make sure Python's include directories (for Python.h, pyconfig.h,
        # etc.) are in the include search path.
        py_include = sysconfig.get_python_inc()
        plat_py_include = sysconfig.get_python_inc(plat_specific=1)
        include_dirs = [py_include]
        if plat_py_include != py_include:
            include_dirs.append(plat_py_include)
        self.compiler.set_include_dirs(include_dirs)

        if os.name == 'posix':
            # Add the Python archive library
            ldlibrary = sysconfig.get_config_var('BLDLIBRARY')
            # MacOSX with frameworks doesn't link against a library
            if ldlibrary:
                # Get the location of the library file
                for d in sysconfig.get_config_vars('LIBDIR', 'LIBP', 'LIBPL'):
                    library = os.path.join(d, ldlibrary)
                    if os.path.exists(library):
                        self.compiler.add_link_object(library)
                        break
        elif os.name == 'nt':
            # Add Python's library directory
            lib_dir = os.path.join(sys.exec_prefix, 'libs')
            self.compiler.add_library_dir(lib_dir)
        return

    def _prep_win32_stub(self):
        if self.force or newer(STUB_MAIN_SOURCE, self.win32_stub_filename):
            # Create the executable stub
            self.announce("building stub executable")

            # Force rebuild of scripts
            self.force = 1

            if os.name == 'nt' and self.debug:
                executable = self.stub_name + '_d'
            else:
                executable = self.stub_name

            objects = self.compiler.compile([STUB_MAIN_SOURCE],
                                            output_dir=self.build_temp,
                                            debug=self.debug)

            # On Windows, non-MSVC compilers need some help finding python
            # libs. This logic comes from distutils/command/build_ext.py.
            libraries = []
            if sys.platform == "win32":
                from distutils.msvccompiler import MSVCCompiler
                if not isinstance(self.compiler, MSVCCompiler):
                    template = "python%d%d"
                    if self.debug:
                        template = template + "_d"
                    pythonlib = (template % (sys.hexversion >> 24,
                                             (sys.hexversion >> 16) & 0xff))
                    libraries.append(pythonlib)

            self.compiler.link_executable(objects, executable,
                                          libraries=libraries,
                                          output_dir=self.build_temp,
                                          debug=self.debug,
                                          extra_preargs=self.link_preargs,
                                          extra_postargs=self.link_postargs)
        else:
            self.announce("skipping stub executable (up-to-date)")
        return

    _include_re = re.compile(r'^\s*#\s*include\s*("|<)(?P<file>[^">]+?)("|>)',
                             re.MULTILINE)

    def _find_includes(self, script, filename, includes):
        source = open(filename).read()
        basename = os.path.basename(filename)

        include_dirs = [os.path.dirname(filename)]
        include_dirs.extend(script.include_dirs)

        match = self._include_re.search(source)
        while match:
            filename = util.convert_path(match.group('file'))
            for base in include_dirs:
                include = os.path.normpath(os.path.join(base, filename))
                if os.path.isfile(include) and include not in includes:
                    includes.append(include)
                    self._find_includes(script, include, includes)
                    break
            match = self._include_re.search(source, match.end())
        return

    def _prep_build(self, script):
        # This should really exist in the CCompiler class, but
        # that would required overriding all compilers.
        result = []
        for source in script.sources:
            includes = []
            self._find_includes(script, util.convert_path(source), includes)
            result.append((source, includes))
        return result

    # -- external interfaces ------------------------------------------

    def get_outputs(self):
        outfiles = []
        for script in self.scripts:
            outfiles.append(self.get_script_filename(script))
        return outfiles

    def get_source_files(self):
        filenames = []
        for script in self.scripts:
            if isinstance(script, ExecutableScript):
                filenames.append(script.source)
        return filenames
