#!/usr/bin/python -tt

# This program 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.
#
# This program 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 Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# (c) pmatilai@laiskiainen.org


import sys
sys.path.insert(0, '/usr/share/yum-cli')

import signal
import re
import fnmatch
import time
import os
import os.path
import exceptions
import urlparse

from optparse import OptionParser
from optparse import SUPPRESS_HELP

import logging
import yum
import yum.misc as misc
import yum.config
import yum.Errors
import yum.packages
from yum.i18n import to_unicode
from rpmUtils.arch import getArchList, getBaseArch
from rpmUtils.miscutils import formatRequire
import output
from urlgrabber.progress import TextMeter
from urlgrabber.progress import format_number

version = "0.0.11"

flags = { 'EQ':'=', 'LT':'<', 'LE':'<=', 'GT':'>', 'GE':'>=', 'None':' '}

std_qf = { 
'nvr': '%{name}-%{version}-%{release}',
'nevra': '%{name}-%{epoch}:%{version}-%{release}.%{arch}',
'envra': '%{epoch}:%{name}-%{version}-%{release}.%{arch}',
'source': '%{sourcerpm}',
'info': """
Name        : %{name}
Version     : %{version}
Release     : %{release}
Architecture: %{arch}
Size        : %{installedsize}
Packager    : %{packager}
Group       : %{group}
URL         : %{url}
Repository  : %{repoid}
Summary     : %{summary}
Source      : %{sourcerpm}
Description :\n%{description}""",
}

querytags = [ 'name', 'version', 'release', 'epoch', 'arch', 'summary',
              'description', 'packager', 'url', 'buildhost', 'sourcerpm',
              'vendor', 'group', 'license', 'buildtime', 'filetime',
              'installedsize', 'archivesize', 'packagesize', 'repoid', 
              'requires', 'provides', 'conflicts', 'obsoletes',
              'relativepath', 'hdrstart', 'hdrend', 'id',
            ]

def sec2isodate(timestr):
    return time.strftime("%F %T", time.gmtime(int(timestr)))

def sec2date(timestr):
    return to_unicode(time.ctime(int(timestr)))

def sec2day(timestr):
    return to_unicode(time.strftime("%a %b %d %Y", time.gmtime(int(timestr))))

def _size2val(size, off, ui):
    size = float(size)
    off = 1024
    if False: pass
    elif size >= (off * 100):
        return "%.0f%s" % ((size / off), ui)
    elif size >= (off *  10):
        return "%.1f%s" % ((size / off), ui)
    return "%.2f%s" % ((size / off), ui)
def size2k(size):
    return _size2val(size,                      1024, " k")
def size2m(size):
    return _size2val(size,               1024 * 1024, " M")
def size2g(size):
    return _size2val(size,        1024 * 1024 * 1024, " G")
def size2t(size):
    return _size2val(size, 1024 * 1024 * 1024 * 1024, " T")
def size2h(size):
    return format_number(size)

convertmap = { 'date': sec2date,
               'day':  sec2day,
               'isodate':  sec2isodate,
               'k':  size2k,
               'm':  size2m,
               'g':  size2g,
               'h':  size2h,
             }

class queryError(exceptions.Exception):
    def __init__(self, value=None):
        Exception.__init__(self)
        self.value = value
    def __str__(self):
        return "%s" %(self.value,)

    def __unicode__(self):
        return '%s' % to_unicode(self.value)
        

class DotPlot(object):
    def __init__(self):

        print 'digraph packages {',
        print """
size="20.69,25.52";
ratio="fill";
rankdir="TB";
orientation=port;
node[style="filled"];
outputorder="edgesfirst";
ranksep="1";
"""
    
    def addPackage(self, pkg, deps):
        # color calculations lifted from rpmgraph 
        h=0.5+(0.6/23*len(deps))
        s=h+0.1
        b=1.0
        
        print '"%s" [color="%s %s %s"];' % (pkg, h, s, b)
        print '"%s" -> {' % pkg
        for req in deps:
            print '"%s"' % req
        print '} [color="%s %s %s"];\n' % (h, s, b)
    
    def __del__(self):
        print "}"

