# Copyright 2020-2021, 2023-2025 NXP

# SPDX-License-Identifier: BSD-3-Clause

from enum import Enum
import inspect
import sys
import traceback
import gdb
import settings
import functools
import re
import os
import subprocess
from error_utils import UniqIdEnum
from loggers import islogger
# following modules are generated at build time
from gta_error_codes import GtaErrorCodes
from ta_error_codes import TaErrorCodes
from fp_error_codes import FpErrorCodes

INTERNAL_ERRORS = 10000
DEBUG_SERVER_STARTED = False

class ErrorCode(UniqIdEnum):
    # Common Errors
    UndefinedError = 1

    # Connection and Context Errors
    ContextError = 100
    AttachError = 101
    ConnectError = 102

    # Core Errors
    CoreError = 200
    CoreInitializationFailed = 201
    CoreNameError = 202
    CoreNotInDebugError = 203

    # Memory Errors
    WriteMemoryError = 300
    ReadMemoryError = 301
    CompareMemoryError = 302
    ECCInitError = 303
    BusError = 304

    # Register Errors
    WriteRegisterError = 400
    ReadRegisterError = 401
    CompareRegisterError = 402

    # FlashErrors
    FlashInitError = 500
    FlashWriteError = 501
    FlashEraseError = 502
    FlashReadError = 503
    FlashCompareError = 504
    FlashParameterError = 505
    FlashUndefinedError = 506
    FlashVerifyError = 507

    # Secure Debug Errors
    InvalidSecureDbgParam = 600
    TargetUnlockError = 601

    #RCONErrors
    RCONInitError = 700
    RCONWriteError = 701
    RCONReadError = 702
    RCONParameterError = 703
    RCONUndefinedError = 704

    # OtherErrors
    DDRLoadFileError = 1000
    DDRTimerExpiredError = 1001
    DDRInitError = 1002

    # internal errors.
    # Make sure that these codes will not promoute to IDE!!!
    ValueError = INTERNAL_ERRORS + 0
    
class GtaLibException(Exception):
    def __init__(self, error):
        self.message = error.error_str
        self.type = error.error_type

    def __str__(self):
        gdb.write(
            "^Error:Exception:%s:%s(%d)\n"
            % (self.message, self.type.name, self.type.value)
        )
        
        return ""

class Error(object):
    def __init__(self, error_type, error_str="", prev_error=None):
        self.stack = traceback.format_stack()[:-1]
        if not isinstance(error_type, (ErrorCode, GtaErrorCodes, TaErrorCodes, FpErrorCodes)):
            raise ValueError("^Error: error_type must be instance of ErrorCode, GtaErrorCodes, TaErrorCodes or FpErrorCodes class")

        if isinstance(prev_error, Result):
            self.prev_error = prev_error.error
        elif isinstance(prev_error, Error):
            self.prev_error = prev_error
        elif prev_error is None:
            self.prev_error = prev_error
        else:
            raise ValueError(
                "^Error: prev_error must be either Error or Result or None"
            )

        f_back = inspect.currentframe().f_back
        self.line = f_back.f_lineno
        self.file = f_back.f_code.co_filename
        self.error_str = error_str
        self.error_type = error_type
        
    def __str__(self):
        err_str = ""
        if self.prev_error is not None:
            err_str += "%s\n" % self.prev_error

        # Clean error string by removing noise:
        # - Replace double newlines (\n\n) with a single newline (\n)
        # - Remove patterns like \n//\n
        formated_error_str = re.sub(r'\n{2,}', '\n', self.error_str)
        formated_error_str = re.sub(r'\n//\n', '\n', formated_error_str)
        formated_error_str = formated_error_str.rstrip('\n')
        lines = [
            formated_error_str,
            'Type of the error: [{0}]'.format(self.error_type.name),
            'Error code: [{0}]'.format(self.error_type.value)
        ]
        err_str = os.linesep.join(lines)

        # Try to get additional info if possible
        err_info = None
        try:
            err_info = gdb.execute("monitor errorinfo", False, True)
            err_str += os.linesep + err_info
            
            # Check if an initialization sequence error is present
            init_seq_error_str = ""
            if "Initialization sequence error" in err_info:
                init_seq_error_str = err_info
            elif "Initialization sequence error" in self.error_str:
                init_seq_error_str = self.error_str

            if init_seq_error_str:
                # Get the index of the init action
                init_cmd_index = re.search(r"0x[0-9A-F]+", init_seq_error_str, re.I)
                error_init_cmd_index =int(init_cmd_index.group(), 16)
                # Use the index to retrive the command of number = idx
                log = islogger.get_logs()
                error_cmd = log[error_init_cmd_index]
                err_str += 'Initialization sequence error at:  {0}, command with number {1}'.format(
                    error_cmd, hex(error_init_cmd_index))
                islogger.write_logs_to_file("gta_init_seq_log.txt")
        except BaseException:
            pass

        # Add the call stack of the error.
        # [currentcall], [urwarp], [check_exit] are functions in the call stack
        # (if the execution flow changes, the code below may become invalid)
        if 500 <= self.error_type.value <= 507:
             return err_str

        stack_lines = traceback.format_stack()[:-3]
        filtered_lines = []
        for line in stack_lines:
            filtered_lines.append(line.rstrip())
        formated =  "\n\n --- STACK TRACE OF THE INIT SCRIPTS ---\n" + os.linesep.join(
            filtered_lines)

        err_str += formated
        return err_str


