import os
import sys
import time
from distutils import spawn, sysconfig, util
from distutils.ccompiler import new_compiler, show_compilers
from distutils.core import Command
from distutils.dep_util import newer_group

from Ft.Lib import ImportUtil
from Ft.Lib.DistExt import Util, ImageHlp
from Ft.Lib.DistExt.Structures import Script, Executable

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

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

class ScriptInfo(ImageHlp.Struct):
    """
    Representation of the SCRIPT_INFO resource in the stub executable.
    """
    __fields__ = [
        (ImageHlp.Dword, 'Signature'),
        (ImageHlp.Word, 'MajorPythonVersion'),
        (ImageHlp.Word, 'MinorPythonVersion'),
        (ImageHlp.Word, 'Subsystem'),
        (ImageHlp.Word, 'Characteristics'),
        (ImageHlp.Word, 'ScriptAddress'),
        (ImageHlp.Word, 'ScriptSize'),
        ]

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']

    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)

        # Build the "plain" (pure-Python) scripts.
        self.build_scripts([ script for script in self.scripts
                             if isinstance(script, Script) ])

        # Build the executable (compiled) scripts.
        self.build_executables([ script for script in self.scripts
                                 if isinstance(script, Executable) ])
        return

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

    def build_scripts(self, scripts):
        for script in scripts:
            self.build_script(script)
        return

    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.
        """
        # Get the destination filename
        outfile = self.get_script_filename(script)

        # Determine if the script needs to be built
        command_mtime = ImportUtil.GetLastModified(__name__)
        if os.name == 'nt':
            stub_mtime = ImportUtil.GetResourceLastModified(__name__,
                                                            'stubmain.exe')
            command_mtime = max(command_mtime, stub_mtime)
        try:
            target_mtime = os.stat(outfile).st_mtime
        except OSError:
            target_mtime = -1
        if not (self.force or command_mtime > target_mtime):
            self.announce("skipping '%s' script (up-to-date)" % script.name)
            return
        else:
            self.announce("building '%s' script" % (script.name), 2)

        repl = {'executable' : self.get_python_executable(),
                'command' : self.get_command_name(),
                'timestamp' : time.asctime(),
                'toplevel' : script.module.split('.', 1)[0],
                }
        repl.update(vars(script))
        script_body = SHELL_SCRIPT_BODY % repl

        if self.dry_run:
            # Don't actually create the script
            pass
        elif os.name == 'nt':
            # Populate the ScriptInfo structure
            script_info = ScriptInfo()
            script_info.Signature = 0x00005446  # "FT\0\0"
            script_info.MajorPythonVersion = sys.version_info[0]
            script_info.MinorPythonVersion = sys.version_info[1]
            script_info.Subsystem = 0x0003;     # CUI
            if self.debug:
                script_info.Characteristics |= 0x0001
            stub_bytes = ImportUtil.GetResourceString(__name__, 'stubmain.exe')
            script_info.ScriptAddress = len(stub_bytes)
            script_info.ScriptSize = len(script_body)

            # Write the script executable
            f = open(outfile, 'w+b')
            try:
                f.write(stub_bytes)
                f.write(script_body)
                ImageHlp.UpdateResource(f, ImageHlp.RT_RCDATA, 1, script_info)
                ImageHlp.SetSubsystem(f, ImageHlp.IMAGE_SUBSYSTEM_WINDOWS_CUI)
            finally:
                f.close()
        else:
            # Create the file with execute permissions set
            fd = os.open(outfile, os.O_WRONLY|os.O_CREAT|os.O_TRUNC, 0755)
            try:
                os.write(fd, script_body)
            finally:
                os.close(fd)
        return

    def build_executables(self, executables):
        if not executables:
            return

        # Create the compiler for compiling the executables.
        self._prep_compiler()

        for executable in executables:
            self.build_executable(executable)
        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):
        if os.name == 'nt':
            executable = sys.executable
        else:
            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 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)

        # If we were asked to build any C/C++ libraries, make sure that the
        # directory where we put them is in the library search path for
        # linking executables.
        if self.distribution.has_c_libraries():
            build_clib = self.get_finalized_command('build_clib')
            self.compiler.set_libraries(build_clib.get_library_names())
            self.compiler.add_library_dir(build_clib.build_clib)

        # 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)
        self.compiler.add_include_dir(py_include)
        if plat_py_include != py_include:
            include_dirs.append(plat_py_include)

        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_build(self, script):
        # This should really exist in the CCompiler class, but
        # that would required overriding all compilers.
        result = []
        for source in script.sources:
            source = util.convert_path(source)
            includes = Util.FindIncludes(source, script.include_dirs)
            result.append((source, includes))
        return result

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

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

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