# -*- coding: utf-8 -*-
"""
    Custom GDB command for enabling default HW breakpoints.

 Copyright 2024-2025 NXP

 NXP Confidential and Proprietary. This software is owned or controlled by NXP
 and may only be used strictly in accordance with the applicable license terms.
 By expressly accepting such terms or by downloading, installing, activating
 and/or otherwise using the software, you are agreeing that you have read, and
 that you agree to comply with and are bound by, such license terms. If you do
 not agree to be bound by the applicable license terms, then you may not retain,
 install, activate or otherwise use the software.
"""

import sys
import gdb  # @UnresolvedImport  # pylint: disable=F0401
import os
import re
CURRENT_DIR = "".join([os.path.dirname(os.path.realpath(__file__)), "/"])
sys.path.insert(0, CURRENT_DIR)
sys.path.insert(0, CURRENT_DIR + "/../../utils")

import mem_reg_utils as utils
from flash.scripts.elfparser import ElfFile, SectionType, SectionFlag
from mem_region import MemRegion


_INVALID_REGION_ID = -1

class BreakConfigCommand(gdb.Command):
    '''
    Configure the type of breakpoints that gdb use. 
    Available mode: onchip (marks whether they are on-chip (HW BP) or not)
                    - Sets the default type of breakpoints that are set to be HW by making the code area
                      read only for the user. (and restoring it at the end).

    usage (case-sensitive): break_config onchip on/off
    '''

    ###########################################################################
    def __init__(self):
        super(BreakConfigCommand, self).__init__("break_config", gdb.COMMAND_USER)
        # list of created read only regions by this command
        self._ro_regions = []
        # list of deleted regions by this commmand
        self._overlap_regions = []
        # execution mode of break_config onchip command on-True / off-False 
        self._is_break_config_onchip_mode = False
        # hardware-breakpoint-limit of the current debug session
        self._hardware_breakpoint_limit = -1
        # mark if breakpoint always inserted mode is set by this command
        self._is_always_inserted_on = False
    
    ###########################################################################
    def invoke(self, arguments, from_tty):
        self.dont_repeat()
        argv = gdb.string_to_argv(arguments)
        self._parse_break_config_args(argv)
        if self._is_break_config_onchip_mode:
            self._prepare_command_on()
            gdb.events.breakpoint_created.connect(self.on_breakpoint_created)
            self._run_break_config_onchip_mode_on()
        else:
            self._prepare_command_off()
            gdb.events.breakpoint_created.disconnect(self.on_breakpoint_created)
            self._run_break_config_onchip_mode_off()
        self._write_status_msg()


    ###########################################################################
    def on_breakpoint_created(self, breakpoint):
        if len(gdb.breakpoints()) > self._hardware_breakpoint_limit:
            gdb.write(
                "[S32Debugger]Warning: Too many breakpoints inserted, maximum is "
                  + str(self._hardware_breakpoint_limit) + ".\n" + 
                  "The execution flow may be interrupted or some gdb commands"
                  " will not be executed.\n")

    ###########################################################################
    def _prepare_command_on(self):
        # 1. Check for maximum number of hwbp
        cmd = "show remote hardware-breakpoint-limit"
        cmd_output = utils.gdb_exec(cmd, False, True).unwrap()
        cmd_output_v = cmd_output.split()
        # last element from the list without '.' character
        self._hardware_breakpoint_limit = int(cmd_output_v[-1][:-1])

        if len(gdb.breakpoints()) > self._hardware_breakpoint_limit:
            self._is_break_config_onchip_mode = False
            error_msg = ("[S32Debugger]Error: Cannot enter into default hardware"
                        " breakpoint mode.\nToo many breakpoints already set,"
                        " maximum is " + str(self._hardware_breakpoint_limit) + "."
                        " Try again when the number of breakpoints is less or"
                        " equal than maximum accepted.")
            raise gdb.GdbError(error_msg)
        
        # 2. Check if the breakpoints are set on the target directly, if not force it.
        cmd = "show breakpoint always-inserted"
        cmd_output = utils.gdb_exec(cmd, False, True).unwrap()
        output_always_inserted_off = "Always inserted breakpoint mode is off."
        if output_always_inserted_off in cmd_output:
            cmd_set_inserted_on = "set breakpoint always-inserted on"
            utils.gdb_exec(cmd_set_inserted_on, False, True).unwrap()
            self._is_always_inserted_on = True

    ###########################################################################
    def _prepare_command_off(self):
        # Restores always-inserted only if it was written by this command
        if self._is_always_inserted_on:
            cmd_set_inserted_on = "set breakpoint always-inserted off"
            utils.gdb_exec(cmd_set_inserted_on, False, True).unwrap()
            self._is_always_inserted_on = False


    ###########################################################################
    # Restore the changes that the command makes
    # - Delete the Read Only regions that were created by this command
    # - Restores the original regions that were deleted by this command 
    # with their original attributes
    def _run_break_config_onchip_mode_off(self):
        self._delete_ro_regions()
        self._restore_overlap_regions()
    
    ###########################################################################
    def _run_break_config_onchip_mode_on(self):
        progspace = gdb.current_progspace()
        for obj in progspace.objfiles():
            code_sections = self._read_elf_code_sections(obj.filename)
            if len(code_sections) == 0:
                error_msg = ("[S32Debugger]Error: No section with executable code detected in " +
                             obj.filename + "\n"
                             "Check if your code section has the executable machine" 
                             " instruction flag set, i.e. SHF_EXECINSTR")
                raise gdb.GdbError(error_msg)
                      
            # In general there is only .text section which contains instructions to run.
            # However, a collection is considered here for the case when the user adds
            # multiple code areas at different addresses.
            for code_section in code_sections:
                code_start = code_section.addr
                code_end = code_section.addr + code_section.size
                self._change_code_section_as_read_only(code_start, code_end)

    ###########################################################################
    def _parse_break_config_args(self, argv):

        if (len(argv) != 1) and (len(argv) != 2):
            error_msg = (
                "[S32Debugger]Error: break_config command supports only one"
                " mode (which is mandatory to be placed), i.e. onchip.\n"
                "Run help break_config for more details.")
            raise gdb.GdbError(error_msg)
        
        if argv[0] != 'onchip':
            error_msg = ("[S32Debugger]Error: invalid mode of use for this command\n"
                         "The only supported mode is onchip, i.e. break_config onchip on/off.\n"
                         "Run help break_config for more details.")
            raise gdb.GdbError(error_msg)

        if len(argv) == 1:
            bc_onchip_status = "on" if self._is_break_config_onchip_mode else "off"
            error_msg = (
                "[S32Debugger]: break_config onchip mode is currently " + bc_onchip_status)
            raise gdb.GdbError(error_msg)

        if argv[1] == 'on':
            if self._is_break_config_onchip_mode:
                error_msg = ("[S32Debugger]Error: break_config onchip is already on")
                raise gdb.GdbError(error_msg)
            self._is_break_config_onchip_mode = True
            return
        
        if argv[1] == 'off':
            if not self._is_break_config_onchip_mode:
                error_msg = ("[S32Debugger]Error: break_config onchip is already off.")
                raise gdb.GdbError(error_msg)
            self._is_break_config_onchip_mode = False
            return
        
        error_msg = (
            "[S32Debugger]Error: wrong input argument, the possible variants are"
            " (case sensitive): \n"
            "- break_config onchip on \n"
            "- break_config onchip off \n")
        raise gdb.GdbError(error_msg)

    
    ###########################################################################
    def _change_code_section_as_read_only(self, text_start, text_end):

        # (text_start ... text_end) - as ro
        text_start_hex = hex(text_start).upper().replace('X', 'x')
        text_end_hex = hex(text_end).upper().replace('X', 'x')
        self._ro_regions.append(MemRegion(
            'y', text_start_hex, text_end_hex, 'ro'))           

        gdb_info_mem_regs = self._get_infomem_regions()
        for region in gdb_info_mem_regs:
            region_start = region.get_region_low_addr()
            region_end = region.get_region_high_addr()

            if (region_start >= text_end) or (region_end <= text_start):
                continue

            # overlap detected
            self._overlap_regions.append(region)
            if region_start < text_start:
                # (region_start ... text_start) ... [ ..
                new_reg_start = hex(region_start).upper().replace('X', 'x')
                new_reg_end = hex(text_start).upper().replace('X', 'x')
                new_reg_enabled = region.get_region_enabled()
                new_reg_attr = region.get_region_attributes()
                self._ro_regions.append(MemRegion(
                    new_reg_enabled, new_reg_start, new_reg_end, new_reg_attr))
                
            if region_end > text_end:
                # ... ] .. (text_end ... region end)
                new_reg_start = hex(text_end).upper().replace('X', 'x')
                new_reg_end = hex(region_end).upper().replace('X', 'x')
                new_reg_enabled = region.get_region_enabled()
                new_reg_attr = region.get_region_attributes()
                self._ro_regions.append(MemRegion(
                    new_reg_enabled, new_reg_start, new_reg_end, new_reg_attr))                    

        self._delete_overlap_regions()
        self._create_ro_regions()
    
    ########################################################################### 
    def _delete_overlap_regions(self):
        for region in self._overlap_regions:
            region_id = region.get_region_id()
            del_cmd = 'delete mem {0}'.format(region_id)
            global _INVALID_REGION_ID
            if region_id is _INVALID_REGION_ID:
                error_msg = ("[S32Debugger]Error: Internal error.\n"
                             "Cannot execute : " + del_cmd)
                raise gdb.GdbError(error_msg)

            gdb.execute(del_cmd)

    def _restore_overlap_regions(self):
        for region in self._overlap_regions:
            reg_start = hex(region.get_region_low_addr()).upper().replace('X', 'x')
            reg_end = hex(region.get_region_high_addr()).upper().replace('X', 'x')
            attr = region.get_region_attributes()
            create_cmd = 'mem {0} {1} {2}'.format(reg_start, reg_end, attr)
            gdb.execute(create_cmd)

        # clear regions that overlapped
        # Note:  clear() api can be also used,
        # for backward compatibility with Python 2.7 we use []
        self._overlap_regions = []

    ###########################################################################            
    def _create_ro_regions(self):
        for region in self._ro_regions:
            reg_start = hex(region.get_region_low_addr()).upper().replace('X', 'x')
            reg_end = hex(region.get_region_high_addr()).upper().replace('X', 'x')
            attr = region.get_region_attributes()
            create_cmd = 'mem {0} {1} {2}'.format(reg_start, reg_end, attr)
            gdb.execute(create_cmd)

    def _delete_ro_regions(self):
        infommem_regs =  self._get_infomem_regions()
        for reg_to_delete in self._ro_regions:
            infommem_reg_to_delete = next(
                (infommem_reg for infommem_reg in infommem_regs 
                 if (infommem_reg.get_region_high_addr() == reg_to_delete.get_region_high_addr()) and 
                 (infommem_reg.get_region_low_addr() == reg_to_delete.get_region_low_addr())),
                None)
            
            if infommem_reg_to_delete:
                region_id = infommem_reg_to_delete.get_region_id()
                del_cmd = 'delete mem {0}'.format(region_id)
                gdb.execute(del_cmd)
        
        # clear ro regions
        # Note:  clear() api can be also used,
        # for backward compatibility with Python 2.7 we use []
        self._ro_regions = []

    ###########################################################################
    def _read_elf_code_sections(self, elf_path):
            with ElfFile(elf_path) as elf_file:
                code_sections = []
                for sect_header in elf_file.iter_section_headers():
                    if (sect_header.sec_type is SectionType.SHT_PROGBITS) and \
                        ((sect_header.flags & SectionFlag.SHF_EXECINSTR) != 0):
                        code_sections.append(sect_header)
                
                return code_sections                        

    ###########################################################################
    # Used to parse regions from GDB "info mem" command
    def _get_infomem_regions(self):
        regions_info_mem = []
        cmd_output = utils.gdb_exec("info mem", False, True).unwrap()
        for line in cmd_output.split("\n"):
            entry = line.split()[0] if len(line.split()) != 0 else None
            if entry != None and re.match("\d+", entry):
                subb = line.split()
                enabled = subb[1]
                low_addr =subb[2]
                high_addr = subb[3]
                attr = subb[4]
                reg_id = subb[0]
                regions_info_mem.append(MemRegion(enabled, low_addr, high_addr, attr, reg_id))
        
        return regions_info_mem
    
    ###########################################################################
    # Log the status message of the command
    def _write_status_msg(self):
        bc_onchip_status = "on" if self._is_break_config_onchip_mode else "off"
        status_msg = (
                "[S32Debugger]: break_config onchip mode is now " +
                bc_onchip_status + "." + os.linesep)
        gdb.write(status_msg)
        
        if self._is_break_config_onchip_mode:
            gdb.write(
                "Code section area is set to read only (RO).\n")
        else:
            gdb.write(
                "Code section area is restored to the initial state.\n")