'''opencos.commands.sim - Base class command handler for: eda sim ...

Intended to be overriden by Tool based classes (such as CommandSimVivado, etc)

Note that CommandSim is also a base class for opencos.commands.elab.CommandElab.'''

# Note - similar code waiver, tricky to eliminate it with inheritance when
# calling reusable methods.
# pylint: disable=R0801

# pylint: disable=too-many-arguments

import os
from pathlib import Path
import re
import shlex


from opencos import util
from opencos.eda_base import CommandDesign, Tool
from opencos.utils import status_constants

from opencos.utils.str_helpers import strip_outer_quotes

THISPATH = os.path.dirname(__file__)
WAVES_PKG_SV_FNAME = str((Path(THISPATH) / '..' / '_waves_pkg.sv').resolve())

# Unique checks for UVM Report
UVM_ERROR_REPORTING_RE = r"^.* UVM_ERROR\s*:\s*0*([1-9]\d*)\s*$"
UVM_FATAL_REPORTING_RE = r"^.* UVM_FATAL\s*:\s*0*([1-9]\d*)\s*$"
UVM_WARNING_REPORTING_RE = r"^.* UVM_WARNING\s*:\s*0*([1-9]\d*)\s*$"

# Check for UVM_ERROR /some/path, or UVM_ERROR: /some/path
# Avoids the reporting section: UVM_ERROR : 1
UVM_ERROR = r"UVM_ERROR\s*\:?\s*[^\s\:0-9]"
UVM_FATAL = r"UVM_FATAL\s*\:?\s*[^\s\:0-9]"
UVM_WARNING = r"UVM_WARNING\s\:?\s*[^\s\:0-9]"

def parameters_dict_get_command_list(
        params: dict, arg_prefix: str = '-G',
        hier_delimiter: str = '.',
        top_hier_str: str = '',
        for_flist: bool = False
) -> list:
    '''Given dict of parameters, returns a command list.

    Some simulators will set ALL parameters with Name=Value at all levels of hierarchy,
    so to avoid that and only set top level parameters, but still allow package or
    hierarchy parameters to be set, you can use:

    hier_delimiter='/'
    top_hier_str=f'/{TOP_MODULE_NAME}/'

    Which will, if a parameter Name does not contain the heir delimiter, the Name
    will be prepended with top_hier_str.
    '''

    ret_list = []
    if ' ' in arg_prefix:
        arg_list_prefix = arg_prefix.split()
        arg_str_prefix = ''
    else:
        arg_list_prefix = []
        arg_str_prefix = arg_prefix

    for k,v in params.items():
        if not isinstance(v, (int, str)):
            util.warning(f'parameter {k} has value: {v}, parameters must be int/string types')

        if top_hier_str and hier_delimiter not in k:
            k = f'{top_hier_str}{k}'

        ret_list.extend(arg_list_prefix)
        if isinstance(v, int):
            ret_list.append(f'{arg_str_prefix}{k}={v}')
        else: # string
            # Note if for_flist=True, then we have to take extra care on what gets
            # double-quotes " and \".
            v = strip_outer_quotes(v.strip('\n'))
            v = '"' + v + '"'

            if for_flist:
                # do not wrap with " unless there are spaces, if there are spaces
                # wrap the entire thing with single quotes (bash)
                if ' ' in v:
                    ret_list.append(shlex.quote(f'{arg_str_prefix}{k}={v}'))
                else:
                    ret_list.append(f'{arg_str_prefix}{k}={v}')
            else:
                # This is for a tool, so simply guard single quotes:
                v = v.replace("'", "\\" + "'")
                ret_list.append(f'{arg_str_prefix}{k}={v}')
    return ret_list