# abstract class
class pkgQuery:
    """
    My implementation of __getitem__ either forwards to an implementation
    of fmt_(name), or to self.pkg.returnSimple(), allowing subclasses
    to override the package's items.

    @type pkg: L{yum.package.YumAvailablePackage}
    @ivar qf:  the query format for this package query
    @type qf:  str
    """

    def __init__(self, pkg, qf, yb=None):
        self.yb = yb
        self.pkg = pkg
        self.qf = qf
        self.name = pkg.name
        self.classname = None
        self._translated_qf = {}
    
    def __getitem__(self, item):
        item = item.lower()
        if hasattr(self, "fmt_%s" % item):
            return getattr(self, "fmt_%s" % item)()
        elif item.startswith('repo.'):
            repo_item = item.split('.')[1]
            try:
                return getattr(self.pkg.repo, repo_item)
            except AttributeError,e:
                raise queryError("Invalid repo querytag '%s' for %s: %s" % (repo_item, self.classname, self.pkg))
        elif hasattr(self.pkg, item):
            return getattr(self.pkg, item)

        res = None
        convert = None

        tmp = item.split(':')
        if len(tmp) > 1:
            item = tmp[0]
            conv = tmp[1]
            if conv in convertmap:
                convert = convertmap[conv]
            else:
                raise queryError("Invalid conversion: %s" % conv)

        # this construct is the way it is because pkg.licenses isn't
        # populated before calling pkg.returnSimple() ?!
        try:
            res = self.pkg.returnSimple(item)
        except (KeyError, ValueError):
            if item == "license":
                res = ", ".join(self.pkg.licenses)
            else:
                raise queryError("Invalid querytag '%s' for %s: %s" % (item, self.classname, self.pkg))

        if convert:
            res = convert(res)
        return res

    def __str__(self):
        return self.fmt_queryformat()

    def doQuery(self, method, *args, **kw):
        if method in std_qf:
            self.qf = std_qf[method]
            return self.fmt_queryformat()
        elif hasattr(self, "fmt_%s" % method):
            return getattr(self, "fmt_%s" % method)(*args, **kw)
        else:
            raise queryError("Invalid package query: %s" % method)

    def isSource(self):
        return self["arch"] == "src"

    def prco(self, what, **kw):
        """
        Query for the provides/requires/conflicts/obsoletes of this package.

        @param what: one of provides, requires, conflicts, obsoletes
        @type  what: str

        @rtype: list of str
        """
        # for subclasses to implement
        raise NotImplementedError

    def fmt_queryformat(self, **kw):

        if not self.qf:
            return self.fmt_nevra()

        # Override .qf for fun and profit...
        if self.qf not in self._translated_qf:
            qf = self.qf

            qf = qf.replace("\\n", "\n")
            qf = qf.replace("\\t", "\t")
            pattern = re.compile('%([-\d]*?){([:\.\w]*?)}')
            fmt = re.sub(pattern, r'%(\2)\1s', qf)
            self._translated_qf[self.qf] = fmt
        return self._translated_qf[self.qf] % self

    def fmt_requires(self, **kw):
        if self.yb.options.output in ("ascii-tree", "dot-tree"):
            self.fmt_tree_requires(output = self.yb.options.output,
                                tree_level = self.yb.options.tree_level,
                                dot = self.yb.options.dot)
        else:
            return "\n".join(self.prco('requires'))

    def fmt_provides(self, **kw):
        return "\n".join(self.prco('provides'))

    def fmt_conflicts(self, **kw):
        if self.yb.options.output in ("ascii-tree", "dot-tree"):
            self.fmt_tree_conflicts(output = self.yb.options.output,
                                tree_level = self.yb.options.tree_level,
                                dot = self.yb.options.dot)
        else:
            return "\n".join(self.prco('conflicts'))

    def fmt_obsoletes(self, **kw):
        if self.yb.options.output in ("ascii-tree", "dot-tree"):
            self.fmt_tree_obsoletes(output = self.yb.options.output,
                                tree_level = self.yb.options.tree_level,
                                dot = self.yb.options.dot)
        else:
            return "\n".join(self.prco('obsoletes'))

    def fmt_list(self, **kw):
        return "\n".join(self.files())

    def fmt_evr(self, **kw):
        return "%(epoch)s:%(version)s-%(release)s" % self
    def fmt_nevr(self, **kw):
        return "%(name)s-%(evr)s" % self
    def fmt_envr(self, **kw):
        return "%(epoch)s:%(name)s-%(version)s-%(release)s" % self
    def fmt_nevra(self, **kw):
        return "%(nevr)s.%(arch)s" % self
    def fmt_envra(self, **kw):
        return "%(envr)s.%(arch)s" % self

    def fmt_location(self, **kw):
        loc = ''
        repo = self.pkg.repo
        if self['basepath']:
            loc = "%(basepath)s/%(relativepath)s" % self
        else:
            repourl = repo.urls[0]
            if repourl[-1] != '/':
                repourl = repourl + '/'
            loc = urlparse.urljoin(repourl, self['relativepath'])
        return loc

    def tree_print_req(self, req, val, level):
        indent = ''
        if level:
            indent = ' |  ' * (level - 1) + ' \_  '
        print "%s%s [%s]" % (indent, str(req), str(val))

    # These are common helpers for the --tree-* options...
    @staticmethod
    def _tree_print_req(req, val, level):
        indent = ''
        if level:
            indent = ' |  ' * (level - 1) + ' \_  '
        print "%s%s [%s]" % (indent, str(req), str(val))
    def _tree_pkg2uniq(self, pkg):
        """ Turn a pkg into a "unique" req."""
        if self.yb and self.yb.conf.showdupesfromrepos:
            return str(pkg)
        return "%s.%s" % (pkg.name, getBaseArch(pkg.arch))
    def _tree_pkg2val(self, reqs, pkg):
        reqs = sorted(reqs[self._tree_pkg2uniq(pkg)])
        return str(len(reqs)) + ": " + ", ".join(reqs)
    def _tree_maybe_add_pkg(self, all_reqs, loc_reqs, pkgs, pkg, val):
        req = self._tree_pkg2uniq(pkg)
        if req in loc_reqs:
            loc_reqs[req].add(val)
            return
        if req in all_reqs:
            pkgs[pkg]     = None
            loc_reqs[req] = set([val])
            return
        pkgs[pkg]     = True
        loc_reqs[req] = set([val])
        all_reqs[req] = True
    def _tree_maybe_add_pkgs(self, all_reqs, tups, tup2pkgs):
        rpkgs    = {}
        loc_reqs = {}
        for rptup in tups:
            (rpn, rpf, (rp,rpv,rpr)) = rptup
            if rpn.startswith('rpmlib'):
                continue

            rname = yum.misc.prco_tuple_to_string(rptup)
            for npkg in sorted(tup2pkgs(rptup, rname), reverse=True):
                self._tree_maybe_add_pkg(all_reqs, loc_reqs, rpkgs, npkg, rname)
        return rpkgs, loc_reqs
    def _fmt_tree_prov(self, prco_type, **kw):
        pkg      = kw.get('pkg', self.pkg)
        req      = kw.get('req', 'cmd line')
        level    = kw.get('level', 0)
        all_reqs = kw.get('all_reqs', {})
        
        if kw['output'].lower() == 'dot-tree':
            if 'dot' not in kw.keys() or kw['dot'] is None:
                kw['dot'] = DotPlot()
        elif 'dot' not in kw.keys() or kw['dot'] is None:
            kw['dot'] = None
        
        if str(kw['tree_level']).lower() != 'all':
            try: 
                kw['tree_level'] = int(kw['tree_level'])
            except ValueError, er:
                kw['tree_level'] = 'all'
        
        if not 'output' in kw.keys():
            kw['output'] = 'ascii-tree'

        __req2pkgs = {}
        def req2pkgs(ignore, req):
            req = str(req)
            if req in __req2pkgs:
                return __req2pkgs[req]

            if self.yb is None:
                return []
            yb = self.yb

            providers = []
            try:
                # XXX rhbz#246519, for some reason returnPackagesByDep() fails
                # to find some root level directories while 
                # searchPackageProvides() does... use that for now
                matches = self.yb.searchPackageProvides([req])
                if self.yb.options.pkgnarrow == 'repos':
                    # Sucks that we do the work, and throw it away...
                    for provider in matches:
                        if provider.repoid != 'installed':
                            providers.append(provider)
                elif self.yb.options.pkgnarrow == 'installed':
                    # Sucks that we do the work, and throw it away...
                    for provider in matches:
                        if provider.repoid == 'installed':
                            providers.append(provider)
                else:
                    # Assume "all"
                    providers = matches.keys()


            except yum.Errors.YumBaseError, err:
                print >>sys.stderr, "No package provides %s" % req
                return []

            __req2pkgs[req] = providers
            return providers 
        
        dot = kw['dot']
        
        tups = getattr(pkg, prco_type)
        rpkgs, loc_reqs = self._tree_maybe_add_pkgs(all_reqs, tups, req2pkgs)
        if dot is None:
            self._tree_print_req(pkg, req, level)
            lim = level + 1
        else:
            dot.addPackage(pkg, rpkgs)
            lim = level + 2
        nlevel = level + 1
        if str(kw['tree_level']).lower() != 'all' and \
            int(kw['tree_level']) < int(lim):
            return
        for rpkg in sorted(rpkgs):
            if pkg.verEQ(rpkg):
                continue
            if rpkgs[rpkg] is None:
                req = self._tree_pkg2val(loc_reqs, rpkg)
                if dot is None:
                    self._tree_print_req(rpkg, req, nlevel)
                continue
            self._fmt_tree_prov(prco_type,
                                pkg = rpkg, level = nlevel, all_reqs = all_reqs,
                                req = self._tree_pkg2val(loc_reqs, rpkg),
                                tree_level = kw['tree_level'],
                                output = kw['output'],
                                dot = dot)
        
    def fmt_tree_requires(self, **kw):
        return self._fmt_tree_prov('requires', **kw)
    def fmt_tree_conflicts(self, **kw):
        return self._fmt_tree_prov('conflicts', **kw)

    def fmt_tree_obsoletes(self, **kw):
        pkg      = kw.get('pkg', self.pkg)
        level    = kw.get('level', 0)
        all_reqs = kw.get('all_reqs', {})
        
        if kw['output'].lower() == 'dot-tree':
            if 'dot' not in kw.keys() or kw['dot'] is None:
                kw['dot'] = DotPlot()
        elif 'dot' not in kw.keys() or kw['dot'] is None:
            kw['dot'] = None
        
        if str(kw['tree_level']).lower() != 'all':
            try: 
                kw['tree_level'] = int(kw['tree_level'])
            except ValueError, er:
                kw['tree_level'] = 'all'
        
        if not 'output' in kw.keys():
            kw['output'] = 'ascii-tree'
        
        def obs2pkgs():
            if self.yb is None:
                return []
            yb = self.yb

            obss = []
            if self.yb.options.pkgnarrow in ('all', 'repos'):
                for obs_n in pkg.obsoletes_names:
                    for opkg in yb.pkgSack.searchNevra(name=obs_n):
                        if opkg.obsoletedBy([pkg]):
                            obss.append(opkg)
            if self.yb.options.pkgnarrow in ('all', 'installed'):
                skip = set([opkg.pkgtup for opkg in obss])
                for obs_n in pkg.obsoletes_names:
                    for opkg in yb.rpmdb.searchNevra(name=obs_n):
                        if opkg.pkgtup in skip:
                            continue
                        if opkg.obsoletedBy([pkg]):
                            obss.append(opkg)

            return obss

        dot = kw['dot']
        
        if level:
            reason = ''
        else:
            reason = 'cmd line'
        rpkgs = obs2pkgs()
        if dot is None:
            self._tree_print_req(pkg, reason, level)
            lim = level + 1
        else:
            dot.addPackage(pkg, rpkgs)
            lim = level + 2
        all_reqs[pkg] = None
        nlevel = level + 1
        if str(kw['tree_level']).lower() != 'all' and \
            int(kw['tree_level']) < int(lim):
            return
        for rpkg in sorted(rpkgs):
            if pkg.verEQ(rpkg):
                continue
            if rpkg in all_reqs and dot is None:
                self._tree_print_req(rpkg, '', nlevel)
                continue
            self.fmt_tree_obsoletes(pkg=rpkg, level=nlevel, all_reqs=all_reqs,
                                    tree_level = kw['tree_level'],
                                    output = kw['output'],
                                    dot = dot)

    def fmt_tree_what_requires(self, **kw):
        pkg      = kw.get('pkg', self.pkg)
        req      = kw.get('req', 'cmd line')
        level    = kw.get('level', 0)
        all_reqs = kw.get('all_reqs', {})
        
        if kw['output'].lower() == 'dot-tree':
            if 'dot' not in kw.keys() or kw['dot'] is None:
                kw['dot'] = DotPlot()

        if str(kw['tree_level']).lower() != 'all':
            try: 
                kw['tree_level'] = int(kw['tree_level'])
            except ValueError, er:
                kw['tree_level'] = 'all'
        
        if not 'output' in kw.keys():
            kw['output'] = 'ascii-tree'

        __prov2pkgs = {}
        def prov2pkgs(prov, ignore):
            if str(prov) in __prov2pkgs:
                return __prov2pkgs[str(prov)]

            if self.yb is None:
                return []
            yb = self.yb

            arequirers = []
            irequirers = []
            try:
                skip = {}
                if yb.options.pkgnarrow in ('all', 'installed'):
                    irequirers = yb.rpmdb.getRequires(prov[0],prov[1],prov[2])
                    irequirers = irequirers.keys()
                if yb.options.pkgnarrow in ('all', 'repos'):
                    areqs = yb.pkgSack.getRequires(prov[0],prov[1],prov[2])
                    if not irequirers:
                        arequirers = areqs.keys()
                    else:
                        skip = set([pkg.pkgtup for pkg in irequirers])
                        arequirers = [pkg for pkg in areqs
                                      if pkg.pkgtup not in skip]

            except yum.Errors.YumBaseError, err:
                print >>sys.stderr, "No package provides %s" % str(prov)
                return []

            __prov2pkgs[str(prov)] = arequirers + irequirers
            return arequirers + irequirers

        filetupes = []
        for n in pkg.filelist + pkg.dirlist + pkg.ghostlist:
            filetupes.append((n, None, (None, None, None)))
        
        tups = pkg.provides + filetupes
        rpkgs, loc_reqs = self._tree_maybe_add_pkgs(all_reqs, tups, prov2pkgs)
        dot = kw['dot']
        
        if dot is None:
            self._tree_print_req(pkg, req, level)
            lim = level + 1
        else:
            dot.addPackage(pkg, rpkgs)
            lim = level + 2
        nlevel = level + 1
        
        if str(kw['tree_level']).lower() != 'all' and \
            int(kw['tree_level']) < int(lim):
            return
        for rpkg in sorted(rpkgs):
            if pkg.verEQ(rpkg): # Remove deps. on self.
                continue
            if rpkgs[rpkg] is None:
                req = self._tree_pkg2val(loc_reqs, rpkg)
                if dot is None:
                    self._tree_print_req(rpkg, req, nlevel)
                continue
            self.fmt_tree_what_requires(pkg=rpkg,
                                        level=nlevel, all_reqs=all_reqs,
                                        req=self._tree_pkg2val(loc_reqs, rpkg),
                                        tree_level = kw['tree_level'],
                                        output = kw['output'],
                                        dot = dot)


