#!/usr/bin/env bash
###############################################################################
# @file make_fortran_deps.sh
# @brief Build Fortran module dependency graph and architecture metrics.
# @details
#   Scans Fortran sources, resolves USE dependencies, detects strongly
#   connected components, and reports compile-order and coupling indicators.
#   Outputs include Graphviz artifacts and text reports consumed by PEM
#   documentation and code-quality analysis workflows.
###############################################################################
set -euo pipefail

src_dirs=""
long_range_threshold=3
cluster_by_dir=1
highlight_critical_path=1
prune_mode="none"
top_n=0

# @brief Print command-line usage and examples.
print_usage() {
    cat <<'EOF'
Usage: make_fortran_deps.sh [options] [source_dir]

Options:
  --long-range-threshold N   Edge jump threshold for dashed long-range edges (default: 3)
  --prune MODE               Prune visualization nodes: none|isolated|leaf|top:N
  --no-cluster               Disable directory clusters in DOT output
  --no-critical-path         Disable critical path highlighting
  -h, --help                 Show this help

Examples:
    ./make_fortran_deps.sh src
    ./make_fortran_deps.sh --long-range-threshold 2 src
    ./make_fortran_deps.sh --prune isolated src
    ./make_fortran_deps.sh --prune top:40 src
EOF
}

while [[ $# -gt 0 ]]; do
    case "$1" in
    --long-range-threshold)
            shift
            if [[ $# -eq 0 || ! "$1" =~ ^[0-9]+$ ]]; then
                echo "ERROR: --long-range-threshold requires a non-negative integer" >&2
                exit 1
            fi
            long_range_threshold=$1
            ;;
        --prune)
            shift
            if [[ $# -eq 0 ]]; then
                echo "ERROR: --prune requires a mode" >&2
                exit 1
            fi
            prune_mode=$1
            ;;
        --no-cluster)
            cluster_by_dir=0
            ;;
        --no-critical-path)
            highlight_critical_path=0
            ;;
        -h|--help)
            print_usage
            exit 0
            ;;
        --)
            shift
            break
            ;;
        -*)
            echo "ERROR: unknown option: $1" >&2
            print_usage >&2
            exit 1
            ;;
        *)
            if [[ -n "$src_dirs" ]]; then
                echo "ERROR: multiple source directories provided: '$src_dirs' and '$1'" >&2
                exit 1
            fi
            src_dirs=$1
            ;;
    esac
    shift
done

if [[ -z "$src_dirs" ]]; then
    src_dirs="src"
fi

if [[ "$prune_mode" =~ ^top:([0-9]+)$ ]]; then
    top_n=${BASH_REMATCH[1]}
    prune_mode="top"
elif [[ "$prune_mode" != "none" && "$prune_mode" != "isolated" && "$prune_mode" != "leaf" ]]; then
    echo "ERROR: unsupported prune mode '$prune_mode'. Use none|isolated|leaf|top:N" >&2
    exit 1
fi

tmp_scan=$(mktemp)
tmp_graph=$(mktemp)
tmp_files=$(mktemp)
tmp_cycle_severity=$(mktemp)
tmp_layer_jumps=$(mktemp)
tmp_fanio=$(mktemp)
tmp_instability=$(mktemp)
cycle_report="cycle_report.txt"
cycle_severity_report="cycle_severity_report.txt"
fanio_metrics="fanio_metrics.txt"
instability_report="instability_index.txt"
layer_jump_report="layer_jump_analysis.txt"

trap 'rm -f "$tmp_scan" "$tmp_graph" "$tmp_files" "$tmp_cycle_severity" "$tmp_layer_jumps" "$tmp_fanio" "$tmp_instability"' EXIT

if [[ ! -d "$src_dirs" ]]; then
    echo "ERROR: source directory not found: $src_dirs" >&2
    exit 1
fi

# @brief Escape file paths for makefile dependency output.
# @param arg1 Raw path string.
escape_make() {
    printf '%s' "$1" | sed 's/ /\\ /g'
}

# @brief Escape node labels for DOT output.
# @param arg1 Raw label string.
escape_dot() {
    printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}

# @brief Append one neighbor entry to an adjacency map.
# @param arg1 Name of associative-array map variable.
# @param arg2 Source node.
# @param arg3 Neighbor node.
append_neighbor() {
    local -n map_ref=$1
    local from=$2
    local to=$3

    if [[ -n "${map_ref[$from]:-}" ]]; then
        map_ref[$from]+=$'\n'
    fi
    map_ref[$from]+="$to"
}

# @brief Check whether a node has an explicit self-loop edge.
# @param arg1 Node to test.
has_self_loop() {
    local node=$1
    local n

    while IFS= read -r n; do
        [[ -z "$n" ]] && continue
        if [[ "$n" == "$node" ]]; then
            return 0
        fi
    done <<< "${adj_dep[$node]:-}"

    return 1
}

# ------------------------------------------------
# Collect source files
# ------------------------------------------------

find "$src_dirs" -type f \( \
    -name "*.f90" -o -name "*.F90" \
    -o -name "*.f" -o -name "*.F" \
\) > "$tmp_files"

