#!/usr/bin/python3

import argparse

# This file provides the necessary data and functions to deal with the
# Coq-related packages in Debian ; it's the basic foundation to be
# used in various tools

#
# FIRST PART: DATA
#

# here is the complete list of Coq-related src:packages in Debian
#
# J.Puydt: I don't see how to compute it... detecting B-Dep on dh-coq
# is the way to go, but I don't see how to get this information in the
# python3-apt API... this page can help maintain this up to date:
# https://release.debian.org/transitions/html/coq.html

known_sources = [

    # level 1

    'coq', 'elpi',

    # level 2

    'aac-tactics', 'coq-bignums', 'coq-dpdgraph', 'coq-elpi',
    'coq-ext-lib', 'coq-hammer', 'coq-hott', 'coq-libhyps',
    'coq-menhirlib', 'coq-record-update', 'coq-reduction-effects',
    'coq-stdpp', 'coq-unicoq', 'coq-unimath', 'flocq', 'ott',
    'paramcoq', 'coq-serapi',

    # level 3

    'coq-equations', 'coq-gappa', 'coq-hierarchy-builder', 'coq-iris',
    'coq-math-classes', 'coq-mtac2', 'coq-simple-io', 'coqprime',

    # level 4

    'ssreflect', 'coq-corn', 'metacoq',

    # level 5

    'coq-deriving', 'coq-reglang', 'coq-relation-algebra',
    'coquelicot', 'mathcomp-bigenough', 'mathcomp-finmap',
    'mathcomp-zify', 'coq-quickchick',

    # level 6

    'coq-extructures', 'coq-interval',
    'mathcomp-algebra-tactics', 'mathcomp-analysis',
    'mathcomp-multinomials', 'mathcomp-real-closed',

    # level 7

    'coqeal' #, 'mathcomp-abel'

]

# dictionary - the keys are source package names in Debian, and the
# values are triples (switches, package, name) from which the test
# will be to run coqtop with those switches and then: From package
# Require Import name.
known_tests = {
    'aac-tactics': (None, 'AAC_tactics', 'AAC'),
    'coq': (None, 'Coq', 'Reals'),
    'coq-bignums': (None, 'Bignums', 'BigZ'),
    'coq-corn': (None, 'CoRN', 'fta.FTA'),
    'coq-deriving': (None, 'deriving', 'deriving'),
    'coq-dpdgraph': (None, 'dpdgraph', 'dpdgraph'),
    'coqeal': (None, 'CoqEAL', 'theory.strassen'),
    'coq-elpi': (None, 'elpi','elpi'),
    'coq-equations': (None, 'Equations', 'Init'),
    'coq-ext-lib': (None, 'ExtLib', 'Structures.Monad'),
    'coq-extructures': (None, 'extructures', 'fmap'),
    'coq-gappa': (None, 'Gappa', 'Gappa_common'),
    'coq-hammer': (None, 'Hammer', 'Plugin.Hammer'),
    'coq-hierarchy-builder': (None, 'HB', 'structures'),
    'coq-hott': (None, 'HoTT', 'Basics.Nat'),
    'coq-interval': (None, 'Interval', 'Tactic'),
    'coq-iris': (None, 'iris', 'prelude'),
    'coq-libhyps': (None, 'LibHyps', 'LibHyps'),
    'coq-math-classes': (None, 'MathClasses', 'theory.functors'),
    'coq-menhirlib': (None, 'MenhirLib', 'Version'),
    'coq-mtac2': (None, 'Mtac2', 'Mtac2'),
    'coqprime': (None, 'Coqprime', 'PrimalityTest.Pocklington'),
    'coq-quickchick': (None, 'QuickChick', 'QuickChick'),
    'coq-record-update': (None, 'RecordUpdate', 'RecordUpdate'),
    'coq-reduction-effects': (None, 'ReductionEffect', 'PrintingEffect'),
    'coq-reglang': (None, 'RegLang', 'shepherdson'),
    'coq-relation-algebra': (None, 'RelationAlgebra', 'all'),
    'coq-simple-io': (None, 'SimpleIO', 'SimpleIO'),
    'coq-stdpp': (None, 'stdpp', 'base'),
    'coquelicot': (None, 'Coquelicot', 'Coquelicot'),
    'coq-unicoq': (None, 'Unicoq', 'Unicoq'),
    'coq-unimath': ('-type-in-type', 'UniMath', 'All'),
    'elpi': (None, 'Coq', 'Reals'),
    'flocq': (None, 'Flocq', 'Version'),
    # 'mathcomp-abel': (None, 'Abel', 'abel'),
    'mathcomp-algebra-tactics': (None, 'mathcomp.algebra_tactics', 'ring'),
    'mathcomp-analysis': (None, 'mathcomp.analysis', 'topology'),
    'mathcomp-bigenough': (None, 'mathcomp', 'bigenough'),
    'mathcomp-finmap': (None, 'mathcomp', 'finmap'),
    'mathcomp-multinomials': (None, 'mathcomp.multinomials', 'mpoly'),
    'mathcomp-real-closed': (None, 'mathcomp.real_closed', 'complex'),
    'mathcomp-zify': (None, 'mathcomp', 'zify'),
    'metacoq': (None, 'MetaCoq.Template', 'All'),
    'ott': (None, 'Ott', 'ott_list'),
    'paramcoq': (None, 'Param', 'Param'),
    'ssreflect': (None, 'mathcomp', 'ssreflect'),
}


