# Copyright 2022 NXP
# SPDX-License-Identifier: Apache-2.0

'''Runner for Lauterbach TRACE32.'''

import argparse
import os
import platform
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import List, Optional

from runners.core import BuildConfiguration, RunnerCaps, RunnerConfig, ZephyrBinaryRunner

DEFAULT_T32_CONFIG = Path('config.t32')


class TRACE32BinaryRunner(ZephyrBinaryRunner):
    '''
    Runner front-end for Lauterbach TRACE32.

    The runner is a wrapper around Lauterbach TRACE32 PowerView. It executes a Lauterbach Practice
    script (.cmm) after launching the debugger, which should be located at
    zephyr/boards/<board>/support/<command>.cmm, where <board> is the board directory and <command>
    is the name of the west runner command executed (e.g. flash or debug). Extra arguments can be
    passed to the startup script by using the command line option --startup-args.
    '''

    def __init__(self,
                 cfg: RunnerConfig,
                 t32_cfg: Path,
                 arch: str,
                 startup_args: Optional[List[str]] = None,
                 timeout: int = 60) -> None:
        super(TRACE32BinaryRunner, self).__init__(cfg)
        self.arch = arch
        self.t32_cfg = t32_cfg
        self.t32_exec: Optional[Path] = None
        self.startup_dir = Path(cfg.board_dir) / 'support'
        self.startup_args = startup_args
        self.timeout = timeout

    @classmethod
    def name(cls) -> str:
        return 'trace32'

    @classmethod
    def capabilities(cls) -> RunnerCaps:
        return RunnerCaps(commands={'flash', 'debug'})

    @classmethod
    def do_add_parser(cls, parser: argparse.ArgumentParser) -> None:
        parser.add_argument('--arch',
                            default='auto',
                            choices=('auto', 'arm', 'riscv', 'xtensa'),
                            help='Target architecture. Set to "auto" to select the architecture '
                                 'based on CONFIG_ARCH value')
        parser.add_argument('--config',
                            default=DEFAULT_T32_CONFIG,
                            type=Path,
                            help='Override TRACE32 configuration file path. Can be a relative path '
                                 'to T32_DIR environment variable, or an absolute path')
        parser.add_argument('--startup-args',
                            nargs='*',
                            help='Arguments to pass to the start-up script')
        parser.add_argument('--timeout',
                            default=60,
                            type=int,
                            help='Timeout, in seconds, of the flash operation')

    @classmethod
    def do_create(cls, cfg: RunnerConfig, args: argparse.Namespace) -> 'TRACE32BinaryRunner':
        build_conf = BuildConfiguration(cfg.build_dir)
        if args.arch == 'auto':
            arch = build_conf.get('CONFIG_ARCH').replace('"', '')
            # there is a single binary for all ARM architectures
            arch = arch.replace('arm64', 'arm')
        else:
            arch = args.arch
        return TRACE32BinaryRunner(cfg, args.config, arch, startup_args=args.startup_args,
                                   timeout=args.timeout)

    def do_run(self, command, **kwargs) -> None:
        t32_dir = os.environ.get('T32_DIR')
        if not t32_dir:
            raise RuntimeError('T32_DIR environment variable undefined')

        if platform.system() == 'Windows':
            os_name = 'windows64'
            suffix = '.exe'
        elif platform.system() == 'Linux':
            os_name = 'pc_linux64'
            suffix = ''
        else:
            raise RuntimeError('Host OS not supported by this runner')

        self.t32_exec = Path(t32_dir) / 'bin' / os_name / f't32m{self.arch}{suffix}'
        if not self.t32_exec.exists():
            raise RuntimeError(f'Cannot find Lauterbach executable at {self.t32_exec}')

        if not self.t32_cfg.is_absolute():
            self.t32_cfg = Path(t32_dir) / self.t32_cfg
        if not self.t32_cfg.exists():
            raise RuntimeError(f'Cannot find Lauterbach configuration at {self.t32_cfg}')

        startup_script = self.startup_dir / f'{command}.cmm'
        if not startup_script.exists():
            raise RuntimeError(f'Cannot find start-up script at {startup_script}')

        if command == 'flash':
            self.flash(**kwargs)
        elif command == 'debug':
            self.debug(**kwargs)

    def flash(self, **kwargs) -> None:
        with TemporaryDirectory(suffix='t32') as tmp_dir:
            # use a temporary config file, based on the provided configuration,
            # to hide the TRACE32 software graphical interface
            cfg_content = f'{self.t32_cfg.read_text()}\n\nSCREEN=OFF\n'
            tmp_cfg = Path(tmp_dir) / DEFAULT_T32_CONFIG.name
            tmp_cfg.write_text(cfg_content)

            cmd = self.get_launch_command('flash', cfg=tmp_cfg)
            self.logger.info(f'Launching TRACE32: {" ".join(cmd)}')
            try:
                self.check_call(cmd, timeout=self.timeout)
                self.logger.info('Finished')
            except subprocess.TimeoutExpired:
                self.logger.error(f'Timed out after {self.timeout} seconds')

    def debug(self, **kwargs) -> None:
        cmd = self.get_launch_command('debug')
        self.logger.info(f'Launching TRACE32: {" ".join(cmd)}')
        self.check_call(cmd)

    def get_launch_command(self, command_name: str,
                           cfg: Optional[Path] = None) -> List[str]:
        cmd = [
            str(self.t32_exec),
            '-c', str(cfg if cfg else self.t32_cfg),
            '-s', str(self.startup_dir / f'{command_name}.cmm')
        ]
        if self.startup_args:
            cmd.extend(self.startup_args)
        return cmd