class Ok(object):
    def __init__(self, value):
        self.value = value


class Result(object):
    error = None
    ok = None

    def __init__(self, result):
        if isinstance(result, Ok):
            self.ok = result
            self.error = None
        elif isinstance(result, Error):
            self.error = result
            self.ok = None
        else:
            raise ValueError("^Error: Unknown type: must be Error or Ok")

    def iserror(self):
        if self.error is not None:
            return True
        return False

    def isok(self):
        if self.ok is not None:
            return True
        return False

    def unwrap(self):
        if settings.ResultException:
            return self.unwrap_exc()
        self.check_exit()
        return self.ok.value

    def unwrap_exc(self):
        if self.error is not None:
            lines = str(self.error).splitlines()
            for line in lines:
                gdb.write(line + os.linesep)
                gdb.flush()
            raise GtaLibException(self.error)
        return self.ok.value

    def check_exit(self):
        if self.error is not None:
            lines = str(self.error).splitlines()
            for line in lines:
                gdb.write(line + os.linesep)
                gdb.flush()
            
            # self._print_stack()
            # If error is for Flash Programmer, do not exit process
            if 500 <= self.error.error_type.value <= 507:
                gdb.write(
                    "\n Internal Flash Programmer Error with code %d "
                    % self.error.error_type.value
                )
            else:
                if self.error.error_type.value < INTERNAL_ERRORS:
                    os._exit(self.error.error_type.value)
                else:
                    gdb.write(
                        "Internal Error with code %d" % self.error.error_type.value
                    )
                    os._exit(ErrorCode.UndefinedError)

    @staticmethod
    def _print_stack():
        frame = sys._getframe().f_back
        while frame is not None:
            gdb.write(
                "%s:%d %s\n"
                % (frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name)
            )
            frame = frame.f_back


def panic(err_code, err_msg):
    Result(Error(err_code, err_msg)).unwrap()


# Automatically unwrap Result value or exit in case of error
def safecall(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        if isinstance(result, Result):
            return result.unwrap()
        else:
            return result

    return wrapper


init_line_idx = {}
init_cmd_idx = 0


# save the file and line number of the function called in the global variable init_line_idx
# TODO: To be updated once all scripts are delivered, so we don't impact the DS delivery.
def traced_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        global init_cmd_idx
        file, line_nr, _, _ = traceback.extract_stack()[-2]
        init_line_idx[init_cmd_idx] = "%s:%s" % (file, line_nr)
        init_cmd_idx = init_cmd_idx + 1
        result = func(*args, **kwargs)
        return result

    return wrapper


# Returns value or generates an Exception in case of error
def safecallexc(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        if isinstance(result, Result):
            return result.unwrap_exc()
        else:
            return result

    return wrapper

# wraps function with try catch with predefined error code
def exceptcall(errorcode):
    def exceptcall_decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            try:
                v = func(*args, **kwargs)
                if v is None:
                    return Result(Ok(None))
                return v
            except BaseException as e:
                return Result(Error(errorcode, "%s" % str(e)))

        return wrapper

    return exceptcall_decorator

def enable_debug(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        #pylint: disable=no-member
        try:
            global DEBUG_SERVER_STARTED
            if not DEBUG_SERVER_STARTED:
                vscode_version = subprocess.run("code --version", capture_output=True, shell=True)
                if not vscode_version.stdout.decode("utf-8"):
                    gdb.write("Warning: vscode is not installed.")
                    return func(*args, **kwargs)
                
                debugpy_version = subprocess.run("code --list-extensions --show-versions | grep python.debugpy",
                                                capture_output=True, shell=True)
                debugpy_version = debugpy_version.stdout.decode("utf-8").replace("@","-").rstrip()
                if debugpy_version:
                    if sys.platform == "win32":
                        userprofile = os.environ['USERPROFILE']
                        _PY_CMD = "python"
                        _DEBUG_PY_DIR = "%s/.vscode/extensions/%s-win32-x64/bundled/libs" % (userprofile, debugpy_version)
                    elif sys.platform == "linux":
                        userprofile = subprocess.run("whoami", capture_output=True, shell=True).stdout.decode("utf-8").rstrip()
                        _PY_CMD = "python3"
                        _DEBUG_PY_DIR = "/home/%s/.vscode/extensions/%s-linux-x64/bundled/libs/" % (userprofile, debugpy_version)
                    else:
                        gdb.write("Warning: @enable_debug decorator not supported on this OS.")
                        return func(*args, **kwargs)

                    sys.path.insert(0, _DEBUG_PY_DIR)
                    import debugpy
                    debugpy.configure(python=_PY_CMD)
                    _port = 5678
                    debugpy.listen(_port)
                    DEBUG_SERVER_STARTED = True
                    gdb.write("vscode python debug adapter listening on port %s" % _port)
                else:
                    gdb.write("Warning: python.debugpy vscode extension not found.")
                    return func(*args, **kwargs)

            debugpy = sys.modules.get('debugpy')
            if not debugpy.is_client_connected():
                gdb.write("waiting for debug client to connect...")
                debugpy.wait_for_client()  # wait for a client to connect
            debugpy.breakpoint()
        except Exception as ex:
            gdb.write("Warning: Exception taken while trying to start debugpy adapter.")
            gdb.write(str(ex))

        return func(*args, **kwargs)

    return wrapper