''' opencos.tools.riviera - Used by opencos.eda for sim/elab commands w/ --tool=riviera.

Contains classes for ToolRiviera, CommandSimRiviera, CommandElabRiviera.
'''

# pylint: disable=too-many-ancestors
# pylint: disable=R0801 # (duplicate code in derived classes, such as if-condition return.)

import argparse
import glob
import os
from pathlib import Path
import shutil
import tempfile
import time

from opencos import util
from opencos.commands import CommandFList, CommandWaves
from opencos.commands.coverage import CommandCoverage
from opencos.files import safe_shutil_which, safe_output_file
from opencos.commands.sim import WAVES_PKG_SV_FNAME
from opencos.tools.questa_common import ToolQuesta, CommonSimQuesta
from opencos.utils import status_constants, vsim_helper
from opencos.utils.str_helpers import sanitize_defines_for_sh

from opencos.tools.riviera_coverage import RivieraSimCoverageAddOn, \
    get_acdb_output_file_from_tcl_line

class ToolRiviera(ToolQuesta):
    '''ToolRiviera used by opencos.eda for --tool=riviera'''

    _TOOL = 'riviera'
    _EXE = 'vsim'
    use_vopt = False
    uvm_versions = set()
    uvm_home_dirs = {} # key (str item in uvm_versions): value (str path where uvm_pkv.sv exists)


    def get_versions(self, **kwargs) -> str:
        if self._VERSION:
            return self._VERSION

        path = self.config.get('auto_tools_found', {}).get(self._TOOL, '')
        if not path and self._EXE == 'vsim':
            # otherwise, use the vsim helper:
            path = vsim_helper.lookup_exe(self._TOOL)
        if not path:
            # otherwise, fallback to shutil which:
            path = safe_shutil_which(self._TOOL)
        if not path:
            if self.get_version_allow_certain_command_if_tool_not_installed():
                return self._VERSION
            self.error(f"{self._EXE} not in path, need to setup or add to PATH")
            util.debug(f"{path=}")
        else:
            self.sim_exe = path
            self.sim_exe_base_path, _ = os.path.split(path)
            self.waveviewer_exe = os.path.join(self.sim_exe_base_path, 'vsim')

        self._VERSION = vsim_helper.lookup_version(self._TOOL)

        # Get the UVM versions in the install directory. Note this may run
        # more than once, so only do this if self.uvm_versions not yet set:
        riviera_path, _ = os.path.split(self.sim_exe_base_path)
        vlib_path = os.path.join(riviera_path, 'vlib')
        if not self.uvm_versions and os.path.isdir(vlib_path):
            for item in os.listdir(vlib_path):
                # uvm-1.1, uvm-1.1d - so don't pick anything > 9 chars (uvm-M.mRr)
                if item.startswith('uvm-') and '1800' not in item and len(item) <= 9:
                    uvm_ver = item[4:]
                    self.uvm_versions.add(uvm_ver)
                    # While we're at it, make note of location of uvm_pkg.sv?
                    if os.path.isfile(os.path.join(vlib_path, item, 'src', 'uvm_pkg.sv')):
                        self.uvm_home_dirs[uvm_ver] = os.path.join(vlib_path, item, 'src')

        return self._VERSION


class CommandWavesRiviera(CommandWaves, ToolRiviera):
    '''Handler for: eda waves --tool=riviera'''

    def __init__(self, config: dict):
        CommandWaves.__init__(self, config=config)
        ToolRiviera.__init__(self, config=self.config)

    def do_it(self) -> None:
        # If you don't like the self.waveviewer_exe, use:
        #    os.path.join(self.sim_exe_base_path, 'vsim') or whichever you prefer
        if not self.waveviewer_exe:
            self.error(f'Unable to find the exe for --tool={self._TOOL} for command waves',
                       'check "eda --help" or PATH for vsim')
        command_list = [
            self.waveviewer_exe, self.wave_file
        ]
        self.exec(os.path.dirname(self.wave_file), command_list)


