''' opencos.tools.quartus - Used by opencos.eda commands with --tool=quartus

Contains classes for ToolQuartus, and command handlers for synth, build, flist.
Used for Intel FPGA synthesis, place & route, and bitstream generation.
'''

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

import os
import re
import shlex
import subprocess
from pathlib import Path

from opencos import util
from opencos.commands import CommandSynth, CommandBuild, CommandFList, CommandProj, \
    CommandUpload, CommandOpen
from opencos.eda_base import Tool, get_eda_exec
from opencos.files import safe_shutil_which
from opencos.utils.str_helpers import sanitize_defines_for_sh, strip_outer_quotes

class ToolQuartus(Tool):
    '''ToolQuartus used by opencos.eda for --tool=quartus'''

    _TOOL = 'quartus'
    _EXE = 'quartus_sh'

    quartus_year = None
    quartus_release = None
    quartus_base_path = ''
    quartus_exe = ''
    quartus_gui_exe = ''

    def __init__(self, config: dict):
        super().__init__(config=config)
        self.args.update({
            'part': 'A3CY135BM16AE6S',
            'family': 'Agilex 3',
        })
        self.args_help.update({
            'part': 'Device used for commands: synth, build.',
            'family': 'FPGA family for Quartus (e.g., Stratix IV, Arria 10, etc.)',
        })

    def _try_set_version_from_version_txt(self) -> None:
        '''Attempts to use VSIM_PATH/../version.txt to get version info

        Return None, may set self._VERSION
        '''
        util.debug(f"quartus path = {self.quartus_exe}")
        version_txt_filepath = os.path.join(self.quartus_base_path, '..', 'version.txt')
        if os.path.isfile(version_txt_filepath):
            with open(version_txt_filepath, encoding='utf-8') as f:
                for line in f.readlines():
                    if line.startswith('Version='):
                        _, version = line.strip().split('=')
                        self._VERSION = version
                        break

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

        Return None, may set self._VERSION
        '''
        m = re.search(r'(\d+)\.(\d+)', self.quartus_exe)
        if m:
            version = m.group(1) + '.' + m.group(2)
            self._VERSION = version

    def _try_set_version_from_exe(self) -> None:
        '''Attempts to run: vsim --version; (max timeout 3 sec) to get version info

        Return None, may set self._VERSION

        Since this may fail if we don't have a valid license, we will not error if the version
        is not determined.
        '''
        try:
            result = subprocess.run(
                [self.quartus_exe, '--version'],
                capture_output=True, text=True, timeout=3, check=False
            )
            version_match = re.search(r'Version (\d+\.\d+)', result.stdout)
            if version_match:
                self._VERSION = version_match.group(1)
            else:
                util.debug("Could not determine Quartus version from: quartus_sh --version")
        except (
            subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError
        ):
            util.debug("Could not determine Quartus version from: quartus_sh --version")


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

        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("Quartus not in path, need to install or add to $PATH",
                       f"(looked for '{self._EXE}')")
        else:
            self.quartus_exe = path
            self.quartus_base_path, _ = os.path.split(path)

        self.quartus_gui_exe = safe_shutil_which(
            os.path.join(self.quartus_base_path, 'quartus') # vs quartus_sh
        )

        # Get version based on install path name:
        # 1. if ../version.txt exists, use that
        # 2. Use the path name if it has version information
        # 3. or by running quartus_sh --version (max timeout=3)
        # If we cannot find the version return '' and warn, do not error.
        self._try_set_version_from_version_txt()
        if not self._VERSION:
            self._try_set_version_from_path()
        if not self._VERSION:
            self._try_set_version_from_exe()

        if self._VERSION:
            numbers_list = self._VERSION.split('.')
            self.quartus_year = int(numbers_list[0])
            self.quartus_release = int(numbers_list[1])
        else:
            util.warning(f"Quartus version not found, quartus path = {self.quartus_exe}")
        return self._VERSION

    def set_tool_defines(self) -> None:
        self.defines['OC_TOOL_QUARTUS'] = None
        def_year_release = f'OC_TOOL_QUARTUS_{self.quartus_year:02d}_{self.quartus_release:d}'
        self.defines[def_year_release] = None

        # Code can be conditional on Quartus versions
        versions = ['20.1', '21.1', '22.1', '23.1', '24.1', '25.1']

        def version_compare(v1, v2):
            v1_parts = [int(x) for x in v1.split('.')]
            v2_parts = [int(x) for x in v2.split('.')]
            l = max(len(v1_parts), len(v2_parts))
            v1_parts += [0] * (l - len(v1_parts))
            v2_parts += [0] * (l - len(v2_parts))
            return (v1_parts > v2_parts) - (v1_parts < v2_parts)

        for ver in versions:
            str_ver = ver.replace('.', '_')
            cmp = version_compare(self._VERSION, ver)
            if cmp <= 0:
                self.defines[f'OC_TOOL_QUARTUS_{str_ver}_OR_OLDER'] = None
            if cmp >= 0:
                self.defines[f'OC_TOOL_QUARTUS_{str_ver}_OR_NEWER'] = None

        util.debug(f"Setup tool defines: {self.defines}")


class CommandSynthQuartus(CommandSynth, ToolQuartus):
    '''CommandSynthQuartus is a command handler for: eda synth --tool=quartus'''

    def __init__(self, config: dict):
        CommandSynth.__init__(self, config)
        ToolQuartus.__init__(self, config=self.config)
        # add args specific to this tool
        self.args.update({
            'gui': False,
            'tcl-file': "synth.tcl",
            'sdc': "",
            'qsf': "",
        })
        self.args_help.update({
            'gui': 'Run Quartus in GUI mode',
            'tcl-file': 'name of TCL file to be created for Quartus',
            'sdc': 'SDC constraints file',
            'qsf': 'Quartus Settings File (.qsf)',
        })

    def do_it(self) -> None:
        CommandSynth.do_it(self)

        if self.is_export_enabled():
            return

        # create TCL
        tcl_file = os.path.abspath(
            os.path.join(self.args['work-dir'], self.args['tcl-file'])
        )

        self.write_tcl_file(tcl_file=tcl_file)

        # execute Quartus synthesis
        command_list = [
            self.quartus_exe, '-t', tcl_file
        ]
        if not util.args['verbose']:
            command_list.append('-q')

        # Add artifact tracking
        util.artifacts.add_extension(
            search_paths=self.args['work-dir'], file_extension='qpf',
            typ='tcl', description='Quartus Project File'
        )
        util.artifacts.add_extension(
            search_paths=self.args['work-dir'], file_extension='qsf',
            typ='tcl', description='Quartus Settings File'
        )
        util.artifacts.add_extension(
            search_paths=self.args['work-dir'], file_extension='rpt',
            typ='text', description='Quartus Synthesis Report'
        )

        self.exec(self.args['work-dir'], command_list)

        saved_qpf_filename = self.args["top"] + '.qpf'
        if not os.path.isfile(os.path.join(self.args['work-dir'], saved_qpf_filename)):
            self.error('Saved project file does not exist:',
                       os.path.join(self.args['work-dir'], saved_qpf_filename))

        util.info(f"Synthesis done, results are in: {self.args['work-dir']}")

        # Note: in GUI mode, if we were to run:
        #   ran: quaruts -t build.tcl
        # it treats the tcl script as running "headless" as a pre-script, and won't open the
        # GUI anyway, and will exit on completion,
        # Instead we:
        # 1. always run with quartus_sh, so text goes to stdout
        # 2. we'll re-open the project in GUI mode, here:
        if self.args['gui'] and self.quartus_gui_exe:
            self.exec(
                work_dir=self.args['work-dir'],
                command_list=[self.quartus_gui_exe, saved_qpf_filename]
            )

    def write_tcl_file(self, tcl_file: str) -> None:  # pylint: disable=too-many-locals,too-many-branches
        '''Writes synthesis capable Quartus tcl file to filepath 'tcl_file'.'''

        top = self.args['top']
        part = self.args['part']
        family = self.args['family']

        tcl_lines = [
            "# Quartus Synthesis Script",
            "load_package flow",
            f"project_new {top} -overwrite",
            f"set_global_assignment -name FAMILY \"{family}\"",
            f"set_global_assignment -name DEVICE {part}",
            f"set_global_assignment -name TOP_LEVEL_ENTITY {top}",
        ]

        # Add source files (convert to relative paths and use forward slashes)
        # Note that default of self.args['all-sv'] is False so we should have added
        # all files to self.files_sv instead of files_v:
        # Note that tcl uses POSIX paths, so \\ -> /
        for f in self.files_v:
            rel_path = Path(os.path.relpath(f, self.args['work-dir'])).as_posix()
            tcl_lines.append(f"set_global_assignment -name VERILOG_FILE \"{rel_path}\"")
        for f in self.files_sv:
            rel_path = Path(os.path.relpath(f, self.args['work-dir'])).as_posix()
            tcl_lines.append(f"set_global_assignment -name SYSTEMVERILOG_FILE \"{rel_path}\"")
        for f in self.files_vhd:
            rel_path = Path(os.path.relpath(f, self.args['work-dir'])).as_posix()
            tcl_lines.append(f"set_global_assignment -name VHDL_FILE \"{rel_path}\"")

        # Add include directories - Quartus needs the base directory where "lib/" can be found
        for incdir in self.incdirs:
            tcl_lines.append(
                f"set_global_assignment -name SEARCH_PATH \"{Path(incdir).as_posix()}\""
            )

        # Parameters -->  set_parameter -name <Parameter_Name> <Value>
        for k,v in self.parameters.items():
            if not isinstance(v, (int, str)):
                util.warning(f'parameter {k} has value: {v}, parameters must be int/string types')
            if isinstance(v, int):
                tcl_lines.append(f"set_parameter -name {k} {v}")
            else:
                v = strip_outer_quotes(v.strip('\n'))
                v = '"' + v + '"'
                tcl_lines.append(f"set_parameter -name {k} {sanitize_defines_for_sh(v)}")


        # Add all include directories as user libraries for better include resolution
        for incdir in self.incdirs:
            if os.path.exists(incdir):
                tcl_lines.append(
                    f"set_global_assignment -name USER_LIBRARIES \"{Path(incdir).as_posix()}\""
                )

        # Add defines
        for key, value in self.defines.items():
            if value is None:
                tcl_lines.append(f"set_global_assignment -name VERILOG_MACRO \"{key}\"")
            else:
                tcl_lines.append(f"set_global_assignment -name VERILOG_MACRO \"{key}={value}\"")

        # Add constraints
        default_sdc = False
        sdc_files = []
        if self.args['sdc']:
            sdc_files = [os.path.abspath(self.args['sdc'])]
        elif self.files_sdc:
            # Use files from DEPS target or command line.
            sdc_files = self.files_sdc
        else:
            default_sdc = True
            sdc_file = self.args['top'] + '.sdc'
            sdc_files = [sdc_file]

        for f in sdc_files:
            for attr in ('SDC_FILE', 'SYN_SDC_FILE', 'RTL_SDC_FILE'):
                tcl_lines.extend([
                    f"set_global_assignment -name {attr} \"{Path(f).as_posix()}\""
                ])
        tcl_lines.append("set_global_assignment -name SYNTH_TIMING_DRIVEN_SYNTHESIS ON")

        if default_sdc:
            self.write_default_sdc(sdc_file=os.path.join(self.args['work-dir'], sdc_file))

        tcl_lines += [
            "# Run synthesis",
            'flng::run_flow_command -flow "compile" -end "dni_synthesis"',
            'flng::run_flow_command -flow "compile" -end "sta_early" -resume',
        ]

        with open(tcl_file, 'w', encoding='utf-8') as ftcl:
            ftcl.write('\n'.join(tcl_lines))


    def write_default_sdc(self, sdc_file: str) -> None:
        '''Writes a default SDC file to filepath 'sdc_file'.'''

        sdc_lines = []
        util.info("Creating default constraints: clock:",
                  f"{self.args['clock-name']}, {self.args['clock-ns']} (ns),")

        clock_name = self.args['clock-name']
        period = self.args['clock-ns']

        sdc_lines += [
            ("create_clock -name {" + clock_name + "} -period {" + str(period) + "} [get_ports "
             "{" + clock_name + "}]")
        ]

        with open( sdc_file, 'w', encoding='utf-8' ) as fsdc:
            fsdc.write('\n'.join(sdc_lines))


class CommandBuildQuartus(CommandBuild, ToolQuartus):
    '''CommandBuildQuartus is a command handler for: eda build --tool=quartus'''

    def __init__(self, config: dict):
        CommandBuild.__init__(self, config)
        ToolQuartus.__init__(self, config=self.config)
        # add args specific to this tool
        self.args.update({
            'gui': False,
            'proj': False,
            'resynth': False,
            'reset': False,
            'add-tcl-files': [],
            'flow-tcl-files': [],
        })

    def do_it(self) -> None: # pylint: disable=too-many-branches,too-many-statements,too-many-locals
        # add defines for this job
        self.set_tool_defines()
        self.write_eda_config_and_args()

        # create FLIST
        flist_file = os.path.abspath(os.path.join(self.args['work-dir'], 'build.flist'))
        util.debug(f"CommandBuildQuartus: top={self.args['top']} target={self.target}",
                   f"design={self.args['design']}")

        command_list = [
            get_eda_exec('flist'), 'flist',
            '--no-default-log',
            '--tool=' + self.args['tool'],
            '--force',
            '--out=' + flist_file,
            '--no-quote-define',
            '--no-quote-define-value',
            '--no-escape-define-value',
            '--equal-define',
            '--bracket-quote-path',
            # Enhanced prefixes for better Quartus integration
            '--prefix-incdir=' + shlex.quote("set_global_assignment -name SEARCH_PATH "),
            '--prefix-define=' + shlex.quote("set_global_assignment -name VERILOG_MACRO "),
            '--prefix-sv=' + shlex.quote("set_global_assignment -name SYSTEMVERILOG_FILE "),
            '--prefix-v=' + shlex.quote("set_global_assignment -name VERILOG_FILE "),
            '--prefix-vhd=' + shlex.quote("set_global_assignment -name VHDL_FILE "),
            '--emit-rel-path',  # Use relative paths for better portability
        ]

        # create an eda.flist_input.f that we'll pass to flist:
        with open(os.path.join(self.args['work-dir'], 'eda.flist_input.f'),
                  'w', encoding='utf-8') as f:

            # defines
            for key,value in self.defines.items():
                if value is None:
                    f.write(f"+define+{key}\n")
                else:
                    f.write(shlex.quote(f"+define+{key}={value}") + "\n")

            # incdirs:
            for incdir in self.incdirs:
                f.write(f'+incdir+{incdir}\n')

            # files:
            f.write('\n'.join(self.files_v + self.files_sv + self.files_vhd + ['']))


        command_list.append('--input-file=eda.flist_input.f')



        # Write out a .sh command for debug
        command_list = util.ShellCommandList(command_list, tee_fpath='run_eda_flist.log')
        util.write_shell_command_file(dirpath=self.args['work-dir'], filename='run_eda_flist.sh',
                                      command_lists=[command_list], line_breaks=True)

        self.exec(work_dir=self.args['work-dir'], command_list=command_list,
                  tee_fpath=command_list.tee_fpath)

        if self.args['job-name'] == "":
            self.args['job-name'] = self.args['design']
        project_dir = 'project.' + self.args['job-name']

        # Create a simple Quartus build TCL script
        build_tcl_file = os.path.abspath(os.path.join(self.args['work-dir'], 'build.tcl'))
        build_tcl_lines = [
            '# Quartus Build Script',
            '',
            f'set Top {self.args["top"]}'
            '',
            'load_package flow',
            f'project_new {self.args["design"]} -overwrite',
            f'set_global_assignment -name FAMILY \"{self.args["family"]}\"',
            f'set_global_assignment -name DEVICE {self.args["part"]}',
            'set_global_assignment -name TOP_LEVEL_ENTITY "$Top"',
            '',
            '# Source the flist file',
            'source build.flist',
            '',
        ]

        # If we have additinal TCL files via --add-tcl-files, then source those too:
        if self.args['add-tcl-files']:
            build_tcl_lines.append('')
            build_tcl_lines.append('# Source TCL files from --add-tcl-files args')
            for fname in self.args['add-tcl-files']:
                fname_abs = os.path.abspath(fname)
                if not os.path.isfile(fname_abs):
                    self.error(f'add-tcl-files: "{fname_abs}"; does not exist')
                build_tcl_lines.append(f'source {Path(fname_abs).as_posix()}')
            build_tcl_lines.append('')

        # If we don't have any args for --flow-tcl-files, then use a default flow:
        if not self.args['flow-tcl-files']:
            build_tcl_lines.extend([
                '# Default flow for compile',
                'flng::run_flow_command -flow "compile"',
                ''
            ])
        else:
            build_tcl_lines.append('')
            build_tcl_lines.append('# Flow TCL files from --flow-tcl-files args')
            for fname in self.args['flow-tcl-files']:
                fname_abs = os.path.abspath(fname)
                if not os.path.isfile(fname_abs):
                    self.error(f'flow-tcl-files: "{fname_abs}"; does not exist')
                build_tcl_lines.append(f'source {Path(fname_abs).as_posix()}')
            build_tcl_lines.append('')

        with open(build_tcl_file, 'w', encoding='utf-8') as ftcl:
            ftcl.write('\n'.join(build_tcl_lines))

        # launch Quartus build, from work-dir:
        command_list_gui = [self.quartus_gui_exe, '-t', 'build.tcl', project_dir]
        command_list = [self.quartus_exe, '-t', 'build.tcl', project_dir]
        saved_qpf_filename = self.args["design"] + '.qpf'
        if not util.args['verbose']:
            command_list.append('-q')

        # Write out a .sh command for debug
        command_list = util.ShellCommandList(command_list, tee_fpath=None)
        util.write_shell_command_file(dirpath=self.args['work-dir'], filename='run_quartus.sh',
                                      command_lists=[command_list], line_breaks=True)
        util.write_shell_command_file(dirpath=self.args['work-dir'], filename='run_quartus_gui.sh',
                                      command_lists=[
                                          command_list_gui,
                                          # reopen when done.
                                          [self.quartus_gui_exe, saved_qpf_filename],
                                      ], line_breaks=True)

        # Add artifact tracking for build
        artifacts_search_paths = [
            self.args['work-dir'],
            os.path.join(self.args['work-dir'], 'output_files'),
        ]

        util.artifacts.add_extension(
            search_paths=artifacts_search_paths, file_extension='sof',
            typ='bitstream', description='Quartus SRAM Object File (bitstream)'
        )
        util.artifacts.add_extension(
            search_paths=artifacts_search_paths, file_extension='pof',
            typ='bitstream', description='Quartus Programmer Object File'
        )
        util.artifacts.add_extension(
            search_paths=artifacts_search_paths, file_extension='rpt',
            typ='text', description='Quartus Timing, Fitter, or other report'
        )
        util.artifacts.add_extension(
            search_paths=artifacts_search_paths, file_extension='summary',
            typ='text', description='Quartus Timing, Fitter, or other summary'
        )

        if self.args['stop-before-compile']:
            util.info(f"--stop-before-compile set: scripts in : {self.args['work-dir']}")
            return


        if self.args['gui'] and self.quartus_gui_exe:
            self.exec(
                work_dir=self.args['work-dir'], command_list=command_list_gui
            )
        else:
            self.exec(
                work_dir=self.args['work-dir'], command_list=command_list,
                tee_fpath=command_list.tee_fpath
            )
        if not os.path.isfile(os.path.join(self.args['work-dir'], saved_qpf_filename)):
            self.error('Saved project file does not exist:',
                       os.path.join(self.args['work-dir'], saved_qpf_filename))

        util.info(f"Build done, results are in: {self.args['work-dir']}")

        # Note: in GUI mode, if you ran: quaruts -t build.tcl, it will exit on completion,
        # so we'll re-open the project.
        if self.args['gui'] and self.quartus_gui_exe:
            self.exec(
                work_dir=self.args['work-dir'],
                command_list=[self.quartus_gui_exe, saved_qpf_filename]
            )


class CommandFListQuartus(CommandFList, ToolQuartus):
    '''CommandFListQuartus is a command handler for: eda flist --tool=quartus'''

    def __init__(self, config: dict):
        CommandFList.__init__(self, config=config)
        ToolQuartus.__init__(self, config=self.config)
        self.args.update({
            # synth/project style Flist, can't emit these:
            'emit-parameter': False,
            'emit-plusargs': False,
        })

    def get_flist_plusargs_list(self) -> list:
        '''Overriden from CommandFList.'''
        if self.args['unprocessed-plusargs']:
            util.warning('Command "flist" for --tool=quartus is not intended for simulation',
                         'and plusargs were present. They will NOT be included in the flist:',
                         f'{self.args["unprocessed-plusargs"]}')

        return []

    def get_flist_parameter_list(self) -> list:
        '''Overriden from CommandFList.'''
        if self.parameters:
            util.warning('Command "flist" for --tool=quartus is not intended for simulation',
                         'and parameters were present. They will NOT be included in the flist:',
                         f'{self.parameters}')

        return []


class CommandProjQuartus(CommandProj, ToolQuartus):
    '''CommandProjQuartus is a command handler for: eda proj --tool=quartus'''

    def __init__(self, config: dict):
        CommandProj.__init__(self, config)
        ToolQuartus.__init__(self, config=self.config)
        # add args specific to this tool
        self.args.update({
            'gui': True,
            'tcl-file': "proj.tcl",
        })
        self.args_help.update({
            'gui': 'Open Quartus in GUI mode (always True for proj)',
            'tcl-file': 'name of TCL file to be created for Quartus project',
        })

    def do_it(self):
        # add defines for this job
        self.set_tool_defines()
        self.write_eda_config_and_args()

        # create TCL
        tcl_file = os.path.abspath(os.path.join(self.args['work-dir'], self.args['tcl-file']))

        part = self.args['part']
        family = self.args['family']
        top = self.args['top']

        tcl_lines = [
            "# Quartus Project Creation Script",
            "load_package flow",
            f"project_new {top}_proj -overwrite",
            f"set_global_assignment -name FAMILY \"{family}\"",
            f"set_global_assignment -name DEVICE {part}",
            f"set_global_assignment -name TOP_LEVEL_ENTITY {top}",
        ]

        # Add source files, tcl prefers POSIX paths even in Windows Powershell.
        for f in self.files_v:
            rel_path = Path(os.path.relpath(f, self.args['work-dir'])).as_posix()
            tcl_lines.append(f"set_global_assignment -name VERILOG_FILE \"{rel_path}\"")
        for f in self.files_sv:
            rel_path = Path(os.path.relpath(f, self.args['work-dir'])).as_posix()
            tcl_lines.append(f"set_global_assignment -name SYSTEMVERILOG_FILE \"{rel_path}\"")
        for f in self.files_vhd:
            rel_path = Path(os.path.relpath(f, self.args['work-dir'])).as_posix()
            tcl_lines.append(f"set_global_assignment -name VHDL_FILE \"{rel_path}\"")

        # Add include directories
        for incdir in self.incdirs:
            tcl_lines.append(
                f"set_global_assignment -name SEARCH_PATH \"{Path(incdir).as_posix()}\""
            )

        # Add defines
        for key, value in self.defines.items():
            if value is None:
                tcl_lines.append(f"set_global_assignment -name VERILOG_MACRO \"{key}\"")
            else:
                tcl_lines.append(f"set_global_assignment -name VERILOG_MACRO \"{key}={value}\"")

        # Add constraints if available
        for sdc_file in self.files_sdc:
            tcl_lines.append(
                f"set_global_assignment -name SDC_FILE \"{Path(sdc_file).as_posix()}\""
            )

        tcl_lines += [
            "project_close",
            f"project_open {top}_proj"
        ]

        with open(tcl_file, 'w', encoding='utf-8') as ftcl:
            ftcl.write('\n'.join(tcl_lines))

        # execute Quartus in GUI mode
        command_list = [
            self.quartus_exe, '-t', tcl_file
        ]
        if not util.args['verbose']:
            command_list.append('-q')

        self.exec(self.args['work-dir'], command_list)
        util.info(f"Project created and opened in: {self.args['work-dir']}")


class CommandUploadQuartus(CommandUpload, ToolQuartus):
    '''CommandUploadQuartus is a command handler for: eda upload --tool=quartus'''

    SUPPORTED_BIT_EXT = ['.sof']

    def __init__(self, config: dict):
        CommandUpload.__init__(self, config)
        ToolQuartus.__init__(self, config=self.config)
        # add args specific to this tool
        self.args.update({
            'cable': "1",
            'device': "1",
            'list-cables': False,
            'list-devices': False,
        })
        self.args_help.update({
            'cable': 'Cable number to use for programming',
            'device': 'Device number on the cable',
            'list-cables': 'List available programming cables',
            'list-devices': 'List available devices on cable',
        })

        # Support mulitple arg keys for bitfile and list-bitfiles, so
        # --sof-file and --list-sof-files work the same.
        self.args_args.update({
            'bitfile': ['sof-file'],
            'list-bitfiles': ['list-sof-files'],
        })
        self.args_help.update({
            'bitfile': 'SOF file to upload (auto-detected if not specified)',
            'list-bitfiles': 'List available SOF files',
        })


    def do_it(self):  # pylint: disable=too-many-branches,too-many-statements,too-many-locals
        '''
        Note this is called directly by opencos.commands.CommandUpload, based
         on which bitfile(s) were found, or if --tool=quartus was set

        We do not need to handle --list-bitfiles, was handled by CommandUpload.
        '''

        # add defines for this job
        self.set_tool_defines()
        self.write_eda_config_and_args()

        # Find quartus_pgm executable, we'll want the one from the same path
        # that was used for our self._EXE (ToolQuartus).
        quartus_pgm = safe_shutil_which(os.path.join(self.quartus_base_path, 'quartus_pgm'))
        if not quartus_pgm:
            self.error("quartus_pgm not found in PATH")
            return

        # Handle --list-cables
        if self.args['list-cables']:
            util.info("Listing available cables...")
            command_list = [quartus_pgm, '--auto']
            _, stdout, _ = self.exec(self.args['work-dir'], command_list)
            util.info("Available cables listed above")
            return

        sof_file = None
        if self.args['bitfile']:
            if os.path.isfile(self.args['bitfile']):
                sof_file = self.args['bitfile']

        # self.bitfiles was already set by CommandUpload.process_tokens()
        if len(self.bitfiles) == 1:
            sof_file = self.bitfiles[0]

        # Auto-discover SOF file if not specified
        if not sof_file:
            # CommandUpload already displayed them, and exited on --list-bitfiles.
            if len(self.bitfiles) > 1:
                self.error("Multiple SOF files found, please specify --sof-file or --bitfile",
                           "or use a different search pattern")
                return

            self.error("No SOF files found")
            return

        util.info(f"Programming with SOF file: {sof_file}")


        # Execute Quartus programmer
        # Format: quartus_pgm -c <cable> -m jtag -o "p;<sof_file>@<device>"
        cable = self.args['cable']
        device = self.args['device']
        operation = f"p;{sof_file}@{device}"

        command_list = [
            quartus_pgm, '-c', cable, '-m', 'jtag', '-o', operation
        ]

        _, stdout, _ = self.exec(self.args['work-dir'], command_list)

        # Do some log scraping
        for line in stdout.split('\n'):
            if any(x in line for x in ('Warning', 'WARNING')):
                self.tool_warning_count += 1
            elif any(x in line for x in ('Error', 'ERROR')):
                self.tool_error_count += 1

        self.report_tool_warn_error_counts()
        self.report_pass_fail()

        util.info("Upload operation completed")


class CommandOpenQuartus(CommandOpen, ToolQuartus):
    '''CommandOpenQuartus is a command handler for: eda open --tool=quartus'''

    def __init__(self, config: dict):
        CommandOpen.__init__(self, config)
        ToolQuartus.__init__(self, config=self.config)
        # add args specific to this tool
        self.args.update({
            'file': "",
            'gui': True,
        })
        self.args_help.update({
            'file': 'Quartus project file (.qpf) to open (auto-detected if not specified)',
            'gui': 'Open Quartus in GUI mode (always True for open)',
        })

    def do_it(self):
        if not self.args['file']:
            util.info("Searching for Quartus project...")
            found_file = False
            all_files = []
            for root, _, files in os.walk("."):
                for file in files:
                    if file.endswith(".qpf"):
                        found_file = os.path.abspath(os.path.join(root, file))
                        util.info(f"Found project: {found_file}")
                        all_files.append(found_file)
            self.args['file'] = found_file
            if len(all_files) > 1:
                all_files.sort(key=os.path.getmtime)
                self.args['file'] = all_files[-1]
                util.info(f"Choosing: {self.args['file']} (newest)")

        if not self.args['file']:
            self.error("Couldn't find a QPF Quartus project to open")

        projdir = os.path.dirname(self.args['file'])

        command_list = [
            self.quartus_exe, self.args['file']
        ]

        self.write_eda_config_and_args()
        self.exec(projdir, command_list)
        util.info(f"Opened Quartus project: {self.args['file']}")
