# Copyright lowRISC contributors (OpenTitan project).
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
r"""
Class describing simulation results
"""

import collections
import re

from Testplan import Result

_REGEX_REMOVE = [
    # Remove UVM time.
    re.compile(r'@\s+[\d.]+\s+[np]s: '),
    re.compile(r'\[[\d.]+\s+[np]s\] '),
    # Remove assertion time.
    re.compile(r'\(time [\d.]+ [PF]S\) '),
    # Remove leading spaces.
    re.compile(r'^\s+'),
    # Remove extra white spaces.
    re.compile(r'\s+(?=\s)'),
]

_REGEX_STRIP = [
    # Strip TB instance name.
    re.compile(r'[\w_]*top\.\S+\.(\w+)'),
    # Strip assertion.
    re.compile(r'(?<=Assertion )\S+\.(\w+)'),
]

# Regular expression for a separator: EOL or some of punctuation marks.
_SEPARATOR_RE = '($|[ ,.:;])'

_REGEX_STAR = [
    # Replace hex numbers with 0x (needs to be called before other numbers).
    re.compile(r'0x\s*[\da-fA-F]+'),
    # Replace hex numbers with 'h (needs to be called before other numbers).
    re.compile(r'\'h\s*[\da-fA-F]+'),
    # Floating point numbers at the beginning of a word, example "10.1ns".
    # (needs to be called before other numbers).
    re.compile(r'(?<=[^a-zA-Z0-9])\d+\.\d+'),
    # Replace all isolated numbers. Isolated numbers are numbers surrounded by
    # special symbols, for example ':' or '+' or '_', excluding parenthesis.
    # So a number with a letter or a round bracket on any one side, is
    # considered non-isolated number and is not starred by these expressions.
    re.compile(r'(?<=[^a-zA-Z0-9\(\)])\d+(?=($|[^a-zA-Z0-9\(\)]))'),
    # Replace numbers surrounded by parenthesis after a space and followed by a
    # separator.
    re.compile(r'(?<= \()\s*\d+\s*(?=\)%s)' % _SEPARATOR_RE),
    # Replace hex/decimal numbers after an equal sign or a semicolon and
    # followed by a separator. Uses look-behind pattern which need a
    # fixed width, thus the apparent redundancy.
    re.compile(r'(?<=[\w\]][=:])[\da-fA-F]+(?=%s)' % _SEPARATOR_RE),
    re.compile(r'(?<=[\w\]][=:] )[\da-fA-F]+(?=%s)' % _SEPARATOR_RE),
    re.compile(r'(?<=[\w\]] [=:])[\da-fA-F]+(?=%s)' % _SEPARATOR_RE),
    re.compile(r'(?<=[\w\]] [=:] )[\da-fA-F]+(?=%s)' % _SEPARATOR_RE),
    # Replace decimal number at the beginning of the word.
    re.compile(r'(?<= )\d+(?=\S)'),
    # Remove decimal number at end of the word and before '=' or '[' or
    # ',' or '.' or '('.
    re.compile(r'(?<=\S)\d+(?=($|[ =\[,\.\(]))'),
    # Replace the instance string.
    re.compile(r'(?<=instance)\s*=\s*\S+'),
]


class SimResults:
    '''An object wrapping up a table of results for some tests

    self.table is a list of Result objects, each of which
    corresponds to one or more runs of the test with a given name.

    self.buckets contains a dictionary accessed by the failure signature,
    holding all failing tests with the same signature.
    '''

    def __init__(self, items, results):
        self.table = []
        self.buckets = collections.defaultdict(list)
        self._name_to_row = {}
        for item in items:
            self._add_item(item, results)

    def _add_item(self, item, results):
        '''Recursively add a single item to the table of results'''
        status = results[item]
        if status in ["F", "K"]:
            bucket = self._bucketize(item.launcher.fail_msg.message)
            self.buckets[bucket].append(
                (item, item.launcher.fail_msg.line_number,
                 item.launcher.fail_msg.context))

        # Runs get added to the table directly
        if item.target == "run":
            self._add_run(item, status)

    def _add_run(self, item, status):
        '''Add an entry to table for item'''
        row = self._name_to_row.get(item.name)
        if row is None:
            row = Result(item.name,
                         job_runtime=item.job_runtime,
                         simulated_time=item.simulated_time)
            self.table.append(row)
            self._name_to_row[item.name] = row

        else:
            # Record the max job_runtime of all reseeds.
            if item.job_runtime > row.job_runtime:
                row.job_runtime = item.job_runtime
                row.simulated_time = item.simulated_time

        if status == 'P':
            row.passing += 1
        row.total += 1

    def _bucketize(self, fail_msg):
        bucket = fail_msg
        # Remove stuff.
        for regex in _REGEX_REMOVE:
            bucket = regex.sub('', bucket)
        # Strip stuff.
        for regex in _REGEX_STRIP:
            bucket = regex.sub(r'\g<1>', bucket)
        # Replace with '*'.
        for regex in _REGEX_STAR:
            bucket = regex.sub('*', bucket)
        return bucket
