#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-only
# Copyright (c) 2019-2020 Max Rees
# See LICENSE for more information.
import argparse # ArgumentParser
import sys      # exit

import apkfoundry           # proj_conf
import apkfoundry.container # Container
import apkfoundry.digraph   # generate_graph
import apkfoundry._log as _log
import apkfoundry._util as _util

_log.init()

def dot(opts, graph, func, recurse=False):
    if opts.dot:
        print("digraph \"af-depgraph\" {")
        print("  rankdir=RL")
        print("  overlap=scale")
        def print_rel(dep, rdep):
            print(f"  \"{rdep}\" -> \"{dep}\"")
        for pkg in opts.startdirs:
            print(
                f"  \"{pkg}\"",
                "[ style=filled, fillcolor=blue, fontcolor=white ]",
            )

    else:
        def print_rel(dep, rdep):
            print(rdep, "->", dep)

    for line in func(opts, graph, recurse):
        print_rel(*line)

    if opts.dot:
        print("}")

# Note: all_downstreams is recursive, while predecessors is not.
# Therefore we want to perform all_downstreams on a copy of the prune
# list ([:]), while with predecessors we let it recurse by growing the
# list we're iterating over.
def print_rdeps(opts, graph, recurse):
    if recurse:
        for pkg in opts.startdirs[:]:
            opts.startdirs += graph.all_downstreams(pkg)

    for pkg, rdeps in graph.graph.items():
        if opts.startdirs and pkg not in opts.startdirs:
            continue
        for rdep in rdeps:
            yield pkg, rdep

def print_deps(opts, graph, recurse):
    if recurse:
        for pkg in opts.startdirs:
            opts.startdirs += graph.predecessors(pkg)

    for pkg, rdeps in graph.graph.items():
        for rdep in rdeps:
            if opts.startdirs and rdep not in opts.startdirs:
                continue
            yield pkg, rdep

def tsort(opts, graph):
    order = graph.topological_sort()
    if opts.startdirs:
        order = [i for i in order if i in opts.startdirs]
    print("\n".join(order))

def dot_arg(parser):
    parser.add_argument(
        "-g", "--graphviz", dest="dot", action="store_true",
        help="print DOT output for Graphviz",
    )

def startdirs_arg(parser):
    parser.add_argument(
        "startdirs", metavar="STARTDIR", nargs="*",
        help="prune graph to examine only these STARTDIRs",
    )

def add_subcmd(parser, name, func=None, startdirs=True, dot=False, **kwargs):
    subcmd = parser.add_parser(
        name, **kwargs,
        prog=f"af-depgraph [options ...] {name}",
    )
    if func:
        subcmd.set_defaults(func=func)
    if dot:
        dot_arg(subcmd)
    if startdirs:
        startdirs_arg(subcmd)
    return subcmd

getopts = argparse.ArgumentParser(
    usage="af-depgraph [options ...] CMD [STARTDIR ...]",
)
getopts.add_argument(
    "-c", "--container", metavar="CDIR",
    help="execute inside container CDIR",
)
getopts.add_argument(
    "-s", "--skip-check", action="store_true",
    help="do not consider $checkdepends",
)
cmds = getopts.add_subparsers(
    metavar="CMD", dest="cmd",
    help="subcommand to run",
)

add_subcmd(
    cmds, "deps", dot=True,
    help="print the dependencies of each STARTDIR",
    func=lambda opts, graph: dot(opts, graph, print_deps),
)
add_subcmd(
    cmds, "all-deps", dot=True,
    help="print all packages on which each STARTDIR ultimately depends",
    func=lambda opts, graph: dot(opts, graph, print_deps, recurse=True),
)
add_subcmd(
    cmds, "rdeps", dot=True,
    help="""print the packages which depend on each STARTDIR (i.e. the
    reverse dependencies)""",
    func=lambda opts, graph: dot(opts, graph, print_rdeps),
)
add_subcmd(
    cmds, "all-rdeps", dot=True,
    help="""print all packages which ultimately depend on each
    STARTDIR""",
    func=lambda opts, graph: dot(opts, graph, print_rdeps, recurse=True),
)

add_subcmd(
    cmds, "acyclic",
    help="exit 0 if graph is acyclic only",
    func=lambda _, graph: sys.exit(0) if graph.is_acyclic() else sys.exit(2),
)
add_subcmd(
    cmds, "build-order",
    help="""consider STARTDIRs as a list of packages to build and output
    the topological sort build order""",
    func=tsort,
)
add_subcmd(
    cmds, "no-deps",
    help="print packages with no dependencies",
    func=lambda _, graph: print("\n".join(graph.ind_nodes())),
)
add_subcmd(
    cmds, "no-rdeps",
    help="print packages with no reverse dependencies",
    func=lambda _, graph: print("\n".join(graph.all_leaves())),
)

opts = getopts.parse_args()
if not hasattr(opts, "func"):
    getopts.print_help()
    sys.exit(1)

conf = apkfoundry.proj_conf(None, _util.get_branch())
if opts.container:
    cont = apkfoundry.container.Container(opts.container, sudo=False)
else:
    cont = None

graph = apkfoundry.digraph.generate_graph(
    conf,
    use_ignore=opts.cmd in ("acyclic", "build-order"),
    cont=cont,
    skip_check=opts.skip_check,
)
if graph is None:
    sys.exit(3)

opts.func(opts, graph)
