import os, re, sys, zipfile, imp
from distutils.command import build, install
from distutils.core import Command
from distutils.dep_util import newer
from distutils.dir_util import remove_tree
from distutils.errors import *
from distutils.sysconfig import get_python_version
from distutils.util import byte_compile

from Ft.Lib import ImportUtil
from Ft.Lib.DistExt import Structures, Util

__zipsafe__ = True

EXTENSION_MODULE_STUB = """# dynamic module loader stub for .egg zipfiles
def __bootstrap__():
    global __bootstrap__, __loader__, __file__
    import imp, pkg_resources
    __file__ = pkg_resources.resource_filename(__name__, %(file)r)
    del __bootstrap__, __loader__
    imp.load_dynamic(__name__, __file__)
__bootstrap__()
"""

NAMESPACE_PACKAGE_STUB = """# loader stub for .egg namespace packages
__import__("pkg_resources").declare_namespace(__name__)
"""

class BDistEgg(Command):

    command_name = 'bdist_egg'

    description = "create a Python Egg (.egg) built distribution"

    user_options = [
        ('bdist-dir=', None,
         "temporary directory for creating the distribution"),
        ('keep-temp', 'k',
         "keep the pseudo-installation tree around after " +
         "creating the distribution archive"),
        ('plat-name=', 'p',
         "platform name to embed in generated filenames"),
        ('dist-dir=', 'd',
         "directory to put final built distributions in"),
        ('skip-build', None,
         "skip rebuilding everything (for testing/debugging)"),
        ]

    boolean_options = ['keep-temp', 'skip-build']

    build_commands = ['build_py', 'build_clib', 'build_ext', 'build_scripts']

    install_commands = ['install_lib', 'install_data', 'install_l10n',
                        'install_config']

    def initialize_options(self):
        self.bdist_dir = None
        self.plat_name = None
        self.keep_temp = None
        self.dist_dir = None
        self.skip_build = None
        return

    def finalize_options(self):
        if self.bdist_dir is None:
            bdist_base = self.get_finalized_command('bdist').bdist_base
            egg_dir = self.distribution.get_name() + '.egg'
            self.bdist_dir = os.path.join(bdist_base, egg_dir)

        self.set_undefined_options('bdist',
                                   ('keep_temp', 'keep_temp'),
                                   ('plat_name', 'plat_name'),
                                   ('dist_dir', 'dist_dir'),
                                   ('skip_build', 'skip_build'))
        return

    def run(self):
        if sys.version < '2.3':
            raise DistutilsPlatformError('Python Eggs require Python 2.3+')

        if not self.skip_build:
            # Run any build commands
            self.run_command_family('build', self.build_commands)

        self.mkpath(self.bdist_dir)

        # Set the options for the installation commands
        install = self.reinitialize_command('install', True)
        install.skip_build = self.skip_build
        install.compile = True
        install.install_base = install.install_platbase = self.bdist_dir
        install.select_scheme('zip')
        install.ensure_finalized()
        # Make the install appear to be a "chroot" install
        install.root = self.bdist_dir
        install.install_base = install.install_platbase = None

        # Run the commands that install "resources"
        commands = self.run_command_family('install', self.install_commands)
        outputs = []
        for command in commands:
            cmd = self.get_finalized_command(command)
            outputs.extend(cmd.get_outputs())

        # Create stub loaders for any extension modules
        py_files = self.write_extension_module_stubs()

        # Create namespace package loaders
        py_files.extend(self.write_namespace_package_stubs())

        # Byte-compile any additional Python source files
        if py_files:
            outputs.extend(py_files)
            # Byte-compile the stubs and record the compiled filenames
            install_lib = self.get_finalized_command('install_lib')
            install_lib.byte_compile(py_files)
            for py_file in py_files:
                if install_lib.compile:
                    outputs.append(py_file + 'c')
                if install_lib.optimize > 0:
                    outputs.append(py_file + 'o')

        # Add the standard metadata (EGG-INFO directory)
        outputs.extend(self.write_metadata())

        # Make the egg distribution
        dist_filename = self.make_distribution(outputs)

        # Add to 'Distribution.dist_files' so that the "upload" command works
        if hasattr(self.distribution, 'dist_files'):
            spec = ('bdist_egg', get_python_version(), dist_filename)
            self.distribution.dist_files.append(spec)

        if not self.keep_temp:
            remove_tree(self.bdist_dir, self.verbose, self.dry_run)
        return

    def run_command_family(self, family, commands):
        command = self.get_finalized_command(family)
        sub_commands = command.get_sub_commands()
        have_run = []
        for cmd_name in commands:
            if cmd_name in sub_commands:
                self.run_command(cmd_name)
                have_run.append(cmd_name)
        return have_run

    def write_extension_module_stubs(self):
        outputs = []
        build_ext = self.get_finalized_command('build_ext')
        for extension in build_ext.extensions:
            fullname = build_ext.get_ext_fullname(extension.name)
            filename = build_ext.get_ext_filename(fullname)
            contents = EXTENSION_MODULE_STUB % {
                'file' : os.path.basename(filename)}
            barename, ext = os.path.splitext(filename)
            if barename.endswith('module'):
                barename = barename[:-6]
            filename = os.path.join(self.bdist_dir, barename + '.py')
            description = "extension module stub for %r" % extension.name
            if self.force or self.newer(description, filename):
                self.write_file(description, contents, filename)
            outputs.append(filename)
        return outputs

    def write_namespace_package_stubs(self):
        outputs = []
        for package in self.distribution.namespace_packages:
            dirname = package.replace('.', os.sep)
            filename = os.path.join(self.bdist_dir, dirname, '__init__.py')
            description = "namespace package stub for %r" % package
            if self.force or self.newer(description, filename):
                self.write_file(description, NAMESPACE_PACKAGE_STUB, filename)
            outputs.append(filename)
        return outputs

    def write_metadata(self):
        outputs = []
        metadata_dir = os.path.join(self.bdist_dir, 'EGG-INFO')
        self.mkpath(metadata_dir)

        filename = os.path.join(metadata_dir, 'PKG-INFO')
        description = "package metadata"
        if self.force or self.newer(description, filename):
            self.announce("writing %s to %s" % (description, filename), 2)
            if not self.dry_run:
                f = open(filename, 'w')
                try:
                    self.distribution.metadata.write_pkg_file(f)
                finally:
                    f.close()
        outputs.append(filename)

        entry_points = self.get_entry_points()
        if entry_points:
            filename = os.path.join(metadata_dir, 'entry_points.txt')
            description = "entry points"
            if self.force or self.newer(description, filename):
                self.write_file_sections(description, entry_points, filename)
            outputs.append(filename)

        eager_resources = self.get_eager_resources()
        if eager_resources:
            prefix = self.bdist_dir + os.sep
            prefix_len = len(prefix)
            lines = []
            for resource in eager_resources:
                if resource.startswith(prefix):
                    resource = resource[prefix_len:]
                lines.append(resource.replace(os.sep, '/'))
            description = "eager resources"
            if self.force or self.newer(description, filename):
                self.write_file_lines(description, lines, filename)
            outputs.append(filename)

        # Create the ZIP safety flag file
        if self.check_zip_safe():
            filename = 'zip-safe'
        else:
            filename = 'not-zip-safe'
        filename = os.path.join(metadata_dir, filename)
        self.write_file_lines('ZIP safety flag', (), filename)
        outputs.append(filename)

        return outputs

    def get_entry_points(self):
        entry_points = {}
        # Get the scripts which will be turned into console_script entries
        if self.distribution.has_scripts():
            entry_points['console_scripts'] = console_scripts = []
            for script in self.distribution.scripts:
                if isinstance(script, Structures.Script):
                    entry_point = '%s = %s:%s' % (script.name, script.module,
                                                  script.function)
                    console_scripts.append(entry_point)
                else:
                    self.announce("WARNING: script '%s' skipped"
                                  " (unsupported format)" % script.name, 3)

        return entry_points

    def get_eager_resources(self):
        eager_resources = []

        # Python's gettext requires true filesystem paths, so add the
        # localization catalogs to the list of "eager resources".
        if self.distribution.has_l10n():
            install_l10n = self.get_finalized_command('install_l10n')
            eager_resources.append(install_l10n.install_dir)
            eager_resources.extend(install_l10n.get_outputs())

        return eager_resources

    def check_zip_safe(self):
        safe = True
        build_py = self.get_finalized_command('build_py')
        for package, module, filename in build_py.find_all_modules():
            if module == '__init__':
                fullname = package
            elif package:
                fullname = package + '.' + module

            # Get the code object for the module.
            f = open(filename, 'rU')
            try:
                code = compile(f.read(), filename, 'exec')
            finally:
                f.close()

            # If the module attribute '__zipsafe__' is defined, use its
            # value to determine zip-safety.  Otherwise check the symbols
            # used for "unsafe" names.
            if '__zipsafe__' in code.co_names:
                constants = get_constants(code)
                try:
                    safe = safe and constants['__zipsafe__']
                except KeyError:
                    # If defined, __zipsafe__ *MUST* be constant.
                    raise ValueError("%s.__zipsafe__ not constant" % fullname)
            else:
                symbols = get_symbols(code)
                unsafe = []
                for bad in ('__file__', '__path__'):
                    if bad in symbols:
                        unsafe.append(bad)
                if 'inspect' in symbols:
                    for bad in ('getsource', 'getabsfile', 'getsourcefile',
                                'getfile', 'getsourcelines', 'findsource',
                                'getcomments', 'getframeinfo',
                                'getinnerframes', 'getouterframes', 'stack',
                                'trace'):
                        if bad in symbols:
                            unsafe.append('inspect.' + bad)
                for symbol in unsafe:
                    self.announce("%s references %s" % (fullname, symbol), 2)
                    safe = False
        return safe

    def make_distribution(self, files):
        self.mkpath(self.dist_dir)
        egg_filename = os.path.join(self.dist_dir, self.get_egg_filename())
        self.announce("creating %s" % egg_filename, 2)
        if not self.dry_run:
            if sys.version < '2.4':
                # avoid 2.3 zipimport bug when 64 bits
                compression = zipfile.ZIP_STORED
            else:
                compression = zipfile.ZIP_DEFLATED
            eggfile = zipfile.ZipFile(egg_filename, "w", compression)
        else:
            eggfile = None

        prefix_len = len(self.bdist_dir) + len(os.sep)
        for filename in files:
            arcname = filename[prefix_len:]
            self.announce("adding %s" % arcname, 1)
            if eggfile is not None:
                eggfile.write(filename, arcname)

        if eggfile is not None:
            eggfile.close()
        return egg_filename

    def get_egg_filename(self):
        def make_egg_safe(name):
            return re.sub('[^a-zA-Z0-9.]+', '_', name)
        name = make_egg_safe(self.distribution.get_name())
        version = make_egg_safe(self.distribution.get_version())
        platform = get_python_version()
        if self.distribution.has_ext_modules():
            platform += '-' + self.plat_name
        return '%s-%s-py%s.egg' % (name, version, platform)

    def newer(self, description, filename):
        try:
            target_mtime = os.stat(filename).st_mtime
        except OSError:
            return True
        if self.distribution.package_file:
            source_mtime = os.stat(self.distribution.package_file).st_mtime
            if source_mtime > target_mtime:
                return True
        command_mtime = ImportUtil.GetLastModified(__name__)
        if command_mtime > target_mtime:
            return True
        self.announce("skipping %s (up-to-date)" % description, 1)
        return False

    def write_file_sections(self, description, sections, filename):
        self.announce("writing %s to %s" % (description, filename), 2)
        if not self.dry_run:
            f = open(filename, 'w')
            try:
                for section in sections:
                    f.write('[%s]\n' % section)
                    for line in sections[section]:
                        f.write(line)
                        f.write('\n')
                    f.write('\n')
            finally:
                f.close()
        return

    def write_file_lines(self, description, lines, filename):
        self.announce("writing %s to %s" % (description, filename), 2)
        if not self.dry_run:
            f = open(filename, 'w')
            try:
                for line in lines:
                    f.write(line)
                    f.write('\n')
            finally:
                f.close()
        return

    def write_file(self, description, contents, filename):
        self.announce("writing %s to %s" % (description, filename), 2)
        if not self.dry_run:
            f = open(filename, 'w')
            try:
                f.write(contents)
            finally:
                f.close()
        return

