import os, sys, types
from distutils import core
from distutils import fancy_getopt
from distutils.core import DEBUG
from distutils.errors import *
from distutils.util import convert_path
from distutils.version import StrictVersion
import Dist

# For .pkg data files
import Structures

core.USAGE = """\
Usage:
  %(script)s [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]
  %(script)s --help [cmd1 cmd2 ...]
  %(script)s cmd --help
  %(script)s --help-commands
  %(script)s --help-packages
"""

INSTALL_LOCATIONS = {
    'install_lib' : 'Python modules including C extensions',
    'install_headers' : 'C/C++ header files',
    'install_scripts' : 'Executable scripts (for PATH environment variable)',
    'install_data' : 'Examples, demos and other miscellaneous data files',
    'install_sysconf': 'Configuration files',
    'install_localstate' : 'Machine-specific variable data space',
    'install_devel' : 'Developer files (regression tests)',
    'install_docs' : 'Documentation',
    'install_l10n' : 'Natural language message catalogs',
    }

# Yes, we do need to know the types of the fields as we will be merging them
# later.  Without this, merging would become quite difficult to implement
# for all cases.
PKGINFO_FORMAT = {
    # setup fields
    'name' : str,
    'description' : str,
    'long_description' : str,
    'dependencies' : list,
    'package_file' : str,
    'timestamp' : (int, long),

    # sdist fields
    'validate_templates' : list,
    'manifest_templates' : list,

    # generate fields
    'bgen_files' : list,        # generate_bgen

    # build fields
    'config_module' : str,      # build_py
    'packages' : list,          # build_py
    'package_dir' : dict,       # build_py
    'py_modules' : list,        # build_py
    'libraries' : list,         # build_clib
    'ext_modules' : list,       # build_ext
    'scripts' : list,           # build_scripts
    'doc_files' : list,         # build_docs
    'l10n' : list,              # build_l10n

    # install fields
    'headers' : list,           # install_headers
    'data_files' : list,        # install_data
    'sysconf_files' : list,     # install_sysconf
    'localstate_files' : list,  # install_localstate
    'devel_files' : list,       # install_devel
    }


def LoadPackageDefinitions(filenames):
    # Add the structures to the global namespace for the package files.
    structs = {'Extension' : core.Extension}
    for name in Structures.__all__:
        structs[name] = getattr(Structures, name)

    package_info = {}
    for pkgfile in filenames:
        pkginfo = {}
        execfile(pkgfile, structs, pkginfo)

        pkgname = pkginfo.get('name')
        if not pkgname:
            print "Unnamed package %r, skipped" % pkgfile
        elif package_info.has_key(pkgname):
            print "Duplicate package %r, ignoring %r" % (pkgname, pkgfile)
        else:
            # Trim the package definition
            for name in tuple(pkginfo):
                # Remove any pkg-private declarations
                if name.startswith('_'):
                    del pkginfo[name]
                # Remove any imported modules
                elif isinstance(pkginfo[name], types.ModuleType):
                    del pkginfo[name]

            # Add package file information
            pkginfo['package_file'] = pkgfile
            pkginfo['timestamp'] = os.stat(pkgfile).st_mtime

            package_info[pkgname] = pkginfo
    return package_info

        
