#
# Copyright (c) 2004 Conectiva, Inc.
#
# Written by Gustavo Niemeyer <niemeyer@conectiva.com>
#
# This file is part of Smart Package Manager.
#
# Smart Package Manager is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation; either version 2 of the License, or (at
# your option) any later version.
#
# Smart Package Manager is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Smart Package Manager; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
from smart.util.strtools import globdistance
from smart.const import BLOCKSIZE
from smart import *
import os

class StateVersionError(Error): pass

class Package(object):

    def __init__(self, name, version):
        self.name = name
        self.version = version
        self.provides = ()
        self.requires = []
        self.recommends = []
        self.upgrades = ()
        self.conflicts = ()
        self.installed = False
        self.essential = False
        self.priority = 0
        self.loaders = {}

    def getInitArgs(self):
        return (self.__class__, self.name, self.version)

    def equals(self, other):
        # These two packages are exactly the same?
        fk = dict.fromkeys
        if (self.name != other.name or
            self.version != other.version or
            len(self.upgrades) != len(other.upgrades) or
            len(self.conflicts) != len(other.conflicts) or
            fk(self.upgrades) != fk(other.upgrades) or
            fk(self.conflicts) != fk(other.conflicts) or
            fk([x for x in self.provides if x.name[0] != "/"]) !=
            fk([x for x in other.provides if x.name[0] != "/"]) or
            fk([x for x in self.requires if x.name[0] != "/"]) !=
            fk([x for x in other.requires if x.name[0] != "/"]) or
            fk([x for x in self.recommends if x.name[0] != "/"]) !=
            fk([x for x in other.recommends if x.name[0] != "/"])):
            return False
        return True

    def coexists(self, other):
        # These two packages with the same name may coexist?
        return False

    def matches(self, relation, version):
        return False

    def search(self, searcher):
        myname = self.name
        myversion = self.version
        ratio = 0
        ic = searcher.ignorecase
        for nameversion, cutoff in searcher.nameversion:
            _, ratio1 = globdistance(nameversion, myname, cutoff, ic)
            _, ratio2 = globdistance(nameversion,
                                     "%s-%s" % (myname, myversion), cutoff, ic)
            ratio = max(ratio, ratio1, ratio2)
        if ratio:
            searcher.addResult(self, ratio)

    def getPriority(self):
        priority = pkgconf.getPriority(self)
        if priority is not None:
            return priority
        channelpriority = None
        for loader in self.loaders:
            priority = loader.getChannel().getPriority()
            if channelpriority is None or priority > channelpriority:
                channelpriority = priority
        return channelpriority+self.priority

    def __repr__(self):
        return str(self)

    def __str__(self):
        return "%s-%s" % (self.name, self.version)

    def __lt__(self, other):
        # This is used for sorting, and should be overloaded.
        rc = -1
        if isinstance(other, Package):
            rc = cmp(self.name, other.name)
            if rc == 0:
                rc = cmp(self.version, other.version)
        return rc == -1

    def __getstate__(self):
        return (self.name,
                self.version,
                self.provides,
                self.requires,
                self.recommends,
                self.upgrades,
                self.conflicts,
                self.installed,
                self.essential,
                self.priority,
                self.loaders)

    def __setstate__(self, state):
        (self.name,
         self.version,
         self.provides,
         self.requires,
         self.recommends,
         self.upgrades,
         self.conflicts,
         self.installed,
         self.essential,
         self.priority,
         self.loaders) = state

