''' 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 os
import subprocess

from pathlib import Path

from opencos import util
from opencos.commands import CommandFList
from opencos.files import safe_shutil_which
from opencos.tools.questa_common import ToolQuesta, CommonSimQuesta
from opencos.utils.str_helpers import sanitize_defines_for_sh
from opencos.utils import status_constants

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

    _TOOL = 'riviera'
    _EXE = 'vsim'
    use_vopt = False
    uvm_versions = set()

    def get_versions(self) -> str:
        if self._VERSION:
            return self._VERSION
        path = safe_shutil_which(self._EXE)
        if not path:
            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)

        # vsim -version is very fast and does not require a license:
        version_ret = subprocess.run(
            [self.sim_exe, '-version'],
            capture_output=True,
            check=False
        )
        stdout = version_ret.stdout.decode('utf-8', errors='replace').rstrip()

        # 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:
                    self.uvm_versions.add(item[4:])

        # For Version, expect:
        #  Aldec, Inc. Riviera-PRO version 2025.04.139.9738 built for Linux64 on May 30, 2025
        left, right = stdout.split('version')
        if 'Riviera' not in left:
            util.warning(f'riviera version parsing: {stdout}: does not show "Riviera"')
        self._VERSION = right.split()[0]
        return self._VERSION


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

    library_map_supported = True

    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,
            'coverage-tcl': '',
            'ext-defines-sv-fname': '_ext_defines.sv',
        })
        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-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. Adds to vlog: -l uvm +incdir+PATH for the PATH to'
                ' uvm_macros.svh for the installed version of Riviera used.'
            ),
            'license-queue': (
                'Set to enable env vars (if unset) LICENSE_QUEUE=1, ALDEC_LICENSE_QUEUE=1,'
                ' FLEXLM_TIMEOUT=3000000 (3us), which should set Riviera to wait for licenses'
                ' instead of immediately exiting with error. You may wish to also set your own'
                ' env vars for ALDEC_LICENSE_FILE instead of LM_LICENSE_FILE.'
            ),
        })

        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: 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'''

        self.add_waves_pkg_file()

        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)

    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
            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']}"
        )


        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

        vsim_vlogonly_dot_do_lines = [
            "if {[file exists work]} { vdel -all work; }",
            "vlib work;",
            vmap_lines_str,
            "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 = [
            "if {[file exists work]} { vdel -all work; }",
            "vlib work;",
            "set qc 30;",
            vmap_lines_str,
            "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 = [
            "if {[file exists work]} { vdel -all work; }",
            "vlib work;",
            "set qc 30;",
            vmap_lines_str,
            "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;",
            "    }",
            "}",
        ]

        vsim_dot_do_lines += [
            "run -all;",
        ]
        if self.args['coverage']:
            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}'
                ]
            else:
                # default TCL for coverage:
                vsim_dot_do_lines += self.tool_config.get('simulate-coverage-tcl', [])


        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 handle_arg_license_queue(self) -> None:
        '''Handles self.args['license-queue'] (bool) to set env vars

        Overriden from opencos.command.sim::CommandSim.
        '''
        if not self.args['license-queue']:
            return

        if 'LICENSE_QUEUE' not in os.environ:
            os.environ['LICENSE_QUEUE'] = '1'
        if 'ALDEC_LICENSE_QUEUE' not in os.environ:
            os.environ['ALDEC_LICENSE_QUEUE'] = '1'

        # Anecdotal evidence, set this which may help
        if 'FLEXLM_TIMEOUT' not in os.environ:
            os.environ['FLEXLM_TIMEOUT'] = '3000000' # 3e6 us = 3 sec


    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(self, base_search_paths: list | None = None) -> None:
        '''Override from sim::CommandSim

        We add some common places to look relative to the Riviera install paths
        '''
        bsp = []
        if isinstance(base_search_paths, list):
            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  AbsolutePathToDotLibFile
        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)
