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

Contains classes for ToolSlang, CommandElabSlang
'''

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

import os

from opencos import util
from opencos.commands import CommandElab
from opencos.eda_base import Tool
from opencos.files import safe_shutil_which
from opencos.utils import slang_helpers
from opencos.utils.str_helpers import sanitize_defines_for_sh


class ToolSlang(Tool):
    '''ToolSlang used by opencos.eda for --tool=slang'''

    _TOOL = 'slang'
    _EXE = 'slang'
    _URL = 'https://github.com/MikePopoloski/slang'

    slang_exe = ''
    slang_base_path = ''
    slang_tidy_exe = ''
    slang_hier_exe = ''

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

        slang_helpers.slang_available.init(exe=self._EXE)
        path = slang_helpers.slang_available.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 get it ({self._URL}')
            return ''

        self.slang_exe = path
        self.slang_base_path, _ = os.path.split(path)
        self.slang_tidy_exe = safe_shutil_which(
            os.path.join(self.slang_base_path, 'slang-tidy')
        )
        self.slang_hier_exe = safe_shutil_which(
            os.path.join(self.slang_base_path, 'slang-hier')
        )
        self._VERSION = slang_helpers.slang_available.version
        return self._VERSION

    def set_tool_defines(self):
        super().set_tool_defines()
        if 'SYNTHESIS' not in self.defines:
            self.defines['SIMULATION'] = None # add define
        # Expected to manually add SYNTHESIS command line or target, otherwise.
        # Similarly, you could use --tool slang_yosys for a synthesis friendly
        # elab in Yosys.


class CommandElabSlang(CommandElab, ToolSlang):
    '''CommandElabSlang is a command handler for: eda elab --tool=slang'''

    def __init__(self, config:dict):
        CommandElab.__init__(self, config=config)
        ToolSlang.__init__(self, config=self.config)
        self.args.update({
            'slang-args': [], # aka, --single-unit, --ast-json <fname>, --ast-json-source-info
            'slang-json': False, # sets all the args I know of for AST.
            'slang-top': '',
            'tidy': False, # run slang-tidy instead of slang
            'hier': False, # run slang-hier instead of slang
            'warnings-ok': False
        })

        self.all_json_args = [
            '--ast-json', ## needs filename: slang.json'
            '--ast-json-source-info',
            '--ast-json-detailed-types',
        ]

        self.args_help.update({
            'tidy': "Runs 'slang-tidy' instead of 'slang', with no ast- args.",
            'hier': "Runs 'slang-hier' instead of 'slang', with no ast- args.",
            'warnings-ok': 'Warnings will not fail from eda, they will still be shown',
        })


        # If we're in elab, so not in general ToolSlang, set define for SLANG
        self.defines.update({
            'SLANG': 1
        })

        self.slang_command_lists = []

    def _remove_zero_warnings_from_log_must_strings(self) -> None:
        '''Removes anything about "0 warning" from the log-must-strings'''
        for i, x in enumerate(self.args.get('log-must-strings', [])):
            if '0 warning' in x:
                self.args['log-must-strings'][i] = ''



    # Note that we follow parent class CommandSim's do_it() flow, that way --export args
    # are handled.
    def prepare_compile(self) -> None:
        ''' prepare_compile() - following parent Commandsim's run() flow'''
        self.set_tool_defines()

        if self.args['warnings-ok']:
            self._remove_zero_warnings_from_log_must_strings()

        self.write_eda_config_and_args()

        self.slang_command_lists = self.get_compile_command_lists()
        self.write_slang_sh()

    def compile(self):
        pass

    def elaborate(self):
        ''' elaborate() - following parent CommandSim's run() flow, runs slang_command_lists'''
        if self.args['stop-before-compile'] or \
           self.args['stop-after-compile']:
            return
        self.run_commands_check_logs(self.slang_command_lists)

    def get_compile_command_lists(self, **kwargs) -> list:
        '''Returns list of util.ShellCommandList, for slang we'll run this in elaborate()'''

        command_list = self._get_slang_command_list_start() # slang vs slang-tidy vs slang-hier
        command_list += self.tool_config.get('compile-args', '--single-unit').split()
        command_list += self.args['slang-args'] # add user args.
        command_list += self._get_slang_json_args(command_exe=command_list[0])
        command_list += self._get_slang_tool_config_waivers()

        # incdirs
        for value in self.incdirs:
            command_list += [ '--include-directory', value ]

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

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

        # Because many elab target-name won't match the --top needed for
        # slang, we'll leave this to arg --slang-top:
        if self.args.get('slang-top', None):
            command_list += [ '--top', self.args['slang-top'] ]

        # Force -j setting if not already set.
        if not any((a.startswith('-j') or a.startswith('--threads')) for a in command_list):
            command_list += [ '-j', str(self.args['threads']) ]

        command_list += self.files_sv + self.files_v

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

    def write_slang_sh(self):
        '''Writes run_slang.sh to work-dir'''
        util.write_shell_command_file(
            dirpath=self.args['work-dir'],
            filename='run_slang.sh',
            command_lists=self.slang_command_lists,
            line_breaks=True
        )

    def get_elaborate_command_lists(self, **kwargs) -> list:
        # all handled by get_compile_command_lists()
        return []

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

    def update_tool_warn_err_counts_from_log_lines(
            self, log_lines: list, bad_strings: list, warning_strings: list
    ) -> None:
        '''
        Overriden from Command, we ignore bad_strings/warning_strings and use a custom
        checker.
        '''
        for line in log_lines:
            if not line.startswith('Build failed: ') and \
               not line.startswith('Build succeeded: '):
                continue
            if not all(x in line for x in ('errors', 'warnings')):
                continue

            parts = line.strip().split()
            if len(parts) < 6:
                continue

            errs = parts[2]
            warns = parts[4]
            if errs.isdigit():
                self.tool_error_count += int(errs)
            if warns.isdigit():
                self.tool_warning_count += int(warns)

    def _get_slang_command_list_start(self) -> list:
        command_list = [self.slang_exe]

        if self.args['tidy']:
            if not safe_shutil_which(self.slang_tidy_exe):
                util.warning("Running tool slang with --tidy, but 'slang-tidy'",
                             "not in PATH, using 'slang' instead")
            else:
                command_list = [self.slang_tidy_exe]

        if self.args['hier']:
            if self.args['tidy']:
                util.warning('Running with --tidy and --heir, will attempt to use slang-hier')
            elif not safe_shutil_which(self.slang_hier_exe):
                util.warning("Running tool slang with --hier, but 'slang-hier'",
                             "not in PATH, using 'slang' instead")
            else:
                command_list = [self.slang_hier_exe]

        return command_list

    def _get_slang_json_args(self, command_exe: str) -> list:
        command_list = []

        _, command_exe_leaf = os.path.split(command_exe)
        if self.args.get('slang-json', False) and command_exe_leaf == 'slang':
            for arg in self.all_json_args:
                if arg not in command_list:
                    command_list.append(arg)
                    if arg == '--ast-json': # needs filename
                        command_list.append('slang.json')
                        util.artifacts.add(
                            name=os.path.join(self.args['work-dir'], 'slang.json'),
                            typ='json',
                            description='Abstract syntax tree from slang --ast-json'
                        )

        return command_list

    def _get_slang_tool_config_waivers(self) -> list:
        # Add compile waivers from config and command-line args
        return [f'-Wno-{waiver}' for waiver in
                self.tool_config.get('compile-waivers', []) + self.args['compile-waivers']]


class CommandLintSlang(CommandElabSlang):
    '''CommandLintSlang is a command handler for: eda lint --tool=slang.'''

    command_name = 'lint'

    def __init__(self, config: dict):
        super().__init__(config)
        # keep stop-after-compile=False, allow's CommandElabSlang.elaborate() to run.
        self.args['slang-args'] = ['--lint-only', '--ignore-unknown-modules']
