# Copyright 2017-2025 NXP
# SPDX-License-Identifier: BSD-3-Clause

import gdb
import sys
import os
import mem_reg_utils as utils
from errors import Result, Error, ErrorCode, Ok, exceptcall
from mem_reg_utils import gdb_exec
from enum import Enum
import traceback
import inspect

CURRENT_DIR = "".join([os.path.dirname(os.path.realpath(__file__)), "/"])
# in case an extension is failing to load/for dev environment
try:
    sys.path.insert(0, CURRENT_DIR + "../gdb_extensions")
    import register_ds_extensions
except:
    gdb.write("Warning: Loading of extensions failed")
    traceback.print_exc()


# Reports error if set log to CONS more than once.
def set_log():
    try:
        gdb.execute("monitor log set category-threshold protocol DEBUG")
        gdb.execute("monitor log destination console CONS out")
        gdb.execute("monitor log set category-destination protocol CONS")
    except:
        gdb.write("Warning: ...Log is set already\n")  # not a critical error


# GDB settings that will be applied to every session.
# Add more settings here if needed.
def configure_gdb(non_stop_mode):
    if non_stop_mode:
        gdb.execute("set pagination off")
        gdb.execute("set non-stop on")
    else:
        gdb.execute("set non-stop off")


# Call for connect with substitute defaults for omitted parameters
def establish_connection(
    ctx,
    probe_ip=None,
    jtag=None,
    port=45000,
    timeout=30,
    log=False,
    preinit_callback_fn=None,
    auth_type=None,
    secure_dbg_pwd=None,
    target_lifecycle=None,
    ccs_port=41475,
    ccs_ip="127.0.0.1"
):
    non_stop_mode = False
    try:
        stack = inspect.stack()
        for item in stack:
            if "_NON_STOP_MODE" in item.frame.f_globals and item.frame.f_globals["_NON_STOP_MODE"]:
                non_stop_mode = True
    except:
        pass

    if probe_ip is None and jtag is None:
        gdb.write("Secondary session connecting...\n")
        return connect(
            ctx,
            None,
            None,
            port,
            timeout,
            log,
            preinit_callback_fn,
            auth_type,
            secure_dbg_pwd,
            target_lifecycle,
            ccs_port,
            ccs_ip,
            non_stop_mode
        )
    elif probe_ip is not None and jtag is not None:
        gdb.write("Primary session connecting...\n")
        return connect(
            ctx,
            probe_ip,
            jtag,
            port,
            timeout,
            log,
            preinit_callback_fn,
            auth_type,
            secure_dbg_pwd,
            target_lifecycle,
            ccs_port,
            ccs_ip,
            non_stop_mode
        )
    else:
        return Result(Error(ErrorCode.ConnectError, "Unknown type of connect scenario"))


# open RSP session. all 'monitor' commands are active after that
@exceptcall(ErrorCode.ConnectError)
def connect(
    ctx,
    probe_ip,
    jtag,
    port=45000,
    timeout=30,
    log=False,
    preinit_callback_fn=None,
    auth_type=None,
    secure_dbg_pwd=None,
    target_lifecycle=None,
    ccs_port=41475,
    ccs_ip="127.0.0.1",
    non_stop_mode=False,
):
    configure_gdb(non_stop_mode)
    gdb.execute("target extended-remote 127.0.0.1:%d" % port)
    gdb.execute("set remotetimeout %d" % timeout)

    if log:
        set_log()

    set_context(ctx)
    # parameters may be omitted for secondary session
    if probe_ip is not None:
        gdb.execute("monitor ctx id :ccs set prop probe %s" % _probe_addr(probe_ip))
    if jtag is not None:
        gdb.execute("monitor ctx id :ccs set prop jtag_speed %d" % jtag)
    gdb.execute("monitor ctx id :ccs set prop ccs_timeout %d" % timeout)
    
    gdb.execute(
        "monitor ctx id :ccs set prop ccs_host:port  %s:%d" % (ccs_ip, ccs_port)
    )

    # unlock target
    if auth_type is not None:
        # unlock is critical. if we don't exit immediately,
        # jtag state machine will be corrupted by init sequence

        # really ugly, but avoids changes in *ALL* NPI scripts
        target_core = None
        debug_signal_map = None
        key_index = None
        debug_card_path = None
        try:
            stack = inspect.stack()
            for item in stack:
                # Python 2/3 compatibility: handle different stack frame structures
                frame = item.frame if hasattr(item, 'frame') else item[0]
                frame_globals = frame.f_globals
                
                if "_CORE_NAME" in frame_globals and frame_globals["_CORE_NAME"]:
                    target_core = frame_globals["_CORE_NAME"]
                if "_DEBUG_SIGNAL_MAP" in frame_globals and frame_globals["_DEBUG_SIGNAL_MAP"]:
                    debug_signal_map = frame_globals["_DEBUG_SIGNAL_MAP"]
                if "_KEY_INDEX" in frame_globals and frame_globals["_KEY_INDEX"]:
                    key_index = frame_globals["_KEY_INDEX"]
                if "_DEBUG_CARD" in frame_globals and frame_globals["_DEBUG_CARD"]:
                    debug_card_path = frame_globals["_DEBUG_CARD"]
        except:
            pass

        debug_auth_target_core = None
        if "FSS" in target_core:
            debug_auth_target_core = "FSS"
        elif "HSE" in target_core:
            debug_auth_target_core = "HSE"
        else:
            debug_auth_target_core = "APP"

        nextGen = ["S32N", "S32K5"]
        if any(x in target_core for x in nextGen) and debug_auth_target_core == "APP":
            authorize_owner_debug(
                password=secure_dbg_pwd,
                key_index=key_index,
                debug_card_path=debug_card_path
            ).unwrap()
        else:
            target_unlock(auth_type, secure_dbg_pwd, target_lifecycle, debug_auth_target_core, debug_signal_map).unwrap()

    if preinit_callback_fn is not None:
        preinit_callback_fn()
    
    gdb_exec("monitor ctx connect", from_tty=True, errcode=ErrorCode.ConnectError).unwrap()