class CommandSimRiviera(CommonSimQuesta, ToolRiviera):
    '''CommandSimRiviera is a command handler for: eda sim --tool=riviera

    From CommonSimQuesta (and opencos.commands.sim.CommandSim), the common entrypoint is:
        -- process_tokens()
             --> run commands from DEPS
             --> run_argparser_on_list() -- populate self.args
             --> do_it() / run()
                 --> prepare_compile()
                       --> get_*_command_list()
                 --> compile(), elaborate(), simulate()
             --> run_post_tool_dep_commands()
             --> report pass/fail
             --> return unparsed args back to eda.py.


    When using --coverage:
    self.coverage_obj (opencos.tools.riviera_coverage.RiveriaSimCoverageAddOn) is used to
    provide addtional coverage features. See riviera_coverage.py for full details.
        - add_coverage_args_to_handler() -- applied with --coverage arg present
        - calculate_coverage_delta(metrics, metrics)
        - check_delta_thresholds(delta metrics)
        - display_coverage_delta(delta metrics)
        - display_coverage_summary(metrics dict)
        - sim_post_simulate_coverage() -- called with --coverage to create better summaries
              outside of the Riviera vsim.do TCL flow
        - generate_baseline_report(acdb fpath) - gets temporary directory with ACDB reports
        - generate_coverage_json(cov.txt, json output, eda target)
        - generate_delta_json(delta metrics, path, json output)
        - get_toggle_coverage_commands() -- generates vsim TCL commands, used prior to
              "run -a" to apply toggle coverage items (after vlog, vsim steps, prior to run step)
        - get_covergroup_coverage_commands() -- generates vsim TCL command to apply covergroup
              (bins) after vlog and vsim steps, prior to run step.
        - parse_coverage_metrics(cov.txt) -- returns metrics dict
    '''

    library_map_supported: bool  = True
    use_add_on_coverage_ast: bool = False

    def __init__(self, config: dict):
        CommonSimQuesta.__init__(self, config=config)

        # This technically inherits ToolQuesta and ToolRiviera, but ToolRiviera will
        # override get_versions(). Just be careful on using issinstance(cls_obj, ToolQuesta),
        # and instead use self._TOOL, or self.args['tool']
        ToolRiviera.__init__(self, config=self.config)

        self.shell_command = os.path.join(self.sim_exe_base_path, 'vsim')
        self.starter_edition = False


        self.args.update({
            'tool': self._TOOL, # override
            'gui': False,
            'waves-fst': True,
            'waves-vcd': False,
            'custom-run-tcl': '',
            'coverage-tcl': '',
            'coverage-covergroups': False,
            'ext-defines-sv-fname': '_ext_defines.sv',
            'license-queue': True,
            'license-queue-value': '30',
        })
        self.args_help.update({
            'waves-fst': (
                '(Default True) If using --waves, apply simulation runtime arg +trace.'
                ' Note that if you do not have SV code using $dumpfile, eda will add'
                ' _waves_pkg.sv to handle this for you with +trace runtime plusarg.'
            ),
            'waves-vcd': 'If using --waves, apply simulation runtime arg +trace=vcd',
            'waves': 'Save a .asdb offline wavefile, can be used with --waves-fst or --waves-vcd',
            'coverage': (
                'See --coverage with --help for many additional --coverage-* args'
                ' many of which require external tool "slang". --coverage on its own will'
                ' run basic Riviera coverage, save a work.acdb and cov.txt report, such as:'
                ' --coverage-summary'
            ),
            'coverage-tcl': (
                'bring your own .tcl file to run in Riviera (vsim) for coverage. The default'
                ' tcl steps are (from tool config in --config-yml):   '
            ) + '; '.join(self.tool_config.get('simulate-coverage-tcl', [])),
            'uvm': (
                'Attempts to support UVM. For Riviera, this adds "-uvmver NUMBER -dbg" to vlog.f.'
                ' You can choose your uvmver using eda arg --uvm-version=NUMBER.'
                ' Also adds +access +r to vopt/vsim. There is no -l or -L library modifications,'
                ' we rely on Riviera to handle this internally based on running: vlog -uvm'
            ),
            'license-queue': (
                'Sets, via TCL, in-tool variables for LICENSE_QUEUE=30, which should set Riviera'
                ' to wait for licenses instead of immediately exiting with error. If unset'
                ' we also check ENV var LICENSE_QUEUE=VALUE, and if present that is used instead'
            ),
            'license-queue-value': (
                'Value used (LICENSE_QUEUE=VALUE) if --license-queue is set, which takes higher'
                ' precedence than ENV var LICENSE_QUEUE=VALUE'
            ),
            'custom-run-tcl': (
                'Bring your own .tcl file that will be included after the "vsim" step, as a'
                ' replacement for the default "run -all"'
            )
        })

        if self.uvm_versions:
            # set default to latest version:
            self.args['uvm-version'] = sorted(self.uvm_versions)[-1]

        self.args_kwargs.update({
            'uvm-version': { 'choices': list(self.uvm_versions) },
        })

        # Note this will NOT update self.args yet:
        self.coverage_obj = RivieraSimCoverageAddOn(handler_obj=self)


    def run_argparser_on_list(
            self, tokens: list, parser_arg_list=None, apply_parsed_args: bool = True
    ) -> (argparse.Namespace, list):
        '''
        Override on Command.run_argparser_on_list - to conditionally add
        args from self.coverage_obj if --coverage or --coverage-* is set
        '''

        # This is a little inefficient, we could instead make a customer argparser just
        # for --coverage, but this also works.
        # Run argparser w/out appyling to fish out --coverage:
        if not self.coverage_obj.have_added_coverage_args_to_handler:
            parsed, unparsed = super().run_argparser_on_list(
                tokens=tokens, parser_arg_list=parser_arg_list, apply_parsed_args=False
            )

            add_on_args = parsed.coverage
            if not add_on_args:
                # Be nice and do this if user ran with --coverage-summary but forgot to
                # add --coverage:
                add_on_args = any(x.startswith('--coverage-') for x in unparsed)

            if add_on_args:
                # Add args from riviera_coverage b/c --coverage, note that self.coverage_obj
                # should only do this once.
                self.coverage_obj.add_coverage_args_to_handler()


        # Run the argparser on the original tokens, applying them this time (if caller
        # wanted them applied)
        parsed, unparsed = super().run_argparser_on_list(
            tokens=tokens, parser_arg_list=parser_arg_list, apply_parsed_args=apply_parsed_args
        )

        self.use_add_on_coverage_ast = self.coverage_obj.any_add_on_coverage_ast_args_set()
        util.debug(f'CoverageAddOn - {self.use_add_on_coverage_ast=}')
        return parsed, unparsed


    # Note: many of these we follow the same flow as CommandSimModelsimAse:
    # do_it, prepare_compile, compile, elaborate, simulate

    def compile(self):
        '''Override for CommandSimModelsimAse.compile() so we can set our own must_strings'''

        if self.args['stop-before-compile']:
            # don't run anything, save everyting we've already run in _prep_compile()
            return
        if self.args['stop-after-compile']:
            vsim_command_lists = self.get_compile_command_lists()
            self.run_commands_check_logs(vsim_command_lists, log_filename='sim.log',
                                         must_strings=['Compile success 0 Errors'],
                                         use_must_strings=False,
                                         sim_step='compile')

    def simulate(self):
        '''Override to add coverage reporting after simulation completes'''

        # Run normal sim.CommandSim.simulate() step:
        super().simulate()

        if self.args.get('coverage', False):
            # Run RiveraCoverageAddOn.sim_post_simulate_coverage() step:
            self.coverage_obj.sim_post_simulate_coverage()
            # Add artifacts
            util.artifacts.add_extension(
                search_paths=self.args['work-dir'], file_extension='acdb',
                typ='coverage_database',
                description='Riviera Coverage database binary file'
            )

    def get_compile_command_lists(self, **kwargs) -> list:
        # This will also set up a compile.
        vsim_command_list = [
            self.sim_exe,
            '' if self.args['gui'] else '-c',
            '-l', 'sim.log', '-do', 'vsim_vlogonly.do'
        ]
        return [vsim_command_list]

    def get_elaborate_command_lists(self, **kwargs) -> list:
        # This will also set up a compile, for vlog + vsim (0 time)
        vsim_command_list = [
            self.sim_exe,
            '' if self.args['gui'] else '-c',
            '-l', 'sim.log', '-do', 'vsim_lintonly.do',
        ]
        return [vsim_command_list]

    def get_simulate_command_lists(self, **kwargs) -> list:
        # This will also set up a compile, for vlog + vsim (with run -all)
        vsim_command_list = [
            self.sim_exe,
            '' if self.args['gui'] else '-c',
            '-l', 'sim.log', '-do', 'vsim.do',
        ]
        return [vsim_command_list]

    def get_post_simulate_command_lists(self, **kwargs) -> list:
        return []


    def write_vlog_dot_f( # pylint: disable=too-many-branches
            self, filename: str = 'vlog.f'
    ) -> None:
        '''Returns none, creates filename (str) for a vlog.f'''
        vlog_dot_f_lines = []

        # Add compile args from config.tool.riviera
        vlog_dot_f_lines += self.tool_config.get(
            'compile-args',
            '-sv -input_ports net').split()

        # Add waivers from config.tool.riviera, convert to warning:
        for waiver in self.tool_config.get('compile-waivers', []) + \
                self.args['compile-waivers']:
            vlog_dot_f_lines += [f'-err {waiver} W1']

        vlog_dot_f_fname = filename
        vlog_dot_f_fpath = os.path.join(self.args['work-dir'], vlog_dot_f_fname)

        if self.args['uvm']:
            vlog_dot_f_lines.extend([
                f'-uvmver {self.args["uvm-version"]}',
                '-dbg'
            ])

        # Note, in Riviera we have to put sim libs in vsim call (with -L) and in vlog (with -l)
        vlog_dot_f_lines.extend([f'-l {x}' for x in self.args['sim-library']])

        for value in self.incdirs:
            vlog_dot_f_lines += [ f"+incdir+{value}" ]

        # For external defines, we will create a file in the work-dir named
        # _ext_defines.sv, it will be the first file in the file list, and it
        # will be valid Verilog/SV, not a list of +define+Name[=Value]
        if self.args['ext-defines-sv-fname']:
            self.create_ext_defines_sv()
        else:
            for k,v in self.defines.items():
                if v is None:
                    vlog_dot_f_lines += [ f'+define+{k}' ]
                else:
                    # if the value v is a double-quoted string, such as v='"hi"', the
                    # entire +define+NAME="hi" needs to wrapped in double quotes with the
                    # value v double-quotes escaped: "+define+NAME=\"hi\""
                    if isinstance(v, str) and v.startswith('"') and v.endswith('"'):
                        str_v = v.replace('"', '\\"')
                        vlog_dot_f_lines += [ f'"+define+{k}={str_v}"' ]
                    else:
                        # Generally we should only support int and str python types passed as
                        # +define+{k}={v}, but also for SystemVerilog plusargs
                        vlog_dot_f_lines += [ f'+define+{k}={sanitize_defines_for_sh(v)}' ]



        vlog_dot_f_lines += self.args['compile-args']
        if self.args['coverage']:
            vlog_dot_f_lines += self.tool_config.get('compile-coverage-args', '').split()

        vlog_dot_f_lines += [
            ] + list(self.files_sv) + list(self.files_v)

        if not self.files_sv and not self.files_v:
            if not self.args['stop-before-compile']:
                self.error(f'{self.target=} {self.files_sv=} and {self.files_v=} are empty,',
                           'cannot create a valid vlog.f')

        with open(vlog_dot_f_fpath, 'w', encoding='utf-8') as f:
            f.writelines(line + "\n" for line in vlog_dot_f_lines)



    def write_vsim_dot_do( # pylint: disable=too-many-branches,too-many-locals,too-many-statements
            self, dot_do_to_write: list
    ) -> None:
        '''Writes files(s) based on dot_do_to_write(list of str)

        list arg values can be empty (all) or have items 'all', 'sim', 'lint', 'vlog'.'''

        vsim_dot_do_fpath = os.path.join(self.args['work-dir'], 'vsim.do')
        vsim_lintonly_dot_do_fpath = os.path.join(self.args['work-dir'], 'vsim_lintonly.do')
        vsim_vlogonly_dot_do_fpath = os.path.join(self.args['work-dir'], 'vsim_vlogonly.do')

        sim_plusargs_str = self._get_sim_plusargs_str()
        vsim_ext_args = ' '.join(self.args.get('sim-args', []))

        if self.args['waves'] and '+trace' not in sim_plusargs_str:
            if self.args.get('waves-vcd', False):
                sim_plusargs_str += ' +trace=vcd'
            elif self.args.get('waves-fst', False):
                sim_plusargs_str += ' +trace'

        voptargs_str = self.tool_config.get('elab-args', '')
        voptargs_str += ' '.join(self.args.get('elab-args', []))
        if self.args['gui'] or self.args['waves'] or self.args['coverage']: # \
           #or self.args['waves-asdb']:
            voptargs_str += self.tool_config.get('simulate-waves-args',
                                                 '+accb +accr +access +r+w')
        if self.args['coverage']:
            voptargs_str += ' ' + self.tool_config.get('coverage-args', '')

        if self.args['uvm']:
            if all(x not in voptargs_str for x in ('+access +r', '+access +w+r')):
                voptargs_str += ' +access +r'

        # parameters, for Riviera we need to use the leading forward slash option
        # aka -G/Name=Value, if our parameter name doesn't already have path information.
        # If you do -GName=Value, all parameters in the hierachy with Name will be set,
        # so to only do top level parameters, if hierarchy isn't mentioned in the Name,
        # would need to do -G/{self.args['top']}/Name=Value.
        if self.parameters:
            voptargs_str += ' ' + ' '.join(
                self.process_parameters_get_list(
                    arg_prefix='-G', hier_delimiter='/', top_hier_str=f'/{self.args["top"]}/'
                )
            )

        # Note, in Riviera we have to put sim libs in vsim call (with -L) and in vlog (with -l)
        # we previously updated self.args['sim-library'] with self.update_library_map()
        vmap_lines_str = self.get_vmap_lines_str()
        vsim_libs = ' '.join([f'-L {x}' for x in self.args['sim-library']])

        vsim_one_liner = (
            "vsim"
            f" -sv_seed {self.args['seed']} {sim_plusargs_str}"
            f" {voptargs_str} {vsim_ext_args} {vsim_libs} work.{self.args['top']}"
        )

        if self.args['waves'] and WAVES_PKG_SV_FNAME in self.files:
            # kind of a hack, but add _waves_module (from _waves_pkg.sv) if that file
            # was in our files list.
            vsim_one_liner += ' work._waves_module'

        for l in self.args['add-top-library']:
            vsim_one_liner += f' {l}'

        vsim_one_liner = vsim_one_liner.replace('\n', ' ') # needs to be a one-liner

        dot_do_start_lines = []

        if self.args['license-queue']:
            dot_do_start_lines.append(
                f'set LICENSE_QUEUE {self.args.get("license-queue-value", 30)};'
            )
        elif value := os.environ.get('LICENSE_QUEUE', ''):
            dot_do_start_lines.append(f'set LICENSE_QUEUE {value};')

        dot_do_start_lines.extend([
            "if {[file exists work]} { vdel -all work; }",
            "vlib work;",
            "set qc 30;",
            vmap_lines_str,
        ])


        vsim_vlogonly_dot_do_lines = dot_do_start_lines + [
            "if {[catch {vlog -f vlog.f} result]} {",
            "    echo \"Caught $result \";",
            "    if {[batch_mode]} {",
            "        quit -code 20 -force;",
            "    }",
            "}",
            "if {[batch_mode]} {",
            "    quit -code 0 -force;",
            "}",
        ]

        vsim_lintonly_dot_do_lines = dot_do_start_lines + [
            "if {[catch {vlog -f vlog.f} result]} {",
            "    echo \"Caught $result \";",
            "    if {[batch_mode]} {",
            "        quit -code 20 -force;",
            "    }",
            "}",
            "if {[catch { " + vsim_one_liner + " } result] } {",
            "    echo \"Caught $result\";",
            "    if {[batch_mode]} {",
            "        quit -code 19 -force;",
            "    }",
            "}",
            "if {[batch_mode]} {",
            "    quit -code 0 -force;",
            "}",
        ]

        vsim_dot_do_lines = dot_do_start_lines + [
            "if {[catch {vlog -f vlog.f} result]} {",
            "    echo \"Caught $result \";",
            "    if {[batch_mode]} {",
            "        quit -code 20 -force;",
            "    }",
            "}",
            "if {[catch { " + vsim_one_liner + " } result] } {",
            "    echo \"Caught $result\";",
            "    if {[batch_mode]} {",
            "        quit -code 19 -force;",
            "    }",
            "}",
        ]

        #
        # --coverage features:
        #
        # Insert toggle or covergroup coverage commands AFTER vsim but BEFORE run -all
        # This is required by Riviera which cannot enable toggle coverage globally
        #
        if self.use_add_on_coverage_ast:

            # get an AST and read it (will be in self.coverage_obj.slang_ast.ast_data)
            # This also handled UVM for slang:
            # The saved .sh script is in work-dir: self.coverage_obj.slang_ast.slang_sh_fname
            self.coverage_obj.run_slang_for_ast_auto()
            # Set this for CommandSim.sim to be aware of in write_sh_scripts_to_work_dir(..)
            self.has_pre_compile_slang_sh = self.coverage_obj.slang_ast.slang_sh_fname


            if self.args.get('coverage-toggle', False):
                vsim_dot_do_lines.extend(self.coverage_obj.get_toggle_coverage_commands())


        if fpath := self.args.get('custom-run-tcl', ''):
            if not os.path.exists(self.args['custom-run-tcl']):
                self.error(f'--custom-run-tcl={fpath} file does not exist')
            with open(fpath, 'r', encoding='utf-8', errors='replace') as f:
                vsim_dot_do_lines.extend(['', f"## Included from: --custom-run-tcl={fpath}"])
                for line in f.readlines():
                    vsim_dot_do_lines.append(line.rstrip('\n'))
                vsim_dot_do_lines.extend([f"## End include from: (--custom-run-tcl={fpath}", ''])
        else:
            vsim_dot_do_lines.append("run -all;")

        if self.args['coverage']:
            will_run_sim = not(any(self.args.get(x, None) for x in ('stop-before-compile',
                                                                    'stop-after-elaborate',
                                                                    'stop-after-compile')) or
                               self.command_name in ('elab', 'lint'))
            if self.args['coverage-tcl']:
                tclfile = os.path.abspath(self.args['coverage-tcl'])
                if not os.path.isfile(tclfile):
                    self.error(f'--coverage-tcl file not found: {tclfile}',
                               error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)
                vsim_dot_do_lines += [
                    f'source {tclfile}'
                ]
                # Unfortunately we won't know what's in the user supplied coverage-tcl file, so
                # for artifacts we have to open it and try to fish them out.
                # Also only add these *if* we're running the simulation
                if will_run_sim:
                    with open(tclfile, 'r', encoding='utf-8') as f:
                        for line in f.readlines():
                            get_acdb_output_file_from_tcl_line(
                                line, work_dir=self.args['work-dir'], add_to_artifacts=True
                            )

            else:
                tcl_lines = self.tool_config.get('simulate-coverage-tcl', [])

                if will_run_sim:
                    # default TCL for coverage, similarly go through the lines for ACDB artifacts
                    # Also only add these *if* we're running the simulation
                    for line in tcl_lines:
                        get_acdb_output_file_from_tcl_line(
                            line, work_dir=self.args['work-dir'], add_to_artifacts=True
                        )

                vsim_dot_do_lines += tcl_lines


        vsim_dot_do_lines += [
            "if {[batch_mode]} {",
            "    quit -code 0 -force;",
            "}",
        ]

        write_all = len(dot_do_to_write) == 0 or 'all' in dot_do_to_write
        if write_all or 'sim' in dot_do_to_write:
            with open(vsim_dot_do_fpath, 'w', encoding='utf-8') as f:
                f.writelines(line + "\n" for line in vsim_dot_do_lines)

        if write_all or 'lint' in dot_do_to_write:
            with open(vsim_lintonly_dot_do_fpath, 'w', encoding='utf-8') as f:
                f.writelines(line + "\n" for line in vsim_lintonly_dot_do_lines)

        if write_all or 'vlog' in dot_do_to_write:
            with open(vsim_vlogonly_dot_do_fpath, 'w', encoding='utf-8') as f:
                f.writelines(line + "\n" for line in vsim_vlogonly_dot_do_lines)


    def _get_vsim_suppress_list_str(self) -> str:
        vsim_suppress_list = []
        # Add waivers from config.tool.modelsim_ase:
        for waiver in self.tool_config.get(
                'simulate-waivers', [
                    #defaults: none
                ]) + self.args['sim-waivers']:
            vsim_suppress_list += ['-filter', str(waiver)]

        return ' '.join(vsim_suppress_list)


    def update_library_map( # pylint: disable=dangerous-default-value
            self, base_search_paths: list = []
    ) -> None:
        '''Override from sim::CommandSim

        We add some common places to look relative to the Riviera install paths
        '''
        bsp = [] + base_search_paths
        bsp += [
            Path(self.sim_exe_base_path),
            Path(self.sim_exe_base_path) / '..',
            Path(self.sim_exe_base_path) / '..' / 'vlib',
        ]
        super().update_library_map(base_search_paths=bsp)

    def get_vmap_lines_str(self) -> str:
        '''Returns an empty str, or single str in the form:

        vmap -global LibraryName  AbsolutePathToADotLibFile
        vmap -global LibraryName2 AbsolutePathToAnotherDotLibFile
        ...
        uses self.args['library-map'], updates self.args['sim-library']
        '''
        if not self.library_map:
            return ''

        lines = []
        for lib_name, lib_file_path in self.library_map.items():
            lines.append(f'vmap -global {lib_name} {lib_file_path}')

        return '\n'.join(lines)