class repoPkgQuery(pkgQuery):
    """
    I wrap a query of a non-installed package available in the repository.
    """
    def __init__(self, pkg, qf, yb=None):
        pkgQuery.__init__(self, pkg, qf, yb)
        self.classname = 'repo pkg'

    def prco(self, what, **kw):
        rpdict = {}
        for rptup in self.pkg.returnPrco(what):
            (rpn, rpf, (rp,rpv,rpr)) = rptup
            if rpn.startswith('rpmlib'):
                continue
            rpdict[misc.prco_tuple_to_string(rptup)] = None
    
        rplist = rpdict.keys()
        rplist.sort()
        return rplist

    def files(self, **kw):
        fdict = {}
        for ftype in self.pkg.returnFileTypes():
            for fn in self.pkg.returnFileEntries(ftype):
                # workaround for yum returning double leading slashes on some 
                # directories - posix allows that but it looks a bit odd
                fdict[os.path.normpath('//%s' % fn)] = None
        files = fdict.keys()
        files.sort()
        return files

    def fmt_changelog(self, **kw):
        changelog = []
        for date, author, message in self.pkg.returnChangelog():
            changelog.append("* %s %s\n%s\n" % (sec2day(date),
                                                to_unicode(author),
                                                to_unicode(message)))
        return "\n".join(changelog)