@exceptcall(ErrorCode.ConnectError)
def connect_remote(ctx, probe, jtag, gta, timeout):
    gdb.execute("target extended-remote %s" % gta)
    gdb.execute("set remotetimeout %d" % timeout)

    set_context(ctx)

    gdb.execute("monitor ctx id :ccs set prop probe %s" % probe)
    gdb.execute("monitor ctx id :ccs set prop jtag_speed %d" % jtag)
    gdb.execute("monitor ctx id :ccs set prop ccs_timeout %d" % timeout)
    gdb_exec("monitor ctx connect").unwrap()

@exceptcall(ErrorCode.ConnectError)
def establish_linkserver_connection(ctx, probe_id="linkserver#1", log = False, preinit_callback_fn=None, port=45000, timeout=30):
    gdb.execute("target extended-remote 127.0.0.1:%d" % port)
    gdb.execute("set remotetimeout %d" % timeout)

    if log:
        set_log()
    set_context(ctx)

    if probe_id is not None:
        gdb.write("Primary session connecting...\n")
        gdb.execute("monitor ctx id :ccs set prop probe %s" % probe_id)
    else:
        gdb.write("Secondary session connecting...\n")
    if preinit_callback_fn is not None:
        preinit_callback_fn()
    
    gdb_exec("monitor ctx connect", from_tty=True, errcode=ErrorCode.ConnectError).unwrap()

@exceptcall(ErrorCode.TargetUnlockError)    
def target_unlock(auth_type, password=None, lifecycle=None, target_core=None, debug_signal_map=None, probe_ip=None, jtag=None):

    AUTH_ALGS = {"PWD": 0, "CR": 1}
    if probe_ip is not None:
        gdb.execute("monitor ctx id :ccs set prop probe %s" % probe_ip)
    if jtag is not None:
        gdb.execute("monitor ctx id :ccs set prop jtag_speed %d" % jtag)
    # IDE uses these constants for secure debug auth types: 'CR' or 'PWD'
    # in the monitor command we must map 'CR' to constant 1 and 'PWD' to 0
    try:
        auth_alg = AUTH_ALGS[auth_type]
    except KeyError:
        return Result(
            Error(ErrorCode.InvalidSecureDbgParam, "Invalid parameter for secure debug")
        )
    if password is None:
        return Result(Error(ErrorCode.InvalidSecureDbgParam, "Missing password"))
    
    gdb_exec(
        "monitor security unlock %s %s %s %s %s"
        % (
            auth_alg,
            password,
            lifecycle if lifecycle else "OEM_PROD",
            target_core if target_core else "APP",
            debug_signal_map if debug_signal_map else "0xA"
        ), errcode=ErrorCode.TargetUnlockError
    ).unwrap()

@exceptcall(ErrorCode.TargetUnlockError)
def authorize_owner_debug(password, key_index, debug_card_path, probe_ip=None, jtag=None):
    if probe_ip is not None:
        gdb.execute("monitor ctx id :ccs set prop probe %s" % probe_ip)
    if jtag is not None:
        gdb.execute("monitor ctx id :ccs set prop jtag_speed %d" % jtag)

    if password is None:
        return Result(Error(ErrorCode.InvalidSecureDbgParam, "Missing password"))
    if not key_index:
        return Result(Error(ErrorCode.InvalidSecureDbgParam, "Missing key index"))
    if not debug_card_path:
        return Result(Error(ErrorCode.InvalidSecureDbgParam, "Missing debug card path"))
    
    gdb_exec(
        "monitor security owner_debug_auth %s %s %s"
        % (
            key_index,
            password,
            debug_card_path
        ), errcode=ErrorCode.TargetUnlockError
    ).unwrap()

# context may be taken by neighbour session so be prepared for errors
@exceptcall(ErrorCode.ContextError)
def set_context(ctx):
    gdb_exec("monitor ctx set current " + ctx)


@exceptcall(ErrorCode.ContextError)
def get_context():
    return Result(Ok(gdb.execute("monitor ctx get current", False, True)))


