#!/usr/bin/env python3
# Copyright lowRISC contributors (OpenTitan project).
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
"""
This script provides common DV simulation specific utilities.
"""

import re
from collections import OrderedDict
from typing import List, Tuple


# Capture the summary results as a list of lists.
# The text coverage report is passed as input to the function, in addition to
# the tool used. The tool returns a 2D list if the coverage report file was read
# and the coverage was extracted successfully. It returns a tuple of:
#   List of metrics and values
#   Final coverage total
#
# Raises the appropriate exception if the coverage summary extraction fails.
def get_cov_summary_table(cov_report_txt, tool):
    with open(cov_report_txt, 'r') as f:
        if tool == 'xcelium':
            return xcelium_cov_summary_table(f)
        if tool == 'vcs':
            return vcs_cov_summary_table(f)
        raise NotImplementedError(f"{tool} is unsupported for cov extraction.")


# Same desc as above, but specific to Xcelium and takes an opened input stream.
def xcelium_cov_summary_table(buf):
    for line in buf:
        if "name" in line:
            # Strip the line and remove the unwanted "* Covered" string.
            metrics = line.strip().replace("* Covered", "").split()
            # Change first item to 'Score'.
            metrics[0] = 'Score'

            # Gather the list of metrics.
            items = OrderedDict()
            for metric in metrics:
                items[metric] = {}
                items[metric]['covered'] = 0
                items[metric]['total'] = 0

            # Next line is a separator.
            line = buf.readline()

            # Subsequent lines are coverage items to be aggregated.
            for line in buf:
                line = re.sub(r"%\s+\(", "%(", line)
                values = line.strip().split()
                for i, value in enumerate(values):
                    value = value.strip()
                    m = re.search(r"\((\d+)/(\d+).*\)", value)
                    if m:
                        items[metrics[i]]['covered'] += int(m.group(1))
                        items[metrics[i]]['total'] += int(m.group(2))
                        items['Score']['covered'] += int(m.group(1))
                        items['Score']['total'] += int(m.group(2))
            # Capture the percentages and the aggregate.
            values = []
            cov_total = None
            for metric in items.keys():
                if items[metric]['total'] == 0:
                    values.append("-- %")
                else:
                    value = items[metric]['covered'] / items[metric][
                        'total'] * 100
                    value = "{0:.2f} %".format(round(value, 2))
                    values.append(value)
                    if metric == 'Score':
                        cov_total = value
            return [items.keys(), values], cov_total

    # If we reached here, then we were unable to extract the coverage.
    raise SyntaxError(f"Coverage data not found in {buf.name}!")


# Same desc as above, but specific to VCS and takes an opened input stream.
def vcs_cov_summary_table(buf):
    for line in buf:
        match = re.match("total coverage summary", line, re.IGNORECASE)
        if match:
            # Metrics on the next line.
            line = buf.readline().strip()
            metrics = line.split()
            # Values on the next.
            line = buf.readline().strip()
            # Pretty up the values - add % sign for ease of post
            # processing.
            values = []
            for val in line.split():
                val += " %"
                values.append(val)
            # first row is coverage total
            cov_total = values[0]
            return [metrics, values], cov_total

    # If we reached here, then we were unable to extract the coverage.
    raise SyntaxError(f"Coverage data not found in {buf.name}!")


def get_job_runtime(log_text: List, tool: str) -> Tuple[float, str]:
    """Returns the job runtime (wall clock time) along with its units.

    EDA tools indicate how long the job ran in terms of CPU time in the log
    file. This method invokes the tool specific method which parses the log
    text and returns the runtime as a floating point value followed by its
    units as a tuple.

    `log_text` is the job's log file contents as a list of lines.
    `tool` is the EDA tool used to run the job.
    Returns the runtime, units as a tuple.
    Raises NotImplementedError exception if the EDA tool is not supported.
    """
    if tool == 'xcelium':
        return xcelium_job_runtime(log_text)
    elif tool == 'vcs':
        return vcs_job_runtime(log_text)
    else:
        raise NotImplementedError(f"{tool} is unsupported for job runtime "
                                  "extraction.")