class PackageManager(Dist.Dist):

    global_options = Dist.Dist.global_options + [
        ('package=', 'p', "limit command(s) to a given package"),
        ]

    display_options = [
        ('help-packages', None, 'list all available packages'),
        ] + Dist.Dist.display_options

    display_option_names = ['help_packages'] + Dist.Dist.display_option_names

    def __init__(self, attrs):
        self.package = None
        self.package_info = {}
        Dist.Dist.__init__(self, attrs)

        # Use the verbosity tricks for Distribution messages when not
        # running Python 2.3+ as 2.3 has better logging facilities.
        if sys.version < '2.3':
            self.verbose = None
        return

    def finalize_options(self):
        self.distributions = {}
        self.sub_packages = []

        # Verify package definitions
        for pkginfo in self.package_info.values():
            if not isinstance(pkginfo, types.DictType):
                raise DistutilsSetupError, \
                      "'package_info' values must be dictionaries"

            for field, value in pkginfo.items():
                if field not in PKGINFO_FORMAT:
                    raise DistutilsSetupError, \
                          "package definition field %r is unknown" % field
                if value is None:
                    # None is the same as not specified
                    del pkginfo[field]
                elif not isinstance(value, PKGINFO_FORMAT[field]):
                    if isinstance(PKGINFO_FORMAT[field], tuple):
                        names = [ t.__name__ for t in PKGINFO_FORMAT[field] ]
                        expected_type = ' or '.join(names)
                    else:
                        expected_type = PKGINFO_FORMAT[field].__name__
                    compared_type = type(value).__name__
                    raise DistutilsSetupError, \
                          "field %r must be %s, not %s" % (field,
                                                           expected_type,
                                                           compared_type)

        # Initialize the containter type data variables before dealing
        # with the information from the package defintions.
        if self.packages is None:
            self.packages = []
        if self.package_dir is None:
            self.package_dir = {}
        if self.py_modules is None:
            self.py_modules = []
        if self.libraries is None:
            self.libraries = []
        if self.headers is None:
            self.headers = []
        if self.ext_modules is None:
            self.ext_modules = []
        if self.include_dirs is None:
            self.include_dirs = []
        if self.scripts is None:
            self.scripts = []
        if self.data_files is None:
            self.data_files = []

        # PKG-INFO is created for source distributions, so allow "developer"
        # friendly features to be enabled/disabled (i.e., build_docs)
        self.developer_mode = not os.path.exists('PKG-INFO')
        if self.developer_mode:
            self.global_options.extend([
                ('developer-mode', 'd', "force 'developer' mode [default]"),
                ('user-mode', 'u', "force 'user' mode"),
                ])
            boolean_options = getattr(self, 'boolean_options', [])
            boolean_options.extend(['developer-mode', 'user-mode'])
            self.boolean_options = boolean_options

            negative_opt = getattr(self, 'negative_opt', {})
            negative_opt['user-mode'] = 'developer-mode'
            self.negative_opt = negative_opt

        Dist.Dist.finalize_options(self)
        return

    # -- Command-line parsing methods ----------------------------------

    def parse_command_line(self):
        rv = Dist.Dist.parse_command_line(self)

        # See above as to why this check exists
        if sys.version < '2.3':
            if self.verbose is None:
                # neither --quiet or --verbose specified, default to quiet
                self.verbose = 0
                self._dist_verbose = self.developer_mode
            else:
                self._dist_verbose = self.verbose

        if self.package:
            if not self.package_info.has_key(self.package):
                raise DistutilsArgError, \
                      "package '%s' is unknown, use --help-packages " \
                      "to get a complete listing" % self.package
            packages = self.resolve_dependencies(self.package)
        else:
            packages = self.package_info.keys()

        self.sub_packages = self.sort_dependencies(packages)

        # Build a composite of requested package and dependencies
        for name in self.sub_packages:
            self.merge_package(self.package_info[name])

        return rv

    def handle_display_options(self, option_order):
        # User just wants a list of packages -- we'll print it out and stop
        # processing now (ie. if they ran "setup --help-packages foo bar",
        # we ignore "foo bar").
        if self.help_packages:
            self.print_packages()
            print
            print core.gen_usage(self.script_name)
            return 1
        return Dist.Dist.handle_display_options(self, option_order)

    def print_packages(self):
        """Print out a help message listing all available packages with a
        description of each.  The descriptions come from the package
        definition's 'description' field.
        """
        packages = self.package_info.keys()
        packages.sort()

        max_length = 0
        for pkg in packages:
            if len(pkg) > max_length:
                max_length = len(pkg)

        print "Available packages:"
        for pkg in packages:
            try:
                description = self.package_info[pkg]['description']
            except KeyError:
                description = '(no description available)'

            print "  %-*s  %s" % (max_length, pkg, description)
        return

    # -- Distribution object methods -----------------------------------

    def get_package_distribution(self, package):
        if self.distributions.has_key(package):
            return self.distributions[package]

        attrs = {'script_name' : self.script_name,
                 'script_args' : [],
                 }

        metadata = vars(self.metadata)
        attrs.update(metadata)

        pkginfo = self.package_info[package]
        attrs.update(pkginfo)

        dist = self.distributions[package] = Dist.Dist(attrs)
        return dist

    # -- Methods that operate on the Distribution ----------------------

    if sys.version < '2.3':
        def announce(self, message, level=1):
            if self._dist_verbose:
                print message
            return

    def run_commands(self):
        if DEBUG: print "PackageManager.run_commands():"

        if self.developer_mode and self.verbose:
            print "running commands in developer mode"

        Dist.Dist.run_commands(self)

        if self.have_run.get('install') and not self.developer_mode:
            print '-'*72
            print '%s version %s has been successfully installed!' % (
                self.get_name(), self.get_version())
            print

            install = self.get_command_obj('install')
            for cmd_name in install.get_sub_commands():
                cmd = self.get_command_obj(cmd_name)
                if cmd_name == 'install_lib':
                    # Use the closest path to the code as the directory
                    install_dir = os.path.commonprefix(cmd.get_outputs())
                else:
                    install_dir = getattr(install, cmd_name)

                # Strip off trailing separator
                if install_dir.endswith(os.sep):
                    install_dir = install_dir[:-len(os.sep)]

                # Only output location if it is relevant (something installed)
                if cmd.get_outputs():
                    print INSTALL_LOCATIONS[cmd_name] + ':'
                    print ' '*4 + install_dir
                    print
        return

    # -- Methods that operate on its Packages --------------------------

    def merge_package(self, package_info):
        # Any attribute that is not already defined is invalid!
        meta_attrs = self.metadata.__dict__
        dist_attrs = self.__dict__
        for attr, value in package_info.items():
            if attr in meta_attrs:
                # Ignore sub-package metadata.  It is only used when
                # get_package_distribution() is called.
                pass
            elif attr in dist_attrs:
                if isinstance(value, tuple):
                    # Make sure sequences are mutable
                    value = list(value)

                current = dist_attrs[attr]
                if current is None:
                    # This attribute has not been set, do it now.
                    dist_attrs[attr] = value
                elif isinstance(current, list) and isinstance(value, list):
                    # Merge sequence-type attributes
                    dist_attrs[attr].extend(value)
                elif isinstance(current, dict) and isinstance(value, dict):
                    # Merge mapping-type attributes
                    dist_attrs[attr].update(value)
                elif attr == 'package_file':
                    dist_attrs[attr] = value
                elif attr == 'timestamp':
                    dist_attrs[attr] = max(value, current)
                else:
                    raise DistutilsSetupError, \
                          "duplicate values for field %r" % attr
            else:
                # Any attribute that does not exist previously is invalid.
                raise DistutilsSetupError, \
                      "invalid distribution field %r" % attr
        return

    def sort_dependencies(self, packages):
        if DEBUG: print "PackageManager.sort_dependencies():"

        # Make sure that the dependencies exist in package_info
        for pkg in packages:
            try:
                pkginfo = self.package_info[pkg]
                for dep in pkginfo.get('dependencies', []):
                    depinfo = self.package_info[dep]
            except KeyError, key:
                raise DistutilsFileError('missing dependency: %s' % key)

        # Remove any duplicates
        set = {}
        for name in packages:
            set[name] = 1
        packages = set.keys()

        # Now we can sort 'em
        sorted = []
        while packages:
            changed = 0
            if DEBUG: print "  begin sort:"
            for pkg in packages[:]:
                if DEBUG: print "    trying", pkg
                pkginfo = self.package_info[pkg]
                dependencies = pkginfo.get('dependencies', [])
                for dep in dependencies:
                    if dep not in sorted:
                        break
                else:
                    if DEBUG: print "    sorted", pkg
                    sorted.append(pkg)
                    packages.remove(pkg)
                    changed = 1
            if not changed:
                raise DistutilsFileError("circular dependency: %s" % packages)

        return sorted

    def resolve_dependencies(self, package):
        if DEBUG: print "PackageManager.resolve_dependencies():"
        packages = [package]
        pkg_info = self.package_info[package]
        for name in pkg_info.get('dependencies', []):
            if self.package_info.has_key(name):
                packages.extend(self.resolve_dependencies(name))
        return packages