# shut RSP session. Do not call 'monitor ctx disconnect' it may corrupt neighbour session
@exceptcall(ErrorCode.UndefinedError)
def close_session():
    gdb.execute("monitor log clear all")
    gdb.execute("disconnect")


@exceptcall(ErrorCode.UndefinedError)
def reset(timeout):
    gdb.execute("monitor template config 4 %d" % timeout)
    gdb.execute("monitor reset run")


@exceptcall(ErrorCode.UndefinedError)
def reset_debug(timeout):
    gdb.execute("monitor template config 4 %d" % timeout)
    gdb.execute("monitor reset debug")


class CoreMode(Enum):
    Debug = 1
    Execute = 2
    Reset = 3
    Sleep = 4
    Doze = 5
    Nap = 6
    Disabled = 7
    DebugPowerDown = 8
    DapPowerDown = 9
    WaitState = 10
    DeepSleep = 11
    Freeze = 12
    Undefined = 13


_dict = {
    "debug": CoreMode.Debug,
    "execute": CoreMode.Execute,
    "reset": CoreMode.Reset,
    "sleep": CoreMode.Sleep,
    "doze": CoreMode.Doze,
    "nap": CoreMode.Nap,
    "disabled": CoreMode.Disabled,
    "undefined": CoreMode.Undefined,
    "unknown state": CoreMode.Undefined,
    "debug_power_down": CoreMode.DebugPowerDown,
    "dap_power_down": CoreMode.DapPowerDown,
    "wait": CoreMode.WaitState,
    "deep_sleep": CoreMode.DeepSleep,
    "freeze": CoreMode.Freeze,
}


@exceptcall(ErrorCode.UndefinedError)
def get_core_mode():
    reply = gdb.execute("monitor core-mode", False, True)
    core_state = reply[reply.rfind(":") + 1 :].strip().lower() 
    return Result(Ok(_dict.get(core_state, CoreMode.Undefined)))

@exceptcall(ErrorCode.UndefinedError)
def stop_core():
    mode = get_core_mode().unwrap()
    if mode is CoreMode.Execute:
        gdb.execute("monitor halt")
    elif mode is CoreMode.Undefined:
        error_msg = (
            "Core can't be stopped. State is undefined/unknown. "
            "Possible causes: "
            "another debug session already started on different GDB server, "
            "CCS connection corrupted OR CCS API reported an unknown core state for the current implementation, "
            "board is powered down."
        )        
        return Result(Error(ErrorCode.CoreError, error_msg))        
    elif mode is not CoreMode.Debug:
        return Result(
            Error(ErrorCode.CoreError, "Core can't be stopped. State: %s" % mode)
        )

    return Result(Ok(None))


@exceptcall(ErrorCode.UndefinedError)
def run_core():
    mode = get_core_mode().unwrap()
    if mode is CoreMode.Debug:
        gdb.execute("monitor run")
    elif mode is not CoreMode.Execute:
        return Result(
            Error(ErrorCode.CoreError, "Core can't be runned. State: %s" % mode)
        )

    return Result(Ok(None))


# deprecated: made a reset for whole SoC
@exceptcall(ErrorCode.UndefinedError)
def reset_core_debug():
    try:
        gdb.execute("mon reset debug")
    except BaseException as e:
        return Result(Error(ErrorCode.UndefinedError, str(e)))
    return Result(Ok(None))


@exceptcall(ErrorCode.UndefinedError)
def attach():
    mode = get_core_mode().unwrap()
    if mode is CoreMode.Debug:
        gdb_exec("attach 1", False, False).unwrap()
    else:
        return Result(
            Error(
                ErrorCode.AttachError,
                "Can not attach wrong core state. State: %s" % mode,
            )
        )


def _probe_addr(ip):
    if ip.startswith("s32dbg"):
        return ip
    elif ip.startswith("cmsisdap"):
        return "cmsisdap"
    elif ip.startswith("ccssim"):
        return ip
    else:
        return "s32dbg:" + ip
    
# This function reads the AXI AP configuration register to determine if the
# Access Port supports long data transfers (64-bits data transfers) and configures the system accordingly.
def read_axiap_longdata_support(axi_ap, context):

    # Calculate the configuration register address for the specified AXI AP
    # NOTE: Address is reconstructed to match CCS read_reg API format, not as it is present in RM.
    adress_reg = (((axi_ap << 12) | (0x0DF4 >> 2)) | 0x80000000)
    MEM_AP_REG_CFG_LD_MASK = (1 << 2)

    try:
        lde = utils.read_reg32_exc(context, adress_reg)
        # Check if the Long Data Enable bit is set in the configuration register
        if ((lde & MEM_AP_REG_CFG_LD_MASK) != MEM_AP_REG_CFG_LD_MASK):
            utils.set_axi_cfg_ap_lde(False)
        else:
            utils.set_axi_cfg_ap_lde(True)
    except Exception as e:
        raise gdb.GdbError("[S32Debugger]Error:  Error reading AXI AP configuration\n")
