'''opencos.commands.waves - command handler for: eda waves ...

Note this command is handled differently than others (such as CommandSim),
it is generally run as simply

    > eda waves

and attempts to auto-find the newest waveform in your work directories. As
a result, no Tool is bound to CommandWaves (there is no CommandWavesVivado
handler).
'''

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

from pathlib import Path
import os

from opencos import eda_config
from opencos.eda_base import Command, Tool
from opencos.utils import markup_helpers
from opencos.util import Colors, debug, info, warning, import_class_from_string


class CommandWaves(Command):
    '''command handler for: eda waves'''

    command_name = 'waves'


    def __init__(self, config: dict):
        Command.__init__(self, config=config)
        self.args.update({
            'test-mode': False,
        })
        self.args_help.update({
            'test-mode': 'Do not run the command to open the located wave file, instead print' \
            + ' to stdout',
        })

        # process_tokens() will populate these:
        self.wave_dirs = [] # Targets presented as positional args
        self.wave_file = None # The one we'll be opening.
        self.unparsed_args = []


    def get_supported_wave_file_ext(self) -> list:
        '''Given the tools in config, return a list of all supported wave file extensions'''
        ret = []

        # tools to check, if this is a Tool class, then only check itself, otherwise
        # check everything from config.auto_tools_order:
        tools = []
        if isinstance(self, Tool):
            if _tool := getattr(self, '_TOOL', ''):
                tools.append(_tool) # one tool
        else:
            tools = self.config.get('auto_tools_order', {}).get('waves', [])

        for _tool in tools:
            if not _tool:
                continue

            _entry = self.config.get('tools', {}).get(_tool, {})
            if not _entry:
                continue

            if _tool not in self.config.get('tools_loaded', []):
                continue

            waves_file_ext_list = _entry.get('waves-file-ext', [])
            if isinstance(waves_file_ext_list, str):
                waves_file_ext_list = waves_file_ext_list.split()
            for ext in waves_file_ext_list:
                if ext not in ret:
                    ret.append(ext)

        return ret


    def get_wave_files_in_dirs(self, wave_dirs: list, quiet: bool = False) -> list:
        '''Returns list of all wave files give wave_dirs (list)'''

        supported_file_ext = self.get_supported_wave_file_ext()
        if not supported_file_ext:
            if not quiet:
                warning(f"No supported wave file extensions found, can't search: {wave_dirs}")
            return []

        if not wave_dirs:
            if not quiet:
                warning(f"No wave_dirs to search, for supported extensions: {supported_file_ext}")
            return []

        all_files = []
        debug(f"Looking for wave files in: {wave_dirs}")
        for d in wave_dirs:
            if not quiet:
                info(f"Looking for wave files below: {d}")
            for root, _, files in os.walk(d):
                for f in files:
                    if f and os.path.splitext(f)[1] in supported_file_ext:
                        if not quiet:
                            info(f"Found wave file: {Colors.byellow}{os.path.join(root, f)}")
                        all_files.append(os.path.join(root, f))
        return all_files


    def find_tool_used_on_wave_file(self) -> str:
        '''Try to find out which tool created self.wave_file

        For FST or VCD files, this doesn't matter, but for WLF files it matters
        '''
        wave_base_path = Path(self.wave_file).parent
        eda_output_config_fpath = wave_base_path / eda_config.EDA_OUTPUT_CONFIG_FNAME
        if eda_output_config_fpath.exists():
            data = markup_helpers.yaml_safe_load(str(eda_output_config_fpath))
            if tool := data.get('args', {}).get('tool', None):
                # This is set if an auto-tool was selected.
                return tool

        return ''


    def _get_child_handling_class(self) -> object: # pylint: disable=too-many-branches
        '''Returns a class handle of a child to process this, which should be a Tool class

        if no appropriate child is found, returns self.

        Uses self.wave_file, and assumes self.wave_file is already set.
        '''
        if isinstance(self, Tool):
            # We're already a tool handling class.
            return self

        assert self.wave_file, f'{self.wave_file=} not set, cant determine child class'

        ext = os.path.splitext(self.wave_file)[1]
        tool_guesses = []

        # Note - use the config.auto_tools_order so the CONFIG-YML tells us what order
        # to search for waveform viewers. Each viewer must have registered its file
        # extensions in config.tools.TOOLNAME.waves-files-ext (list)
        for _tool in self.config.get('auto_tools_order', {}).get('waves', []):
            if not _tool:
                continue

            _entry = self.config.get('tools', {}).get(_tool, {})
            if not _entry:
                continue

            if _tool not in tool_guesses and \
               _tool in self.config.get('tools_loaded', []) and \
               ext in _entry.get('waves-file-ext', []):
                tool_guesses.append(_tool) # append, need ordered list here.

        # There's an added wrinkle that if you have > 1 version of Questa available
        # in PATH (Questa, ModelsimAse, etc) we can't smartly pick a default waveform
        # viewer using the file type alone, if that was created under a different tool.
        # In this case, we should check to see if the self.wave_file directory has
        # an eda_output_config.yml file, and if so what tool was used to run the
        # simulation.
        prefer_waves_tool = None
        original_sim_tool = self.find_tool_used_on_wave_file()
        if original_sim_tool and \
           original_sim_tool in self.config.get('auto_tools_order', {}).get('waves', []):
            _entry = self.config.get('tools', {}).get(original_sim_tool, {})
            if ext in _entry.get('waves-file-ext', []):
                # We found the original, it supports "waves" commands, and it supports
                # this file extension.
                prefer_waves_tool = original_sim_tool


        debug(f'waves: {self.wave_file}: {tool_guesses=}')
        if not tool_guesses:
            warning(f'For wave file {self.wave_file} no tool found that can open it')
            return self

        if prefer_waves_tool and prefer_waves_tool in tool_guesses:
            tool_guesses = [prefer_waves_tool] # only try this one.

        for tool in tool_guesses:
            cls_str, cls, sco = None, None, None
            tool_cfg = self.config.get('tools', {}).get(tool, {})
            debug(f'waves: tool_guess {tool=}, {tool_cfg=}')
            if tool_cfg:
                cls_str = tool_cfg.get('handlers', {}).get(self.command_name, None)
            debug(f'waves: tool_guess {tool=}, {cls_str=}')
            if cls_str:
                cls = import_class_from_string(cls_str)
            debug(f'waves: tool_guess {tool=}, {cls=}')
            if cls and issubclass(cls, Command):
                info(f'For wave file {self.wave_file}, can use tool={tool}')
                debug(f'For wave file {self.wave_file}, tool={tool} has handler {cls}')
                sco = cls(config=self.config)
                return sco
            # warn if this 'tool' isn't setup properly.
            warning(f'No handler found for tool={tool} to process wave file: {self.wave_file}')

        debug(f'config -- tools_loaded: {self.config["tools_loaded"]}')
        return self


    def get_and_set_wave_file_from_args( # pylint: disable=too-many-branches
            self, quiet: bool = False
    ) -> str:
        '''Returns abspath of a wave file or empty str, will modify self.unparsed_args

        and sets self.wave_file and self.wave_dirs'''

        if self.wave_file:
            # If this was already set, then return it.
            return self.wave_file

        while self.unparsed_args:
            if os.path.isfile(self.unparsed_args[0]):
                if self.wave_file:
                    self.error(f"Already using positional arg FILE: {self.wave_file}; not",
                               f"sure what to do with another wave file: {self.unparsed_args[0]}")
                self.wave_file = os.path.abspath(self.unparsed_args[0])
                self.unparsed_args.pop(0)
                continue
            if os.path.isdir(self.unparsed_args[0]):
                try_dir = Path(self.unparsed_args[0]).resolve()
                if self.wave_file and not quiet:
                    info(f"Already using positional arg FILE: {self.wave_file};",
                              f"ignoring search directory: {try_dir}")
                elif try_dir not in self.wave_dirs:
                    self.wave_dirs.append(try_dir)
            self.warning_show_known_args()
            self.error(f"Didn't understand command arg/token: '{self.unparsed_args[0]}'",
                       "in CommandWaves")

        if not self.wave_file:
            if not quiet:
                info("Need to look for wave file")
            # we weren't given a wave file, so we will look for one!
            if not self.wave_dirs and os.path.isdir(self.args['eda-dir']):
                try_dir = Path(self.args['eda-dir']).resolve()
                if try_dir not in self.wave_dirs:
                    self.wave_dirs.append(str(try_dir))
            if not self.wave_dirs:
                self.wave_dirs.append(os.getcwd())
            all_files = self.get_wave_files_in_dirs(self.wave_dirs)
            if len(all_files) > 1:
                all_files.sort(key=os.path.getmtime)
                if not quiet:
                    info(f"Choosing: {all_files[-1]} (newest)")
            if all_files:
                self.wave_file = all_files[-1]
            else:
                self.error(f"Couldn't find any wave files below: {','.join(self.wave_dirs)}")

        self.wave_file = os.path.abspath(self.wave_file)
        return self.wave_file


    def process_tokens( # pylint: disable=too-many-branches,too-many-statements
            self, tokens: list, process_all: bool = True,
            pwd: str = os.getcwd()
    ) -> list:

        self.unparsed_args = Command.process_tokens(
            self, tokens=tokens, process_all=False, pwd=pwd
        )

        if self.args['test-mode']:
            self.exec = self._test_mode_exec

        self.get_and_set_wave_file_from_args()

        # Since we're already in a class, and not back in eda.py, we can
        # determine which child handling class should service self.wave_file based
        # on the auto tool order and the extension.
        sco = self._get_child_handling_class()
        if sco is None or not isinstance(sco, Tool):
            self.error(f'Could not find a suitable tool to process file: {self.wave_file}')
            return []

        # Create the child handling class, parse the same original tokens, set
        # somethings ahead of time.
        sco.wave_dirs = self.wave_dirs
        sco.wave_file = self.wave_file
        sco.unparsed_args = Command.process_tokens(
            sco, tokens=tokens, process_all=False, pwd=pwd
        )
        if self.args['test-mode']:
            sco.exec = self._test_mode_exec

        debug(f'We will use {sco=} {type(sco)=} for {self.wave_file=}')
        sco.do_it()
        return sco.unparsed_args


    def _test_mode_exec( # pylint: disable=unused-argument
            self, work_dir: str,
            command_list: list,
            **kwargs
    ) -> None:
        '''Override for Command.exec if arg --test-mode was set, does not run

        the command_list, instead prints to stdout'''

        info(f'waves.py: {Colors.off}{Colors.bcyan}test_mode exec{Colors.info} stdout:',
             f'{Colors.off}{" ".join(command_list)}{Colors.info};   ({work_dir=}')
