# Copyright (c) 2010 ActiveState Software Inc. All rights reserved.
"""
    pypm.client.installer
    ~~~~~~~~~~~~~~~~~~~~~
    
    Routines related installing and uninstalling packages
"""

import logging
from operator import attrgetter

from applib.textui import askyesno
from pkg_resources import Requirement

from pypm.common.util import wrapped

from pypm.client import depgraph
from pypm.client import hooks
from pypm.client.fs import Downloader
from pypm.client.fs import Extractor
from pypm.client.store import InstalledPackage


LOG = logging.getLogger(__name__)


class PyPMDepGraph(depgraph.DepGraph):
    
    def __init__(self, pypmenv):
        self.pypmenv = pypmenv
        super(PyPMDepGraph, self).__init__()
    
    def get_installed_distributions(self):
        return self.pypmenv.installed_store.find_all_packages()
    
    def get_available_distributions(self, name):
        return self.pypmenv.repo_store.find_package_releases(name)
    

class Installer:

    def __init__(self, pypmenv):
        self.pypmenv = pypmenv
        
    def apply(self, depgraph, _requested_rmlist=[]):
        """Apply marks from the given depgraph
        
        This effectly installs/removes/changes packages in ``pyenv`` based on
        what is marked in the depgraph.
        
        `_requested_rmlist` - packages requested to be removed; not to confirm
        
        Return the applied marks (``DepGraph.get_marks``); return None if no
        no change was made.
        """
        marks = depgraph.get_marks()
        
        if not marks['install'] and not marks['change'] and not marks['remove']:
            return None
        
        # Display to the user what we are going to do
        messages = []
        def mark_show(verb, list, item_print_func):
            if not list:
                return
            messages.append(
                'The following packages will be {verb} {0}:\n{1}'.format(
                    self.pypmenv.pyenv.printable_location,
                    wrapped(' '.join(item_print_func(item) for item in list),
                            prefix=' ',
                            break_on_hyphens=False),
                    verb=verb))
        mark_show('installed into', marks['install'], attrgetter('full_name'))
        def change_printer(p_tup):
            p1, p2 = p_tup
            return '{0.name}({0.printable_version}->{1.printable_version})'.format(
                    p1,p2)
        mark_show('upgraded in',
                  [(p1,p2) for (p1,p2) in marks['change'] if p1.version_key < p2.version_key],
                  change_printer)
        mark_show('DOWNGRADED in',
                  [(p1,p2) for (p1,p2) in marks['change'] if p1.version_key >= p2.version_key],
                  change_printer)
        
        mark_show('UNINSTALLED from', marks['remove'], attrgetter('full_name'))
        print('\n'.join(messages))
        
        # Confirm if necessary
        if self.pypmenv.options['interactive']:
            i, r, c = map(len, [
                marks['install'],
                marks['remove'],
                marks['change']])
            
            # rdiff: no. of unrequested packages to be removed
            a = [p.name for p in marks['remove']]
            b = [Requirement.parse(n).project_name.lower() for n in _requested_rmlist]
            rdiff = len(set(a).difference(set(b)))
            
            trivial_action = any([
                (i and not r and not c),  # install, without rm/upgrade
                (not i and r and not rdiff and not c),  # no extra uninstalls
            ])
            if not trivial_action:
                if not askyesno('Continue?', default=True):
                    return None
            del i, r, c
        
        # 1. download packages
        to_download = sorted(
            set.union(set(marks['install']),
                      [p2 for p1, p2 in marks['change']]),
            key=lambda p: p.name)
        locations = Downloader(self.pypmenv).download_packages(to_download)
        
        # 2. uninstall packages
        for ipkg in marks['remove']:
            self._uninstall(ipkg)
            
        # 3. install new packages
        for pkg in marks['install']:
            self._install(pkg, locations[pkg])
            
        # 4. upgrade/downgrade existing packages
        for ipkg, pkg in marks['change']:
            # XXX: need transaction here
            self._uninstall(ipkg)
            self._install(pkg, locations[pkg])
            
        return marks

    def _install(self, package, file_location):
        # 1. extract files
        LOG.info('Installing {0.full_name}'.format(package))
        files_list = Extractor(self.pypmenv).extract_package(
            file_location, package.name)
        ipkg = InstalledPackage.create_from(
            package, files_list=files_list)
        assert ipkg.files_list is not None
        assert len(ipkg.files_list) > 0

        # 2. post-install hooks
        for hook in hooks.POSTINSTALL:
            hook(self.pypmenv, ipkg)

        # 3. add to installed_store
        self.pypmenv.installed_store.add_packages([ipkg])

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