class instPkgQuery(pkgQuery):
    """
    I wrap a query of an installed package 
    of type L{yum.packages.YumInstalledPackage}
    """
    # hmm, thought there'd be more things in need of mapping to rpm names :)
    tagmap = { 'installedsize': 'size',
             }

    def __init__(self, pkg, qf, yb=None):
        pkgQuery.__init__(self, pkg, qf, yb)
        self.classname = 'installed pkg'

    def __getitem__(self, item):
        if item in self.tagmap:
            return self.pkg.tagByName(self.tagmap[item])
        elif item.startswith('yumdb_info.'):
            yumdb_item = item.split('.')[1]
            try:
                return getattr(self.pkg.yumdb_info, yumdb_item)
            except AttributeError,e:
                raise queryError("Invalid yumdb querytag '%s' for %s: %s" % (yumdb_item, self.classname, self.pkg))
        else:
            return pkgQuery.__getitem__(self, item)
            
    def prco(self, what, **kw):
        prcodict = {}
        # rpm names are without the trailing s :)
        what = what[:-1]

        names = self.pkg.tagByName('%sname' % what)
        flags = self.pkg.tagByName('%sflags' % what)
        ver = self.pkg.tagByName('%sversion' % what)
        if names is not None:
            for (n, f, v) in zip(names, flags, ver):
                req = formatRequire(n, v, f)
                # filter out rpmlib deps
                if n.startswith('rpmlib'):
                    continue
                prcodict[req] = None

        prcolist = prcodict.keys()
        prcolist.sort()
        return prcolist
    
    def files(self, **kw):
        return self.pkg.tagByName('filenames')

    def fmt_changelog(self, **kw):
        changelog = []
        times = self.pkg.tagByName('changelogtime')
        if times is not None:
            names = self.pkg.tagByName('changelogname')
            texts = self.pkg.tagByName('changelogtext')
            for date, author, message in zip(times, names, texts):
                author = to_unicode(author)
                message = to_unicode(message)
                changelog.append("* %s %s\n%s\n" % (sec2day(date), author, message))
        return "\n".join(changelog)


