''' opencos.tools.verilator - Used by opencos.eda for sim and elab commands with --tool=verilator.

Contains classes for ToolVerilator and VerilatorSim, VerilatorElab.
'''

# pylint: disable=R0801 # (calling functions with same arguments)

import os
from pathlib import Path
import subprocess

from opencos import util
from opencos.commands import CommandSim
from opencos.eda_base import Tool
from opencos.files import safe_shutil_which, get_source_files_paths
from opencos.util import Colors
from opencos.utils import docker_checks
from opencos.utils.docker_checks import docker_ok
from opencos.utils.str_helpers import sanitize_defines_for_sh


class ToolVerilator(Tool):
    '''ToolVerilator used by opencos.eda for --tool=verilator'''

    _TOOL = 'verilator'
    _EXE = 'verilator'
    _URL = 'github.com/verilator/verilator'

    # use if config.tools.TOOL.docker-default-image is missing:
    DEFAULT_DOCKER_IMAGE = 'verilator/verilator:latest'

    verilator_base_path = ''
    verilator_exe = ''
    verilator_coverage_exe = ''

    def get_versions(self, **kwargs) -> str: # pylint: disable=too-many-branches
        if self._VERSION:
            return self._VERSION
        # __init__ would have set self.EXE to full path.
        path = safe_shutil_which(self._EXE)
        if not path:
            if self.get_version_allow_certain_command_if_tool_not_installed():
                return self._VERSION
            # but can we run via docker? Note docker isn't required, this is a fall-back
            if not docker_ok():
                self.error(f'"{self._EXE}" not in path or not installed, see {self._URL})')
                return self._VERSION

            self.get_set_default_docker_image()

            if not docker_checks.check_image_available(image=self.DEFAULT_DOCKER_IMAGE):
                # Be nice to eda --help, don't call docker run to get versioning if
                # the image hasn't been pulled.
                self._VERSION = 'IMAGE-NOT-PULLED'
                return self._VERSION

            self.verilator_exe = 'verilator'
            self.verilator_base_path = '' # clear these so they work in docker image
            self.no_exe_but_has_docker = True
        else:
            self.verilator_exe = path
            self.verilator_base_path, _ = os.path.split(path)

        # Let's get the verilator_coverage path from the same place as verilator.
        if path:
            self.verilator_coverage_exe = safe_shutil_which(
                os.path.join(self.verilator_base_path, 'verilator_coverage')
            )
        if not self.verilator_coverage_exe and not docker_ok():
            util.warn_once(
                '"verilator_coverage" not in path, need from same path',
                'as "{self.verilator_exe}"',
                warn_name='verilator_get_versions_coverage'
            )

        if path:
            version_ret = subprocess.run(
                [self.verilator_exe, '--version'],
                capture_output=True,
                check=False
            )
            version_ret = version_ret.stdout.decode('utf-8', errors='replace')
        elif docker_ok():
            env_dict = docker_checks.get_docker_env(cmd_des_obj=self)
            version_ret = docker_checks.run_docker_for_version_info(
                image=self.DEFAULT_DOCKER_IMAGE,
                env_dict=env_dict,
                commands=['-c', 'verilator --version']
            )
        else:
            self._VERSION = 'unknown'
            return self._VERSION

        util.debug(f'{path=} {version_ret=}')
        words = version_ret.split() # 'Verilator 5.027 devel rev v5.026-92-g403a197e2
        if len(words) < 2:
            util.warning(
                f'{self.verilator_exe} --version: returned unexpected string {version_ret=}'
            )
            version = ''
        else:
            version = words[1]
            ver_list = version.split('.')
            if len(ver_list) != 2:
                util.warning(f'{self.verilator_exe} --version: returned unexpected',
                             f'string {version_ret=} {version=}')
        self._VERSION = version
        return self._VERSION


