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

Contains classes for ToolQuesta, and CommonSimQuesta.

'''

# pylint: disable=R0801 # (setting similar, but not identical, self.defines key/value pairs)

# TODO(drew): fix these pylint eventually:
# pylint: disable=too-many-branches

import os
import re

from opencos import util
from opencos.commands import sim, CommandSim, CommandFList, CommandWaves
from opencos.eda_base import Tool
from opencos.files import safe_shutil_which
from opencos.utils import vsim_helper
from opencos.utils.str_helpers import sanitize_defines_for_sh

class ToolQuesta(Tool):
    '''Base class for CommandSimQuesta, collects version information about qrun'''

    _TOOL = 'questa'
    _EXE = 'vsim'

    starter_edition = False
    use_vopt = False # set manually or by get_versions() (after __init__ has set self._EXE)
    sim_exe = '' # vsim or qrun
    sim_exe_base_path = ''
    waveviewer_exe = ''
    questa_major = None
    questa_minor = None

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


    def _try_set_version_from_release_notes(self) -> None:
        '''Attempts to use a RELEASE_NOTES.txt file to get version info

        Return None, may set self._VERSION

        {path}/../docs/rlsnotes/RELEASE_NOTES.txt, where first line
        '''
        release_notes_txt_filepath = os.path.join(
            self.sim_exe_base_path, '..', 'docs', 'rlsnotes', 'RELEASE_NOTES.txt'
        )
        if not os.path.isfile(release_notes_txt_filepath):
            return

        with open(release_notes_txt_filepath, encoding='utf-8') as f:
            for line in f.readlines():
                if line.strip().startswith('Release Notes For'):
                    m = re.search(r'(\d+)\.(\d+)', line)
                    if m:
                        _maj, _min = int(m.group(1)), int(m.group(2))
                        self._VERSION = f'{_maj}.{_min}'
                        if self._TOOL.startswith('questa'):
                            # don't set these for ModelsimASE:
                            self.questa_major = int(m.group(2))
                            self.questa_minor = int(m.group(2))
                        util.debug(f'version {self._VERSION} ({release_notes_txt_filepath})')
                        break


    def _try_set_version_from_path(self) -> None:
        '''Attempts to use portions of exe path to get version info

        Return None, may set self._VERSION
        '''
        m = re.search(r'(\d+)\.(\d+)', self.sim_exe_base_path)
        if m:
            self.questa_major = int(m.group(1))
            self.questa_minor = int(m.group(2))
            self._VERSION = str(self.questa_major) + '.' + str(self.questa_minor)
        else:
            util.warning("Questa path doesn't specificy version, expecting (d+.d+)")


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

        # For vsim tools, since we might have > 1 in PATH, use what eda figured out already
        path = self.config.get('auto_tools_found', {}).get(self._TOOL, '')
        if not path:
            # 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._EXE)

        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",
                       "(i.e. source /opt/intelFPGA_pro/23.4/settings64.sh")
            util.debug(f"{path=}")
            if self._EXE.endswith('qrun') and \
               any(x in path for x in ('modelsim_ase', 'questa_fse')):
                util.warning(f"{self._EXE=} Questa path is for starter edition",
                             "(modelsim_ase, questa_fse), consider using --tool=modelsim_ase",
                             "or --tool=questa_fse, or similar")
        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')

        # For Questa family, we will get the version using:
        # 1. vsim -version (it's fast), from vsim_helper.lookup_version(TOOL)
        # 2. else, if present: {path}/../docs/rlsnotes/RELEASE_NOTES.txt, where first line
        #   shows the full version.
        # 3. else, use the path information only.
        self._VERSION = vsim_helper.lookup_version(self._TOOL)
        if not self._VERSION:
            self._try_set_version_from_release_notes()
        if not self._VERSION:
            self._try_set_version_from_path()

        return self._VERSION


    def set_tool_defines(self) -> None:
        '''Override from class Tool, which handles picking up config['tools'][self._TOOL]

        defines. We also set the OC_TOOL_QUESTA_[major]_[minor] if those exist
        '''
        super().set_tool_defines() # Tool, set from config.
        if self.questa_major and self.questa_minor:
            self.defines[f'OC_TOOL_QUESTA_{self.questa_major:d}_{self.questa_minor:d}'] = None


class CommonWavesQuesta(CommandWaves, ToolQuesta):
    '''Handler for: eda waves --tool=QUESTA-FAMILY WAVE-FILE

    for tools - modelsim_ase, questa, questa_fse, and others, that use
    vsim or this as a parent class.
    '''


    def __init__(self, config: dict):
        CommandWaves.__init__(self, config=config)
        ToolQuesta.__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 CommonSimQuesta(CommandSim, ToolQuesta):
    '''CommonSimQuesta is a the base command handler for:

    eda sim --tool=[modelsim_ase|questa|questa_fse]
    '''

    def __init__(self, config: dict):
        CommandSim.__init__(self, config=config)
        ToolQuesta.__init__(self, config=self.config)
        self.shell_command = os.path.join(self.sim_exe_base_path, 'vsim')
        self.starter_edition = True
        self.args.update({
            'tool': self._TOOL, # override
            'gui': False,
            'vopt': self.use_vopt,
        })
        self.args_help.update({
            'vopt': (
                'Boolean to enable/disable use of vopt step prior to vsim step'
                ' Note that vopt args can be controlled with --elab-args=<value1>'
                ' --elab-args=<value2> ...'
            )
        })


    def run_in_batch_mode(self) -> bool:
        '''Returns bool if we should run in batch mode (-c) from command line'''
        if self.args['test-mode']:
            return True
        if self.args['gui']:
            return False
        return True


    def prepare_compile(self) -> None:
        self.set_tool_defines()
        self.update_library_map()
        self.add_waves_pkg_file()
        self.write_vlog_dot_f()
        self.write_vsim_dot_do(dot_do_to_write='all')

        vsim_command_lists = self.get_compile_command_lists()
        util.write_shell_command_file(
            dirpath=self.args['work-dir'],
            filename='compile_only.sh',
            command_lists=vsim_command_lists
        )

        vsim_command_lists = self.get_elaborate_command_lists()
        util.write_shell_command_file(
            dirpath=self.args['work-dir'],
            filename='compile_elaborate_only.sh',
            command_lists=vsim_command_lists
        )

        # Write simulate.sh and all.sh to work-dir:
        vsim_command_lists = self.get_simulate_command_lists()
        self.write_sh_scripts_to_work_dir(
            compile_lists=[], elaborate_lists=[], simulate_lists=vsim_command_lists
        )

    def compile(self):
        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=['Errors: 0'], use_must_strings=False,
                                         sim_step='compile')

    def elaborate(self):
        if self.args['stop-before-compile']:
            return
        if self.args['stop-after-compile']:
            return
        if self.args['stop-after-elaborate']:
        # only run this if we stop after elaborate (simulate run it all)
            vsim_command_lists = self.get_elaborate_command_lists()
            self.run_commands_check_logs(vsim_command_lists, log_filename='sim.log',
                                         sim_step='elaborate')

    def simulate(self):
        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
        vsim_command_lists = self.get_simulate_command_lists()
        self.run_commands_check_logs(vsim_command_lists, log_filename='sim.log',
                                     sim_step='simulate')

    def get_compile_command_lists(self, **kwargs) -> list:
        # This will also set up a compile.
        vsim_command_list = [
            self.sim_exe,
            '-c' if self.run_in_batch_mode() else '',
            '-do', 'vsim_vlogonly.do', '-logfile', 'sim.log',
        ]
        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,
            '-c' if self.run_in_batch_mode() else '',
            '-do', 'vsim_lintonly.do', '-logfile', 'sim.log',
        ]
        return [vsim_command_list]

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

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

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

        # Add compile args from config.tool.TOOL (questa, etc):
        vlog_dot_f_lines += self.tool_config.get(
            'compile-args',
            '-sv -svinputport=net -lint').split()
        # Add waivers from config.tool.TOOL (questa, modelsim_ase, etc)
        for waiver in self.tool_config.get(
                'compile-waivers',
                [ #defaults:
                    '2275', # 2275 - Existing package 'foo_pkg' will be overwritten.
                ]) + self.args['compile-waivers']:
            vlog_dot_f_lines += ['-suppress', str(waiver)]

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

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

        for value in self.incdirs:
            vlog_dot_f_lines += [ f"+incdir+{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']

        vlog_dot_f_lines += [
            '-source',
            ] + 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 vopt_handle_parameters(self) -> (str, list):
        '''Returns str for vopt or voptargs, and list of vopt tcl

        Note this is used for self.use_vopt = True or False.
        '''

        voptargs_str = ''
        vopt_do_lines = []

        # Note that if self.use_vopt=True, we have to do some workarounds for how
        # some questa-like tools behave for: tcl/.do + vopt arg processing
        # This affects string based parameters that have spaces (vopt treats spaces unique args,
        # vsim does not). Since we'd like to keep the vopt/vsim split into separate steps, we can
        # work around this by setting tcl varaibles for each parameter.
        if self.parameters:
            if not self.use_vopt:
                voptargs_str += ' ' + ' '.join(
                    self.process_parameters_get_list(
                        arg_prefix='-G', hier_delimiter='/', top_hier_str=f'/{self.args["top"]}/'
                    )
                )
            else:
                for k,v in self.parameters.items():
                    s = sim.parameters_dict_get_command_list(
                        params={k: v}, arg_prefix='', hier_delimiter='/',
                        top_hier_str=f'/{self.args["top"]}/'
                    )[0]
                    # At this point, s should be a str in form {k}={v}
                    if not s or '=' not in s:
                        continue
                    if ' ' in s:
                        # Instead of:
                        #   vopt -GMyParam="hi bye"
                        # we'll do:
                        #   set PARAMETERS(MyParam) "hi bye"
                        #  vopt -GMyParam=$PARAMETERS(MyParam)
                        parts = s.split('=')
                        _name = parts[0]
                        _value = '='.join(parts[1:])
                        s = f'set PARAMETERS({_name}) {_value}'
                        vopt_do_lines.append(s)
                        voptargs_str += f' -G{_name}=$PARAMETERS({_name}) '
                    else:
                        voptargs_str += f' -G{s} '

        return voptargs_str, vopt_do_lines


    def write_vsim_dot_do( # pylint: disable=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_suppress_list_str = self._get_vsim_suppress_list_str()
        vsim_ext_args = ' '.join(self.args.get('sim-args', []))

        voptargs_str = self.tool_config.get('elab-args', '')
        voptargs_str += ' '.join(self.args.get('elab-args', []))
        if self.args['gui'] or self.args['waves']:
            voptargs_str += ' ' + self.tool_config.get('simulate-waves-args', '+acc')
            util.artifacts.add_extension(
                search_paths=self.args['work-dir'], file_extension='wlf',
                typ='waveform', description='Modelsim/Questa Waveform WLF (Wave Log Format) file'
            )

        # TODO(drew): support self.args['sim_libary'] (1 lists)
        vlog_do_lines = []
        vsim_do_lines = []

        # parameters, use helper method to get voptargs_str and vopt_do_lines
        more_voptargs_str, vopt_do_lines = self.vopt_handle_parameters()
        voptargs_str += more_voptargs_str


        vopt_one_liner = ""
        if self.use_vopt:
            vopt_one_liner = (
                f"vopt {voptargs_str} work.{self.args['top']} -o opt__{self.args['top']}"
            )
            vopt_one_liner = vopt_one_liner.replace('\n', ' ') # needs to be a one-liner
            # vopt doesn't need -voptargs=(value) like vsim does, simply use (value).
            vopt_one_liner = vopt_one_liner.replace('-voptargs=', '')

            vsim_one_liner = "vsim -onfinish stop" \
                + f" -sv_seed {self.args['seed']} {sim_plusargs_str} {vsim_suppress_list_str}" \
                + f" {vsim_ext_args} opt__{self.args['top']}"
        else:
            # vopt doesn't exist, use single vsim call after vlog call:
            vsim_one_liner = "vsim -onfinish stop" \
                + f" -sv_seed {self.args['seed']} {sim_plusargs_str} {vsim_suppress_list_str}" \
                + f" {voptargs_str} {vsim_ext_args} work.{self.args['top']}"

        # If there are additional top level libs, we wont' add them to the vopt step, add
        # to vsim only (after work.TOP):
        for l in self.args['add-top-library']:
            vsim_one_liner += f' {l}'

        vsim_one_liner = vsim_one_liner.replace('\n', ' ')

        vlog_do_lines += [
            "if {[file exists work]} { vdel -all work; }",
            "vlib work;",
            "quietly set qc 30;",
            "if {[catch {vlog -f vlog.f} result]} {",
            "    echo \"Caught $result \";",
            "    if {[batch_mode]} {",
            "        quit -f -code 20;",
            "    }",
            "}",
        ]

        if self.use_vopt:
            vopt_do_lines += [
                "if {[catch { " + vopt_one_liner + " } result] } {",
                "    echo \"Caught $result\";",
                "    if {[batch_mode]} {",
                "        quit -f -code 19;",
                "    }",
                "}",
            ]

        vsim_do_lines += [
            "if {[catch { " + vsim_one_liner + " } result] } {",
            "    echo \"Caught $result\";",
            "    if {[batch_mode]} {",
            "        quit -f -code 18;",
            "    }",
            "}",
        ]

        vsim_vlogonly_dot_do_lines = vlog_do_lines + [
            "if {[batch_mode]} {",
            "    quit -f -code 0;",
            "}",
        ]

        final_check_teststatus_do_lines = [
            "set TestStatus [coverage attribute -name SEED -name TESTSTATUS];",
            "if {[regexp \"TESTSTATUS += 0\" $TestStatus]} {",
            "    quietly set qc 0;",
            "} elseif {[regexp \"TESTSTATUS += 1\" $TestStatus]} {",
            "    quietly set qc 0;",
            "} else {",
            "    quietly set qc 2;",
            "}",
            "if {[batch_mode]} {",
            "    quit -f -code $qc;",
            "}",
        ]

        # final vlog/vopt/vsim lint-only .do command (want to make sure it can completely
        # build for 'elab' style eda job), runs for 0ns, logs nothing for a waveform, quits
        vsim_lintonly_dot_do_lines = vlog_do_lines + vopt_do_lines + vsim_do_lines \
            + final_check_teststatus_do_lines

        # final vlog/opt/vsim full simulation .do command.
        vsim_dot_do_lines = vlog_do_lines + vopt_do_lines + vsim_do_lines + [
            "onbreak { resume; };",
            "catch {log -r *};",
            "run -a;",
        ] + final_check_teststatus_do_lines

        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_sim_plusargs_str(self) -> str:
        sim_plusargs = []

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

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

        return ' '.join(sim_plusargs)


    def _get_vsim_suppress_list_str(self) -> str:
        vsim_suppress_list = []
        # Add waivers from config.tool.TOOL:
        for waiver in self.tool_config.get(
                'simulate-waivers', [
                    #defaults:
                    '3009', # 3009: [TSCALE] - Module 'foo' does not have a timeunit/timeprecision
                            #       specification in effect, but other modules do.
                ]) + self.args['sim-waivers']:
            vsim_suppress_list += ['-suppress', str(waiver)]

        return ' '.join(vsim_suppress_list)


    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
        '''
        _, leafname = os.path.split(name)
        if leafname == 'sim.log':
            description = 'Modelsim/Questa Transcript log file'

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


class CommonElabQuesta(CommonSimQuesta):
    '''CommonElabQuesta is a command handler for: eda elab --tool=(questa family)'''

    command_name = 'elab'

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


class CommonLintQuesta(CommonSimQuesta):
    '''CommonSimQuesta is a command handler for: eda lint --tool=(questa family)'''

    command_name = 'lint'

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


class CommonFListQuesta(CommandFList, ToolQuesta):
    '''CommonFListQuesta is a command handler for: eda flist --tool=(questa family)'''

    def __init__(self, config: dict):
        CommandFList.__init__(self, config=config)
        ToolQuesta.__init__(self, config=self.config)
        self.args.update({
            # an Flist, like vlog.f, cannot support parameters or sim-plusargs, so warn
            # if they are present b/c they will not be emitted.
            'emit-parameter': False,
            'emit-plusargs': False,
        })
