#!/usr/bin/env python3

"""
CLI script to process timing report from OpenSTA and generate timing metrics CSV.

This script parses the output from report_checks and calculates:
- Total Paths
- WNS (Worst Negative Slack)
- TNS (Total Negative Slack)
- NFE (Number of Failing Endpoints)
- Sum of Arrival/Required Ratio
- Individual path slacks
"""

import re
import sys
import os
import csv


def parse_timing_report(report_file: str) -> list:
    """
    Parse timing report file and extract timing metrics.

    Expected format:
    Endpoint                                 Delay     Delay     Slack
    ------------------------------------------------------------------
    _42791_/D (sky130_fd_sc_hs__dfrtp_1)    9.8396   12.9553   -3.1157 (VIOLATED)
    """

    paths = []

    with open(report_file, 'r', encoding='utf-8', errors='replace') as f:
        lines = f.readlines()

    # Find the data section (after the header line with dashes)
    in_data_section = False

    for line in lines:
        # Look for the header separator
        if re.match(r'^-+$', line.strip()):
            in_data_section = True
            continue

        if not in_data_section:
            continue

        # Skip empty lines
        if not line.strip():
            continue

        # Parse data lines
        # Format: endpoint_name  required_delay  actual_delay  slack  (status)
        # Example: _42791_/D (sky130_fd_sc_hs__dfrtp_1)    9.8396   12.9553   -3.1157 (VIOLATED)
        match = re.match(
                r'^(\S+)\s+\(([^)]+)\)\s+([\d.]+)\s+([\d.]+)\s+([-\d.]+)(?:\s+\((\w+)\))?',
                line
        )

        if match:
            endpoint = match.group(1)
            cell_type = match.group(2)
            required_delay = float(match.group(3))
            actual_delay = float(match.group(4))
            slack = float(match.group(5))
            status = match.group(6) if match.group(6) else "MET"

            paths.append({
                'endpoint': endpoint,
                'cell_type': cell_type,
                'required': required_delay,
                'arrival': actual_delay,
                'slack': slack,
                'status': status
            })

    return paths


def calculate_metrics(paths: list, top_module: str):
    """Calculate timing metrics from parsed paths."""

    total_paths = len(paths)

    if total_paths == 0:
        return {
            'top_module': top_module,
            'total_paths': 0,
            'wns': 0.0,
            'tns': 0.0,
            'nfe': 0,
            'sum_arrival_required_ratio': 0.0,
            'slack_list': []
        }

    # Initialize metrics
    wns = float('inf')  # Worst Negative Slack (minimum slack)
    tns = 0.0           # Total Negative Slack
    nfe = 0             # Number of Failing Endpoints
    sum_arrival_required_ratio = 0.0
    slack_list = []

    for path in paths:
        slack = path['slack']
        slack_list.append(slack)

        # Update WNS (minimum slack)
        wns = min(wns, slack)

        # Update TNS and NFE for negative slack
        if slack < 0:
            tns += slack
            nfe += 1

        # Calculate arrival/required ratio
        required = path['required']
        arrival = path['arrival']

        if required != 0:
            ratio = arrival / required
            sum_arrival_required_ratio += ratio

    # If no negative slack found, WNS should be the best (least negative or most positive)
    if wns == float('inf'):
        wns = min(slack_list) if slack_list else 0.0

    return {
        'top_module': top_module,
        'total_paths': total_paths,
        'wns': wns,
        'tns': tns,
        'nfe': nfe,
        'sum_arrival_required_ratio': sum_arrival_required_ratio,
        'slack_list': slack_list
    }


def write_csv(metrics: dict, output_file: str) -> None:
    """Write metrics to CSV file."""

    with open(output_file, 'w', encoding='utf=8', newline='') as f:
        writer = csv.writer(f)

        # Write header
        header = ['Top_Module', 'Total_Paths', 'WNS', 'TNS', 'NFE',
                  'Sum_Arrival_Required_Ratio', 'Path_Slacks']
        writer.writerow(header)

        # Write data row
        row = [
            metrics['top_module'],
            metrics['total_paths'],
            f"{metrics['wns']:.4f}",
            f"{metrics['tns']:.4f}",
            metrics['nfe'],
            f"{metrics['sum_arrival_required_ratio']:.4f}"
        ]

        # Append all slack values
        for slack in metrics['slack_list']:
            row.append(f"{slack:.4f}")

        writer.writerow(row)