class groupQuery:
    def __init__(self, group, grouppkgs="required"):
        self.grouppkgs = grouppkgs
        self.id = group.groupid
        self.name = group.name
        self.group = group

    def doQuery(self, method, *args, **kw):
        if hasattr(self, "fmt_%s" % method):
            return "\n".join(getattr(self, "fmt_%s" % method)(*args, **kw))
        else:
            raise queryError("Invalid group query: %s" % method)

    # XXX temporary hack to make --group -a query work
    def fmt_queryformat(self):
        return self.fmt_nevra()

    def fmt_nevra(self):
        return ["%s - %s" % (self.id, self.name)]

    def fmt_list(self):
        pkgs = []
        for t in self.grouppkgs.split(','):
            if t == "mandatory":
                pkgs.extend(self.group.mandatory_packages)
            elif t == "default":
                pkgs.extend(self.group.default_packages)
            elif t == "optional":
                pkgs.extend(self.group.optional_packages)
            elif t == "all":
                pkgs.extend(self.group.packages)
            else:
                raise queryError("Unknown group package type %s" % t)
            
        return pkgs
        
    def fmt_requires(self):
        return self.group.mandatory_packages

    def fmt_info(self):
        return ["%s:\n\n%s\n" % (self.name, self.group.description)]


class YumBaseQuery(yum.YumBase):
    def __init__(self, pkgops = [], sackops = [], options = None):
        """
        @type  pkgops:  list of str
        @type  sackops: list of str
        @type  options: L{optparse.Values}
        """
        yum.YumBase.__init__(self)
        self.logger = logging.getLogger("yum.verbose.repoquery")
        console_stderr = logging.StreamHandler(sys.stderr)
        console_stderr.setFormatter(logging.Formatter("%(message)s"))
        self.logger.propagate = False
        self.logger.addHandler(console_stderr)
        self.options = options
        self.pkgops = pkgops
        self.sackops = sackops
        self._sacks = []
        if self.options.pkgnarrow in ('all', 'extras', 'installed'):
            self._sacks.append('rpmdb')
        if self.options.pkgnarrow not in ('extras', 'installed'):
            self._sacks.append('pkgSack')

    def queryPkgFactory(self, pkgs, plain_pkgs=False):
        """
        For each given package, create a query.

        @type  pkgs: list of L{yum.package.YumAvailablePackage}

        @rtype: list of L{queryPkg}
        """
        qf = self.options.queryformat or std_qf["nevra"]
        qpkgs = []
        for pkg in pkgs:
            if isinstance(pkg, yum.packages.YumInstalledPackage):
                if self.options.pkgnarrow not in ('all', 'installed', 'extras'):
                    continue

            if plain_pkgs:
                qpkgs.append(pkg)
                continue

            if isinstance(pkg, yum.packages.YumInstalledPackage):
                qpkg = instPkgQuery(pkg, qf, self)
            else:
                qpkg = repoPkgQuery(pkg, qf, self)
            qpkgs.append(qpkg)
        return qpkgs

    def returnByName(self, name):
        """
        Given a name, return a list of package queries matching the name.

        @type  name: str

        @rtype: list of L{queryPkg}
        """
        pkgs = []
        try:
            pkgs = self.returnPkgList(patterns=[name])
        except yum.Errors.PackageSackError, err:
            self.logger.error(err)
        return self.queryPkgFactory(pkgs)

    def returnPkgList(self, **kwargs):
        pkgs = []
        if 'patterns' in kwargs:
            if len(kwargs['patterns']) == 1 and kwargs['patterns'][0] == '*':
                kwargs['patterns'] = None

        if self.options.pkgnarrow == "repos":
            # self.pkgSack is a yum.packageSack.MetaSack
            if self.conf.showdupesfromrepos:
                pkgs = self.pkgSack.returnPackages(**kwargs)
            else:
                try:
                    pkgs = self.pkgSack.returnNewestByNameArch(**kwargs)
                except yum.Errors.PackageSackError:
                    pkgs = []
        else:
            what = self.options.pkgnarrow
            ygh = self.doPackageLists(what, **kwargs)

            if what == "all":
                pkgs = ygh.available + ygh.installed
            elif hasattr(ygh, what):
                pkgs = getattr(ygh, what)
            else:
                self.logger.error("Unknown pkgnarrow method: %s" % what)

        return pkgs

    def returnPackagesByDepStr(self, depstring):
        provider = []
        try:
            # XXX rhbz#246519, for some reason returnPackagesByDep() fails
            # to find some root level directories while 
            # searchPackageProvides() does... use that for now
            matches = yum.YumBase.searchPackageProvides(self, [str(depstring)])
            provider = matches.keys()
            # provider.extend(yum.YumBase.returnPackagesByDep(self, depstring))
        except yum.Errors.YumBaseError, err:
            self.logger.error("No package provides %s" % depstring)
        return self.queryPkgFactory(provider)

    def returnGroups(self):
        grps = []
        for group in self.comps.get_groups():
            grp = groupQuery(group, grouppkgs = self.options.grouppkgs)
            grps.append(grp)
        return grps

    def matchGroups(self, items):
        grps = []
        for grp in self.returnGroups():
            for expr in items:
                if grp.name == expr or fnmatch.fnmatch("%s" % grp.name, expr):
                    grps.append(grp)
                elif grp.id == expr or fnmatch.fnmatch("%s" % grp.id, expr):
                    grps.append(grp)
        return grps
                    
    def matchPkgs(self, items, plain_pkgs=False):
        pkgs = self.returnPkgList(patterns=items)
        return self.queryPkgFactory(pkgs, plain_pkgs)

    def matchSrcPkgs(self, items):
        srpms = []
        for name in items:
            for pkg in self.returnByName(name):
                if pkg.isSource(): 
                    continue
                src = pkg["sourcerpm"][:-4]
                srpms.extend(self.returnByName(src))
        return srpms
    
    def yum_search(self, terms):
        """use yum's search generator to search arbitrary fields"""
        
        pkgs = []
        fields = self.options.searchfields
        if not fields:
            fields = ['name', 'summary']
        try:
            matching = self.searchGenerator(fields, terms, searchtags=False)
        
            for (po, matched_value) in matching:
                if isinstance(po, yum.packages.YumInstalledPackage):
                    if self.options.pkgnarrow not in ('all', 'installed', 'extras'):
                        continue
                if isinstance(po, yum.sqlitesack.YumAvailablePackageSqlite):
                    if self.options.pkgnarrow not in ('all', 'available', 'repos'):
                        continue
                pkgs.append(po)

        except (yum.Errors.RepoError,ValueError), e:
            raise queryError("Could not run search: %s" % e)
        
        return self.queryPkgFactory(pkgs)
        
    def _at_grps(self, items):
        #  We want to move from @foo => lists of package names here, to make
        # a bunch of things easier. Ie. pkgs. ops. on lists of packages from
        # groups.
        nitems = []
        for item in items:
            if item and item[0] == '@':
                for grp in self.matchGroups([item[1:]]):
                    nitems.extend(grp.group.packages)
                # Give warning when no matches?
                continue
            nitems.append(item)
        return nitems

    def runQuery(self, items):
        plain_pkgs = False
        if self.options.group:
            pkgs = self.matchGroups(items)
        elif self.options.groupmember:
            pkglist = self.matchPkgs(items, plain_pkgs=True)
            for pkg in sorted(pkglist):
                print to_unicode(pkg)
                for group in sorted(self.find_groupmember(pkg.name)):
                    print to_unicode('  @%s' % group)
            pkgs = []
        elif self.options.search:
            plain_pkgs = False
            pkgs = []
            try:
                pkgs = self.yum_search(items)
            except queryError, e:
                self.logger.error(e)
        else:
            items = self._at_grps(items)
            if self.options.srpm:
                pkgs = self.matchSrcPkgs(items)

            else:
                pkgs = []
                if not self.sackops:
                    plain_pkgs = True
                    try:
                        pkgs = self.matchPkgs(items, plain_pkgs=plain_pkgs)
                    except yum.Errors.RepoError, e:
                        raise queryError("Could not match packages: %s" % to_unicode(e))
                for prco in items:
                    for oper in self.sackops:
                        try:
                            for p in self.doQuery(oper, prco): 
                                if p:
                                    pkgs.append(p)
                        except queryError, e:
                            self.logger.error(e)

        if plain_pkgs:
            iq = None
            rq = None
            qf = self.options.queryformat or std_qf["nevra"]
            pkgs = sorted(pkgs)
        for pkg in pkgs:
            if plain_pkgs:
                if isinstance(pkg, yum.packages.YumInstalledPackage):
                    if iq is None:
                        iq = instPkgQuery(pkg, qf, self)
                    iq.pkg = pkg
                    iq.name = pkg.name
                    pkg = iq
                else:
                    if rq is None:
                        rq = repoPkgQuery(pkg, qf, self)
                    rq.pkg = pkg
                    rq.name = pkg.name
                    pkg = rq
            if not self.pkgops:
                print to_unicode(pkg)
            for oper in self.pkgops:
                try:
                    out = pkg.doQuery(oper, 
                        tree_level = self.options.tree_level,
                        output = self.options.output,
                        dot = self.options.dot)
                    if out:
                        print to_unicode(out)
                except queryError, e:
                    self.logger.error(e)

    def doQuery(self, method, *args, **kw):
        return getattr(self, "fmt_%s" % method)(*args, **kw)

    def find_groupmember(self, name, **kw):
        grps = []
        for group in self.comps.get_groups():
            if name in group.packages:
                grps.append(group.groupid)
        return grps

    def fmt_whatprovides(self, name, **kw):
        return self.returnPackagesByDepStr(name)

    def fmt_whatrequires(self, name, **kw):
        pkgs = {}
        done = [] # keep track of names we have already visited

        def require_recursive(name):
            if name in done:
                return
            done.append(name)

            provs = [name]
                    
            if self.options.alldeps:
                for pkg in self.returnByName(name):
                    provs.extend(pkg.prco("provides"))
                    provs.extend(pkg.files())

            for prov in provs:
                for sackstr in self._sacks:
                    sack = getattr(self, sackstr)
                    for pkg in sack.searchRequires(prov):
                        pkgs[pkg.pkgtup] = pkg
                        if self.options.recursive:
                            require_recursive(pkg.name)
                

        
        if self.options.output not in ('ascii-tree','dot-tree'):
            require_recursive(name)
        return self.queryPkgFactory(sorted(pkgs.values()))

    def fmt_whatobsoletes(self, name, **kw):
        pkgs = []
        for sackstr in self._sacks:
            sack = getattr(self, sackstr)
            for pkg in sack.searchObsoletes(name):
                pkgs.append(pkg)
        return self.queryPkgFactory(pkgs)
            
    def fmt_whatconflicts(self, name, **kw):
        pkgs = []
        for sackstr in self._sacks:
            sack = getattr(self, sackstr)
            for pkg in sack.searchConflicts(name):
                pkgs.append(pkg)
        return self.queryPkgFactory(pkgs)

    def fmt_requires(self, name, **kw):
        pkgs = {}
        for pkg in self.returnByName(name):
            for req in pkg.prco("requires"):
                for res in self.fmt_whatprovides(req):
                    pkgs[(res.name, res.pkg.arch)] = res
        return pkgs.values()

    def fmt_location(self, name):
        loc = []
        for pkg in self.returnByName(name):
            repo = self.repos.getRepo(pkg['repoid'])
            if pkg['basepath']:
                loc.append("%s/%s" % (pkg['basepath'], pkg['relativepath']))
            else:
                loc.append("%s/%s" % (repo.urls[0], pkg['relativepath']))
        return loc