#
# SECOND PART: FUNCTIONS
#

import apt
cache = apt.Cache()

def check_known_tests():
    for pkg in known_sources:
        if not pkg in known_tests:
            print(f"- {pkg} doesn't have a corresponding test to run!")

def guess_binary_name(srcname):
    if srcname == 'coq':
        return 'libcoq-stdlib'
    if srcname == 'coq-serapi':
        return 'libcoq-serapi-ocaml'
    if srcname == 'elpi':
        return 'libelpi-ocaml'
    if srcname == 'ssreflect':
        return 'libcoq-mathcomp-ssreflect'
    if srcname.startswith('coq-'):
        return 'lib' + srcname
    return 'libcoq-' + srcname

def get_source_name(pkgname):
    pkg = cache[pkgname]
    version = pkg.versions[0]
    return version.source_name

def check_get_source_name():
    for srcname in known_sources:
        pkgname = guess_binary_name(srcname)
        guessed = get_source_name(pkgname)
        if guessed !=  srcname:
            print(f"- For {srcname} I guessed binary name {pkgname} then source {guessed} and it's not correct")
            return False
    return True

def guess_deps(srcname):
    pkg = cache[guess_binary_name(srcname)].candidate

    if srcname in ['coq', 'elpi']:
        res = []
    else:
        res = ['coq']

    for dep in pkg.get_dependencies('Depends'):
        rawname = dep[0].name
        # we look for a 'libcoq-foo-deadbeef' provided by 'libcoq-foo'
        if not rawname.startswith('libcoq-'):
            if rawname == 'libelpi-ocaml-dev':
                res.append('elpi')
            else:
                continue
        try:
            pkgname = rawname[:rawname.rfind('-')]
            deppkg = cache[pkgname] # read just to detect non-existing
            depsrcname = get_source_name(pkgname)
            if depsrcname != srcname and depsrcname not in res and not depsrcname.endswith('-ocaml'):
                res.append(depsrcname)
        except:
            continue
    return res

coq_packages_deps = {srcname: guess_deps(srcname) for srcname in known_sources}

def group_by(ll, criterium):
    if ll == []:
        return []
    res = []
    val = criterium(ll[0])
    chunk = []
    for obj in ll:
        if criterium(obj) == val:
            chunk.append(obj)
        else:
            val = criterium(obj)
            res.append(chunk)
            chunk = [obj]
    res.append(chunk)
    return res

def hauteur(paquet):
    deps = coq_packages_deps[paquet]
    if deps == []:
        return 0
    return 1 + max([hauteur(child) for child in deps])

def transitive_deps(paquets):
    res = set(paquets)
    new_len = 1
    old_len = 0
    while new_len > old_len:
        old_len = len(res)
        neuu = set()
        for pkg in res:
            neuu.update(coq_packages_deps[pkg])
        res.update(neuu)
        new_len = len(res)
    res = list(res)
    res.sort(key=hauteur)
    return res

def transitive_rdeps(paquets):
    res = paquets
    found_something = True
    while found_something:
        found_something = False
        neuu = []
        for other, deps in coq_packages_deps.items():
            ens = set(deps)
            ens.intersection_update(res)
            if ens != set() and other not in res:
                found_something = True
                neuu.append(other)
        res.extend(neuu)
    res.sort(key=hauteur)
    return res

def minimal_to_install_all():
    res = ['coqide']
    for pkgname in coq_packages_deps.keys():
        if transitive_rdeps([pkgname]) == [pkgname]:
            res.append(guess_binary_name(pkgname))
    return ' '.join(res)

def next_binary_version(version):
    binindex = version.find('+b')
    if binindex == -1:
        return version + '+b1'
    return version[:binindex]+ '+b' + str(int(version[binindex+2:])+1)

def run_checks():
    print('List of issues:')
    check_known_tests()
    check_get_source_name()
    print('done.')

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Basic functions')
    parser.add_argument('-rc', '--run-checks', action='store_true',
                        help='run basic checks on the package list')
    parser.add_argument('-m', '--minimal', action='store_true',
                        help='list the leaf packages for the Coq ecosystem in Debian')
    args = parser.parse_args()
    if args.run_checks:
        run_checks()
    if args.minimal:
        print(minimal_to_install_all())