# pylint: disable=too-many-locals,too-many-branches,too-many-statements
def print_slack_histogram(
        slack_list: list, bin_width = None, paths_per_char: int = 1
) -> None:
    """
    Print an ASCII-art histogram of slack distribution.

    Args:
        slack_list: List of slack values
        bin_width: Width of each bin (auto-calculated if None)
        paths_per_char: Number of paths represented by each character
    """

    if not slack_list:
        print("\nNo paths to display in histogram.")
        return

    # Calculate the range and ensure 0 is a bin boundary
    min_slack = min(slack_list)
    max_slack = max(slack_list)

    # If bin_width not specified, calculate a reasonable default
    # Target around 20-30 total bins
    if bin_width is None:
        total_range = max(abs(min_slack), abs(max_slack)) * 2
        bin_width = total_range / 25  # Aim for ~25 bins

        # Round to a nice number for readability
        magnitude = 10 ** (len(str(int(bin_width))) - 1)
        bin_width = max(0.1, round(bin_width / magnitude) * magnitude)

    # Build bin edges with 0 as an exact boundary
    bin_edges = []

    # Create negative bins (from min_slack up to 0)
    if min_slack < 0:
        # Calculate how many bins we need to cover the negative range
        num_neg_bins = int(abs(min_slack) / bin_width) + 1
        # Start from a multiple of bin_width below min_slack
        neg_start = -num_neg_bins * bin_width

        for i in range(num_neg_bins):
            edge = neg_start + i * bin_width
            if edge >= min_slack:
                bin_edges.append(edge)

    # Add zero boundary
    bin_edges.append(0.0)

    # Create positive bins (from 0 up to max_slack)
    if max_slack > 0:
        # Calculate how many bins we need to cover the positive range
        num_pos_bins = int(max_slack / bin_width) + 1

        for i in range(1, num_pos_bins + 1):
            edge = i * bin_width
            bin_edges.append(edge)
            if edge >= max_slack:
                break

    # Remove duplicates and sort (in case of floating point issues)
    bin_edges = sorted(list(set(bin_edges)))

    # Initialize bins
    num_bins = len(bin_edges) - 1
    bins = [0] * num_bins

    # Count paths in each bin
    for slack in slack_list:
        # Find the appropriate bin
        for i in range(num_bins):
            if bin_edges[i] <= slack < bin_edges[i + 1]:
                bins[i] += 1
                break
        else:
            # Handle the edge case where slack equals the maximum
            if slack == bin_edges[-1]:
                bins[-1] += 1

    # Print histogram
    print("\n" + "=" * 70)
    print("Slack Distribution Histogram")
    print("=" * 70)
    print(f"Total Paths: {len(slack_list)}")
    print(f"Slack Range: [{min_slack:.4f}, {max_slack:.4f}]")
    print(f"Uniform Bin Width: {bin_width:.4f}")
    print(f"Each character represents {paths_per_char} path(s)")
    print()

    # Print header
    print(f"{'Slack Range':<25} {'Count':<8} Distribution")
    print("-" * 70)

    # Print each bin
    for i in range(num_bins):
        bin_start = bin_edges[i]
        bin_end = bin_edges[i + 1]
        count = bins[i]

        # Calculate bar width based on paths_per_char
        bar_width = count // paths_per_char

        # Choose character based on whether slack is negative (violated)
        if bin_end <= 0:
            bar_char = 'X'  # Violated paths
        else:
            bar_char = '='  # Paths meeting timing

        # Create bar
        _bar = bar_char * bar_width

        # Format bin range
        bin_label = f"[{bin_start:>7.4f}, {bin_end:>7.4f})"

        # Print row
        print(f"{bin_label:<25} {count:<8} {_bar}")

    print("-" * 70)
    print("Legend: X = Violated (slack < 0), = = Met (slack >= 0)")
    print("=" * 70)


def print_summary(metrics):
    """Print timing metrics summary to console."""

    print("\n" + "=" * 42)
    print("Timing Metrics Summary")
    print("=" * 42)
    print(f"Top Module:                    {metrics['top_module']}")
    print(f"Total Paths to Registers:      {metrics['total_paths']}")
    print(f"Worst Negative Slack (WNS):    {metrics['wns']:.4f}")
    print(f"Total Negative Slack (TNS):    {metrics['tns']:.4f}")
    print(f"Number of Failing Endpoints:   {metrics['nfe']}")
    print(f"Sum Arrival/Required Ratio:    {metrics['sum_arrival_required_ratio']:.4f}")
    print("=" * 42)


def main():
    """Main function."""

    # Get environment variables or use defaults
    timing_report_file = os.getenv('TIMING_REPORT_FILE', 'timing_report.txt')
    top_module = os.getenv('TOP_MODULE', 'unknown')
    output_csv = os.getenv('OUTPUT_CSV', 'timing_metrics.csv')

    # Allow command line override
    if len(sys.argv) > 1:
        timing_report_file = sys.argv[1]
    if len(sys.argv) > 2:
        top_module = sys.argv[2]
    if len(sys.argv) > 3:
        output_csv = sys.argv[3]

    # Check if report file exists
    if not os.path.exists(timing_report_file):
        print(f"Error: Timing report file '{timing_report_file}' not found.", file=sys.stderr)
        sys.exit(1)

    print(f"Processing timing report: {timing_report_file}")

    # Parse timing report
    paths = parse_timing_report(timing_report_file)

    # Calculate metrics
    metrics = calculate_metrics(paths, top_module)

    # Write CSV output
    write_csv(metrics, output_csv)
    print(f"CSV file written to: {output_csv}")

    # Print summary
    print_summary(metrics)

    # Print slack histogram
    print_slack_histogram(metrics['slack_list'])


if __name__ == '__main__':
    main()