def main(args):

    needother = 0
    needgroup = 0
    needsource = 0

    signal.signal(signal.SIGPIPE, signal.SIG_DFL)
    signal.signal(signal.SIGINT, signal.SIG_DFL)

    parser = OptionParser(version = "Repoquery version %s" % version)
    # query options
    parser.add_option("-l", "--list", action="store_true",
                      help="list files in this package/group")
    parser.add_option("-i", "--info", action="store_true",
                      help="list descriptive info from this package/group")
    parser.add_option("-f", "--file", action="store_true",
                      help="query which package provides this file")
    parser.add_option("--qf", "--queryformat", dest="queryformat",
                      help="specify a custom output format for queries")
    parser.add_option("--groupmember", action="store_true",
                      help="list which group(s) this package belongs to")
    # dummy for rpmq compatibility
    parser.add_option("-q", "--query", action="store_true",
                      help="no-op for rpmquery compatibility")
    parser.add_option("-a", "--all", action="store_true",
                      help="query all packages/groups")
    parser.add_option("-R", "--requires", action="store_true",
                      help="list package dependencies")
    parser.add_option("--provides", action="store_true",
                      help="list capabilities this package provides")
    parser.add_option("--obsoletes", action="store_true",
                      help="list other packages obsoleted by this package")
    parser.add_option("--conflicts", action="store_true",
                      help="list capabilities this package conflicts with")
    parser.add_option("--changelog", action="store_true",
                      help="show changelog for this package")
    parser.add_option("--location", action="store_true",
                      help="show download URL for this package")
    parser.add_option("--nevra", action="store_true",
                      help="show name-epoch:version-release.architecture info of package")
    parser.add_option("--envra", action="store_true",
                      help="show epoch:name-version-release.architecture info of package")
    parser.add_option("--nvr", action="store_true",
                      help="show name, version, release info of package")
    parser.add_option("-s", "--source", action="store_true",
                      help="show package source RPM name")
    parser.add_option("--srpm", action="store_true",
                      help="operate on corresponding source RPM")
    parser.add_option("--resolve", action="store_true",
                      help="resolve capabilities to originating package(s)")
    parser.add_option("--alldeps", action="store_true", default=True,
                      help="check non-explicit dependencies (files and Provides:) as well, defaults to on")
    parser.add_option("--exactdeps", dest="alldeps", action="store_false",
                      help="check dependencies exactly as given, opposite of --alldeps")
    parser.add_option("--recursive", action="store_true",
                      help="recursively query for packages (for whatrequires)")
    parser.add_option("--whatprovides", action="store_true",
                      help="query what package(s) provide a capability")
    parser.add_option("--whatrequires", action="store_true",
                      help="query what package(s) require a capability")
    parser.add_option("--whatobsoletes", action="store_true",
                      help="query what package(s) obsolete a capability")
    parser.add_option("--whatconflicts", action="store_true",
                      help="query what package(s) conflicts with a capability")
    # group stuff
    parser.add_option("-g", "--group", default=0, action="store_true", 
                      help="query groups instead of packages")
    parser.add_option("--grouppkgs", default="default",
                      help="filter which packages (all,optional etc) are shown from groups")
    # other opts
    parser.add_option("--archlist",
                      help="only query packages of certain architecture(s)")
    parser.add_option("--releasever", default=None,
                      help="set value of $releasever in yum config and repo files")
    parser.add_option("--pkgnarrow", default="repos",
                      help="limit query to installed / available / recent / updates / extras / all (available + installed) / repository (default) packages")
    parser.add_option("--installed", action="store_true", default=False,
                      help="limit query to installed pkgs only")
    parser.add_option("--show-duplicates", action="store_true",
                      dest="show_dupes",
                      help="show all versions of packages")
    parser.add_option("--show-dupes", action="store_true",
                      help=SUPPRESS_HELP)
    parser.add_option("--repoid", action="append",
                      help="specify repoids to query, can be specified multiple times (default is all enabled)")
    parser.add_option("--enablerepo", action="append", dest="enablerepos",
                      help="specify additional repoids to query, can be specified multiple times")
    parser.add_option("--disablerepo", action="append", dest="disablerepos",
                      help="specify repoids to disable, can be specified multiple times")                      
    parser.add_option("--repofrompath", action="append",
                      help="specify repoid & paths of additional repositories - unique repoid and complete path required, can be specified multiple times. Example. --repofrompath=myrepo,/path/to/repo")
    parser.add_option("--plugins", action="store_true", default=False,
                      help="enable yum plugin support")
    parser.add_option("--quiet", action="store_true", 
                      help="quiet output, only error output to stderr (default enabled)", default=True)
    parser.add_option("--verbose", action="store_false",
                      help="verbose output (opposite of quiet)", dest="quiet")
    parser.add_option("-C", "--cache", action="store_true",
                      help="run from cache only")
    parser.add_option("--tempcache", action="store_true",
                      help="use private cache (default when used as non-root)")
    parser.add_option("--querytags", action="store_true",
                      help="list available tags in queryformat queries")
    parser.add_option("-c", "--config", dest="conffile",
                      help="config file location")
    parser.add_option("--tree-requires", action="store_true",
                      dest="tree_requires",
                      help=SUPPRESS_HELP)
    parser.add_option("--tree-conflicts", action="store_true",
                      dest="tree_conflicts",
                      help=SUPPRESS_HELP)
    parser.add_option("--tree-obsoletes", action="store_true",
                      dest="tree_obsoletes",
                      help=SUPPRESS_HELP)
    parser.add_option("--tree-whatrequires", action="store_true",
                      dest="tree_what_requires",
                      help=SUPPRESS_HELP)
    parser.add_option("--level", dest="tree_level", default="all",
                      help="levels to display (can be any number or 'all', default to 'all')")
    parser.add_option("--output", dest="output", default="text",
                      help="output format to use (can be text|ascii-tree|dot-tree, default to 'text')")
    parser.add_option("--search", action="store_true",
                      dest="search", default=False,
                      help="Use yum's search to return pkgs")
    parser.add_option("--search-fields", action="append", dest="searchfields",
                      default=[],
                      help="search fields to search using --search")
                      

    (opts, regexs) = parser.parse_args()

    if opts.querytags:
        querytags.sort()
        for tag in querytags:
            print tag
        sys.exit(0)

    if len(regexs) < 1:
        if opts.all:
            regexs = ['*']
        else:
            print parser.format_help()
            sys.exit(1)

    pkgops = []
    sackops = []
    archlist = None
    if opts.info:
        pkgops.append("info")
    if opts.requires:
        if opts.resolve:
            sackops.append("requires")
        else:
            pkgops.append("requires")
    if opts.provides:
        pkgops.append("provides")
    if opts.obsoletes:
        pkgops.append("obsoletes")
    if opts.conflicts:
        pkgops.append("conflicts")
    if opts.changelog:
        needother = 1
        pkgops.append("changelog")
    if opts.list:
        pkgops.append("list")
    if opts.envra:
        pkgops.append("envra")
    if opts.nvr:
        pkgops.append("nvr")
    if opts.source:
        pkgops.append("source")
    if opts.tree_requires:
        opts.output = "ascii-tree"
        pkgops.append("tree_requires")
    if opts.tree_conflicts:
        opts.output = "ascii-tree"
        pkgops.append("tree_conflicts")
    if opts.tree_obsoletes:
        opts.output = "ascii-tree"
        pkgops.append("tree_obsoletes")
    if opts.tree_what_requires:
        opts.output = "ascii-tree"
        pkgops.append("tree_what_requires")
    if opts.output == "dot-tree":
        opts.dot = DotPlot()
    else:
        opts.dot = None
    if opts.srpm:
        needsource = 1
    if opts.whatrequires:
        sackops.append("whatrequires")
        if opts.output != 'text':
            pkgops.append("tree_what_requires")
    if opts.whatprovides:
        sackops.append("whatprovides")
    if opts.whatobsoletes:
        sackops.append("whatobsoletes")
    if opts.whatconflicts:
        sackops.append("whatconflicts")
    if opts.file:
        sackops.append("whatprovides")
    if opts.location:
        pkgops.append("location")
    if opts.groupmember:
        needgroup = 1
    if opts.group:
        needgroup = 1
    if opts.installed:
        opts.pkgnarrow = 'installed'
        opts.disablerepos = ['*']

    if opts.nevra:
        pkgops.append("nevra")
    elif len(pkgops) == 0 and len(sackops) == 0:
        pkgops.append("queryformat")

    for exp in regexs:
        if exp.endswith('.src'):
            needsource = 1
            break

    if opts.archlist:
        archlist = opts.archlist.split(',')
    elif needsource:
        archlist = getArchList()
        archlist.append('src')

    if opts.searchfields:
        opts.search = True
        
    repoq = YumBaseQuery(pkgops, sackops, opts)

    # silence initialisation junk from modules etc unless verbose mode
    initnoise = (not opts.quiet) * 2
    repoq.preconf.releasever = opts.releasever
    if archlist and not archlist[0] == 'src':
        repoq.preconf.arch = archlist[0]
    if opts.conffile is not None:
        repoq.preconf.fn = opts.conffile
    repoq.preconf.debuglevel = initnoise
    repoq.preconf.init_plugins = opts.plugins
    repoq.conf

    if opts.repofrompath:
        # setup the fake repos
        for repo in opts.repofrompath:
            tmp = tuple(repo.split(','))
            if len(tmp) != 2:
                repoq.logger.error("Error: Bad repofrompath argument: %s" %repo)
                continue
            repoid,repopath = tmp
            if repopath[0] == '/':
                baseurl = 'file://' + repopath
            else:
                baseurl = repopath
                
            repoq.add_enable_repo(repoid, baseurls=[baseurl], 
                    basecachedir=repoq.conf.cachedir)
            if not opts.quiet:
                repoq.logger.info( "Added %s repo from %s" % (repoid,repopath))

        
    # Show what is going on, if --quiet is not set.
    if not opts.quiet and sys.stdout.isatty():
        yumout = output.YumOutput()
        freport = ( yumout.failureReport, (), {} )
        if hasattr(repoq, 'prerepoconf'):
            repoq.prerepoconf.progressbar = TextMeter(fo=sys.stdout)
            repoq.prerepoconf.callback = output.CacheProgressCallback()
            repoq.prerepoconf.failure_callback = freport
        else:
            repoq.repos.setProgressBar(TextMeter(fo=sys.stdout))
            repoq.repos.callback = output.CacheProgressCallback()
            repoq.repos.setFailureCallback(freport)
    
    if not repoq.setCacheDir(opts.tempcache):
        repoq.logger.error("Error: Could not make cachedir, exiting")
        sys.exit(50)

    if opts.cache:
        repoq.conf.cache = True
        if not opts.quiet:
            repoq.logger.info('Running from cache, results might be incomplete.')
        

    if opts.show_dupes:
        repoq.conf.showdupesfromrepos = True
            
    if opts.repoid:
        found_repos = set()
        for repo in repoq.repos.findRepos('*'):
            if repo.id not in opts.repoid:
                repo.disable()
            else:
                found_repos.add(repo.id)
                repo.enable()
        for not_found in set(opts.repoid).difference(found_repos):
            repoq.logger.error('Repoid %s was not found.' % not_found)

    if opts.disablerepos:
        for repo_match in opts.disablerepos:
            for repo in repoq.repos.findRepos(repo_match):
                repo.disable()

    if opts.enablerepos:    
        for repo_match in opts.enablerepos:
            for repo in repoq.repos.findRepos(repo_match):
                repo.enable()

    try:
        if not hasattr(repoq, 'arch'):
            repoq.doSackSetup(archlist=archlist)
        elif archlist is not None:
            repoq.arch.archlist = archlist

        #  Don't do needfiles, because yum will do it automatically and it's
        # not trivial to get it "right" so we don't download them when not
        # needed.
        if needother:
            repoq.repos.populateSack(mdtype='otherdata')
        if needgroup:
            repoq.doGroupSetup()
    except (yum.Errors.RepoError, yum.Errors.GroupsError), e:
        repoq.logger.error(e)
        sys.exit(1)
    try:
        repoq.runQuery(regexs)
    except queryError, e:
        repoq.logger.error(e)

if __name__ == "__main__":
    misc.setup_locale()
    main(sys.argv)
                
# vim:sw=4:sts=4:expandtab              