if [[ ! -s "$tmp_files" ]]; then
    echo "ERROR: no Fortran source files found in $src_dirs" >&2
    exit 1
fi

# ------------------------------------------------
# Single-pass scan with continuation handling
# ------------------------------------------------

while IFS= read -r f; do
    sed -e 's/!.*//' \
        -e ':a;/&[[:space:]]*$/N;s/&[[:space:]]*\n/ /;ta' "$f" |
    awk -v file="$f" '
BEGIN{IGNORECASE=1}

/^[[:space:]]*module[[:space:]]+[a-zA-Z0-9_]+/{
    if($0 !~ /procedure/)
        print "DEF\t" tolower($2) "\t" file
}

/^[[:space:]]*submodule[[:space:]]*\(/{
    if(match($0,/submodule[[:space:]]*\(([^:]+):([^)]+)\)/,a)){
        print "DEF\t" tolower(a[2]) "\t" file
        print "PARENT\t" tolower(a[2]) "\t" tolower(a[1])
    }
}

/^[[:space:]]*use[[:space:],]/{
    line=$0

    if(line ~ /use[[:space:]]*,[[:space:]]*intrinsic[[:space:]]*::/)
        next

    sub(/^[[:space:]]*use[[:space:]]*,[[:space:]]*non_intrinsic[[:space:]]*::[[:space:]]*/,"",line)
    sub(/^[[:space:]]*use[[:space:]]*::[[:space:]]*/,"",line)
    sub(/^[[:space:]]*use[[:space:]]*/,"",line)

    split(line, parts, /[[:space:],]+/)
    mod=tolower(parts[1])

    if(mod!="" &&
       mod!="iso_c_binding" &&
       mod!="iso_fortran_env" &&
       mod!="ieee_arithmetic")
        print "USE\t" file "\t" mod
}
'
done < "$tmp_files" > "$tmp_scan"

# ------------------------------------------------
# Build module map
# ------------------------------------------------

declare -A mod_file
declare -A parent_mod
declare -A warned_duplicate_mod

while IFS=$'\t' read -r type a b; do
    [[ -z "${type:-}" ]] && continue

    if [[ "$type" == "DEF" ]]; then
        if [[ -n "${mod_file[$a]:-}" && "${mod_file[$a]}" != "$b" ]]; then
            if [[ -z "${warned_duplicate_mod[$a]:-}" ]]; then
                echo "WARNING: duplicate module definition for '$a':" >&2
                echo "  - ${mod_file[$a]}" >&2
                warned_duplicate_mod[$a]=1
            fi
            echo "  - $b" >&2
            continue
        fi
        mod_file[$a]=$b
    fi

    if [[ "$type" == "PARENT" ]]; then
        parent_mod[$a]=$b
    fi
done < "$tmp_scan"

# ------------------------------------------------
# Build dependency graph (TAB-separated)
# ------------------------------------------------

> "$tmp_graph"

while IFS=$'\t' read -r type a b; do
    [[ -z "${type:-}" ]] && continue

    if [[ "$type" == "USE" ]]; then
        file=$a
        mod=$b
        if [[ -n "${mod_file[$mod]:-}" ]]; then
            printf '%s\t%s\n' "$file" "${mod_file[$mod]}" >> "$tmp_graph"
        fi
    fi
done < "$tmp_scan"

for child in "${!parent_mod[@]}"; do
    parent=${parent_mod[$child]}
    if [[ -n "${mod_file[$child]:-}" && -n "${mod_file[$parent]:-}" ]]; then
        printf '%s\t%s\n' "${mod_file[$child]}" "${mod_file[$parent]}" >> "$tmp_graph"
    fi
done

sort -u "$tmp_graph" -o "$tmp_graph"

# ------------------------------------------------
# Build graph structures
# ------------------------------------------------

declare -A nodes
declare -A indeg
declare -A adj_dep
declare -A adj_rev
declare -A dep_count
declare -A dependent_count
edge_count=0

while IFS=$'\t' read -r a b; do
    [[ -z "${a:-}" || -z "${b:-}" ]] && continue

    nodes["$a"]=1
    nodes["$b"]=1

    append_neighbor adj_dep "$a" "$b"
    append_neighbor adj_rev "$b" "$a"

    indeg[$a]=$(( ${indeg[$a]:-0} + 1 ))
    dep_count[$a]=$(( ${dep_count[$a]:-0} + 1 ))
    dependent_count[$b]=$(( ${dependent_count[$b]:-0} + 1 ))
    edge_count=$(( edge_count + 1 ))
done < "$tmp_graph"

while IFS= read -r f; do
    nodes["$f"]=1
    indeg[$f]=${indeg[$f]:-0}
    dep_count[$f]=${dep_count[$f]:-0}
    dependent_count[$f]=${dependent_count[$f]:-0}
done < "$tmp_files"

# ------------------------------------------------
# Cycle detection (Tarjan SCC)
# ------------------------------------------------

declare -A index
declare -A lowlink
declare -A onstack
declare -a stack

idx=0
cycles_found=0
cycle_group_count=0