class PackageInfo(object):
    def __init__(self, package, order=0):
        self._package = package
        self._order = order

    def getPackage(self):
        return self._package

    def getDescription(self):
        return ""

    def getSummary(self):
        return ""

    def getSource(self):
        return ""

    def getGroup(self):
        return ""

    def getLicense(self):
        return ""

    def getPathList(self):
        return []

    def getChangeLog(self):
        return []

    def pathIsDir(self, path):
        return None

    def pathIsLink(self, path):
        return None

    def pathIsFile(self, path):
        return None

    def pathIsSpecial(self, path):
        return None

    def getBuildTime(self):
        return None

    def getInstalledSize(self):
        return None

    def getReferenceURLs(self):
        return []

    def getURLs(self):
        return []

    def getSize(self, url):
        return None

    def getMD5(self, url):
        return None

    def getSHA(self, url):
        return None

    def getSHA256(self, url):
        return None

    def validate(self, url, localpath, withreason=False):
        try:
            if not os.path.isfile(localpath):
                raise Error, _("File not found")

            size = self.getSize(url)
            if size:
                lsize = os.path.getsize(localpath)
                if lsize != size:
                    raise Error, _("Unexpected size (expected %d, got %d)") % \
                                 (size, lsize)

            filemd5 = self.getMD5(url)
            if filemd5:
                try:
                    from hashlib import md5
                except ImportError:
                    from md5 import md5
                digest = md5()
                file = open(localpath)
                data = file.read(BLOCKSIZE)
                while data:
                    digest.update(data)
                    data = file.read(BLOCKSIZE)
                lfilemd5 = digest.hexdigest()
                if lfilemd5 != filemd5:
                    raise Error, _("Invalid MD5 (expected %s, got %s)") % \
                                 (filemd5, lfilemd5)

            filesha256 = self.getSHA256(url)
            if filesha256:
                try:
                    from hashlib import sha256
                except ImportError:
                    from smart.util.sha256 import sha256
                digest = sha256()
                file = open(localpath)
                data = file.read(BLOCKSIZE)
                while data:
                    digest.update(data)
                    data = file.read(BLOCKSIZE)
                lfilesha256 = digest.hexdigest()
                if lfilesha256 != filesha256:
                   raise Error, _("Invalid SHA256 (expected %s, got %s)") % \
                                 (filesha256, lfilesha256)
            else:
                filesha = self.getSHA(url)
                if filesha:
                    try:
                        from hashlib import sha1 as sha
                    except ImportError:
                        from sha import sha
                    digest = sha()
                    file = open(localpath)
                    data = file.read(BLOCKSIZE)
                    while data:
                        digest.update(data)
                        data = file.read(BLOCKSIZE)
                    lfilesha = digest.hexdigest()
                    if lfilesha != filesha:
                        raise Error, _("Invalid SHA (expected %s, got %s)") % \
                                     (filesha, lfilesha)
        except Error, reason:
            if withreason:
                return False, reason
            return False
        else:
            if withreason:
                return True, None
            return True

    def __lt__(self, other):
        return self._order < other._order

class Provides(object):
    def __init__(self, name, version):
        self.name = name
        self.version = version
        self.packages = []
        self.requiredby = ()
        self.recommendedby = ()
        self.upgradedby = ()
        self.conflictedby = ()

    def getInitArgs(self):
        return (self.__class__, self.name, self.version)

    def search(self, searcher):
        myname = self.name
        myversion = self.version
        ratio = 0
        ic = searcher.ignorecase
        for provides, cutoff in searcher.provides:
            _, ratio1 = globdistance(provides, myname, cutoff, ic)
            _, ratio2 = globdistance(provides, "%s-%s" % (myname, myversion),
                                     cutoff, ic)
            ratio = max(ratio, ratio1, ratio2)
        if ratio:
            searcher.addResult(self, ratio)

    def __repr__(self):
        return str(self)

    def __str__(self):
        if self.version:
            return "%s = %s" % (self.name, self.version)
        return self.name

    def __cmp__(self, other):
        rc = cmp(self.name, other.name)
        if rc == 0:
            rc = cmp(self.version, other.version)
            if rc == 0:
                rc = cmp(self.__class__, other.__class__)
        return rc

    def __reduce__(self):
        return (self.__class__, (self.name, self.version))