class CommandSim(CommandDesign): # pylint: disable=too-many-public-methods
    '''Base class command handler for: eda sim ...

    The common entrypoint is from eda.py (opencos.eda):
    ***--> process_tokens()
             --> create work-dir, fail early
             --> run commands from DEPS
             --> run_argparser_on_list() -- populate self.args
          ***--> run() / do_it()
                 --> prepare_compile()
                       --> get_*_command_lists(), or similar
                 --> write out eda config to work-dir for debug
                 --> handle "export" args if present
                 --> compile(), elaborate(), simulate()
             --> run_post_tool_dep_commands()
             --> report pass/fail
             --> return unparsed args back to eda.py.

    Note that this flow is also often used by CommandElab (eda elab ..., or eda lint ...)
    And follows a similar pattern, except usually falls short of elaborate() or simulate()
    steps in self.run()
    '''

    CHECK_REQUIRES = [Tool] # Used by check_command_handler_cls()
    error_on_no_files_or_targets = True
    error_on_missing_top = True
    tool_config = {} # Children with Tool parent classes will set on Tool constructor.
    library_map_supported = False
    has_pre_compile_slang_sh: str = '' # if set, save in all.sh before compile.sh

    command_name = 'sim'

    def __init__(self, config: dict):
        CommandDesign.__init__(self, config=config)
        self.args.update({
            "pre-sim-tcl": [],
            'compile-args': [],
            'compile-waivers': [],
            'elab-args': [],
            'sim-args': [],
            'sim-plusargs': [], # lists are handled by 'set_arg(k,v)' so they append.
            'sim-library': [],
            'sim-waivers': [],
            'coverage': False,
            'waves': False,
            'waves-start': 0,
            'pass-pattern': "",
            'optimize': False,
            'log-bad-strings': ['ERROR: ', 'FATAL: ', 'Error: ', 'Fatal: '],
            'log-must-strings': [],
            'log-warning-strings': ['WARNING: ', 'Warning: '],
            'uvm': False,
            'uvm-version': '1.2',
            # verilate-args: list of args you can only pass to Verilator,
            # not used by other simulators, so these can go in DEPS files for custom things
            # like -CFLAGS -O0, etc.
            'verilate-args': [],
            'ext-defines-sv-fname': '',
            'license-queue': False,
            'license-queue-value': '30',
            'library-map': [],
            'add-top-library': [],
            'docker-run': False,
            'docker-image': ''
        })
        self.args_help.update({
            'pre-sim-tcl': (
                'Tool dependent, TCL file to run prior to simulation (if tool supports TCL)'
            ),
            'compile-args': 'args added to sim/elab "compile" step',
            'compile-waivers': (
                'simulation tool depenent waivers by name, applied at compilation step'
            ),
            'coverage': 'attempt to run coverage steps on the compile/elab/simulation',
            'elab-args':    'args added to sim/elab "elaboration" step, if required by tool',
            'log-bad-strings': 'strings that if present in the log will fail the simulation',
            'log-must-strings': ('strings that are required by the log to not-fail the simulation.'
                                 ' Some tools use these at only certain phases'
                                 ' (compile/elab/sim).'),
            'log-warning-strings': ('strings that if present will be counted in the total tool'
                                    ' warnings at end of simulation'),
            'pass-pattern': ('Additional string required to pass a simulation, appends to'
                             ' log-must-strings'),
            'sim-args':      'args added to final "simulation" step',
            'sim-plusargs':  ('"simulation" step run-time args passed to tool, these can also'
                              ' be set using --sim-plusargs=name[=value], or simply +name[=value]'),
            'sim-library': 'named libraries added to simulation, tool dependent',
            'sim-waivers': (
                'simulation tool depenent waivers by name, applied at "simulation" step'
            ),
            'stop-before-compile': ('Create work-dir sh scripts for compile/elab/simulate, but do'
                                    ' not run them.'),
            'stop-after-compile': 'Create work-dir sh scripts, but only run the compile step.',
            'stop-after-elaborate': ('Create work-dir sh scripts, but run compile+elab, skip'
                                     ' simulation step.'),
            'top': 'Name of topmost Verilog/SystemVerilog module, or VHDL entity',
            'verilate-args': ('args added to "compile" step in Verilator simulation'
                              ' (for --tool=verilator) these are identical to --compile-args'),
            'waves': 'Include waveforms, if possible for tool',
            'waves-start': 'Starting time of waveform capture, if possible for tool',
            'test-mode': ('stops the command early without executing, if --gui is present will'
                          ' instead test without spawning gui'),
            'uvm': (
                'Attempts to support UVM (aka 1.2 if --uvm-version=1.2) for the'
                ' the simulation tool. May add libraries for uvm, tool dependent.'
            ),
            'uvm-version': (
                'Used if --uvm is set, for example --uvm-version=1.2'
            ),
            'ext-defines-sv-fname': (
                'If set to empty str, --ext-defines-sv-fname="", applies defines to tool via args.'
                ' If set to non-emtpy str as a .sv file name, such as'
                ' --ext-defines-sv-fname=_ext_defines.sv, saves defines to a file name in the'
                ' work-dif, and adds this file to the SV/Verilog file list ahead of other files.'
            ),
            'license-queue': (
                'Tool dependent, if supported help will show more details:'
                ' eda COMMAND --help --license-queue'
            ),
            'license-queue-value': (
                'Tool dependent, value used internally if --license-queue set'
            ),
            'library-map': (
                'list arg (can set multiple) with values LibName:Path, only supported'
                ' by certain simulation tools.'
                ' The Path search is relative to the tool exe, one level above the tool exe,'
                ' or can be absolute path.'
            ),
            'add-top-library': (
                'Add additional top levels, for example if you added the "unisim" library'
                ' you may need to --add-top-library=unisim.glbl for it to work with Xilinx'
                ' based projects that require "glbl" to be present at simulation $root scope.'
            ),
            'docker-run': (
                'Run the simulation using "docker run" with image specified by'
                ' --docker-image=NAME. Assumes the entrypoint can be /bin/bash and your --tool'
                ' executable is installed in PATH. NOTE: docker-in-docker is not supported yet,'
                ' if you are already in docker this option is not allowed and will fail checks.'
            ),
            'docker-image': (
                'Used with arg --docker. IMAGE used by "docker run" for the sim, lint, or'
                ' elab eda command.'
            )

        })

        self.args_kwargs.update({
            'uvm-version': { 'choices': ['1.2'] },
        })

        # only used if self.library_map_supported=True:
        self.library_map = {} # lib-name: path-to.lib dict from self.args['library-map']


    def process_parameters_get_list(
            self, arg_prefix: str = '-G', **kwargs
    ) -> list:
        '''Returns list (suitable command list for shell or for tool) from self.parameters

        Some simulators will set ALL parameters with Name=Value at all levels of hierarchy,
        so to avoid that and only set top level parameters, but still allow package or
        hierarchy parameters to be set, you can use args (in kwargs):

        hier_delimiter='/'
        top_hier_str=f'/{TOP_MODULE_NAME}/'

        Which will, if a parameter Name does not contain the heir delimiter, the Name
        will be prepended with top_hier_str.
        '''
        return parameters_dict_get_command_list(
            params=self.parameters, arg_prefix=arg_prefix, **kwargs
        )

    def process_plusarg(self, plusarg: str, pwd: str = os.getcwd()) -> None:
        '''Override for CommandDesign.process_plusarg(..)'''
        maybe_plusarg = CommandDesign.process_plusarg(self, plusarg, pwd)
        # Support for self.args['unprocessed-plusargs'] --> self.args['sim-plusargs']:
        if maybe_plusarg and \
           maybe_plusarg in self.args['unprocessed-plusargs'] and \
           maybe_plusarg not in self.args['sim-plusargs']:
            self.args['sim-plusargs'].append(maybe_plusarg)
            self.args['unprocessed-plusargs'].remove(maybe_plusarg)
            util.debug(f'For parent "sim" command (CommandSim), moved plusarg: {maybe_plusarg}',
                       'to sim-plusargs (from unprocessed-plusargs)')

    def process_tokens(self, tokens: list, process_all: bool = True,
                       pwd: str = os.getcwd()) -> list:
        self.defines['SIMULATION'] = None
        unparsed = CommandDesign.process_tokens(
            self, tokens=tokens, process_all=process_all, pwd=pwd
        )

        if self.stop_process_tokens_before_do_it():
            return unparsed

        # add defines for this job type
        if self.args['lint'] or self.args['stop-after-elaborate']:
            self.args['lint'] = True
            self.args['stop-after-elaborate'] = True
        if self.args['top']:
            # create our work dir
            self.create_work_dir()
            self.run_dep_commands()
            self.do_it()
            self.run_post_tool_dep_commands()
            self.report_pass_fail()
        return unparsed


    def set_tool_config_from_config(self) -> None:
        '''Sets self.tool_config (from original --config-yml=YAML|Default) and overrides
        self.defines, and self.args log-must-strings and log-bad-strings.'''
        tool = self.args.get('tool', '') # get from Command's self.args['tool']
        if tool:
            self.tool_config = self.config.get('tools', {}).get(tool, {})
            self.override_log_strings_from_tool_config()
            self.defines.update(self.tool_config.get('defines', {}))
            util.debug(f'set_tool_config_from_config: {tool=}')

    def update_tool_config(self):
        self.override_log_strings_from_tool_config()


    def override_log_strings_from_tool_config(self) -> None:
        '''Returns None, overrides memvers of self.args based on our tool_config

        (tool_config from eda.py --config-yml=YAML-CONFIG-FILE)
        '''
        if not getattr(self, 'tool_config', None):
            return

        # Collect (overwrite CommandSim) the bad and must strings, if present,
        # from our config.tools.verilator:
        for tool_config_key in ('log-bad-strings', 'log-must-strings', 'log-warning-strings'):
            if len(self.tool_config.get(tool_config_key, [])) > 0:
                self.args[tool_config_key] = self.tool_config.get(tool_config_key, [])


    # Methods that derived classes may override:

    def run_commands_check_logs( # pylint: disable=dangerous-default-value,too-many-locals
            self, commands: list , check_logs: bool = True, log_filename=None,
            bad_strings: list = [],
            must_strings: list = [],
            use_bad_strings: bool = True, use_must_strings: bool = True,
            sim_step: str = ''
    ) -> None:
        '''Returns None, runs all commands (each element is a list) and checks logs

        for bad-strings and must-strings (args or class member vars).

        Will add any bad_strings or tool log-warning-strings to self, so tool
        related warning/error counts can be reported later.

        sim_step can be one of: compile, elaborate, simulate, or blank
        '''

        for obj in commands:

            assert isinstance(obj, list), \
                (f'{self.target=} command {obj=} is not a list or util.ShellCommandList,'
                 ' not going to run it.')

            clist = list(obj).copy()
            tee_fpath = getattr(obj, 'tee_fpath', None)
            work_dir = getattr(obj, 'work_dir', None)
            if not work_dir:
                work_dir = self.args['work-dir']

            util.debug(f'run_commands_check_logs: {clist=}, {tee_fpath=}, {log_filename=}')

            log_fname = None
            if tee_fpath:
                log_fname = tee_fpath
            if log_filename:
                log_fname = log_filename

            # track the retcode because we run stop_on_error=False (to count warnings/errors)
            _, stdout, retcode = self.exec(
                work_dir=work_dir, command_list=clist, tee_fpath=tee_fpath,
                stop_on_error=False,
            )

            if check_logs and not log_fname and bad_strings:
                util.warning(
                    f'{self.get_info_job_name()}: check_logs=True but no log file set',
                    f'(run --debug for more info), for command: {" ".join(clist)}'
                )

            if check_logs and log_fname:
                # Note this call will check on stdout if not GUI, not opening the log_fname,
                # but if this is GUI we normally lose stdout and have to open the log.
                if self.args.get('gui', False):
                    file_contents_str = ''
                else:
                    file_contents_str = stdout

                self.check_logs_for_errors(
                    sim_retcode=retcode,
                    filename=os.path.join(work_dir, log_fname),
                    file_contents_str=file_contents_str,
                    bad_strings=bad_strings, must_strings=must_strings,
                    use_bad_strings=use_bad_strings, use_must_strings=use_must_strings,
                    sim_step=sim_step
                )
            if log_fname:
                self.artifacts_add(
                    name=os.path.join(work_dir, log_fname),
                    typ='text', description='Simulator stdout/stderr log file'
                )
            if retcode:
                self.error(
                    f"{self.get_info_job_name()}: exec returned with error code: {retcode}"
                )

    def do_export(self, **kwargs) -> None:
        '''CommandSim helper for handling args --export*, override from Command'''
        super().do_export(
            deps_file_args=[
                '--compile-args',
                '--elab-args',
                '--sim-',
                '--coverage',
                '--waves',
                '--pass-pattern',
                '--optimize',
                '--stop-',
                '--lint-',
                '--verilat',
            ],
            support_export_run=True
        )


    def do_it(self) -> None:
        self.prepare_compile()
        self.write_eda_config_and_args()

        if self.is_export_enabled():
            # If we're exporting the target, we do NOT run the test here
            # (do_export() may run the test in a separate process and
            # from the out_dir if --export-run was set)
            self.do_export()
            return

        self.compile()
        self.elaborate()
        self.simulate()

    # Methods that derived classes may override:

    def prepare_compile(self) -> None:
        '''Derived classes may override if they want to create .sh scripts in the work-dir.

        Common use-case is to call:
            self.set_tool_defines()
            cmds0 = self.get_compile_command_lists()
            cmds1 = self.get_elaborate_command_lists()
            cmds2 = self.get_simulate_command_lists()
            cmds3 = self.get_post_simulate_command_lists()
        '''
        self.update_library_map()


    def check_lines_for_uvm_reporting(self, log_lines: list, log_fname: str) -> None:
        '''Scrapes lines looking for UVM reporting, adds to tool warning/errors'''
        if not self.args['uvm']:
            return

        # We'd like to check for UVM reporting, which we'll handle outside of _bad_strings
        # or _warning_strings b/c we have to fish out the count use regex:
        for lineno, line in enumerate(log_lines):
            me = re.match(UVM_ERROR_REPORTING_RE, line)
            mf = re.match(UVM_FATAL_REPORTING_RE, line)
            mw = re.match(UVM_WARNING_REPORTING_RE, line)
            if me:
                _count = me.group(1)
                self.incr_tool_error(int(_count))
                self.error(
                    f'log {log_fname}:{lineno+1} observed UVM reporting errors: {_count}',
                    do_exit=False, error_code=status_constants.EDA_SIM_UVM_ERROR_OR_FATAL
                )
            if mf:
                _count = mf.group(1)
                self.incr_tool_error(int(_count))
                util.error(
                    f'log {log_fname}:{lineno} observed UVM reporting fatals: {_count}',
                    do_exit=False, error_code=status_constants.EDA_SIM_UVM_ERROR_OR_FATAL
                )
            if mw:
                _count = mw.group(1)
                self.incr_tool_warning(int(_count))
                util.warning(
                    f'log {log_fname}:{lineno} observed UVM reporting warnings: {_count}'
                )

    def _do_log_scrape_lines(self, lines: list, must_strings: list, bad_strings: list,
                             log_fname: str, sim_step: str) -> None:
        '''Performs the actual Must-have checks and Bad strings checks, on list "lines"'''

        hit_must_string_dict = dict.fromkeys(must_strings)

        # Pre-compile some regex for better performance
        combined_regex_bad_strings = None
        combined_regex_must_strings = None
        if bad_strings:
            combined_regex_bad_strings = re.compile('|'.join(f'({p})' for p in bad_strings))
        if must_strings:
            combined_regex_must_strings = re.compile('|'.join(f'({p})' for p in must_strings))

        if lines:
            for lineno, line in enumerate(lines):

                if combined_regex_must_strings and combined_regex_must_strings.search(line):
                    for _must_str in must_strings:
                        if re.search(_must_str, line): # found it
                            hit_must_string_dict[_must_str] = True

                if combined_regex_bad_strings and combined_regex_bad_strings.search(line):
                    _bad_str = ''
                    for _bad_str in bad_strings:
                        if re.search(_bad_str, line):
                            break # found the offending one
                    self.error(
                        f"{sim_step}log {log_fname}:{lineno+1} contains: {_bad_str}",
                        error_code=status_constants.EDA_SIM_LOG_HAS_BAD_STRING
                    )

        if any(x is None for x in hit_must_string_dict.values()):
            self.error(
                f"{sim_step}Didn't get all passing patterns in log {log_fname}: {must_strings},",
                f" {hit_must_string_dict}",
                error_code=status_constants.EDA_SIM_LOG_MISSING_MUST_STRING
            )


    # pylint: disable=too-many-locals,too-many-branches,too-many-statements
    def check_logs_for_errors( # pylint: disable=dangerous-default-value
            self,
            sim_retcode: int = 0,
            filename: str = '',
            file_contents_str: str = '',
            bad_strings: list = [], must_strings: list = [],
            warning_strings: list = [],
            use_bad_strings: bool = True, use_must_strings: bool = True,
            sim_step: str = ''
    ) -> None:
        '''Returns None, checks logs using args bad_strings, must_strings,

        and internals self.args["log-[bad|must]-strings"] (lists).

        file_contents_str will take precedence over opening filename, but filename is still
        used in messaging.

        sim_step can be one of: compile, elaborate, simulate, or blank
        '''

        _bad_strings = bad_strings
        _must_strings = must_strings
        # append, if not they would 'replace' the args values:
        if use_bad_strings:
            _bad_strings = bad_strings + self.args.get('log-bad-strings', [])
        if use_must_strings:
            _must_strings = must_strings + self.args.get('log-must-strings', [])

        _warning_strings = warning_strings + self.args.get('log-warning-strings', [])

        if self.args['pass-pattern'] != "":
            _must_strings.append(self.args['pass-pattern'])

        if self.args['uvm']:
            _bad_strings.extend([
                UVM_ERROR, UVM_FATAL
            ])
            _warning_strings.append(UVM_WARNING)

        if sim_step:
            sim_step = f'({sim_step}) '

        if len(_bad_strings) == 0 and len(_must_strings) == 0:
            return

        lines = []

        log_fpath = ''
        if os.path.isfile(filename):
            log_fpath = filename

        if file_contents_str:
            lines = file_contents_str.split('\n')
            log_fname = log_fpath + '(STDOUT)'
            util.debug(f'{sim_step}Checking log for errors: {log_fpath=} but checking from',
                       'STDOUT string')
        elif log_fpath:
            log_fname = log_fpath
            util.info(f'{sim_step}Checking log filename: {log_fpath}')
            with open(log_fpath, 'r', encoding='utf-8') as f:
                lines = f.read().splitlines()
        else:
            log_fname = filename
            self.error(f'sim.check_logs_for_errors: {sim_step}{log_fname=} is not a file or does',
                       'not exist, and no file_contents_str exists to check')

        self.update_tool_warn_err_counts_from_log_lines(
            log_lines=lines, bad_strings=_bad_strings, warning_strings=_warning_strings
        )

        self.check_lines_for_uvm_reporting(log_lines=lines, log_fname=log_fname)

        func = getattr(self, 'report_tool_warn_error_counts', None)
        if func and isinstance(self, Tool) and callable(func):
            self.report_tool_warn_error_counts(clear_counts=True) # pylint: disable=no-member

        if sim_retcode > 0:
            # We have to update artifacts first, have the caller set the error.
            # Skip the checking for bad strings or must strings, because we've already
            # failed, but we did the important counting of Error and Warning lines above,
            # and reported it.
            return

        self._do_log_scrape_lines(
            lines=lines, must_strings=_must_strings, bad_strings=_bad_strings,
            log_fname=log_fname, sim_step=sim_step
        )


    def write_sh_scripts_to_work_dir(
            self, compile_lists: list, elaborate_lists: list, simulate_lists: list,
            compile_line_breaks: bool = True,
            elaborate_line_breaks: bool = False,
            simulate_line_breaks: bool = False,
            simulate_sh_fname: str = 'simulate.sh',
            all_sh_fname: str = 'all.sh'
    ) -> None:
        '''Writes compile.sh, elaborate.sh, simulate.sh (if present), all.sh to work-dir

        Will include the pre_compile_dep_shell_commands.sh if those are present.
        compile_line_breaks defaults to True (one word per line w/ line breaks added)
        '''

        all_lists = [] # list - of - (command-list)
        if self.has_pre_compile_dep_shell_commands:
            all_lists.append(
                ['./pre_compile_dep_shell_commands.sh']
            )

        if self.has_pre_compile_slang_sh and \
           os.path.exists(os.path.join(self.args['work-dir'], self.has_pre_compile_slang_sh)):
            # Derived classes would have run Slang on their own if needed, but for repro
            # purposes we'll add it to all.sh:
            all_lists.append(
                [f'./{self.has_pre_compile_slang_sh}']
            )

        if compile_lists:
            util.write_shell_command_file(dirpath=self.args['work-dir'], filename='compile.sh',
                                          command_lists=compile_lists,
                                          line_breaks=compile_line_breaks)
            all_lists.append(['./compile.sh'])

        if elaborate_lists:
            util.write_shell_command_file(dirpath=self.args['work-dir'], filename='elaborate.sh',
                                          command_lists=elaborate_lists,
                                          line_breaks=elaborate_line_breaks)
            all_lists.append(['./elaborate.sh'])

        if simulate_lists:
            util.write_shell_command_file(dirpath=self.args['work-dir'], filename=simulate_sh_fname,
                                          command_lists=simulate_lists,
                                          line_breaks=simulate_line_breaks)
            all_lists.append(['./' + simulate_sh_fname])

        if self.has_post_tool_dep_shell_commands:
            all_lists.append(
                ['./post_tool_dep_shell_commands.sh']
            )

        util.write_shell_command_file(dirpath=self.args['work-dir'], filename=all_sh_fname,
                                      command_lists=all_lists)

        self.write_eda_config_and_args()



    # Methods that derived classes must override:

    def compile(self) -> None:
        '''compile() runs as part of the CommandSim.do_it() flow'''
        raise NotImplementedError

    def elaborate(self) -> None:
        '''elaborate() runs as part of the CommandSim.do_it() flow, after compile()'''
        raise NotImplementedError

    def simulate(self) -> None:
        '''simulate() runs as part of the CommandSim.do_it() flow, after elaborate()'''
        raise NotImplementedError

    def get_compile_command_lists(self, **kwargs) -> list:
        ''' Returns a list of lists (list of command lists).'''
        raise NotImplementedError

    def get_elaborate_command_lists(self, **kwargs) -> list:
        ''' Returns a list of lists (list of command lists).'''
        raise NotImplementedError

    def get_simulate_command_lists(self, **kwargs) -> list:
        ''' Returns a list of lists (list of command lists).'''
        raise NotImplementedError

    def get_post_simulate_command_lists(self, **kwargs) -> list:
        ''' Returns a list of lists (list of command lists).'''
        raise NotImplementedError

    def add_waves_pkg_file(self) -> None:
        '''If --waves is present, and one of --waves-fst or --waves-vcd or --dump-vcd, and

        the user is missing any $dumpfile(), then adds a pre-written
        SystemVerilog package to their source code. Note that individual tools have
        to call this prior to their compile step, CommandSim does not run this method for
        you.
        '''
        if not self.args['waves']:
            return
        if not any(self.args.get(x, False) for x in ('waves-fst', 'waves-vcd', 'dump-vcd')):
            return
        found_dumpfile = False
        for fname in self.files_v + self.files_sv:
            if found_dumpfile:
                break
            with open(fname, encoding='utf-8') as f:
                for line in f.readlines():
                    if '$dumpfile' in line:
                        found_dumpfile = True
                        break

        if not found_dumpfile:
            file_to_add = WAVES_PKG_SV_FNAME
            util.info(f'--waves arg present, no $dumpfile found, adding SV file: {file_to_add}')
            self.add_file(file_to_add)

        # register .vcd or .fst artifacts:
        util.artifacts.add_extension(
            search_paths=self.args['work-dir'], file_extension='fst',
            typ='waveform', description='Simulation Waveform FST (Fast Signal Trace) file'
        )
        util.artifacts.add_extension(
            search_paths=self.args['work-dir'], file_extension='vcd',
            typ='waveform', description='Simulation Waveform VCD (Value Change Dump) file'
        )


    def create_ext_defines_sv(self) -> None:
        '''Creates _ext_defines.sv in the work-dir, adds to front of self.files_sv'''

        if not self.defines:
            return

        if not self.args['ext-defines-sv-fname']:
            return

        fpath = os.path.join(self.args['work-dir'], self.args['ext-defines-sv-fname'])
        fpath_abs = os.path.abspath(fpath)
        if fpath_abs in self.files:
            util.warning(f'{fpath} already exists, will overwrite. Consider changing the value',
                         f'of arg: --ext-defines-sv-fname={self.args["ext-defines-sv-fname"]}')
        if not fpath.endswith('.sv') and not fpath.endswith('.v'):
            util.error(f'--ext-defines-sv-fname={self.args["ext-defines-sv-fname"]} must end with',
                       '.v or .sv')
            return

        ifdef_guard = os.path.split(self.args['ext-defines-sv-fname'])[1].upper()
        ifdef_guard = ifdef_guard.replace('.', '__').replace('/', '__')
        with open(fpath, 'w', encoding='utf-8') as f:
            f.write(f'`ifndef {ifdef_guard}\n')
            f.write(f'`define {ifdef_guard}\n')
            f.write('\n')
            for key, value in self.defines.items():
                if value is None:
                    f.write(f'`define {key}\n')
                else:
                    # if the value v is a double-quoted string, such as v='"hi"' that needs
                    # to be preserved.
                    f.write(f'`define {key} {value}\n')

            f.write('\n')
            f.write(f'`endif // {ifdef_guard}\n')

        self.files_sv.insert(0, fpath_abs)
        self.files[fpath_abs] = True
        self.files_caller_info[fpath_abs] = (
            'opencos.commands.sim::CommandSim::create_ext_defines_sv'
        )
        util.info(f'command "sim": {fpath} created for defines: {list(self.defines.keys())}')


    def update_library_map( # pylint: disable=dangerous-default-value
            self, base_search_paths: list = []
    ) -> None:
        '''
        Returns None, uses self.args['library-map'] to update:

        - self.library_map (dict)
        - self.args['sim-library']
        '''
        if not self.args['library-map']:
            return

        if not self.library_map_supported:
            tool = self.config.get('tool', None)
            util.warning(f'Command: sim, --tool={tool}, args for --library-map are not supported:',
                         f'{self.args["library-map"]}')
            return

        for libmap in self.args['library-map']:
            parts = libmap.split(':')
            lib_name = parts[0]
            lib_path = Path(':'.join(parts[1:]))
            lib_abs_path = None

            if not lib_name or not lib_path:
                util.warning(f'library-map={libmap} could not parse {lib_name=} or {lib_path=}')
                continue

            if lib_name in self.library_map:
                util.warning(
                    f'We have already mapped "{lib_name}" to {self.library_map[lib_name]}'
                )

            # We have to go looking for lib_path though
            search_files = [lib_path]
            if base_search_paths:
                for x in base_search_paths:
                    search_files.append(Path(x) / lib_path)
            for x in search_files:
                if x.exists():
                    lib_abs_path = x.resolve()
                    util.info(f'library-map={libmap} found "{lib_name}" in: {lib_abs_path}')
                    break
                util.debug(f'library-map={libmap} searching for "{lib_name}" in: {x}')


            if lib_abs_path:
                self.library_map[lib_name] = lib_abs_path
                if lib_name not in self.args['sim-library']:
                    self.args['sim-library'].append(lib_name)
            else:
                util.warning(f'library-map={libmap}, for "{lib_name}" could not find {lib_path=}')
