#!/usr/bin/python3
#
# 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 argparse
import sys

from distro_info import DistroDataOutdated

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

from ubuntutools import getLogger
Logger = getLogger()

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.warning(e)
        default_release = 'unstable'

    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 = argparse.ArgumentParser(description=description)
    parser.add_argument('-r', '--release', default=default_release,
                        help='Query dependencies in RELEASE. '
                        'Default: %s' % default_release)
    parser.add_argument('-R', '--without-recommends', action='store_false',
                        dest='recommends',
                        help='Only consider Depends relationships, '
                        'not Recommends')
    parser.add_argument('-s', '--with-suggests', action='store_true',
                        help='Also consider Suggests relationships')
    parser.add_argument('-b', '--build-depends', action='store_true',
                        help='Query build dependencies (synonym for --arch=source)')
    parser.add_argument('-a', '--arch', default='any',
                        help='Query dependencies in ARCH. Default: any')
    parser.add_argument('-c', '--component', action='append',
                        help='Only consider reverse-dependencies in COMPONENT. '
                        'Can be specified multiple times. Default: all')
    parser.add_argument('-l', '--list', action='store_true',
                        help='Display a simple, machine-readable list')
    parser.add_argument('-u', '--service-url', metavar='URL',
                        dest='server', default=None,
                        help='Reverse Dependencies webservice URL. '
                        'Default: UbuntuWire')
    parser.add_argument('-x', '--recursive', action='store_true',
                        help='Consider to find reverse dependencies recursively.')
    parser.add_argument('-d', '--recursive-depth', type=int,
                        default=DEFAULT_MAX_DEPTH,
                        help='If recusive, you can specify the depth.')
    parser.add_argument('package')

    options = parser.parse_args()

    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 logged a warning
        pass

    if options.build_depends:
        options.arch = 'source'

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

    def build_results(package, result, fields, component, recursive):
        try:
            data = query_rdepends(package, options.release, options.arch, **opts)
        except RDependsException as e:
            Logger.error(str(e))
            sys.exit(1)
        if not data:
            return

        if fields:
            data = {k: v for k, v in data.items() if k in fields}
        if component:
            data = {k: [rdep for rdep in v
                        if rdep['Component'] in component]
                    for k, v in data.items()}
            data = {k: v for k, v in data.items() if v}

        result[package] = data

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

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

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


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

    def log_field(field):
        Logger.info(field)
        Logger.info('=' * len(field))

    def log_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
        Logger.info(line)
        data = values.get(package)
        if data:
            offset = offset + 1
            for rdeps in data.values():
                for rdep in rdeps:
                    log_package(values,
                                rdep['Package'],
                                rdep.get('Architectures', all_archs),
                                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.values():
        for rdeps in data.values():
            for rdep in rdeps:
                if 'Architectures' in rdep:
                    all_archs.update(rdep['Architectures'])

    for field, rdeps in values[package].items():
        Logger.info(field)
        rdeps.sort(key=lambda x: x['Package'])
        for rdep in rdeps:
            log_package(values,
                        rdep['Package'],
                        rdep.get('Architectures', all_archs),
                        rdep.get('Dependency'))
        Logger.info("")

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


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

    Logger.info('\n'.join(sorted(list(result))))


if __name__ == '__main__':
    main()