echo "# Cycle report" > "$cycle_report"
echo "# Generated by make_fortran_deps.sh" >> "$cycle_report"
echo >> "$cycle_report"

# @brief Tarjan SCC recursion step for one node.
# @param arg1 Node to visit.
strongconnect() {
    local v=$1
    local w

    index[$v]=$idx
    lowlink[$v]=$idx
    idx=$((idx + 1))

    stack+=("$v")
    onstack[$v]=1

    while IFS= read -r w; do
        [[ -z "$w" ]] && continue

        if [[ -z "${index[$w]:-}" ]]; then
            strongconnect "$w"
            if (( lowlink[$v] > lowlink[$w] )); then
                lowlink[$v]=${lowlink[$w]}
            fi
        elif [[ -n "${onstack[$w]:-}" ]]; then
            if (( lowlink[$v] > index[$w] )); then
                lowlink[$v]=${index[$w]}
            fi
        fi
    done <<< "${adj_dep[$v]:-}"

    if [[ "${lowlink[$v]}" == "${index[$v]}" ]]; then
        local component=()
        local self_loop=0

        while :; do
            w=${stack[-1]}
            stack=("${stack[@]:0:${#stack[@]}-1}")
            unset onstack[$w]
            component+=("$w")
            [[ "$w" == "$v" ]] && break
        done

        if (( ${#component[@]} == 1 )); then
            if has_self_loop "${component[0]}"; then
                self_loop=1
            fi
        fi

        if (( ${#component[@]} > 1 || self_loop == 1 )); then
            cycles_found=1
            cycle_group_count=$((cycle_group_count + 1))
            echo "CIRCULAR DEPENDENCY GROUP:"
            printf '  %s\n' "${component[@]}"
            echo

            echo "## Group $cycle_group_count" >> "$cycle_report"
            echo "nodes:" >> "$cycle_report"
            for n in "${component[@]}"; do
                echo "  $n" >> "$cycle_report"
            done

            local -A in_component
            for n in "${component[@]}"; do
                in_component["$n"]=1
            done

            echo "edges_in_group:" >> "$cycle_report"
            mapfile -t scc_edges < <(
                for u in "${component[@]}"; do
                    while IFS= read -r v; do
                        [[ -z "$v" ]] && continue
                        if [[ -n "${in_component[$v]:-}" ]]; then
                            printf '  %s -> %s\n' "$u" "$v"
                        fi
                    done <<< "${adj_dep[$u]:-}"
                done | sort -u
            )

            if (( ${#scc_edges[@]} == 0 )); then
                echo "  (none)" >> "$cycle_report"
            else
                printf '%s\n' "${scc_edges[@]}" >> "$cycle_report"
            fi

            echo "sample_cycle_path:" >> "$cycle_report"

            local found_cycle=0
            local cycle_line=""

            if (( ${#component[@]} == 1 )); then
                local node0=${component[0]}
                if has_self_loop "$node0"; then
                    cycle_line="$node0 -> $node0"
                    found_cycle=1
                fi
            else
                local -a walk
                local -a cycle_path
                local -A pos
                local start current next steps cycle_begin

                start=${component[0]}
                walk=("$start")
                pos["$start"]=0
                current=$start

                for ((steps = 0; steps < ${#component[@]} * ${#component[@]} + 1; steps++)); do
                    next=""

                    while IFS= read -r cand; do
                        [[ -z "$cand" ]] && continue
                        if [[ -n "${in_component[$cand]:-}" ]]; then
                            next=$cand
                            break
                        fi
                    done <<< "${adj_dep[$current]:-}"

                    [[ -z "$next" ]] && break

                    if [[ -n "${pos[$next]+x}" ]]; then
                        cycle_begin=${pos[$next]}
                        cycle_path=("${walk[@]:$cycle_begin}")
                        cycle_path+=("$next")

                        cycle_line=${cycle_path[0]}
                        for ((ci = 1; ci < ${#cycle_path[@]}; ci++)); do
                            cycle_line+=" -> ${cycle_path[$ci]}"
                        done

                        found_cycle=1
                        break
                    fi

                    walk+=("$next")
                    pos["$next"]=$(( ${#walk[@]} - 1 ))
                    current=$next
                done
            fi

            if (( found_cycle == 1 )); then
                echo "  $cycle_line" >> "$cycle_report"
            else
                echo "  (not found)" >> "$cycle_report"
            fi

            local node_count edge_count_local max_possible_edges
            local density severity
            node_count=${#component[@]}
            edge_count_local=${#scc_edges[@]}

            if (( node_count > 1 )); then
                max_possible_edges=$(( node_count * (node_count - 1) ))
            else
                max_possible_edges=1
            fi

            density=$(awk -v e="$edge_count_local" -v m="$max_possible_edges" 'BEGIN{printf "%.3f", e/m}')
            severity=$(awk -v n="$node_count" -v e="$edge_count_local" -v d="$density" 'BEGIN{printf "%.3f", n*e*(1+1.25*d)}')
            printf '%d\t%s\t%d\t%d\t%s\t%s\n' \
                "$cycle_group_count" "$severity" "$node_count" "$edge_count_local" "$density" "$cycle_line" >> "$tmp_cycle_severity"

            echo >> "$cycle_report"
        fi
    fi
}

for node in "${!nodes[@]}"; do
    if [[ -z "${index[$node]:-}" ]]; then
        strongconnect "$node"
    fi
done

if (( cycle_group_count == 0 )); then
    echo "No cycles detected." >> "$cycle_report"
fi

echo "Generated $cycle_report"

echo "# Cycle severity report" > "$cycle_severity_report"
echo "# Formula: severity = node_count * edge_count * (1 + 1.25 * density)" >> "$cycle_severity_report"
echo "# density = edge_count / max_possible_edges_in_group" >> "$cycle_severity_report"

if [[ ! -s "$tmp_cycle_severity" ]]; then
    echo "No cycles detected." >> "$cycle_severity_report"
else
    {
        echo ""
        printf '%-8s %-10s %-7s %-7s %-8s %s\n' "Group" "Severity" "Nodes" "Edges" "Density" "Sample cycle path"
        printf '%-8s %-10s %-7s %-7s %-8s %s\n' "-----" "--------" "-----" "-----" "-------" "-----------------"
        sort -t $'\t' -k2,2nr "$tmp_cycle_severity" | while IFS=$'\t' read -r group severity nodes_count edges_count density sample; do
            printf '%-8s %-10s %-7s %-7s %-8s %s\n' "$group" "$severity" "$nodes_count" "$edges_count" "$density" "$sample"
        done
    } >> "$cycle_severity_report"
fi

echo "Generated $cycle_severity_report"

if (( cycles_found == 1 )); then
    echo "ERROR: circular module dependencies exist" >&2
    exit 1
fi

# ------------------------------------------------
# Makefile dependencies
# ------------------------------------------------

echo "# generated dependencies" > deps.mk

while IFS=$'\t' read -r src dep; do
    [[ -z "${src:-}" || -z "${dep:-}" ]] && continue

    obj=$(basename "$src")
    obj=${obj%.*}.o

    mod=$(basename "$dep")
    mod=${mod%.*}.mod

    printf '%s: %s\n' "$(escape_make "$obj")" "$(escape_make "$mod")" >> deps.mk
done < "$tmp_graph"

echo "Generated deps.mk"

# ------------------------------------------------
# Compute parallel layers
# ------------------------------------------------

> compile_layers.txt

queue=()
layer=0
declare -A layer_nodes
declare -A node_layer

for f in "${!indeg[@]}"; do
    if (( indeg[$f] == 0 )); then
        queue+=("$f")
    fi
done

if (( ${#queue[@]} > 0 )); then
    mapfile -t queue < <(printf '%s\n' "${queue[@]}" | sort)
fi

while (( ${#queue[@]} > 0 )); do
    echo "# layer $layer" >> compile_layers.txt

    next=()

    for n in "${queue[@]}"; do
        echo "$n" >> compile_layers.txt
        append_neighbor layer_nodes "$layer" "$n"
        node_layer["$n"]=$layer

        while IFS= read -r m; do
            [[ -z "$m" ]] && continue
            indeg[$m]=$(( indeg[$m] - 1 ))
            if (( indeg[$m] == 0 )); then
                next+=("$m")
            fi
        done <<< "${adj_rev[$n]:-}"
    done

    layer=$((layer + 1))

    if (( ${#next[@]} > 0 )); then
        mapfile -t queue < <(printf '%s\n' "${next[@]}" | sort -u)
    else
        queue=()
    fi
done

echo "Generated compile_layers.txt"

# ------------------------------------------------
# Critical compile path (longest chain)
# ------------------------------------------------

declare -A longest_dist
declare -A longest_parent
declare -A critical_nodes
declare -A critical_edges

critical_path_end=""
critical_path_len=0

if (( highlight_critical_path == 1 )); then
    for node in "${!nodes[@]}"; do
        longest_dist[$node]=0
    done

    for ((l = 0; l < layer; l++)); do
        while IFS= read -r u; do
            [[ -z "$u" ]] && continue

            while IFS= read -r v; do
                [[ -z "$v" ]] && continue
                cand=$(( ${longest_dist[$u]:-0} + 1 ))
                if (( cand > ${longest_dist[$v]:-0} )); then
                    longest_dist[$v]=$cand
                    longest_parent[$v]=$u
                fi
            done <<< "${adj_rev[$u]:-}"
        done <<< "${layer_nodes[$l]:-}"
    done

    for node in "${!nodes[@]}"; do
        if (( ${longest_dist[$node]:-0} > critical_path_len )); then
            critical_path_len=${longest_dist[$node]}
            critical_path_end=$node
        fi
    done

    if [[ -n "$critical_path_end" ]]; then
        cur=$critical_path_end
        critical_nodes[$cur]=1

        while [[ -n "${longest_parent[$cur]:-}" ]]; do
            prev=${longest_parent[$cur]}
            critical_nodes[$prev]=1
            critical_edges["$cur"$'\t'"$prev"]=1
            cur=$prev
        done
    fi
fi

echo "# Critical compile path" > critical_path.txt
echo "length_edges=$critical_path_len" >> critical_path.txt

if [[ -n "$critical_path_end" ]]; then
    path_nodes=()
    cur=$critical_path_end
    path_nodes+=("$cur")
    while [[ -n "${longest_parent[$cur]:-}" ]]; do
        cur=${longest_parent[$cur]}
        path_nodes+=("$cur")
    done

    for ((i = ${#path_nodes[@]} - 1; i >= 0; i--)); do
        echo "${path_nodes[$i]}" >> critical_path.txt
    done
fi

echo "Generated critical_path.txt"

# ------------------------------------------------
# Module centrality (fan-in)
# ------------------------------------------------

echo "# Module centrality (number of dependent files)" > module_centrality.txt

declare -A fanin
declare -A node_fanin
max_node_fanin=0

while IFS=$'\t' read -r src dep; do
    [[ -z "${src:-}" || -z "${dep:-}" ]] && continue

    mod=$(basename "$dep")
    mod=${mod%.*}

    fanin[$mod]=$(( ${fanin[$mod]:-0} + 1 ))
    node_fanin[$dep]=$(( ${node_fanin[$dep]:-0} + 1 ))
    if (( node_fanin[$dep] > max_node_fanin )); then
        max_node_fanin=${node_fanin[$dep]}
    fi
done < "$tmp_graph"

for m in "${!fanin[@]}"; do
    printf '%s %s\n' "${fanin[$m]}" "$m"
done | sort -nr >> module_centrality.txt

echo "Generated module_centrality.txt"

# ------------------------------------------------
# Fan-in/fan-out and instability metrics
# ------------------------------------------------

for node in "${!nodes[@]}"; do
    fan_in=${dependent_count[$node]:-0}
    fan_out=${dep_count[$node]:-0}
    combined=$(( fan_in + fan_out ))

    role="balanced"
    if (( fan_in > fan_out )); then
        role="provider"
    elif (( fan_out > fan_in )); then
        role="consumer"
    fi

    printf '%d\t%d\t%d\t%s\t%s\n' "$fan_in" "$fan_out" "$combined" "$role" "$node" >> "$tmp_fanio"

    if (( combined == 0 )); then
        printf '%s\t%s\t%s\n' "-1" "isolated" "$node" >> "$tmp_instability"
    else
        instability=$(awk -v ce="$fan_out" -v total="$combined" 'BEGIN{printf "%.3f", ce/total}')
        class="stable"
        awk -v i="$instability" 'BEGIN{exit !(i >= 0.67)}' && class="volatile"
        awk -v i="$instability" 'BEGIN{exit !(i >= 0.34 && i < 0.67)}' && class="mixed"
        printf '%s\t%s\t%s\n' "$instability" "$class" "$node" >> "$tmp_instability"
    fi
done

echo "# Fan-in/Fan-out metrics" > "$fanio_metrics"
echo "# Columns: fan_in fan_out combined role node" >> "$fanio_metrics"
echo "" >> "$fanio_metrics"
printf '%-7s %-7s %-8s %-10s %s\n' "FanIn" "FanOut" "Combined" "Role" "Node" >> "$fanio_metrics"
printf '%-7s %-7s %-8s %-10s %s\n' "-----" "------" "--------" "----" "----" >> "$fanio_metrics"
sort -t $'\t' -k3,3nr -k1,1nr -k2,2nr "$tmp_fanio" | while IFS=$'\t' read -r fan_in fan_out combined role node; do
    printf '%-7s %-7s %-8s %-10s %s\n' "$fan_in" "$fan_out" "$combined" "$role" "$node"
done >> "$fanio_metrics"
echo "Generated $fanio_metrics"

echo "# Instability index" > "$instability_report"
echo "# Formula: instability = fan_out / (fan_in + fan_out)" >> "$instability_report"
echo "# Class thresholds: stable < 0.34, mixed [0.34,0.67), volatile >= 0.67" >> "$instability_report"
echo "" >> "$instability_report"
printf '%-11s %-10s %s\n' "Instability" "Class" "Node" >> "$instability_report"
printf '%-11s %-10s %s\n' "-----------" "-----" "----" >> "$instability_report"
sort -t $'\t' -k1,1nr -k3,3 "$tmp_instability" | while IFS=$'\t' read -r instability class node; do
    if [[ "$instability" == "-1" ]]; then
        instability_display="n/a"
    else
        instability_display="$instability"
    fi
    printf '%-11s %-10s %s\n' "$instability_display" "$class" "$node"
done >> "$instability_report"
echo "Generated $instability_report"

# ------------------------------------------------
# Layer jump analysis
# ------------------------------------------------

max_layer_jump=0
sum_layer_jump=0
layer_jump_count=0
long_range_jump_count=0

while IFS=$'\t' read -r src dep; do
    [[ -z "${src:-}" || -z "${dep:-}" ]] && continue

    src_layer=${node_layer[$src]:-0}
    dep_layer=${node_layer[$dep]:-0}
    layer_jump=$(( src_layer - dep_layer ))

    if (( layer_jump > 0 )); then
        layer_jump_count=$(( layer_jump_count + 1 ))
        sum_layer_jump=$(( sum_layer_jump + layer_jump ))
        if (( layer_jump > max_layer_jump )); then
            max_layer_jump=$layer_jump
        fi
        if (( long_range_threshold > 0 && layer_jump >= long_range_threshold )); then
            long_range_jump_count=$(( long_range_jump_count + 1 ))
        fi

        printf '%d\t%s\t%s\t%d\t%d\n' "$layer_jump" "$src" "$dep" "$src_layer" "$dep_layer" >> "$tmp_layer_jumps"
    fi
done < "$tmp_graph"

echo "# Layer jump analysis" > "$layer_jump_report"
echo "# jump = layer(source) - layer(dependency)" >> "$layer_jump_report"
echo "# long-range threshold = ${long_range_threshold}" >> "$layer_jump_report"
echo "total_jumps=${layer_jump_count}" >> "$layer_jump_report"
echo "max_jump=${max_layer_jump}" >> "$layer_jump_report"

if (( layer_jump_count > 0 )); then
    avg_jump=$(awk -v s="$sum_layer_jump" -v n="$layer_jump_count" 'BEGIN{printf "%.2f", s/n}')
else
    avg_jump="0.00"
fi

echo "avg_jump=${avg_jump}" >> "$layer_jump_report"
echo "long_range_jumps=${long_range_jump_count}" >> "$layer_jump_report"
echo "" >> "$layer_jump_report"
echo "# Top jump edges" >> "$layer_jump_report"

if [[ -s "$tmp_layer_jumps" ]]; then
    printf '%-6s %-10s %-10s %s\n' "Jump" "SrcLayer" "DepLayer" "Edge" >> "$layer_jump_report"
    printf '%-6s %-10s %-10s %s\n' "----" "--------" "--------" "----" >> "$layer_jump_report"
    sort -t $'\t' -k1,1nr -k2,2 "$tmp_layer_jumps" | awk 'NR<=25' | while IFS=$'\t' read -r jump src dep src_l dep_l; do
        printf '%-6s %-10s %-10s %s -> %s\n' "$jump" "$src_l" "$dep_l" "$src" "$dep"
    done >> "$layer_jump_report"
else
    echo "No positive layer jumps detected." >> "$layer_jump_report"
fi

echo "Generated $layer_jump_report"

# ------------------------------------------------
# Graphviz visualization
# ------------------------------------------------

high_node_fanin=0
mid_node_fanin=0

if (( max_node_fanin > 0 )); then
    high_node_fanin=$(( (2 * max_node_fanin + 2) / 3 ))
    mid_node_fanin=$(( (max_node_fanin + 2) / 3 ))
fi

declare -A dot_include
for node in "${!nodes[@]}"; do
    dot_include[$node]=1
done

if [[ "$prune_mode" == "isolated" ]]; then
    for node in "${!nodes[@]}"; do
        if (( ${dep_count[$node]:-0} == 0 && ${dependent_count[$node]:-0} == 0 )); then
            unset dot_include[$node]
        fi
    done
elif [[ "$prune_mode" == "leaf" ]]; then
    for node in "${!nodes[@]}"; do
        if (( ${dep_count[$node]:-0} == 0 )); then
            unset dot_include[$node]
        fi
    done
elif [[ "$prune_mode" == "top" ]]; then
    declare -A selected
    for node in "${!nodes[@]}"; do
        unset dot_include[$node]
    done

    if (( top_n > 0 )); then
        mapfile -t top_rows < <(
            for node in "${!nodes[@]}"; do
                printf '%s\t%s\n' "${node_fanin[$node]:-0}" "$node"
            done | sort -t $'\t' -k1,1nr -k2,2 | awk -v n="$top_n" 'NR<=n'
        )

        for row in "${top_rows[@]}"; do
            seed=${row#*$'\t'}
            [[ -z "$seed" ]] && continue

            selected[$seed]=1

            while IFS= read -r dep; do
                [[ -z "$dep" ]] && continue
                selected[$dep]=1
            done <<< "${adj_dep[$seed]:-}"

            while IFS= read -r rev; do
                [[ -z "$rev" ]] && continue
                selected[$rev]=1
            done <<< "${adj_rev[$seed]:-}"
        done

        for node in "${!selected[@]}"; do
            dot_include[$node]=1
        done
    fi
fi

included_node_count=0
for node in "${!dot_include[@]}"; do
    included_node_count=$(( included_node_count + 1 ))
done

included_edge_count=0
long_range_edge_count=0
critical_edge_count=0

while IFS=$'\t' read -r src dep; do
    [[ -z "${src:-}" || -z "${dep:-}" ]] && continue
    if [[ -z "${dot_include[$src]:-}" || -z "${dot_include[$dep]:-}" ]]; then
        continue
    fi

    included_edge_count=$(( included_edge_count + 1 ))
    if [[ -n "${critical_edges[$src$'\t'$dep]:-}" ]]; then
        critical_edge_count=$(( critical_edge_count + 1 ))
    fi

    src_layer=${node_layer[$src]:-0}
    dep_layer=${node_layer[$dep]:-0}
    layer_jump=$(( src_layer - dep_layer ))
    if (( long_range_threshold > 0 && layer_jump >= long_range_threshold )); then
        long_range_edge_count=$(( long_range_edge_count + 1 ))
    fi
done < "$tmp_graph"

echo "digraph FortranDeps {" > fortran_deps.dot
echo "  rankdir=LR;" >> fortran_deps.dot
echo "  splines=true;" >> fortran_deps.dot
echo "  overlap=false;" >> fortran_deps.dot
echo "  concentrate=true;" >> fortran_deps.dot
echo "  ranksep=1.0;" >> fortran_deps.dot
echo "  nodesep=0.35;" >> fortran_deps.dot
echo "  graph [fontname=\"Helvetica-Bold\", fontsize=28, labelloc=\"t\", labeljust=\"c\", pad=0.45, label=\"Fortran Module Dependencies\"];" >> fortran_deps.dot
echo "  node [shape=box, style=\"rounded,filled\", fillcolor=\"#f8fafc\", color=\"#334155\", fontname=\"Helvetica\", fontsize=10];" >> fortran_deps.dot
echo "  edge [color=\"#475569\", arrowsize=0.7, penwidth=1.2];" >> fortran_deps.dot

declare -A dot_node_seen
declare -A basename_count
declare -A parentbase_count
declare -A cluster_nodes

for node in "${!dot_include[@]}"; do
    base_name=$(basename "$node")
    parent_name=$(basename "$(dirname "$node")")
    parent_base="$parent_name/$base_name"

    basename_count[$base_name]=$(( ${basename_count[$base_name]:-0} + 1 ))
    parentbase_count[$parent_base]=$(( ${parentbase_count[$parent_base]:-0} + 1 ))

    if (( cluster_by_dir == 1 )); then
        rel_node=$node
        if [[ "$rel_node" == "$src_dirs/"* ]]; then
            rel_node=${rel_node#"$src_dirs"/}
        fi
        cluster_dir=$(dirname "$rel_node")
        if [[ "$cluster_dir" == "." ]]; then
            cluster_key="root"
        else
            cluster_key=${cluster_dir%%/*}
        fi
        append_neighbor cluster_nodes "$cluster_key" "$node"
    fi
done

for ((l = 0; l < layer; l++)); do
    while IFS= read -r node; do
        [[ -z "$node" ]] && continue
        [[ -z "${dot_include[$node]:-}" ]] && continue

        node_id=$(escape_dot "$node")

        if [[ -z "${dot_node_seen[$node_id]:-}" ]]; then
            dot_node_seen[$node_id]=1

            base_name=$(basename "$node")
            parent_name=$(basename "$(dirname "$node")")
            parent_base="$parent_name/$base_name"

            if (( ${basename_count[$base_name]:-0} <= 1 )); then
                node_label=$base_name
            elif (( ${parentbase_count[$parent_base]:-0} <= 1 )); then
                node_label=$parent_base
            else
                node_label=$node
            fi

            fanin_count=${node_fanin[$node]:-0}
            node_fill="#f8fafc"
            node_color="#334155"
            node_penwidth="1.1"

            if (( fanin_count > 0 )); then
                if (( fanin_count >= high_node_fanin )); then
                    node_fill="#fee2e2"
                    node_color="#b91c1c"
                    node_penwidth="2.0"
                elif (( fanin_count >= mid_node_fanin )); then
                    node_fill="#fff7ed"
                    node_color="#c2410c"
                    node_penwidth="1.6"
                fi
                node_label="$node_label (in:$fanin_count)"
            fi

            if [[ -n "${critical_nodes[$node]:-}" ]]; then
                node_color="#991b1b"
                if [[ "$node_penwidth" == "1.1" ]]; then
                    node_penwidth="1.8"
                fi
            fi

            printf '  "%s" [label="%s", fillcolor="%s", color="%s", penwidth=%s];\n' \
                "$node_id" "$(escape_dot "$node_label")" "$node_fill" "$node_color" "$node_penwidth" >> fortran_deps.dot
        fi
    done <<< "${layer_nodes[$l]:-}"
done

if (( cluster_by_dir == 1 )); then
    cluster_idx=0
    for cluster_key in "${!cluster_nodes[@]}"; do
        echo "  subgraph cluster_dir_$cluster_idx {" >> fortran_deps.dot
        printf '    label="dir: %s";\n' "$(escape_dot "$cluster_key")" >> fortran_deps.dot
        echo "    color=\"#cbd5e1\";" >> fortran_deps.dot
        echo "    penwidth=1.0;" >> fortran_deps.dot
        echo "    style=\"rounded\";" >> fortran_deps.dot

        while IFS= read -r node; do
            [[ -z "$node" ]] && continue
            printf '    "%s";\n' "$(escape_dot "$node")" >> fortran_deps.dot
        done <<< "${cluster_nodes[$cluster_key]}"

        echo "  }" >> fortran_deps.dot
        cluster_idx=$(( cluster_idx + 1 ))
    done
fi

echo "  subgraph cluster_legend {" >> fortran_deps.dot
echo "    label=\"Legend\";" >> fortran_deps.dot
echo "    fontsize=14;" >> fortran_deps.dot
echo "    fontname=\"Helvetica-Bold\";" >> fortran_deps.dot
echo "    color=\"#94a3b8\";" >> fortran_deps.dot
echo "    style=\"rounded,dashed\";" >> fortran_deps.dot
echo "    legend_low [label=\"Normal module\", shape=box, style=\"rounded,filled\", fillcolor=\"#f8fafc\", color=\"#334155\", penwidth=1.1, fontsize=12, margin=\"0.12,0.08\"];" >> fortran_deps.dot
echo "    legend_mid [label=\"Medium fan-in\", shape=box, style=\"rounded,filled\", fillcolor=\"#fff7ed\", color=\"#c2410c\", penwidth=1.6, fontsize=12, margin=\"0.12,0.08\"];" >> fortran_deps.dot
echo "    legend_high [label=\"High fan-in\", shape=box, style=\"rounded,filled\", fillcolor=\"#fee2e2\", color=\"#b91c1c\", penwidth=2.0, fontsize=12, margin=\"0.12,0.08\"];" >> fortran_deps.dot
echo "    legend_edge_a [label=\"Short dependency\", shape=plain, fontsize=12];" >> fortran_deps.dot
echo "    legend_edge_b [label=\"\", shape=point, width=0.09];" >> fortran_deps.dot
echo "    legend_edge_c [label=\"Long-range dependency\", shape=plain, fontsize=12];" >> fortran_deps.dot
echo "    legend_edge_d [label=\"\", shape=point, width=0.09];" >> fortran_deps.dot
echo "    legend_edge_e [label=\"Critical path\", shape=plain, fontsize=12];" >> fortran_deps.dot
echo "    legend_edge_f [label=\"\", shape=point, width=0.09];" >> fortran_deps.dot
echo "    { rank=same; legend_edge_a; legend_edge_b; }" >> fortran_deps.dot
echo "    { rank=same; legend_edge_c; legend_edge_d; }" >> fortran_deps.dot
echo "    { rank=same; legend_edge_e; legend_edge_f; }" >> fortran_deps.dot
echo "    legend_edge_a -> legend_edge_b [color=\"#475569\", penwidth=1.2, minlen=2, constraint=true];" >> fortran_deps.dot
echo "    legend_edge_c -> legend_edge_d [color=\"#94a3b8\", penwidth=1.0, style=dashed, minlen=2, constraint=true];" >> fortran_deps.dot
echo "    legend_edge_e -> legend_edge_f [color=\"#dc2626\", penwidth=2.6, minlen=2, constraint=true];" >> fortran_deps.dot
echo "    stats [shape=note, fontsize=11, label=\"nodes=$included_node_count\\ledges=$included_edge_count\\llayers=$layer\\lcritical_len=$critical_path_len\\llong_edges=$long_range_edge_count\"];" >> fortran_deps.dot
echo "  }" >> fortran_deps.dot

for ((l = 0; l < layer; l++)); do
    if [[ -n "${layer_nodes[$l]:-}" ]]; then
        echo "  { rank=same;" >> fortran_deps.dot
        while IFS= read -r node; do
            [[ -z "$node" ]] && continue
            [[ -z "${dot_include[$node]:-}" ]] && continue
            printf '    "%s";\n' "$(escape_dot "$node")" >> fortran_deps.dot
        done <<< "${layer_nodes[$l]}"
        echo "  }" >> fortran_deps.dot
    fi
done

while IFS=$'\t' read -r src dep; do
    [[ -z "${src:-}" || -z "${dep:-}" ]] && continue
    [[ -z "${dot_include[$src]:-}" || -z "${dot_include[$dep]:-}" ]] && continue

    s=$(escape_dot "$src")
    d=$(escape_dot "$dep")
    src_layer=${node_layer[$src]:-0}
    dep_layer=${node_layer[$dep]:-0}
    layer_jump=$(( src_layer - dep_layer ))

    if [[ -n "${critical_edges[$src$'\t'$dep]:-}" ]]; then
        printf '  "%s" -> "%s" [color="#dc2626", penwidth=2.6];\n' "$s" "$d" >> fortran_deps.dot
    elif (( long_range_threshold > 0 && layer_jump >= long_range_threshold )); then
        printf '  "%s" -> "%s" [color="#94a3b8", style=dashed, penwidth=1.0, constraint=false];\n' "$s" "$d" >> fortran_deps.dot
    else
        printf '  "%s" -> "%s" [color="#475569", penwidth=1.2];\n' "$s" "$d" >> fortran_deps.dot
    fi
done < "$tmp_graph"

echo "}" >> fortran_deps.dot

echo "Generated fortran_deps.dot"

if command -v dot >/dev/null 2>&1; then
    dot -Tpng fortran_deps.dot -o fortran_deps.png
    dot -Tsvg fortran_deps.dot -o fortran_deps.svg
    echo "Generated fortran_deps.png"
    echo "Generated fortran_deps.svg"
fi