def vcs_job_runtime(log_text: List) -> Tuple[float, str]:
    """Returns the VCS job runtime (wall clock time) along with its units.

    Search pattern example:
    CPU time: 22.170 seconds to compile + .518 seconds to elab + 1.901 \
        seconds to link
    CPU Time:      0.610 seconds;       Data structure size:   1.6Mb

    Returns the runtime, units as a tuple.
    Raises RuntimeError exception if the search pattern is not found.
    """
    pattern = r"^CPU [tT]ime:\s*(\d+\.?\d*?)\s*(seconds|minutes|hours).*$"
    for line in reversed(log_text):
        m = re.search(pattern, line)
        if m:
            return float(m.group(1)), m.group(2)[0]
    raise RuntimeError("Job runtime not found in the log.")


def xcelium_job_runtime(log_text: List) -> Tuple[float, str]:
    """Returns the Xcelium job runtime (wall clock time) along with its units.

    Search pattern example:
    TOOL:	xrun(64)	21.09-s006: Exiting on Aug 01, 2022 at 00:21:18 PDT \
        (total: 00:00:05)

    Returns the runtime, units as a tuple.
    Raises RuntimeError exception if the search pattern is not found.
    """
    pattern = (r"^TOOL:\s*xrun.*: Exiting on .*\(total:\s*(\d+):(\d+):(\d+)\)"
               r"\s*$")
    for line in reversed(log_text):
        m = re.search(pattern, line)
        if m:
            t = int(m.group(1)) * 3600 + int(m.group(2)) * 60 + int(m.group(3))
            return t, "s"
    raise RuntimeError("Job runtime not found in the log.")


def get_simulated_time(log_text: List, tool: str) -> Tuple[float, str]:
    """Returns the simulated time along with its units.

    EDA tools indicate how long the design was simulated for in the log file.
    This method invokes the tool specific method which parses the log text and
    returns the simulated time as a floating point value followed by its
    units (typically, pico|nano|micro|milliseconds) as a tuple.

    `log_text` is the job's log file contents as a list of lines.
    `tool` is the EDA tool used to run the job.
    Returns the simulated, units as a tuple.
    Raises NotImplementedError exception if the EDA tool is not supported.
    """
    if tool == 'xcelium':
        return xcelium_simulated_time(log_text)
    elif tool == 'vcs':
        return vcs_simulated_time(log_text)
    else:
        raise NotImplementedError(f"{tool} is unsupported for simulated time "
                                  "extraction.")


def xcelium_simulated_time(log_text: List) -> Tuple[float, str]:
    """Returns the Xcelium simulated time along with its units.

    Search pattern example:
    Simulation complete via $finish(2) at time 11724965 PS + 13

    Returns the simulated time, units as a tuple.
    Raises RuntimeError exception if the search pattern is not found.
    """
    pattern = r"^Simulation complete .* at time (\d+\.?\d*?)\s*(.?[sS]).*$"
    for line in reversed(log_text):
        m = re.search(pattern, line)
        if m:
            return float(m.group(1)), m.group(2).lower()
    raise RuntimeError("Simulated time not found in the log.")


def vcs_simulated_time(log_text: List) -> Tuple[float, str]:
    """Returns the VCS simulated time along with its units.

    Search pattern example:
               V C S   S i m u l a t i o n   R e p o r t
    Time: 12241752 ps

    Returns the simulated time, units as a tuple.
    Raises RuntimeError exception if the search pattern is not found.
    """
    pattern = r"^Time:\s*(\d+\.?\d*?)\s*(.?[sS])\s*$"
    next_line = ""
    for line in reversed(log_text):
        if "V C S   S i m u l a t i o n   R e p o r t" in line:
            m = re.search(pattern, next_line)
            if m:
                return float(m.group(1)), m.group(2).lower()
        next_line = line
    raise RuntimeError("Simulated time not found in the log.")
