#!/usr/bin/python
#
# Copyright (C) 2011, Stefano Rivera <stefanor@ubuntu.com>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import optparse
import sys

from distro_info import DistroDataOutdated

from ubuntutools.logger import Logger
from ubuntutools.misc import (system_distribution, vendor_to_distroinfo,
                              codename_to_distribution)
from ubuntutools.rdepends import query_rdepends, RDependsException

DEFAULT_MAX_DEPTH = 10  # We want avoid any infinite loop...


def main():
    system_distro_info = vendor_to_distroinfo(system_distribution())()
    try:
        default_release = system_distro_info.devel()
    except DistroDataOutdated as e:
        Logger.warn(e)
        default_release = 'unstable'

    parser = optparse.OptionParser(
        '%prog [options] package',
        description="List reverse-dependencies of package. "
                    "If the package name is prefixed with src: then the "
                    "reverse-dependencies of all the binary packages that "
                    "the specified source package builds will be listed.")
    parser.add_option('-r', '--release', metavar='RELEASE',
                      default=default_release,
                      help='Query dependencies in RELEASE. '
                      'Default: %s' % default_release)
    parser.add_option('-R', '--without-recommends',
                      action='store_false', dest='recommends', default=True,
                      help='Only consider Depends relationships, '
                           'not Recommends')
    parser.add_option('-s', '--with-suggests',
                      action='store_true', dest='suggests', default=False,
                      help='Also consider Suggests relationships')
    parser.add_option('-b', '--build-depends',
                      action='store_const', dest='arch', const='source',
                      help='Query build dependencies (synonym for --arch=source)')
    parser.add_option('-a', '--arch', metavar='ARCH', default='any',
                      help='Query dependencies in ARCH. '
                           'Default: any')
    parser.add_option('-c', '--component', metavar='COMPONENT',
                      action='append',
                      help='Only consider reverse-dependencies in COMPONENT. '
                           'Can be specified multiple times. Default: all')
    parser.add_option('-l', '--list',
                      action='store_true', default=False,
                      help='Display a simple, machine-readable list')
    parser.add_option('-u', '--service-url', metavar='URL',
                      dest='server', default=None,
                      help='Reverse Dependencies webservice URL. '
                           'Default: UbuntuWire')
    parser.add_option('-x', '--recursive',
                      action='store_true', dest='recursive', default=False,
                      help='Consider to find reverse dependencies recursively.')
    parser.add_option('-d', '--recursive-deph', type="int",
                      metavar='RECURSIVE_DEPTH', dest='recursive_depth', default=DEFAULT_MAX_DEPTH,
                      help='If recusive, you can specify the depth.')

    options, args = parser.parse_args()

    if len(args) != 1:
        parser.error("One (and only one) package must be specified")
    package = args[0]

    opts = {}
    if options.server is not None:
        opts['server'] = options.server

    # Convert unstable/testing aliases to codenames:
    distribution = codename_to_distribution(options.release)
    if not distribution:
        parser.error('Unknown release codename %s' % options.release)
    distro_info = vendor_to_distroinfo(distribution)()
    try:
        options.release = distro_info.codename(options.release,
                                               default=options.release)
    except DistroDataOutdated:
        # We already printed a warning
        pass

    def query(package):
        try:
            return query_rdepends(package, options.release, options.arch, **opts)
        except RDependsException as e:
            Logger.error(str(e))
            sys.exit(1)

    def filter_out_fiels(data, fields):
        for field in data.keys():
            if field not in fields:
                del data[field]

    def filter_out_component(data, component):
        for field, rdeps in data.items():
            filtered = [rdep for rdep in rdeps
                        if rdep['Component'] in component]
            if not filtered:
                del data[field]
            else:
                data[field] = filtered

    if options.arch == 'source':
        fields = ['Reverse-Build-Depends', 'Reverse-Build-Depends-Indep']
    else:
        fields = ['Reverse-Depends']
        if options.recommends:
            fields.append('Reverse-Recommends')
        if options.suggests:
            fields.append('Reverse-Suggests')

    def build_results(package, result, fields, component, recursive):
        data = query(package)
        if not data:
            return

        result[package] = data

        if fields:
            filter_out_fiels(result[package], fields)
        if component:
            filter_out_component(result[package], component)

        if recursive > 0:
            for rdeps in result[package].itervalues():
                for rdep in rdeps:
                    build_results(
                        rdep['Package'], result, fields, component, recursive - 1)

    result = {}
    build_results(
        package, result, fields, options.component,
        options.recursive and options.recursive_depth or 0)

    if options.list:
        display_consise(result)
    else:
        display_verbose(package, result)


def display_verbose(package, values):
    if not values:
        print("No reverse dependencies found")
        return

    def print_field(field):
        print(field)
        print('=' * len(field))

    def print_package(values, package, arch, dependency, offset=0):
        line = '  ' * offset + '* %s' % package
        if all_archs and set(arch) != all_archs:
            line += ' [%s]' % ' '.join(sorted(arch))
        if dependency:
            if len(line) < 30:
                line += ' ' * (30 - len(line))
                line += '  (for %s)' % dependency
        print(line)
        data = values.get(package)
        if data:
            offset = offset + 1
            for rdeps in data.itervalues():
                for rdep in rdeps:
                    print_package(values,
                                  rdep['Package'],
                                  rdep['Architectures'],
                                  rdep.get('Dependency'),
                                  offset)

    all_archs = set()
    # This isn't accurate, but we make up for it by displaying what we found
    for data in values.itervalues():
        for rdeps in data.itervalues():
            for rdep in rdeps:
                if 'Architectures' in rdep:
                    all_archs.update(rdep['Architectures'])

    for field, rdeps in values[package].iteritems():
        print_field(field)
        rdeps.sort(key=lambda x: x['Package'])
        for rdep in rdeps:
            print_package(values,
                          rdep['Package'],
                          rdep['Architectures'],
                          rdep.get('Dependency'))
        print

    if all_archs:
        print("Packages without architectures listed are "
              "reverse-dependencies in: %s"
              % ', '.join(sorted(list(all_archs))))


def display_consise(values):
    result = set()
    for data in values.itervalues():
        for rdeps in data.itervalues():
            for rdep in rdeps:
                result.add(rdep['Package'])

    print(u'\n'.join(sorted(list(result))))


if __name__ == '__main__':
    main()
