import os, sys, struct, re, time
from distutils import spawn, sysconfig, util
from distutils.ccompiler import new_compiler, show_compilers
from distutils.command import build_scripts
from distutils.core import Command, DEBUG
from distutils.dep_util import newer, newer_group
from distutils.errors import *

from Ft.Lib import ImportUtil
from Structures import Script, ExecutableScript, Executable

SHELL_SCRIPT_BODY = """#!%(executable)s
# %(name)s script generated by %(command)s on %(timestamp)s.
# DO NOT EDIT THIS FILE!

# Skip over the directory from where the script is executing when importing
# the top-level module or package that contains the "main" function.
import sys, imp
toplevel = %(toplevel)r
imp.load_module(toplevel, *imp.find_module(toplevel, sys.path[1:]))

import %(module)s
status = %(module)s.%(function)s()
raise SystemExit(status)
"""

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
        self.exe_extension = sysconfig.get_config_var('EXE') or ''
        if self.debug and os.name == 'nt':
            self.exe_extension = '_d' + self.exe_extension
        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._stub_bytes = self._get_stub_bytes()

        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.
        """
        repl = {'executable' : self.get_python_executable(),
                'command' : self.get_command_name(),
                'timestamp' : time.asctime(),
                'toplevel' : script.module.split('.', 1)[0],
                }
        repl.update(vars(script))
        body = SHELL_SCRIPT_BODY % repl
        self.write_script(script, body)
        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.
        """
        source = util.convert_path(exec_script.source)
        try:
            f = open(source, 'r')
        except OSError, error:
            raise DistutilsFileError(
                "could not open '%s': %s" % (source, error.strerror))
        try:
            body = f.read()
        finally:
            f.close()

        chunks = body.split('\n', 1)
        match = build_scripts.first_line_re.match(chunks[0])
        if match:
            first_line = '#!' + self.get_python_executable()
            arguments = match.group(1)
            if arguments:
                first_line += ' ' + arguments
            chunks[0] = first_line
            body = '\n'.join(chunks)

        self.write_script(exec_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 get_python_executable(self):
        executable = spawn.find_executable('env')
        if executable is None:
            # No 'env' executable found; use the interpreter directly
            executable = sys.executable
        else:
            # Use the python found runtime (via env)
            executable += ' python'
        return executable

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

        # Determine if the script needs to be built
        command_mtime = ImportUtil.GetLastModified(__name__)
        if not (self.force or newer_group(sources, outfile, 'newer')
                or command_mtime > os.stat(outfile).st_mtime):
            self.announce("skipping '%s' script (up-to-date)" % script.name)
            return
        else:
            self.announce("building '%s' script" % (script.name), 2)

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

        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_body)
            os.close(fd)
        elif os.name == 'nt':
            # append the data to the stub executable
            f = open(outfile, 'wb')

            # Write the executable
            f.write(self._stub_bytes)

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

            # Write the script string
            f.write(script_body)

            # Write the script_info struct
            # struct script_info {
            #   int tag;
            #   int offset;
            # }
            script_info = struct.pack("ii", 0x1234567A, len(self._stub_bytes))
            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 _build_stub(self, executable):
        # Get the source for the stub executable
        source = os.path.join(self.build_temp, 'stubmain.c')
        if self.force or not os.path.exists(source):
            self.announce("copying stubmain.c -> %s" % source, 2)
            self.mkpath(self.build_temp)
            data = ImportUtil.GetResourceString(__name__, 'stubmain.c')
            f = open(source, 'w')
            try:
                f.write(data)
            finally:
                f.close()
        else:
            self.announce("not copying stubmain.c (output up-to-date)", 1)

        if self.force or newer(source, executable):
            # Create the executable stub
            self.announce("building stub executable", 2)
            if sys.version < '2.3':
                output_dir = self.build_temp
            else:
                # output_dir is computed from the source files
                output_dir = None
            objects = self.compiler.compile([source],
                                            output_dir=output_dir,
                                            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)

            output_dir, progname = os.path.split(executable)
            progname = os.path.splitext(progname)[0]
            self.compiler.link_executable(objects, progname,
                                        output_dir=output_dir,
                                        libraries=libraries,
                                        debug=self.debug,
                                        extra_preargs=self.link_preargs,
                                        extra_postargs=self.link_postargs)

            # The scripts will also need to be rebuilt as they are just
            # appended to the end of the stub executable.
            self.force = True
        else:
            self.announce("skipping stub executable (up-to-date)", 1)

        # Now add the built stub to the package_data
        # Note, 4Suite installs only!
        stub_pkg = '.'.join(__name__.split('.')[:-1])
        if stub_pkg in self.distribution.package_data:
            source_dir, basename = os.path.split(executable)
            build_py = self.get_finalized_command('build_py')
            globs = self.distribution.package_data[stub_pkg]
            if basename not in globs:
                globs.append(basename)
                # add the stub executable to build_py's 'data_files'.
                build_dir = stub_pkg.replace('.', os.sep)
                build_dir = os.path.join(build_py.build_lib, build_dir)
                package_data = (stub_pkg, source_dir, build_dir, [basename])
                build_py.data_files.append(package_data)
                build_py.build_package_data()
        return

    def _get_stub_bytes(self):
        # Try to load a pre-built stub executable from a binary install
        filename = self._get_stub_filename()
        try:
            bytes = ImportUtil.GetResourceString(__name__, filename)
        except IOError:
            # See if the stub executable has been built locally.
            # Note, this only exists for a 4Suite source install!
            filename = os.path.join(self.build_temp, filename)
            if not os.path.exists(filename):
                self._build_stub(filename)
            f = open(filename, 'rb')
            try:
                bytes = f.read()
            finally:
                f.close()
        return bytes

    def _get_stub_filename(self):
        return 'stubmain%s%s' % (sys.version[:3], self.exe_extension)

    _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
