# =============================================================================
# ppa_metrics.tcl — Reusable PPA Metric Extraction for OpenROAD Flow
# =============================================================================
# Provides extract_ppa_metrics proc that captures timing, area, power, and
# congestion metrics at any flow milestone and writes structured output files.
#
# Usage (from pnr_flow.tcl):
#   source ppa_metrics.tcl
#   extract_ppa_metrics "post_place_opt"
#   extract_ppa_metrics "post_route_opt"
#
# Output per call:
#   $OUT_DIR/metrics_${stage}.json  — Structured JSON
#   $OUT_DIR/metrics_${stage}.txt   — Key=value flat file (shell-parseable)
#
# Environment Variables:
#   Required:
#     OUT_DIR — Output directory
#
# IMPORTANT: OpenROAD report_* commands print to stdout but return empty
# strings. To capture their output, we must use the > file redirection
# syntax, then read the file back. The _capture_report helper does this.
# =============================================================================

# --- Helper: escape string for JSON ---
proc _json_str {s} {
    set s [string map {\\ \\\\ \" \\\"} $s]
    return "\"$s\""
}

# --- Helper: format number or null for JSON ---
proc _json_num {val} {
    if {$val eq "null" || $val eq ""} {
        return "null"
    }
    return $val
}

# --- Helper: capture report command output via temp file ---
# OpenROAD report_* commands print to stdout but return "".
# This proc writes the report to a temp file and reads it back.
# Usage: set output [_capture_report "$out_dir/_tmp.rpt" {report_wns -digits 4}]
proc _capture_report {tmpfile cmd} {
    if {[catch {
        eval $cmd > $tmpfile
    } err]} {
        catch {file delete -force $tmpfile}
        error $err
    }
    if {![file exists $tmpfile]} {
        return ""
    }
    set _fp [open $tmpfile r]
    set _data [read $_fp]
    close $_fp
    file delete -force $tmpfile
    return $_data
}

# =============================================================================
# extract_ppa_metrics — Main metric extraction proc
# =============================================================================
# Args:
#   stage_name    — Milestone identifier (e.g., "post_place_opt")
#   drc_count     — Optional DRC violation count (only for post_route_opt).
#                   Pass "null" or omit for stages without DRC data.
# =============================================================================
proc extract_ppa_metrics {stage_name {drc_count "null"}} {
    set out_dir $::env(OUT_DIR)
    set metrics_json "$out_dir/metrics_${stage_name}.json"
    set metrics_txt  "$out_dir/metrics_${stage_name}.txt"

    puts ""
    puts "========================================================"
    puts "PPA METRICS: $stage_name"
    puts "========================================================"

    # -------------------------------------------------------------------
    # Timing metrics
    # -------------------------------------------------------------------
    set wns_setup "null"
    set tns_setup "null"
    set wns_hold  "null"
    set worst_path_delay "null"
    set failing_endpoints 0
    set total_endpoints 0
    set total_failing_path_delay 0.0

    # Clock period from env var (set by run_eda.sh)
    set clock_period_ns "null"
    if {[info exists ::env(CLOCK_NS)] && $::env(CLOCK_NS) ne ""} {
        set clock_period_ns $::env(CLOCK_NS)
    }

    # Temp file for report capture (in the work dir, which is writable)
    set _tmpf "$out_dir/_ppa_capture_${stage_name}.rpt"

    # WNS (setup)
    if {[catch {
        set _wns_out [_capture_report $_tmpf {report_wns -digits 4}]
        if {[regexp {([-\d.]+)} $_wns_out match val]} {
            set wns_setup $val
        }
    } err]} {
        puts "WARNING: Could not capture WNS: $err"
    }

    # TNS (setup)
    if {[catch {
        set _tns_out [_capture_report $_tmpf {report_tns -digits 4}]
        if {[regexp {([-\d.]+)} $_tns_out match val]} {
            set tns_setup $val
        }
    } err]} {
        puts "WARNING: Could not capture TNS: $err"
    }

    # Worst path delay (arrival time from max delay check)
    if {[catch {
        set _path_out [_capture_report $_tmpf {report_checks -path_delay max -digits 4 -format full_clock_expanded}]
        # Format: "                                    4.2456   data arrival time"
        # The number comes BEFORE the text. The report may contain multiple
        # path groups (async + clk). We want the longest arrival time, which
        # is the last "data arrival time" value (the critical path).
        foreach line [split $_path_out "\n"] {
            if {[regexp {([\d.]+)\s+data arrival time} $line match val]} {
                # Keep the largest arrival time seen
                if {$worst_path_delay eq "null" || $val > $worst_path_delay} {
                    set worst_path_delay $val
                }
            }
        }
    } err]} {
        puts "WARNING: Could not capture worst path delay: $err"
    }

    # Hold WNS (min delay)
    if {[catch {
        set _hold_out [_capture_report $_tmpf {report_checks -path_delay min -digits 4 -format end}]
        foreach line [split $_hold_out "\n"] {
            if {[regexp {([-\d.]+)\s+\((VIOLATED|MET)\)\s*$} $line match slack _]} {
                if {$wns_hold eq "null" || $slack < $wns_hold} {
                    set wns_hold $slack
                }
            }
        }
    } err]} {
        puts "WARNING: Could not capture hold WNS: $err"
    }

    # Endpoint counts and total failing path delay (from setup check)
    if {[catch {
        set _ep_out [_capture_report $_tmpf {report_checks -slack_max 1e30 -format end -digits 4 -no_line_splits -path_delay max -group_path_count 1000000}]
        set _in_data 0
        foreach line [split $_ep_out "\n"] {
            if {[regexp {^-+$} $line]} {
                set _in_data 1
                continue
            }
            if {!$_in_data} { continue }
            if {[string trim $line] eq ""} { continue }
            if {[regexp {([-\d.]+)\s+\((VIOLATED|MET)\)\s*$} $line match slack status]} {
                incr total_endpoints
                if {$slack < 0} {
                    incr failing_endpoints
                    # Path delay = clock_period - slack (slack is negative,
                    # so this adds clock_period + |slack|)
                    if {$clock_period_ns ne "null"} {
                        set total_failing_path_delay [expr {$total_failing_path_delay + $clock_period_ns - $slack}]
                    }
                }
            }
        }
    } err]} {
        puts "WARNING: Could not capture endpoint counts: $err"
    }

    puts "  Clock period: $clock_period_ns ns"
    puts "  WNS (setup): $wns_setup ns"
    puts "  TNS (setup): $tns_setup ns"
    puts "  WNS (hold):  $wns_hold ns"
    puts "  Worst delay: $worst_path_delay ns"
    puts "  Endpoints:   $failing_endpoints failing / $total_endpoints total"
    if {$failing_endpoints > 0} {
        puts "  Failing path delay sum: $total_failing_path_delay ns"
    }

    # -------------------------------------------------------------------
    # Area metrics
    # -------------------------------------------------------------------
    set cell_area "null"
    set design_area "null"
    set utilization "null"
    set instance_count "null"

    # Try file-based capture first (works for STA-integrated report commands).
    # If that fails, fall back to ODB API.
    if {[catch {
        set _area_out [_capture_report $_tmpf {report_design_area}]
        # Output format: "Design area 7341 um^2 69% utilization."
        if {[regexp {Design area\s+([\d.]+)\s+u.*?([\d.]+)%} $_area_out match area util]} {
            set design_area $area
            set utilization $util
            set cell_area [expr {$design_area * $utilization / 100.0}]
        }
    } err]} {
        puts "INFO: report_design_area file capture failed ($err), trying ODB API"
    }

    # Fallback: use OpenROAD's ODB API for area
    if {$design_area eq "null"} {
        if {[catch {
            set _block [ord::get_db_block]
            set _dbu [$_block getDbUnitsPerMicron]
            set _core [$_block getCoreArea]
            set _dx [expr {([$_core xMax] - [$_core xMin]) / double($_dbu)}]
            set _dy [expr {([$_core yMax] - [$_core yMin]) / double($_dbu)}]
            set design_area [format "%.0f" [expr {$_dx * $_dy}]]

            # Sum cell area from placeable instances
            set _cell_dbu2 0
            foreach _inst [$_block getInsts] {
                set _master [$_inst getMaster]
                if {![$_master isBlock]} {
                    set _cell_dbu2 [expr {$_cell_dbu2 + [$_master getWidth] * [$_master getHeight]}]
                }
            }
            set cell_area [format "%.0f" [expr {$_cell_dbu2 / (double($_dbu) * $_dbu)}]]
            if {$design_area > 0} {
                set utilization [format "%.0f" [expr {$cell_area * 100.0 / $design_area}]]
            }
        } err]} {
            puts "WARNING: Could not get area via ODB: $err"
        }
    }

    # Instance count and register count — report_cell_usage doesn't support
    # > file (STA-0566). Use ODB API.
    set register_count "null"
    if {[catch {
        set _block [ord::get_db_block]
        set _all_insts [$_block getInsts]
        set instance_count [llength $_all_insts]
        # Count sequential (register) cells via master isSequential flag
        set _reg_count 0
        foreach _inst $_all_insts {
            if {[[$_inst getMaster] isSequential]} {
                incr _reg_count
            }
        }
        set register_count $_reg_count
    } err]} {
        puts "WARNING: Could not get instance/register count: $err"
    }

    puts "  Cell area:   $cell_area um2"
    puts "  Design area: $design_area um2"
    puts "  Utilization: $utilization %"
    puts "  Instances:   $instance_count"
    puts "  Registers:   $register_count"

    # -------------------------------------------------------------------
    # Power metrics
    # -------------------------------------------------------------------
    set leakage_w "null"
    set internal_w "null"
    set switching_w "null"
    set total_w "null"

    if {[catch {
        set _pwr_out [_capture_report $_tmpf {report_power -digits 6}]
        # OpenROAD report_power output format:
        # --------------------------------------------------------------------------
        # Group                  Internal  Switching    Leakage      Total
        #                           Power      Power      Power      Power (Watts)
        # --------------------------------------------------------------------------
        # Total              X.XXXXe-XX X.XXXXe-XX X.XXXXe-XX X.XXXXe-XX
        #                                                       <percentage>
        foreach line [split $_pwr_out "\n"] {
            if {[regexp {^Total\s+([\d.eE+-]+)\s+([\d.eE+-]+)\s+([\d.eE+-]+)\s+([\d.eE+-]+)} $line match int sw leak tot]} {
                set internal_w $int
                set switching_w $sw
                set leakage_w $leak
                set total_w $tot
            }
        }
    } err]} {
        puts "WARNING: Could not capture power: $err"
    }

    puts "  Leakage:     $leakage_w W"
    puts "  Internal:    $internal_w W"
    puts "  Switching:   $switching_w W"
    puts "  Total:       $total_w W"

    # -------------------------------------------------------------------
    # Congestion metrics — NOT extracted here.
    # global_route prints the GRT-0096 congestion table to stdout which
    # cannot be captured via > file.  pnr_flow.tcl brackets the call
    # with >>> REPORT_CONGESTION_START / <<< REPORT_CONGESTION_END
    # markers, and run_eda.sh parses the flow log post-hoc to inject
    # congestion data into the JSON files.
    # -------------------------------------------------------------------

    # -------------------------------------------------------------------
    # Write key=value flat file
    # -------------------------------------------------------------------
    set fp [open $metrics_txt "w"]
    puts $fp "STAGE=$stage_name"
    puts $fp "CLOCK_PERIOD_NS=$clock_period_ns"
    puts $fp "WNS_SETUP=$wns_setup"
    puts $fp "TNS_SETUP=$tns_setup"
    puts $fp "WNS_HOLD=$wns_hold"
    puts $fp "WORST_PATH_DELAY=$worst_path_delay"
    puts $fp "FAILING_ENDPOINTS=$failing_endpoints"
    puts $fp "TOTAL_ENDPOINTS=$total_endpoints"
    puts $fp "TOTAL_FAILING_PATH_DELAY=$total_failing_path_delay"
    puts $fp "CELL_AREA=$cell_area"
    puts $fp "DESIGN_AREA=$design_area"
    puts $fp "UTILIZATION=$utilization"
    puts $fp "INSTANCE_COUNT=$instance_count"
    puts $fp "REGISTER_COUNT=$register_count"
    puts $fp "LEAKAGE_W=$leakage_w"
    puts $fp "INTERNAL_W=$internal_w"
    puts $fp "SWITCHING_W=$switching_w"
    puts $fp "TOTAL_W=$total_w"
    # Congestion placeholders — filled by run_eda.sh from flow log
    puts $fp "CONGESTION_RESOURCE=null"
    puts $fp "CONGESTION_DEMAND=null"
    puts $fp "CONGESTION_OVERFLOW=null"
    puts $fp "CONGESTION_WIRELENGTH_UM=null"
    # DRC count — only populated for post_route_opt
    puts $fp "DRC_COUNT=$drc_count"
    close $fp

    # -------------------------------------------------------------------
    # Write JSON
    # -------------------------------------------------------------------
    set jp [open $metrics_json "w"]
    puts $jp "\{"
    puts $jp "  \"stage\": [_json_str $stage_name],"
    puts $jp "  \"timestamp\": [_json_str [clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%SZ" -gmt true]],"
    puts $jp "  \"timing\": \{"
    puts $jp "    \"clock_period_ns\": [_json_num $clock_period_ns],"
    puts $jp "    \"wns_setup_ns\": [_json_num $wns_setup],"
    puts $jp "    \"tns_setup_ns\": [_json_num $tns_setup],"
    puts $jp "    \"wns_hold_ns\": [_json_num $wns_hold],"
    puts $jp "    \"worst_path_delay_ns\": [_json_num $worst_path_delay],"
    puts $jp "    \"failing_endpoints\": $failing_endpoints,"
    puts $jp "    \"total_endpoints\": $total_endpoints,"
    puts $jp "    \"total_failing_path_delay_ns\": $total_failing_path_delay"
    puts $jp "  \},"
    puts $jp "  \"area\": \{"
    puts $jp "    \"cell_area_um2\": [_json_num $cell_area],"
    puts $jp "    \"design_area_um2\": [_json_num $design_area],"
    puts $jp "    \"utilization_pct\": [_json_num $utilization],"
    puts $jp "    \"instance_count\": [_json_num $instance_count],"
    puts $jp "    \"register_count\": [_json_num $register_count]"
    puts $jp "  \},"
    puts $jp "  \"power\": \{"
    puts $jp "    \"leakage_w\": [_json_num $leakage_w],"
    puts $jp "    \"internal_w\": [_json_num $internal_w],"
    puts $jp "    \"switching_w\": [_json_num $switching_w],"
    puts $jp "    \"total_w\": [_json_num $total_w]"
    puts $jp "  \},"

    # Congestion placeholder — run_eda.sh replaces this from flow log
    puts $jp "  \"congestion\": null,"

    # DRC count — only meaningful for post_route_opt
    puts $jp "  \"drc_count\": [_json_num $drc_count]"

    puts $jp "\}"
    close $jp

    puts "  Metrics:     $metrics_json"
    puts "  Flat:        $metrics_txt"
    puts "========================================================"

    return $metrics_json
}

# NOTE: Congestion parsing has been moved to run_eda.sh (shell-side).
# The flow log is scraped between >>> REPORT_CONGESTION_START / END
# markers to extract the GRT-0096 congestion table, then injected
# into the JSON files post-hoc.