class Depends(object):
    def __init__(self, name, relation, version):
        self.name = name
        self.relation = relation
        self.version = version
        self.packages = []
        self.providedby = ()

    def getInitArgs(self):
        return (self.__class__, self.name, self.relation, self.version)

    def getMatchNames(self):
        return (self.name,)

    def matches(self, prv):
        return False

    def __repr__(self):
        return str(self)

    def __str__(self):
        if self.version:
            return "%s %s %s" % (self.name, self.relation, self.version)
        else:
            return self.name

    def __cmp__(self, other):
        rc = cmp(self.name, other.name)
        if rc == 0:
            rc = cmp(self.version, other.version)
            if rc == 0:
                rc = cmp(self.__class__, other.__class__)
        return rc

    def __reduce__(self):
        return (self.__class__, (self.name, self.relation, self.version))

class PreRequires(Depends): pass
class Requires(Depends): pass
class Upgrades(Depends): pass
class Conflicts(Depends): pass

class Loader(object):

    def __init__(self):
        self._packages = []
        self._channel = None
        self._cache = None
        self._installed = False

    def getPackages(self):
        return self._packages

    def getChannel(self):
        return self._channel

    def setChannel(self, channel):
        self._channel = channel

    def getCache(self):
        return self._cache

    def setCache(self, cache):
        self._cache = cache

    def getInstalled(self):
        return self._installed

    def setInstalled(self, flag):
        self._installed = flag

    def getLoadSteps(self):
        return 0

    def getInfo(self, pkg):
        return None

    def reset(self):
        del self._packages[:]

    def load(self):
        pass

    def unload(self):
        self.reset()

    def loadFileProvides(self, fndict):
        pass

    def buildPackage(self, pkgargs, prvargs, reqargs, upgargs, cnfargs, recargs = None):
        cache = self._cache
        pkg = pkgargs[0](*pkgargs[1:])
        relpkgs = []
        if prvargs:
            pkg.provides = []
            for args in prvargs:
                prv = cache._objmap.get(args)
                if not prv:
                    prv = args[0](*args[1:])
                    cache._objmap[args] = prv
                    cache._provides.append(prv)
                relpkgs.append(prv.packages)
                pkg.provides.append(prv)

        if reqargs:
            pkg.requires = []
            for args in reqargs:
                req = cache._objmap.get(args)
                if not req:
                    req = args[0](*args[1:])
                    cache._objmap[args] = req
                    cache._requires.append(req)
                relpkgs.append(req.packages)
                pkg.requires.append(req)

        if recargs:
            pkg.recommends = []
            for args in recargs:
                rec = cache._objmap.get(args)
                if not rec:
                    rec = args[0](*args[1:])
                    cache._objmap[args] = rec
                    cache._recommends.append(rec)
                relpkgs.append(rec.packages)
                pkg.recommends.append(rec)

        if upgargs:
            pkg.upgrades = []
            for args in upgargs:
                upg = cache._objmap.get(args)
                if not upg:
                    upg = args[0](*args[1:])
                    cache._objmap[args] = upg
                    cache._upgrades.append(upg)
                relpkgs.append(upg.packages)
                pkg.upgrades.append(upg)

        if cnfargs:
            pkg.conflicts = []
            for args in cnfargs:
                cnf = cache._objmap.get(args)
                if not cnf:
                    cnf = args[0](*args[1:])
                    cache._objmap[args] = cnf
                    cache._conflicts.append(cnf)
                relpkgs.append(cnf.packages)
                pkg.conflicts.append(cnf)

        found = False
        lst = cache._objmap.get(pkgargs)
        if lst is not None:
            for lstpkg in lst:
                if pkg.equals(lstpkg):
                    pkg = lstpkg
                    found = True
                    break
            else:
                lst.append(pkg)
        else:
            cache._objmap[pkgargs] = [pkg]

        if not found:
            cache._packages.append(pkg)
            for pkgs in relpkgs:
                pkgs.append(pkg)

        pkg.installed |= self._installed
        self._packages.append(pkg)

        return pkg

    def buildFileProvides(self, pkg, prvargs):
        cache = self._cache
        prv = cache._objmap.get(prvargs)
        if not prv:
            prv = prvargs[0](*prvargs[1:])
            cache._objmap[prvargs] = prv
            cache._provides.append(prv)
        elif prv in pkg.provides:
            return

        prv.packages.append(pkg)
        pkg.provides.append(prv)

        for req in pkg.requires[:]:
            if req.name == prv.name:
                pkg.requires.remove(req)
                req.packages.remove(pkg)
                if not req.packages:
                    cache._requires.remove(req)

    def search(self, searcher):
        # Loaders are responsible for searching on PackageInfo. They
        # should use the fastest possible method. The one here is
        # generic, and should be replaced if possible.
        ic = searcher.ignorecase
        for pkg in self._packages:
            info = self.getInfo(pkg)
            ratio = 0
            if searcher.url:
                for url, cutoff in searcher.url:
                    for refurl in info.getReferenceURLs():
                        _, newratio = globdistance(url, refurl, cutoff, ic)
                        if newratio > ratio:
                            ratio = newratio
                            if ratio == 1:
                                break
                    else:
                        continue
                    break
            if ratio == 1:
                searcher.addResult(pkg, ratio)
                continue
            if searcher.path:
                for spath, cutoff in searcher.path:
                    for path in info.getPathList():
                        _, newratio = globdistance(spath, path, cutoff, ic)
                        if newratio > ratio:
                            ratio = newratio
                            if ratio == 1:
                                break
                    else:
                        continue
                    break
            if ratio == 1:
                searcher.addResult(pkg, ratio)
                continue
            if searcher.group:
                for pat in searcher.group:
                    if pat.search(info.getGroup()):
                        ratio = 1
                        break
            if ratio == 1:
                searcher.addResult(pkg, ratio)
                continue
            if searcher.summary:
                for pat in searcher.summary:
                    if pat.search(info.getSummary()):
                        ratio = 1
                        break
            if ratio == 1:
                searcher.addResult(pkg, ratio)
                continue
            if searcher.description:
                for pat in searcher.description:
                    if pat.search(info.getDescription()):
                        ratio = 1
                        break
            if ratio:
                searcher.addResult(pkg, ratio)

    __stateversion__ = 1
    
    def __getstate__(self):
        state = self.__dict__.copy()
        state["__stateversion__"] = self.__stateversion__
        return state

    def __setstate__(self, state):
        if state["__stateversion__"] != self.__stateversion__:
            raise StateVersionError
        self.__dict__.update(state)
        del self.__stateversion__

