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

"""
    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, timedelta
from pkg_resources import Requirement

from sqlalchemy import and_, or_
from sqlalchemy.orm import mapper

from pypm.common import db
from pypm.common.util import url_join
from pypm.common.textui import ProgressBar
from pypm.common.package import RepoPackage, InstalledPackage
from pypm.common.repository import RemoteRepositoryManager
from pypm.client.version import sort_packages_by_version
from pypm.client import error

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


class InstalledPackageDatabase(db.SqlAlchemyDatabase):
    pass

db.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):
        """Sync the local cache of remote repositories

        - force: if True, ignore cache.. and forcefully redownload index

        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))
        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"""
        if version:
            reqstring = '{0}=={1}'.format(name, version)
        else:
            reqstring = name
        return self.find_requirement(Requirement.parse(reqstring))

    def find_requirement(self, req):
        """Find the best matching package for the given requirement"""
        packages = self.find_package_releases(req.project_name)
        for p in packages:
            if p.version in req:
                return p
        raise error.NoPackageAvailable, req

    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()))
        sort_packages_by_version(packages)
        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


## old -- need to refactor
class BasePackageStore(object):

    def add_packages(self, packages):
        with self._db.transaction() as session:
            session.add_all(packages)
            session.commit()

    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 = db.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)

    def find_package_all_versions(self, name):
        packages = list(self._findby_with_name(name, []))
        return packages

    def find_package(self, name, version):
        packages = list(self._findby_with_name(name, []))
                        
        if packages:
            if version is None:
                sort_packages_by_version(packages)
                return packages[0]
            else:
                raise NotImplementedError, 'need to do this'
        else:
            self._raise_package_not_found(name, version)
        
    def find_latest_package(self, name):
        return self.find_package(name, None)

    def find_all_packages(self):
        return self._findby([])

    def find_only_package(self, name):
        packages = list(self._findby_with_name(name, []))
        if not packages:
            self._raise_package_not_found(name, None)
            raise PackageNotFound(name)
        
        assert len(packages) == 1, 'expecting 1 package, but got %d' % len(packages)
        return packages[0]

    def _raise_package_not_found(self, name, version):
        raise NotImplementedError, \
            'derived class must derive me and raise the appropriate error'



class InstalledPackageStore(BasePackageStore):
    """Package store that contains all installed packages
    """

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

    def remove_package(self, installed_package):
        with self._db.transaction() as session:
            session.delete(installed_package)
            session.commit()

    def _raise_package_not_found(self, name, version):
        raise error.NoPackageInstalled(name, version)
        


        
    
