# Copyright (c) 2010 ActiveState Software Inc. All rights reserved.

"""
    pypm.client.store
    ~~~~~~~~~~~~~~~~~

    Store contains two things:

      1) repository index cache (~/.pypm/idx/<url-md5>/index)
      2) installed database (<pyenv>/_pypm/installed.db)
"""

import logging
from datetime import datetime
from datetime import timedelta
from operator import attrgetter

from sqlalchemy import and_
from sqlalchemy import or_
from applib import _simpledb

from pypm.common.util import url_join
from pypm.common.package import RepoPackage
from pypm.common.package import InstalledPackage
from pypm.common.repository import RemoteRepositoryManager
from pypm.client import error

LOG = logging.getLogger('pypm.client')


class InstalledPackageDatabase(_simpledb.SimpleDatabase):
    pass

_simpledb.setup(InstalledPackageDatabase, InstalledPackage,
         primary_keys=['name', 'version',
                       'pyver', 'osarch',
                       'pkg_version'])


class RepoPackageStore(object):
    """Store to manage repositories"""
    
    def __init__(self, rrmanager, repository_list):
        """
        rrmanager - pypm.common.repository.RemoteRepositoryManager
        repository_list - list<pypm.commmon.repository.RemoteRepository>
        """
        self.rrmanager = rrmanager
        self.repository_list = repository_list

    def sync(self, force=True, interactive=True):
        """Sync the local cache of remote repositories

        - force: if True, ignore cache.. and forcefully redownload index
        - interactive: if False, don't show progress bars.

        Return a sequence of bools denoting if the repositories are actually
        downloaded or not.
        """
        retvals = []
        for repo in self.repository_list:
            retvals.append(self.rrmanager.sync_repository(
                repo, force, interactive=interactive))
        return tuple(retvals)

    def autosync(self):
        """Check for outdated repositories and upate them if necessary

        This method works like `sync` (above) does except for the fact that it
        only attempts (cache-friendly as sync(force=True)) to sync the
        repository when last sync/autosync was run more than a day ago.

        This is useful to do every day for the user typically do not remember to
        run sync every day. Honestly, do you think you run 'sudo apt-get
        update' every day? It is for this reason Ubuntu came with their auto
        update thing (update-manager).
        """
        utcnow = datetime.utcnow()
        single_day = timedelta(days=1)
        
        retvals = []    # return values of rrmanager.sync_repository
        outdateds = []  # was the repository out-of-date, or local cache missing?
        for repo in self.repository_list:
            prevtime = self.rrmanager.get_remote_index_last_download_attempt_time(repo)
            if prevtime is None or prevtime + single_day < utcnow:
                outdateds.append(True)
                retvals.append(
                    self.rrmanager.sync_repository(
                        repo, force=False, verbose=False))
            else:
                outdateds.append(False)
                retvals.append(None)

        if True in retvals:
            LOG.info('autosync: synced %s repositories', retvals.count(True))
        elif True in outdateds:
            LOG.info('autosync: nothing new to sync')
        else:
            LOG.debug('autosync: nothing was outdated')

        return retvals

    def search(self, *keywords):
        """Search for ``keywords`` in all repositories"""
        if not keywords:
            q = self._query()
        else:
            q = self._query(
                lambda C: and_(*[or_(C.name.contains(keyword),
                                     C.summary.contains(keyword))
                                 for keyword in keywords]))
        for r in q:
            yield r
            
    def find_package(self, name, version=None):
        """Find a package"""
        for pkg in self.find_package_releases(name):
            if version:
                if version == pkg.version:
                    return pkg
            else:
                return pkg
        
        raise error.PackageNotFound(
            '{0}=={1}'.format(name, version) if version else name)

    def find_package_releases(self, name):
        """Find all available releases of a package
        
        Return a sorted (by version) list of packages
        """
        packages = list(self._query(lambda C: C.name == name.lower()))
        packages.sort(key=attrgetter('version_key'), reverse=True)
        return packages

    def _query(self, *expr_list):
        found = set()
        # search every repository
        for repo in self.repository_list:
            db = self.rrmanager.get_index_db(repo)

            with db.transaction() as session:
                query = session.query(db.mapper_class)
                for expr_fn in expr_list:
                    query = query.filter(expr_fn(db.mapper_class))
                    
                for pkg in query:
                    # return newly found items
                    if pkg.full_name not in found:
                        found.add(pkg.full_name)
                        # set download URL now
                        pkg.set_download_url(
                            url_join(repo.url, [pkg.relpath]))
                        yield pkg



class InstalledPackageStore:
    """Package store that contains all installed packages"""

    def __init__(self, storepath):
        self.storepath = storepath
        self._db = InstalledPackageDatabase(self.storepath, touch=True)

    def add_packages(self, packages):
        with self._db.transaction() as session:
            session.add_all(packages)
            session.commit()
    
    def remove_package(self, installed_package):
        with self._db.transaction() as session:
            session.delete(installed_package)
            session.commit()
            
    def find_all_packages(self):
        """Return all installed packages"""
        return self._findby([])

    def find_only_package(self, name):
        """Return the given installed package"""
        packages = list(self._findby_with_name(name, []))
        if not packages:
            raise error.NoPackageInstalled(name, None)
        
        assert len(packages) == 1, 'expecting 1 package, but got %d' % len(packages)
        return packages[0]

    def _findby(self, expression_list):
        with self._db.transaction() as session:
            query = session.query(self._db.mapper_class)
            for expr in expression_list:
                query = query.filter(expr)
            return query

    def _findby_with_name(self, name, expression_list):
        escape_char = '\\'
        # escape sqlite LIKE's special chars - _ and %
        escaped_name = _simpledb.sqlalchemy_escape(name, escape_char, '%_')
        search_expr = self._db.mapper_class.name.like(
            escaped_name, escape=escape_char)
        
        return self._findby([search_expr]+expression_list)