class Cache(object):

    def __init__(self):
        self._loaders = []
        self._packages = []
        self._provides = []
        self._requires = []
        self._recommends = []
        self._upgrades = []
        self._conflicts = []
        self._objmap = {}

    def reset(self):
        for prv in self._provides:
            del prv.packages[:]
            if prv.requiredby:
                del prv.requiredby[:]
            if prv.recommendedby:
                del prv.recommendedby[:]
            if prv.upgradedby:
                del prv.upgradedby[:]
            if prv.conflictedby:
                del prv.conflictedby[:]
        for req in self._requires:
            del req.packages[:]
            if req.providedby:
                del req.providedby[:]
        for rec in self._recommends:
            del rec.packages[:]
            if rec.providedby:
                del rec.providedby[:]
        for upg in self._upgrades:
            del upg.packages[:]
            if upg.providedby:
                del upg.providedby[:]
        for cnf in self._conflicts:
            del cnf.packages[:]
            if cnf.providedby:
                del cnf.providedby[:]
        del self._packages[:]
        del self._provides[:]
        del self._requires[:]
        del self._recommends[:]
        del self._upgrades[:]
        del self._conflicts[:]
        self._objmap.clear()

    def addLoader(self, loader):
        if loader:
            if loader not in self._loaders:
                self._loaders.append(loader)
                loader.setCache(self)

    def removeLoader(self, loader):
        if loader:
            if loader in self._loaders:
                self._loaders.remove(loader)
                loader.setCache(None)
                loader.unload()

    def _reload(self):
        packages = {}
        provides = {}
        requires = {}
        recommends = {}
        upgrades = {}
        conflicts = {}
        objmap = self._objmap
        loaders = dict.fromkeys(self._loaders, True)
        for loader in loaders:
            for pkg in loader._packages:
                if pkg in packages:
                    pkg.installed |= loader._installed
                else:
                    pkg.installed = loader._installed
                    packages[pkg] = True
                    objmap.setdefault(pkg.getInitArgs(), []).append(pkg)
                    for pkgloader in pkg.loaders.keys():
                        if pkgloader not in loaders:
                            del pkg.loaders[pkgloader]
                    for prv in pkg.provides:
                        prv.packages.append(pkg)
                        if prv not in provides:
                            provides[prv] = True
                            objmap[prv.getInitArgs()] = prv
                    for req in pkg.requires[:]:
                        req.packages.append(pkg)
                        if req not in requires:
                            objmap[req.getInitArgs()] = req
                            requires[req] = True
                    for rec in pkg.recommends[:]:
                        rec.packages.append(pkg)
                        if rec not in recommends:
                            objmap[rec.getInitArgs()] = rec
                            recommends[rec] = True
                    for upg in pkg.upgrades:
                        upg.packages.append(pkg)
                        if upg not in upgrades:
                            upgrades[upg] = True
                            objmap[upg.getInitArgs()] = upg
                    for cnf in pkg.conflicts:
                        cnf.packages.append(pkg)
                        if cnf not in conflicts:
                            conflicts[cnf] = True
                            objmap[cnf.getInitArgs()] = cnf
        self._packages[:] = packages.keys()
        self._provides[:] = provides.keys()
        self._requires[:] = requires.keys()
        self._recommends[:] = recommends.keys()
        self._upgrades[:] = upgrades.keys()
        self._conflicts[:] = conflicts.keys()

    def load(self):
        self._reload()
        prog = iface.getProgress(self)
        prog.start()
        prog.setTopic(_("Updating cache..."))
        prog.set(0, 1)
        prog.show()
        total = 1
        for loader in self._loaders:
            if not loader._packages:
                total += loader.getLoadSteps()
        prog.set(0, total)
        prog.show()
        for loader in self._loaders:
            if not loader._packages:
                loader.load()
        self.loadFileProvides()
        hooks.call("cache-loaded-pre-link", self)
        self._objmap.clear()
        self.linkDeps()
        prog.setDone()
        prog.show()
        prog.stop()
        hooks.call("cache-loaded", self)

    def unload(self):
        self.reset()
        for loader in self._loaders:
            loader.unload()

    def loadFileProvides(self):
        fndict = {}
        for req in self._requires:
            name = req.name
            if name[0] == "/":
                fndict[name] = name
        for loader in self._loaders:
            loader.loadFileProvides(fndict)

    def linkDeps(self):
        reqnames = {}
        for req in self._requires:
            for name in req.getMatchNames():
                lst = reqnames.get(name)
                if lst:
                    lst.append(req)
                else:
                    reqnames[name] = [req]
        recnames = {}
        for rec in self._recommends:
            for name in rec.getMatchNames():
                lst = recnames.get(name)
                if lst:
                    lst.append(rec)
                else:
                    recnames[name] = [rec]
        upgnames = {}
        for upg in self._upgrades:
            for name in upg.getMatchNames():
                lst = upgnames.get(name)
                if lst:
                    lst.append(upg)
                else:
                    upgnames[name] = [upg]
        cnfnames = {}
        for cnf in self._conflicts:
            for name in cnf.getMatchNames():
                lst = cnfnames.get(name)
                if lst:
                    lst.append(cnf)
                else:
                    cnfnames[name] = [cnf]
        for prv in self._provides:
            lst = reqnames.get(prv.name)
            if lst:
                for req in lst:
                    if req.matches(prv):
                        if req.providedby:
                            req.providedby.append(prv)
                        else:
                            req.providedby = [prv]
                        if prv.requiredby:
                            prv.requiredby.append(req)
                        else:
                            prv.requiredby = [req]
            lst = recnames.get(prv.name)
            if lst:
                for rec in lst:
                    if rec.matches(prv):
                        if rec.providedby:
                            rec.providedby.append(prv)
                        else:
                            rec.providedby = [prv]
                        if prv.recommendedby:
                            prv.recommendedby.append(rec)
                        else:
                            prv.recommendedby = [rec]
            lst = upgnames.get(prv.name)
            if lst:
                for upg in lst:
                    if upg.matches(prv):
                        if upg.providedby:
                            upg.providedby.append(prv)
                        else:
                            upg.providedby = [prv]
                        if prv.upgradedby:
                            prv.upgradedby.append(upg)
                        else:
                            prv.upgradedby = [upg]
            lst = cnfnames.get(prv.name)
            if lst:
                for cnf in lst:
                    if cnf.matches(prv):
                        if cnf.providedby:
                            cnf.providedby.append(prv)
                        else:
                            cnf.providedby = [prv]
                        if prv.conflictedby:
                            prv.conflictedby.append(cnf)
                        else:
                            prv.conflictedby = [cnf]

    def getPackages(self, name=None):
        if not name:
            return self._packages
        else:
            return [x for x in self._packages if x.name == name]

    def getProvides(self, name=None):
        if not name:
            return self._provides
        else:
            return [x for x in self._provides if x.name == name]

    def getRequires(self, name=None):
        if not name:
            return self._requires
        else:
            return [x for x in self._requires if x.name == name]

    def getRecommends(self, name=None):
        if not name:
            return self._recommends
        else:
            return [x for x in self._recommends if x.name == name]

    def getUpgrades(self, name=None):
        if not name:
            return self._upgrades
        else:
            return [x for x in self._upgrades if x.name == name]

    def getConflicts(self, name=None):
        if not name:
            return self._conflicts
        else:
            return [x for x in self._conflicts if x.name == name]

    def search(self, searcher):
        if searcher.nameversion:
            for pkg in self._packages:
                pkg.search(searcher)
        if searcher.provides:
            for prv in self._provides:
                prv.search(searcher)
        if searcher.requires:
            for prv in searcher.requires:
                prvname = prv.name
                for req in self._requires:
                    if prvname in req.getMatchNames() and req.matches(prv):
                        searcher.addResult(req)
        if searcher.recommends:
            for prv in searcher.recommends:
                prvname = prv.name
                for req in self._recommends:
                    if prvname in req.getMatchNames() and req.matches(prv):
                        searcher.addResult(req)
        if searcher.upgrades:
            for prv in searcher.upgrades:
                prvname = prv.name
                for upg in self._upgrades:
                    if prvname in upg.getMatchNames() and upg.matches(prv):
                        searcher.addResult(upg)
        if searcher.conflicts:
            for prv in searcher.conflicts:
                prvname = prv.name
                for cnf in self._conflicts:
                    if prvname in cnf.getMatchNames() and cnf.matches(prv):
                        searcher.addResult(cnf)
        if searcher.needsPackageInfo():
            for loader in self._loaders:
                loader.search(searcher)

    __stateversion__ = 1

    def __getstate__(self):
        state = {}
        state["__stateversion__"] = self.__stateversion__
        state["_loaders"] = self._loaders
        state["_packages"] = self._packages
        return state

    def __setstate__(self, state):
        if state["__stateversion__"] != self.__stateversion__:
            raise StateVersionError
        self._loaders = state["_loaders"]
        self._packages = state["_packages"]
        provides = {}
        requires = {}
        recommends = {}
        upgrades = {}
        conflicts = {}
        for pkg in self._packages:
            for prv in pkg.provides:
                prv.packages.append(pkg)
                provides[prv] = True
            for req in pkg.requires:
                req.packages.append(pkg)
                requires[req] = True
            for rec in pkg.recommends:
                rec.packages.append(pkg)
                recommends[rec] = True
            for upg in pkg.upgrades:
                upg.packages.append(pkg)
                upgrades[upg] = True
            for cnf in pkg.conflicts:
                cnf.packages.append(pkg)
                conflicts[cnf] = True
        self._provides = provides.keys()
        self._requires = requires.keys()
        self._recommends = recommends.keys()
        self._upgrades = upgrades.keys()
        self._conflicts = conflicts.keys()
        self._objmap = {}

from ccache import *

# vim:ts=4:sw=4:et
