# Copyright (c) 2009 ActiveState Software Inc.
# See http://www.activestate.com/activepython/license/ for licensing
# information.

import logging

from pypm.common import activestate
from pypm.common.util import askyesno
from pypm.common.repository import RepoPackage
from pypm.client.base import PyPMFeature
from pypm.client import error
from pypm.client.store import InstalledPackage
from pypm.client.dependency import Dependency
from pypm.client.fixer import Fixer
from pypm.client.fs import (Downloader, Extractor)
from pypm.client.version import compare_version

LOG = logging.getLogger(__name__)


class Installer(PyPMFeature):

    def install(self, package, extras=None):
        """Install a package and its dependencies
        
        `extras` is setuptools' extras
        """
        with self.pypmenv.locked():
            return self.perform_actions(
                InstallerActions(self.pypmenv).install_actions(package, extras))

    def uninstall(self, package):
        """Uninstall a package and its reverse-dependents"""
        with self.pypmenv.locked():
            return self.perform_actions(
                InstallerActions(self.pypmenv).uninstall_actions(package))
                

    def perform_actions(self, action_sequence):
        """Perform the actions listed in `action_sequence` which is an instance
        of ``SimulatedInstaller.ActionSequence``
        """
        if action_sequence:
            LOG.info('%s', action_sequence)
        else:
            LOG.warn('nothing to perform')
        
        # Confirm any non-trivial action. A trivial action is that in which
        # either packages are *either* installed or uninstalled; nothing else
        # happens. XXX: this has to be revised when install()/uninstall()
        # accepts more than one package as arguments (as is the case with
        # apt-get).
        if self.pypmenv.interactive:
            i,r,u = map(len, [action_sequence.to_install,
                              action_sequence.to_uninstall,
                              action_sequence.to_upgrade])
            trivial_action = any([
                (i>0 and not r and not u), # install, without rm/upgrade
                (not i and r==1 and not u), # merely uninstall one package
            ])
            if not trivial_action:
                if not askyesno('Continue?', default=True):
                    return None
            del i,r,u

        # 1. download packages
        package_file_locations = self._download_packages(set.union(
            set(action_sequence.to_install),
            set([pkg for installed_pkg, pkg in action_sequence.to_upgrade])))
            
        # 2. uninstall packages
        for installed_pkg in action_sequence.to_uninstall:
            self._uninstall(installed_pkg)

        # 3. install new packages
        for pkg in action_sequence.to_install:
            self._install(pkg, package_file_locations[pkg])

        # 4. upgrade existing packages
        for installed_pkg, pkg in action_sequence.to_upgrade:
            # XXX: need transaction here
            self._uninstall(installed_pkg)
            self._install(pkg, package_file_locations[pkg])

        return action_sequence

    def _download_packages(self, packages):
        """Download packages to cache directory
        
        Return a dictionary of target package locations
        """
        # download BE packages first to catch any license related errors early
        # and bail out immediately. Note the the errors may occur either due
        # to missing license (locally) or expired license (remotely). So we
        # must sort for both free and BE users.
        be_list, free_list = [], []
        for p in packages:
            (be_list if p.requires_be_license else free_list).append(p)
        packages = be_list + free_list
            
        package_file_locations = {}
        for p in packages:
            package_file_locations[p] = Downloader(self.pypmenv).download_package(p)
            
        return package_file_locations
        

    def _install(self, package, file_location):
        # 1. extract files
        LOG.info('Installing {0.name}-{0.full_version}'.format(package))
        files_list = Extractor(self.pypmenv).extract_package(file_location)
        ipkg = InstalledPackage.create_from(
            package, files_list=files_list)
        assert len(ipkg.files_list) > 0
        
        # 2. apply fixes
        for fixer in Fixer(self.pypmenv).get_applicable_fixers(ipkg):
            fixer.fix()
            
        # 3. add to installed_store
        self.pypmenv.installed_store.add_packages([ipkg])


    def _uninstall(self, ipkg):
        LOG.info('Uninstalling {0.name}-{0.full_version}'.format(ipkg))
        Extractor(self.pypmenv).undo_extract(ipkg.files_list)
        self.pypmenv.installed_store.remove_package(ipkg)
        

class InstallerActions(PyPMFeature):

    class ActionSequence(object):

        def __init__(self):
            self.to_uninstall = set()# [installed_pkg1, installed_pkg2, ...]
            self.to_install = set()  # [pkg1, pkg2, ...]
            self.to_upgrade = set()  # [(installed_pkg1, pkg1), ...]
        
        def __nonzero__(self):
            return (len(self.to_uninstall) or len(self.to_install) or \
                    len(self.to_upgrade))

        def __str__(self):
            messages = []
            def package_str(pkg, to_pkg=None):
                if to_pkg is None:
                    return '{0.name}-{0.full_version}'.format(pkg)
                else:
                    return '{0.name} ({0.full_version}->{1.full_version})'.format(
                        pkg, to_pkg)
                
            if self.to_uninstall:
                messages.append(
                    ('The following packages will be uninstalled:\n ' +
                     ' '.join(package_str(p) for p in self.to_uninstall)))
            if self.to_install:
                messages.append(
                    ('The following packages will be installed:\n ' +
                     ' '.join(package_str(p) for p in self.to_install)))
            if self.to_upgrade:
                messages.append(
                    ('The following packages will be upgraded:\n ' +
                     ' '.join(package_str(p1, p2)  for (p1,p2) in self.to_upgrade)))

            return '\n'.join(messages)
                

    def install_actions(self, package, extras):
        """Return an ``ActionSequence``

        The action sequence represents the list of packages to
        uninstall/install/upgrade in order to eventuate a successful
        installation of ``package``.
        """
        assert type(package) is RepoPackage

        action_sequence = InstallerActions.ActionSequence()
        
        # XXX: since we are calculating *all* dependencies (i.e. even the
        # installed ones), this one might be a slower operation, but we are
        # confident that most packages in the Python world (well, except the
        # Zope ones at least), do not have so many nested dependencies.
        pkglist = Dependency(self.pypmenv).get_dependencies(package, extras)

        for pkg in pkglist:
            try:
                installed_package = self.pypmenv.installed_store.find_only_package(
                    pkg.canonical_name)
            except error.NoPackageInstalled:
                # if not already installed, install it now
                action_sequence.to_install.add(pkg)
            else:
                c = compare_version(installed_package, pkg)
                if c == 0:  # already installed; skip it
                    pass 
                elif c < 0: # upgrde
                    action_sequence.to_upgrade.add(
                        (installed_package, pkg)
                    )
                else:
                    raise error.VersionConflict(
                        'cannot install an older version of "{0}" ({1}); '
                        '{2} is already installed. Type "pypm uninstall {0}" '
                        'to uninstall the existing version first.'.format(
                            pkg.canonical_name,
                            pkg.full_version, 
                            installed_package.full_version))

        return action_sequence

    def uninstall_actions(self, package):
        """Return an ``ActionSequence`` for uninstalling a package
        """
        assert type(package) is InstalledPackage

        action_sequence = InstallerActions.ActionSequence()
            
        pkglist = Dependency(self.pypmenv).get_removable_dependents(package)

        action_sequence.to_uninstall.update(pkglist)

        return action_sequence