def get_constants(code):
    """Returns the mapping of top-level "constants" defined by
    'code'.
    """
    # perform a "mini-eval" of the global assignments
    import dis
    LOAD_CONST = dis.opmap['LOAD_CONST']
    LOAD_NAME = dis.opmap['LOAD_NAME']
    STORE_NAME = dis.opmap['STORE_NAME']
    EXTENDED_ARG = dis.opmap['EXTENDED_ARG']
    # convert the bytecode string to a series of integers
    opcodes = iter(map(ord, code.co_code))
    next_instr = opcodes.next
    constants = {}
    value = undefined = object()
    for opcode in opcodes:
        if opcode >= dis.HAVE_ARGUMENT:
            oparg = next_instr() + next_instr()*256
            while opcode == EXTENDED_ARG:
                opcode = next_instr()
                oparg = oparg << 16 + next_instr() + next_instr()*256
        if opcode == LOAD_CONST:
            value = code.co_consts[oparg]
        elif opcode == LOAD_NAME:
            name = code.co_names[oparg]
            if name in constants:
                value = constants[name]
            elif name in __builtins__:
                value = __builtins__[name]
            else:
                # not a constant delaration
                value = undefined
        elif opcode == STORE_NAME:
            name = code.co_names[oparg]
            if value is not undefined:
                constants[name] = value
                value = undefined
        else:
            # either not a variable binding opcode or not a LOAD/STORE pairing
            value = undefined
    return constants

def get_symbols(code, _symbols=None):
    """Returns the set of names and string constants used by 'code'
    and any nested code objects."""
    if _symbols is None:
        _symbols = {}
    for name in code.co_names:
        _symbols[name] = code
    for const in code.co_consts:
        if isinstance(const, (str, unicode)):
            _symbols[const] = code
        elif isinstance(const, type(code)):
            get_symbols(const, _symbols)
    return _symbols