class VerilatorSim(CommandSim, ToolVerilator):
    '''VerilatorSim is a command handler for: eda sim --tool=verilator'''

    def __init__(self, config: dict):
        CommandSim.__init__(self, config=config)
        ToolVerilator.__init__(self, config=self.config)
        self.args.update({
            'gui': False,
            'dump-vcd': False,
            'waves-fst': True,
            'waves-vcd': False,
            'lint-only': False,
            'cc-mode': False,
            'verilator-coverage-args': [],
            'x-assign': '',
            'x-initial': '',
            'docker-image': self.DEFAULT_DOCKER_IMAGE
        })

        self.args_help.update({
            'waves': (
                'Include waveforms, if possible for Verilator by applying'
                ' simulation runtime arg +trace. User will need SV code to interpret the'
                ' plusarg and apply $dumpfile("dump.fst").'
            ),
            'waves-fst': (
                '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. User'
                ' will need SV code to interpret the plusarg and apply'
                ' $dumpfile("dump.vcd").'
            ),
            'dump-vcd': 'Same as --waves-vcd',
            'lint-only': 'Run verilator with --lint-only, instead of --binary',
            'gui':       'Not supported for Verilator',
            'cc-mode':   'Run verilator with --cc, requires a sim_main.cpp or similar sources',
            'optimize':  'Run verilator with: -CLAGS -O3, if no other CFLAGS args are presented',
            'x-assign': ('String value to added to verilator call: --x-assign <string>;'
                         ' where valid string values are: 0, 1, unique, fast.'
                         ' Also conditinally adds to verilated exe call:'
                         ' +verilator+rand+reset+[0,1,2] for arg values 0, 1, unique|fast'),
            'x-initial': ('String value to added to verilator call: --x-initial <string>;'
                         ' where valid string values are: 0, unique, fast.'
                         ' Also conditinally adds to verilated exe call:'
                         ' +verilator+rand+reset+[0,2] for arg values 0, unique|fast'),
            'uvm': (
                'Enables UVM. Warns on Verilator < 5.042, or missing $UVM_HOME environment'
                ' var set (or in .env, $UVM_HOME/uvm_pkg.sv should exist), and will run verilator'
                ' with args: -Wno-fatal +define+UVM_NO_DPI'
            ),
            'verilator-coverage-args': (
                'Requires --coverage, args to be applied to verilator_coverage, which runs'
                ' after running the compiled executable simulation'
            ),
        })


        self.args_kwargs.update({
            'x-assign':    { 'choices': ['0', '1', 'unique', 'fast'] },
            'x-initial':   { 'choices': ['0', 'unique', 'fast'] },
        })

        self.verilate_command_lists = []
        self.lint_only_command_lists = []
        self.verilated_exec_command_lists = []
        self.verilated_post_exec_coverage_command_lists = []


    def set_tool_defines(self):
        ToolVerilator.set_tool_defines(self)
        self.defines.update(
            self.tool_config.get('defines', {})
        )

    # We do not override CommandSim.do_it()
    def prepare_compile(self) -> None:
        self.set_tool_defines()
        self.update_library_map()

        self.docker_arg_set = self.args['docker-run']
        if self.no_exe_but_has_docker and not self.docker_arg_set:
            self.error('"verilator" was not found in PATH, but can be run with --docker-run',
                       f'and optionally --docker-image={self.DEFAULT_DOCKER_IMAGE}')
            return

        if self.docker_arg_set:
            # check that user can run docker?
            if docker_ok():
                # If this is docker, we cannot use the full path from self.verilator_exe,
                # we have to simply use 'verilator' in the shell script that the docker image
                # will run.
                self.verilator_exe = 'verilator'
                util.info(
                    f'Running with --docker-run --docker-image={self.args["docker-image"]}',
                    color=Colors.byellow
                )
            else:
                self.error(docker_checks.get_error_str_cant_run_docker())

        # If there are C++ files here, then we will run Verilator in --cc mode:
        if self.files_cpp:
            self.args['cc-mode'] = True

        self.add_waves_pkg_file()

        # Each of these should be a list of util.ShellCommandList()
        self.verilate_command_lists = self.get_compile_command_lists()
        self.lint_only_command_lists = self.get_compile_command_lists(lint_only=True)
        self.verilated_exec_command_lists  = self.get_simulate_command_lists()
        self.verilated_post_exec_coverage_command_lists = self.get_post_simulate_command_lists()

        paths = ['obj_dir', 'logs']
        util.safe_mkdirs(base=self.args['work-dir'], new_dirs=paths)


        util.write_shell_command_file(
            dirpath=self.args['work-dir'], filename='lint_only.sh',
            command_lists=self.lint_only_command_lists, line_breaks=True
        )

        if self.args.get('lint-only', False):
            sim_cmd_lists = []
        else:
            sim_cmd_lists = self.verilated_exec_command_lists + \
                (self.verilated_post_exec_coverage_command_lists
                 if self.args.get('coverage', True) else [])

        self.write_sh_scripts_to_work_dir(
            compile_lists=self.verilate_command_lists,
            elaborate_lists=[],
            simulate_lists=sim_cmd_lists
        )

        if self.docker_arg_set:
            self.prepare_compile_docker()


    def prepare_compile_docker(self) -> None:
        '''Takes existing generated shell scripts, and saves out different ones.

        will be used in compile() and simulate() steps.

        1. Assume the existing compile.sh exists.
           a. Create docker_wrap_compile.sh that has:
               docker [run|exec] --entrypoint /bin/bash IMAGE \
                   -c "cd WORK-DIR && ./copmile.sh"
           b. where compile.sh is
              i. The original contents from self.verilate_command_lists, but with
                 'verilator' instead of '/path/to/verilator', which was handled
                 elsewhere looking at self.args['docker']

        2. Assume simulate.sh exists (or not)
           a. Create docker_wrap_simulate.sh that has:
               docker [run|exec] --entrypoint /bin/bash IMAGE \
                  -c "cd WORK-DIR && ./in_docker_simulate.sh"
           b. simulate.sh unchanged, should already have the coverage commands

        3. Overwrite all.sh to call:
           - docker_wrap_compile.sh
           - docker_wrap_simulate.sh

        4. Destroy the self.*_command_lists so they point to the bash scripts.
        '''
        if not self.docker_arg_set:
            return

        # TODO(drew): Windows friendly? Probably not. a lot of ./ going on here.

        work_dir_abs = Path(self.args['work-dir']).resolve()
        docker_command = 'run'
        docker_env_dict = docker_checks.get_docker_env(cmd_des_obj=self)
        docker_run_base_cmd = docker_checks.get_docker_run_command(
            image=self.args['docker-image'],
            docker_command=docker_command,
            env_dict=docker_env_dict,
            ro_volumes=get_source_files_paths(cmd_des_obj=self, minimal_root_paths=True),
            rw_volumes=[work_dir_abs],
            threads=self.args['threads']
        )

        util.write_shell_command_file(
            dirpath=self.args['work-dir'],
            filename='docker_wrap_compile.sh',
            command_lists=[
                util.ShellCommandList(docker_run_base_cmd + [
                    '-c', f'\'cd {work_dir_abs} && ./compile.sh\'',
                ], tee_fpath='compile.log')
            ],
            line_breaks=False
        )

        util.write_shell_command_file(
            dirpath=self.args['work-dir'],
            filename='docker_wrap_lint_only.sh',
            command_lists=[
                util.ShellCommandList(docker_run_base_cmd + [
                    '-c', f'\'cd {work_dir_abs} && ./lint_only.sh\'',
                ], tee_fpath='lint_only.log')
            ],
            line_breaks=False
        )

        util.write_shell_command_file(
            dirpath=self.args['work-dir'],
            filename='docker_wrap_simulate.sh',
            command_lists=[
                util.ShellCommandList(docker_run_base_cmd + [
                    '-c', f'\'cd {work_dir_abs} && ./simulate.sh\'',
                ], tee_fpath='sim.log')
            ],
            line_breaks=False
        )


        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.args.get('lint-only', False):
            all_lists.append(['./docker_wrap_lint_only.sh'])
        else:
            all_lists.append(['./docker_wrap_compile.sh'])
            all_lists.append(['./docker_wrap_simulate.sh'])
        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',
            command_lists=all_lists, line_breaks=True
        )

        # Overwrite our command lists so compile(), simulate() work correctly.
        if self.verilate_command_lists:
            self.verilate_command_lists = [
                util.ShellCommandList(
                    ['./docker_wrap_compile.sh'], tee_fpath='docker_wrap_compile.log'
                )
            ]
        if self.lint_only_command_lists:
            self.lint_only_command_lists = [
                util.ShellCommandList(
                    ['./docker_wrap_lint_only.sh'], tee_fpath='docker_wrap_lint_only.log'
                )
            ]
        if self.verilated_exec_command_lists:
            self.verilated_exec_command_lists = [
                util.ShellCommandList(
                    ['./docker_wrap_simulate.sh'], tee_fpath='docker_wrap_simulate.log'
                )
            ]
        self.verilated_post_exec_coverage_command_lists = [ ] # clear, in docker_wrap_simulate

        return


    def compile(self):
        if self.args['stop-before-compile']:
            return
        if self.args.get('lint-only', False):
            # We do not scrape compile logs for "must" strings (use_must_strings=False)
            self.run_commands_check_logs(self.lint_only_command_lists, use_must_strings=False,
                                         sim_step='compile')
        else:
            self.run_commands_check_logs(self.verilate_command_lists, use_must_strings=False,
                                         sim_step='compile')

    def elaborate(self):
        pass

    def simulate(self):
        if self.args.get('lint-only', False):
            return

        if self.args['stop-before-compile'] or self.args['stop-after-compile'] or \
           self.args['stop-after-elaborate']:
            # don't run this if we're stopping before/after compile/elab
            return

        if not os.path.isfile(os.path.join(self.args['work-dir'], 'obj_dir', 'sim.exe')):
            self.error('Verilated executable obj_dir/sim.exe does not exist')
            return

        # Note that this is not returning a pass/fail bash return code,
        # so we will likely have to log-scrape to deterimine pass/fail.
        # Also, if we ran in cc-mode, we will not get the "R e p o r t: Verilator"
        # in the sim.exe results, unless that was added to the C++ main.
        use_must_strings = not self.args['cc-mode'] # disable in --cc mode.
        self.run_commands_check_logs(
            commands=self.verilated_exec_command_lists,
            use_must_strings=use_must_strings, sim_step='simulate'
        )

        if self.args.get('coverage', True):
            # do not check logs on verilator_coverage commands:
            self.run_commands_check_logs(
                commands=self.verilated_post_exec_coverage_command_lists,
                check_logs=False
            )

    def get_compile_command_lists( # pylint: disable=too-many-branches
            self, **kwargs
    ) -> list:

        # Support for lint_only (bool) in kwargs:
        lint_only = kwargs.get('lint_only', False)

        verilate_command_list = self._get_start_verilator_command_list(lint_only=lint_only)

        # Handle UVM things (return args), but also handles uvm_pkg.sv in self.files_sv:
        # since we run this 2x (lint-only and normal) only do warnings for one of them:
        verilate_command_list += self._verilator_args_uvm(
            warnings=(not lint_only), add_uvm_pkg_if_found=True
        )

        verilate_command_list += self.get_verilator_tool_config_waivers()

        verilate_command_list += self._verilator_args_defaults_cflags_nproc()

        verilate_command_list += self._get_verilator_waves_args(lint_only=lint_only)


        if self.args.get('coverage', True):
            verilate_command_list += self.tool_config.get(
                'compile-coverage-args', '--coverage').split()

        verilate_command_list += [
            '-top', self.args['top'],
        ]

        if self.args['add-top-library']:
            util.warning('--add-top-library: currently not supported in eda --tool=verilator:',
                         f'{self.args["add-top-library"]}')

        if not lint_only:
            verilate_command_list += [
                '-o', 'sim.exe',
            ]

        for arg in ('x-assign', 'x-initial'):
            if self.args[arg] and f'--{arg}' not in verilate_command_list:
                # Only add this if arg is set, and not present in verilator call
                # this takes care of it being in our self.tool_config for compile-args.
                verilate_command_list += [
                    f'--{arg}', self.args[arg]
                ]

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

        # defines
        if self.args['ext-defines-sv-fname']:
            self.create_ext_defines_sv()
        else:
            for k,v in self.defines.items():
                if v is None:
                    verilate_command_list += [ f'+define+{k}' ]
                else:
                    # Generally we should only support int and str python types passed as
                    # +define+{k}={v}, but also for SystemVerilog plusargs
                    verilate_command_list += [ f'+define+{k}={sanitize_defines_for_sh(v)}' ]

        # parameters
        verilate_command_list.extend(
            self.process_parameters_get_list(arg_prefix='-G')
        )

        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 call verilator')

        verilate_command_list += list(self.files_sv) + list(self.files_v)

        if self.args['cc-mode']:
            # Verilator --cc mode, we have to also add the C++ file to our verilate command:
            verilate_command_list += list(self.files_cpp)

        return [ util.ShellCommandList(verilate_command_list, tee_fpath='compile.log') ]

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

    def get_simulate_command_lists(self, **kwargs) -> list:

        # verilator needs the seed to be < 2*31-1
        verilator_seed = int(self.args['seed']) & 0xfff_ffff

        assert isinstance(self.args['sim-plusargs'], list), \
            f'{self.target=} {type(self.args["sim-plusargs"])=} but must be list'

        sim_plusargs = []
        for x in self.args['sim-plusargs']:
            # For Verilator we need to add a +key=value if the + is missing
            if x[0] != '+':
                x = f'+{x}'
            sim_plusargs.append(x)

        # TODO(drew): don't have a use-case yet for self.args['sim-library', 'elab-args'] in the
        # verilated executable 'simulation' command list, but we may need to support them if we
        # have more than 'work' library.

        verilated_exec_command_list = [
            './obj_dir/sim.exe',
        ]

        config_sim_args = self.tool_config.get(
            'simulate-args', ''
        ).split()

        if self.args['waves']:
            sim_waves_args_list = self.tool_config.get('simulate-waves-args', '').split()
            config_sim_args += sim_waves_args_list
            if not any(x.startswith('+trace=') or x == '+trace' for x in \
                        config_sim_args + sim_plusargs + self.args['sim-args']):
                # Built-in support for eda args --waves and/or --dump-vcd to become runtime
                # plusargs +trace or +trace=vcd, if +trace or +trace= was not already in our
                # plusargs.
                if self.args.get('dump-vcd', False) or \
                   self.args.get('waves-vcd', False):
                    sim_plusargs.append('+trace=vcd')
                elif self.args.get('waves-fst', False):
                    sim_plusargs.append('+trace')

        verilated_exec_command_list += config_sim_args + sim_plusargs + self.args['sim-args']


        # We need to set the seed if none of the other args did:
        if not any(x.startswith('+verilator+seed+') for x in verilated_exec_command_list):
            verilated_exec_command_list.append(f'+verilator+seed+{verilator_seed}')

        if any(self.args[arg] in ('unique', 'fast') for arg in ('x-assign', 'x-initial')) and \
           not any(x.startswith('+verilator+rand+reset') for x in verilated_exec_command_list):
            # Only add this if arg is one of x-assign/x-initial is set to "unique" or "fast",
            # we use the encoded value "2" for +verilator+rand+reset+2
            verilated_exec_command_list.append('+verilator+rand+reset+2')

        if self.args['x-assign'] == '1' and \
           not any(x.startswith('+verilator+rand+reset') for x in verilated_exec_command_list):
            # Only add this if --x-assign=1 (not valid for --x-initial),
            # we use the encoded value "1" for +verilator+rand+reset+1
            verilated_exec_command_list.append('+verilator+rand+reset+1')


        return [
            util.ShellCommandList(verilated_exec_command_list, tee_fpath='sim.log')
        ] # single entry list


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

        if self.args.get('coverage', True):
            if not self.verilator_coverage_exe:
                self.error(f'verilator_coverage not found in path with {self.verilator_exe}')
                return []

            verilated_post_exec_coverage_command_list = [self.verilator_coverage_exe]
            config_coverage_args = self.tool_config.get(
                'coverage-args',
                '--annotate logs/annotated coverage.dat').split()

            verilated_post_exec_coverage_command_list += \
                config_coverage_args + self.args['verilator-coverage-args']


            return [ util.ShellCommandList(verilated_post_exec_coverage_command_list,
                                           tee_fpath='coverage.log') ] # single entry list

        return []

    def add_waves_pkg_file(self) -> None:
        '''If --waves present, and the user is missing any $dumpfile(), then adds a pre-written
        SystemVerilog package to their source code.
        '''
        if self.args['cc-mode']:
            # We won't do this if the user brought a .cpp file and verilator not
            # called with --binary.
            return

        super().add_waves_pkg_file() # call from CommandSim



    def _get_start_verilator_command_list(self, lint_only: bool = False) -> list:

        verilate_command_list = [
            self.verilator_exe,
        ]
        if self.args['cc-mode']:
            verilate_command_list += [
                '--cc',
                '--build',
                '--exe',
            ]
        elif lint_only:
            verilate_command_list.append('--lint-only')
        else:
            verilate_command_list.append('--binary')

        return verilate_command_list

    def get_verilator_tool_config_waivers(self) -> list:
        '''Returns list of args to verilator for waviers, from --compile-waivers and

        --config-yml for tool: (config)'''

        # Add compile waivers from self.config (tools.verilator.compile-waivers list):
        # list(set(mylist)) to get unique.
        ret = []
        for waiver in self.tool_config.get(
                'compile-waivers',
                [ #defaults:
                    'CASEINCOMPLETE',
                    'TIMESCALEMOD', # If one file has `timescale, then they all must
                ]) + self.args['compile-waivers']:
            ret.append(f'-Wno-{waiver}')
        return ret

    def _get_verilator_waves_args(self, lint_only: bool = False) -> list:

        ret = []
        if self.args.get('waves', False) and not lint_only:
            # Skip waves if this is elab or lint_only=True
            config_waves_args = self.tool_config.get(
                'compile-waves-args',
                '--trace-structs --trace-params').split()
            ret += config_waves_args
            if self.args.get('dump-vcd', False):
                ret += [ '--trace' ]
            else:
                ret += [ '--trace-fst' ]
        return ret


    def _verilator_args_defaults_cflags_nproc(self) -> list:
        '''Returns list of args to be added to verilator (compile) step

        Uses self.args['verilate-args'], self.args['compile-args'], and self.tool_config

        Sets -j <value> and -CFLAGS -O<value> if not present in --config-yml, --compile-args,
        or --verilate-args. If present, chooses the first instance (does not present duplicates
        to verilator call).
        '''

        # We can only support one -CFLAGS followed by one -O[0-9] arg in
        # --verilate-args or --compile-args.

        # Add compile args from our self.config (tools.verilator.compile-args str)
        verilate_args = self.args['verilate-args'] + \
            self.args['compile-args'] + \
            self.tool_config.get(
                'compile-args',
                '--timing --assert --autoflush -sv').split()

        util.debug(f"{self.args['verilate-args']=}")
        util.debug(f"{self.args['compile-args']=}")

        dash_j_arg_indices = []
        cflags_dasho_args_indices = []
        for i, arg in enumerate(list(verilate_args)):
            # There can only be one of these: -j <value>, similarly can only be one of
            # -CFLAGS -O<value>
            if (i + 1) < len(verilate_args):
                if arg == '-j':
                    dash_j_arg_indices.extend([i, i + 1])
                if arg == '-CFLAGS':
                    next_arg = verilate_args[i + 1]
                    if next_arg.startswith('-O') and len(next_arg) == 3:
                        cflags_dasho_args_indices.extend([i, i + 1])

        # For -j <value> we'll pick the first one, remove the rest.
        # Same goes for -CFLAGS -O<value>
        for index in dash_j_arg_indices[2:] + cflags_dasho_args_indices[2:]:
            verilate_args[index] = ''

        verilate_args = [x for x in verilate_args if x != ''] # strip empty str.

        # Support for --optimize which will use -CFLAGS -O3, if -CFLAGS is not present at all.
        if cflags_dasho_args_indices:
            # add whatever args were passed via 'compile-args' or 'verilate_args'. Note these will
            # take precedence over the --optimize arg.
            pass
        elif self.args['optimize']:
            # translate CommandSim.args['optimize'] into verilator -CFLAGS -O3.
            # (slower compile, better runtime)
            verilate_args += '-CFLAGS', '-O3'
        else:
            # Default to -O1:
            verilate_args += '-CFLAGS', '-O1'

        # If there was no -j setting, then use from Command.args['threads']
        if not dash_j_arg_indices:
            verilate_args += '-j', str(self.args['threads'])

        return verilate_args


    def _verilator_support_uvm_pkg_fpath(self, add_if_found: bool = True) -> bool:
        '''Returns False if we could not find a suitable uvm_pkg.sv to use, or if --no-uvm.

        This will also auto-add uvm_pkg.sv from $UVM_HOME/uvm_pkg.sv if not present in
        self.files already (adds to front of self.files_sv)
        '''

        if not self.args['uvm']:
            return False

        for fname, exists in self.files.items():
            if exists and os.path.split(fname)[1] == 'uvm_pkg.sv':
                # already present in our source files (assume someone doing it manually
                # or via DEPS)
                return True

        uvm_home = os.environ.get('UVM_HOME', '')
        if not uvm_home:
            return False

        uvm_pkg_fpath = os.path.join(uvm_home, 'uvm_pkg.sv')
        if add_if_found and os.path.isfile(uvm_pkg_fpath):
            uvm_pkg_fpath = os.path.abspath(uvm_pkg_fpath)
            util.info(f'For --uvm, adding to source files: {uvm_pkg_fpath}')
            self.files[uvm_pkg_fpath] = True
            self.files_sv.insert(0, uvm_pkg_fpath)
            self.files_caller_info[uvm_pkg_fpath] = 'verilator.py'
            util.info(f'For --uvm, adding +incdir+: {uvm_home}')
            self.incdirs.append(os.path.abspath(uvm_home))
            return True

        return False


    def _verilator_args_uvm(
            self, warnings: bool = True, add_uvm_pkg_if_found: bool = True
    ) -> list:
        '''Returns list of args to be added to verilator (compile) step if --uvm present

        Warnings on potential issues (Veriltor version, missing uvm_pkg.sv).
        Optionally adds uvm_pkg.sv to source files.
        '''

        # Handle --uvm args:
        if not self.args['uvm']:
            return []

        if warnings:

            # prefers Verilator >= v5.042, $UVM_HOME to be set, or warning.
            version_list = self._VERSION.split('.')
            if int(version_list[0]) < 5 or \
               (int(version_list[0]) == 5 and int(version_list[1]) < 42):
                util.warning(f'Verilator version is {self._VERSION}, --uvm set prefers Verilator',
                             'version > v5.042')

            if not os.environ.get('UVM_HOME', ''):
                util.warning('--uvm set, however env (or .env or --env-file)',
                             '$UVM_HOME is not set')

        uvm_pkg_found = self._verilator_support_uvm_pkg_fpath(add_if_found=add_uvm_pkg_if_found)
        if warnings and not uvm_pkg_found:
            util.warning(
                '--uvm set, however no suitable uvm_pkg.sv is source files,',
                f'nor in $UVM_HOME/uvm_pkg.sv. $UVM_HOME={os.environ.get("UVM_HOME", "")}'
            )

        return ['-Wno-fatal', '+define+UVM_NO_DPI']


    def artifacts_add(self, name: str, typ: str, description: str) -> None:
        '''Override from Command.artifacts_add, so we can catch known file

        names to make their typ/description better, such as CommandSim using
        sim.log or compile.log
        '''
        _, leafname = os.path.split(name)
        if leafname == 'sim.log':
            description = 'Verilated executable log from stdout/stderr'
        elif leafname == 'compile.log':
            description = 'Verilator compile step log from verilator call'

        super().artifacts_add(name=name, typ=typ, description=description)


class VerilatorElab(VerilatorSim):
    '''VerilatorElab is a command handler for: eda elab --tool=verilator'''
    command_name = 'elab'

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


class VerilatorLint(VerilatorSim):
    '''VerilatorLint is a command handler for: eda lint --tool=verilator.

    For practical reasons, this is identical to VerilatorElab with --stop-after-compile.'''
    command_name = 'lint'

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