# Generate ontology visualization for the ASG
#
#  Copyright (C) 2014-2023 Ryan Specialty, LLC.
#
#  This file is part of TAME.
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

# Use the sibling `asg-ontvis` script to invoke this for you with the
#   necessary files.
#
# The relationships between objects on the ASG are defined declaratively
#   using the `object_rel!` macro.
# They follow this syntax:
#
#   object_rel! {
#       Source -> {
#           tree TargetA,
#           tree TargetB,
#           cross TargetC,
#       }
#   }
#
# This script expects to receive a list of files containing such
#   definitions.
# It will output,
#   to stdout,
#   a DOT definition representing the ASG's ontology,
#     which can be used to render a visualization.
#
# To render the output of this script,
#   pipe it to e.g. `dot -Tsvg > out.svg`.
#
# This tool is supposed to generate human-readable DOT output;
#   please retain that quality in any changes you make.
# This is helpful not only to help the user figure out what output they're
#   seeing,
#     but is also useful because the DOT output is quite understandable by
#     itself without the need to generate a graphical representation,
#       depending on the information the user is looking for.

BEGIN {
    print "# Representation of the TAMER ASG ontology generated by asg-ontviz."
    print "# Generate visualization using e.g. `dot -Tsvg > out.svg`."

    print "digraph {"

    # Whether we're parsing a `object_rel!` block.
    in_block = 0

    # The current source relation, if `in_block_subexpr`.
    block_src = ""

    # The final exit status (0 = success)
    exit_code = 0
}

BEGINFILE {
    # Whether any object relationships were found in the file.
    # This is used in `ENDFILE` to notify the user that something may be
    #   wrong.
    found_rels = 0
}

# Predicates will be reset for each line,
#   allowing the remainder of the script to be read more declaratively.
{ in_block = in_block_subexpr = 0 }

# Block predicates for each line of text.
/^object_rel! {$/, /^}$/ { in_block = 1 }

# `Foo -> {` line declares the source of the relation.
in_block && /->/ {
    block_src = $1

    printf "  # `%s` from `%s:%d`\n", block_src, FILENAME, FNR

    next
}

# A closing curly brace always means that we've finished with the current
#   source relation,
#     since we're at the innermost level of nesting.
block_src && /}/ {
    block_src = ""
    print ""
}

# "// empty" means that the lack of edges is intentional.
block_src && /^ *\/\/ empty$/ {
    # Suppress error from the edge check below.
    found_rels++
}

# Skip comments.
/^ *\/+/ { next }

# For each target object,
#   output a relation.
#
# The DOT format is pretty rigid;
#   because we are styling,
#     we cannot use curly braces similar to how the relation is defined in
#     Rust using the `object_rel!` macro;
#       we must independently define each one.
# But that's okay;
#   the output is quite legible.
block_src && $NF ~ /\w+,$/ {
    # Edge type (cross, tree)
    ty = $(NF-1)

    # Dashed is visually in-between solid and dotted,
    #   and `dyn` is either `tree` or `cross`,
    #     determined at runtime.
    # But we may need some other representation if `dotted` is too visually
    #   sparse and difficult/annoying to see;
    #     let's see where the ASG visualization ends up first.
    attrs = ""
    switch (ty) {
        case "tree":
            attrs="[style=solid,arrowhead=normal]";
            break;
        case "cross":
            attrs="[style=dotted,arrowhead=open]";
            break;
        case "dyn":
            attrs="[style=dashed,arrowhead=normal]";
            break;
    }

    gsub(/,$/, "")

    # This may need updating over time as object names in Rust sources
    #   exceed the fixed-width definition here.
    # This output is intended to form a table that is easy to read and
    #   visually scan.
    # For this reason,
    #   the source object is right-aligned and target is left-aligned,
    #     so that `Src -> Target` can be easily read regardless of the width
    #     of the objects involved.
    printf "  %5s -> %-5s %32s;  # %s edge\n", block_src, $NF, attrs, ty

    found_rels++
}

ENDFILE {
    if (found_rels == 0) {
        # Note that this can happen if the macro is changed or the calling
        #   `asg-ontvis` shell script is not providing the correct
        #   filenames.
        printf "error: no relations found in `%s`\n", FILENAME >"/dev/stderr"
        exit_code = 1
    }
}

END {
    print "}"
    exit exit_code
}
