#!/usr/bin/env python3
# remove virtual packages from disjunctions with real packages
# the idea to clean up a repository like this came from Helmut Grohne
#
# applying this solution to the problem might create uninstallable packages due
# to conflicts:
#
# Package: foo
# Depends: A | B, C
#
# Package: bar
# Provides: B
#
# Package: A
#
# Package: C
# Depends: D
#
# Package: D
# Conflicts: A
#
# In above case, B would be removed from the disjunction of package "foo",
# leaving only A. But through D, C conflicts with A, making "foo"
# uninstallable

import sys
sys.path.append('/usr/share/botch')
from util import get_fh_out, read_tag_file
import re


def extract_name_ver(pkgrel):
    m = re.match(
        r"\s*([a-zA-Z0-9][a-zA-Z0-9+.-]*)(?::[a-zA-Z0-9][a-zA-Z0-9-]*)?" +
        r"(?:\s+\(\s*(<<|<=|=|>=|>>|<|>).*?\))?.*", pkgrel)
    return (pkgrel, m.group(1), m.group(2))


def cleandisj(disj, real_pkgs, virtual_pkgs):
    # do not handle simple disjunctions
    if len(disj) == 1:
        return disj
    res = []
    # reduce the disjunction to dependencies on real packages
    extdisj = [extract_name_ver(pkgrel) for pkgrel in disj]
    for pkgrel, pkg, ver in extdisj:
        # versioned dependencies must be provided by real packages
        if not ver and pkg not in real_pkgs:
            continue
        res.append(pkgrel)
    if not res:
        # if nothing remains return first virtual dependency that is provided
        # by something or first versioned dependency
        for pkgrel, pkg, ver in extdisj:
            if ver or pkg in virtual_pkgs:
                return [pkgrel]
        # no package satisfying this disjunction was found so return original
        return disj
    else:
        # if one or more real package remains, return only first
        return [res[0]]


def cleansrcdisj(disj, real_pkgs, virtual_pkgs):
    # for source packages, remove all but the first part of the disjunction as
    # done by sbuild in setup_apt_archive in lib/Sbuild/ResolverBase.pm
    # do not handle simple disjunctions
    if len(disj) == 1:
        return disj
    extdisj = [extract_name_ver(pkgrel) for pkgrel in disj]
    res = [extdisj[0]]
    # keep disjunctions like "foo (rel x) | foo (rel y)"
    if res[0][2]:
        for pkgrel, pkg, ver in extdisj[1:]:
            # compare package name with the first
            if ver and pkg == res[0][1]:
                res.append((pkgrel,))
    return [r[0] for r in res]


def modify_field(field, real_pkgs, virtual_pkgs, cleanfunc):
    # we do it without regexes so that we can assemble the original
    # dependency string later
    deps = [[pkgrel for pkgrel in disj.split('|')]
            for disj in field.split(',')]
    deps = [cleanfunc(disj, real_pkgs, virtual_pkgs) for disj in deps]
    return ",".join(["|".join(disj) for disj in deps])


def remove_virtual_disjunctions(
        inPackages, inSources, outPackages, outSources, remove_nonvirtual,
        verbose=False):
    real_pkgs = set()
    virtual_pkgs = set()
    # create a list of real packages
    for pkg in inPackages:
        real_pkgs.add(pkg['Package'])
        if not pkg.get('Provides'):
            continue
        for pkg in pkg['Provides'].split(','):
            virtual_pkgs.add(pkg.strip())
    # modify binary dependencies
    for pkg in inPackages:
        for field in ['Depends', 'Pre-Depends']:
            if pkg.get(field):
                if remove_nonvirtual:
                    # if --remove-nonvirtual was given, use source package
                    # behaviour for binary packages as well
                    pkg[field] = modify_field(
                        pkg[field], real_pkgs, virtual_pkgs, cleansrcdisj)
                else:
                    pkg[field] = modify_field(
                        pkg[field], real_pkgs, virtual_pkgs, cleandisj)
    # modify build dependencies
    for pkg in inSources:
        for field in ['Build-Depends', 'Build-Depends-Indep',
                      'Build-Depends-Arch']:
            if pkg.get(field):
                pkg[field] = modify_field(
                    pkg[field], real_pkgs, virtual_pkgs, cleansrcdisj)
    with outPackages as outfile:
        for pkg in inPackages:
            pkg.dump(outfile)
            outfile.write(b"\n")
    with outSources as outfile:
        for pkg in inSources:
            pkg.dump(outfile)
            outfile.write(b"\n")
    return True


if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser(
        description=("remove virtual packages from disjunctions with real " +
                     "packages"))
    parser.add_argument(
        'inPackages', type=read_tag_file, help='input Packages files')
    parser.add_argument(
        'inSources', type=read_tag_file, help='input Sources files')
    parser.add_argument(
        'outPackages', type=get_fh_out, help='output Packages file')
    parser.add_argument(
        'outSources', type=get_fh_out, help='output Sources file')
    parser.add_argument('--remove-nonvirtual', action='store_true',
                        help="also remove all but the first alternative from "
                             "binary packages. This applies the same "
                             "algorithm to binary packages as is applied to "
                             "source packages")
    parser.add_argument('--verbose', action='store_true', help='be verbose')
    args = parser.parse_args()
    ret = remove_virtual_disjunctions(args.inPackages, args.inSources,
                                      args.outPackages, args.outSources,
                                      args.remove_nonvirtual, args.verbose)
    exit(not ret)