class CommandElabRiviera(CommandSimRiviera):
    '''CommandElabRiviera is a command handler for: eda elab --tool=riviera'''

    command_name = 'elab'

    def __init__(self, config:dict):
        super().__init__(config)
        self.args['stop-after-elaborate'] = True


class CommandLintRiviera(CommandSimRiviera):
    '''CommandLintRiviera is a command handler for: eda lint --tool=riviera'''

    command_name = 'lint'

    def __init__(self, config:dict):
        super().__init__(config)
        self.args['stop-after-compile'] = True
        self.args['stop-after-elaborate'] = True


class CommandFListRiviera(CommandFList, ToolRiviera):
    '''CommonFListQuesta is a command handler for: eda flist --tool=riviera'''

    def __init__(self, config: dict):
        CommandFList.__init__(self, config=config)
        ToolRiviera.__init__(self, config=self.config)


class CommandCoverageRiviera(CommandCoverage, ToolRiviera):
    '''Riviera-specific coverage merge and compare handler'''

    def __init__(self, config: dict):
        CommandCoverage.__init__(self, config=config)
        ToolRiviera.__init__(self, config=self.config)

        # Note this will NOT update self.args yet:
        self.coverage_obj = RivieraSimCoverageAddOn(handler_obj=self)


    def do_it(self) -> None:
        '''Execute the coverage operation based on subcommand'''

        if self.subcommand == 'merge':
            self.run_merge()
        elif self.subcommand == 'compare':
            self.run_compare()
        else:
            self.error(f"Subcommand '{self.subcommand}' not implemented for Riviera",
                      error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)

    def run_merge( # pylint: disable=too-many-locals,too-many-branches,too-many-statements
            self
    ) -> None:
        '''Execute the coverage merge operation'''

        # If no path on the output arg, put it in work-dir:
        self.create_work_dir()
        output_file = safe_output_file(
            work_dir=self.args['work-dir'], default_fname='merged.acdb',
            user_fpath=self.args['output']
        )
        output_dir = os.path.dirname(output_file)

        tcl_script_path = os.path.join(self.args['work-dir'], 'merge_coverage.tcl')
        merge_command = (
            'acdb merge ' + ' '.join(f'-i {f}' for f in self.coverage_files) + f' -o {output_file}'
        )
        tcl_lines = [
            '# Auto-generated TCL script for coverage merging',
            merge_command
        ]

        if self.args['coverage-report']:
            report_txt = os.path.join(output_dir, self.args['coverage-report-txt'])
            tcl_lines.extend([
                f'acdb report -i {output_file} -txt -o {report_txt}',
                f'acdb report -i {output_file} -txt -o' + \
                f' {report_txt.replace(".txt", "_covergroups.txt")} -show covergroups',
            ])
            if self.args['coverage-report-html']:
                tcl_lines.append(
                    f'acdb report -i {output_file} -html -o '
                    f'{os.path.join(output_dir, self.args["coverage-report-html"])}'
                )

        tcl_lines.append('exit')

        with open(tcl_script_path, 'w', encoding='utf-8') as f:
            f.write('\n'.join(tcl_lines) + '\n')

        util.info(f"Generated TCL script: {tcl_script_path}")

        util.info(f"Merging {len(self.coverage_files)} coverage databases...")
        self.exec(self.args['work-dir'], [self.sim_exe, '-c', '-do', 'merge_coverage.tcl'])

        if not os.path.exists(output_file):
            self.error(f"Coverage merge failed: output file not created: {output_file}",
                       error_code=status_constants.EDA_COVERAGE_TOOL_COMMAND_FAIL)

        util.info(f"Coverage merge successful: {output_file}")
        if not self.args['coverage-report']:
            return

        report_txt = os.path.join(output_dir, self.args['coverage-report-txt'])
        if not os.path.exists(report_txt):
            return

        util.info(f"Coverage report: {report_txt}")

        metrics = self.coverage_obj.parse_coverage_metrics(report_txt)
        if metrics and self.args.get('coverage-summary', True):
            self.coverage_obj.display_coverage_summary(metrics)

        if self.args.get('coverage-json', ''):
            json_path = str(Path(self.args['coverage-json']).absolute())
            self.coverage_obj.generate_coverage_json(
                report_txt, json_path, target_name='merged_coverage'
            )

        baseline_path = ''
        if self.args.get('coverage-baseline', ''):
            baseline_path = os.path.abspath(self.args['coverage-baseline'])
        elif self.args.get('coverage-baseline-auto', False):
            baseline_dir_abs = str(
                Path(self.args.get('coverage-baseline-dir', '.coverage_baselines')).absolute()
            )
            latest_link = os.path.join(baseline_dir_abs, 'merged_latest.acdb')
            if os.path.exists(latest_link):
                baseline_path = os.path.realpath(latest_link)
            else:
                candidates = sorted(
                    glob.glob(os.path.join(baseline_dir_abs, 'merged_*.acdb')),
                    key=os.path.getmtime, reverse=True
                )
                baseline_path = candidates[0] if candidates else ''

        if baseline_path and os.path.exists(baseline_path):
            util.info(f"Comparing merged coverage against baseline: {baseline_path}")
            temp_dir, baseline_report_temp = self.coverage_obj.generate_baseline_report(
                baseline_path
            )
            if baseline_report_temp and os.path.exists(baseline_report_temp):
                baseline_metrics = self.coverage_obj.parse_coverage_metrics(
                    baseline_report_temp
                )
                if baseline_metrics and metrics:
                    delta_metrics = self.coverage_obj.calculate_coverage_delta(
                        baseline_metrics, metrics
                    )
                    if self.args.get('coverage-show-delta', False):
                        self.coverage_obj.display_coverage_delta(delta_metrics)
                    if delta_metrics and \
                       not self.coverage_obj.check_delta_thresholds(delta_metrics):
                        self.error(
                            "Merged coverage delta thresholds not met",
                            error_code=status_constants.EDA_COVERAGE_REQUIREMENT_NOT_MET
                        )
                    if self.args.get('coverage-delta-json', ''):
                        delta_json_path = str(Path(self.args['coverage-delta-json']).absolute())
                        self.coverage_obj.generate_delta_json(
                            delta_metrics, baseline_path, delta_json_path
                        )
            temp_dir.cleanup()
        elif baseline_path:
            util.warning(f"Baseline not found: {baseline_path}")

        if self.args.get('coverage-save-baseline', False) and os.path.exists(output_file):
            baseline_dir_abs = str(
                Path(self.args.get('coverage-baseline-dir', '.coverage_baselines')).absolute()
            )
            os.makedirs(baseline_dir_abs, exist_ok=True)
            baseline_name = f"merged_{time.strftime('%Y%m%d_%H%M%S')}.acdb"
            baseline_path_save = os.path.join(baseline_dir_abs, baseline_name)
            shutil.copy2(output_file, baseline_path_save)
            util.info(f"Saved merged baseline: {baseline_path_save}")
            latest_link = os.path.join(baseline_dir_abs, 'merged_latest.acdb')
            if os.path.exists(latest_link) or os.path.islink(latest_link):
                os.remove(latest_link)
            os.symlink(baseline_name, latest_link)
            util.info(f"Updated latest merged baseline link: {latest_link}")


    def run_compare(self) -> None:
        '''Execute coverage comparison between two ACDB files'''

        if len(self.coverage_files) != 2:
            util.warning('Need exactly two coverage files, however there are',
                         f'{len(self.coverage_files)} from positional args')
        if not self.coverage_files:
            self.error('No coverage files to compare, exiting',
                       error_code=status_constants.EDA_COMMAND_OR_ARGS_ERROR)

        self.create_work_dir()
        baseline_acdb, current_acdb = self.coverage_files[0], self.coverage_files[-1]
        util.info(f"Baseline: {baseline_acdb}")
        util.info(f"Current:  {current_acdb}")

        temp_dir = tempfile.TemporaryDirectory( # pylint: disable=consider-using-with
            dir=self.args['work-dir'],
            prefix='coverage_compare_'
        )

        baseline_report = os.path.join(temp_dir.name, 'baseline_report.txt')
        with open(os.path.join(temp_dir.name, 'baseline_report.tcl'), 'w', encoding='utf-8') as f:
            f.write(
                f'acdb report -i {baseline_acdb} -txt -o {baseline_report}\nexit\n'
            )

        self.exec(
            temp_dir.name,
            [self.sim_exe, '-c', '-do', 'baseline_report.tcl']
        )
        if not os.path.exists(baseline_report):
            self.error(
                f"Failed to generate baseline report from {baseline_acdb} at: {baseline_report}",
                error_code=status_constants.EDA_COVERAGE_TOOL_COMMAND_FAIL)

        current_report = os.path.join(temp_dir.name, 'current_report.txt')
        with open(os.path.join(temp_dir.name, 'current_report.tcl'), 'w', encoding='utf-8') as f:
            f.write(f'acdb report -i {current_acdb} -txt -o {current_report}\nexit\n')

        self.exec(
            temp_dir.name,
            [self.sim_exe, '-c', '-do', 'current_report.tcl']
        )
        if not os.path.exists(current_report):
            self.error(
                f"Failed to generate current report from {current_acdb}",
                error_code=status_constants.EDA_COVERAGE_TOOL_COMMAND_FAIL
            )

        util.info("Parsing coverage reports...")
        baseline_metrics = self.coverage_obj.parse_coverage_metrics(baseline_report)
        current_metrics = self.coverage_obj.parse_coverage_metrics(current_report)

        if not baseline_metrics:
            util.warning("No coverage metrics found in baseline report")
        if not current_metrics:
            util.warning("No coverage metrics found in current report")

        if baseline_metrics and current_metrics:
            delta_metrics = self.coverage_obj.calculate_coverage_delta(
                baseline_metrics, current_metrics
            )
            if self.args.get('coverage-show-delta', False):
                self.coverage_obj.display_coverage_delta(delta_metrics)
            if json_path := self.args.get('coverage-delta-json', ''):
                json_path = str(Path(json_path).absolute())
                self.coverage_obj.generate_delta_json(
                    delta_metrics, baseline_acdb, json_path
                )
            util.info("Coverage comparison complete")
        else:
            util.warning("Could not complete coverage comparison due to missing metrics")

        # If we made it this far, cleanup the tempdir, otherwise leave it for debug:
        temp_dir.cleanup()
