"""Core debugging infrastructure and utilities for Winston.

This module provides utility classes, data structures, and debugging
utilities used throughout Winston's debugging and inspection system.

Key Classes:
    Timer: Context manager for timing code execution
    Context: Debug context tracking frame and exception state
    WinStack: Stack for tracking exceptions and call frames
    LogDialog: Simple logging dialog
    PythonInterpreter: Interactive Python console
    Tracer: Function call tracer

Utility classes like AdultContent, StatementSuccessfullyExecuted, and
FrameAndLineno support the debugging and inspection system.

Note:
    InputPromptDialog has been moved to subdialogs.py and InputPromptElement
    has been moved to form_elements.py as they are general-purpose dialog
    components, not debugging-specific.
"""

from __future__ import annotations
from typing import Any, TYPE_CHECKING
from collections.abc import Callable, Generator, Iterable
import sys
import os
import code
import inspect
import time
import types
import linecache
import traceback as tb
import re
import textwrap
from datetime import datetime as dt
import bdb
import threading
from .key_handler import KeyHandler
from .elements import Element
from .form_elements import TextElement, SingleTextElement, NumberElement, ListElement, ButtonElement
from .dialog import Dialog
from .subdialogs import Applet, AppletDialog, HeteroDialog

if TYPE_CHECKING:
    from .dialog import Dialog as DialogType
    from .inspection import Inspector, InspectionDialog

class StatementSuccessfullyExecuted:
    """Sentinel indicating successful statement execution.

    Returned by PythonInterpreter.interpret() to indicate that
    the provided code string was successfully executed as a statement
    (not an expression).
    """
    pass


class AdultContent:
    """Base class for input not consumed by child dialogs.

    Subclasses represent input that should only be handled by parent/home
    dialogs. When child dialog receives AdultContent via get_input(), it
    raises ChildQuit which percolates back to home dialog sharing the key queue.
    """
    pass


def is_primitive(class_name: str) -> bool:
    return class_name  in ('int','float','complex', 'bool','NoneType')

class Timer:
    """Context manager for timing code execution.

    Usage:
        with Timer() as t:
            # code to time
        print(f'elapsed time = {t.elapsed:.2f}')

    The timer starts on __enter__ and calculates elapsed time on __exit__.
    The Timer instance remains in scope after the with block.

    Attributes:
        start: Start time from perf_counter().
        end: End time from perf_counter().
        time_taken: Calculated elapsed time.
    """

    def __init__(self) -> None:
        """Initialize timer with current time."""
        self.start: float = time.perf_counter()
        self.end: float | None = None
        self.time_taken: float | None = None

    def __enter__(self) -> Timer:
        """Enter context, return self for 'as' variable.

        Returns:
            This Timer instance.
        """
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_value: BaseException | None,
        exc_traceback: types.TracebackType | None
    ) -> None:
        """Exit context and calculate elapsed time.

        Args:
            exc_type: Exception type if raised, None otherwise.
            exc_value: Exception instance if raised, None otherwise.
            exc_traceback: Traceback if exception raised, None otherwise.
        """
        self.end = time.perf_counter()
        self.time_taken = self.end - self.start

    @property
    def elapsed(self) -> float | None:
        """Get elapsed time in seconds.

        Returns:
            Elapsed time, or None if timer hasn't exited yet.
        """
        return self.time_taken
def non_blocking_lock(lock: threading.Lock) -> Generator[bool, None, None]:
    """Use lock as non-blocking context manager.

    Args:
        lock: Threading lock to acquire.

    Yields:
        True if lock was acquired, False otherwise.

    Example:
        with non_blocking_lock(lock) as acquired:
            if acquired:
                # Lock acquired, do work
                pass
            else:
                # Lock not available
                pass
    """
    acquired = lock.acquire(blocking=False)
    try:
        yield acquired
    finally:
        if acquired:
            lock.release()


class Context(AdultContent):
    """Debug context tracking frame and exception state.

    Captures debugging trap state including trap type, applet,
    frame, exception, arguments, and return value.

    Attributes:
        trap_type: Type of debugging trap (e.g., 'call', 'return', 'exception').
        applet: Applet being debugged.
        frame: Current frame object.
        exc: Exception if trap_type is 'exception', None otherwise.
        arguments_list: Function arguments if available.
        return_value: Return value if trap_type is 'return'.
    """

    def __init__(
        self,
        trap_type: str,
        applet: AppletDialog,
        frame: types.FrameType,
        exc: BaseException | None,
        arguments_list: list[Any] | None = None,
        return_value: Any = None
    ) -> None:
        """Initialize debug context.

        Args:
            trap_type: Type of trap ('call', 'return', 'exception', etc).
            applet: Applet being debugged.
            frame: Current execution frame.
            exc: Exception if this is exception trap, None otherwise.
            arguments_list: Function arguments if available.
            return_value: Return value if this is return trap.
        """
        self.trap_type = trap_type
        self.applet = applet
        self.frame = frame
        self.exc = exc
        self.arguments_list = arguments_list
        self.return_value = return_value


class FrameAndLineno:
    """Frame wrapper with captured line number.

    Wraps a frame object and captures its line number at construction time.
    Useful for stack snapshots where f_lineno may change.

    Attributes:
        frame: The wrapped frame object.
        lineno: Captured line number.
    """

    def __init__(self, frame: types.FrameType, lineno: int | None = None) -> None:
        """Initialize frame wrapper.

        Args:
            frame: Frame object to wrap.
            lineno: Line number to capture, or None to use frame.f_lineno.
        """
        self.frame = frame
        self.lineno = lineno if lineno is not None else frame.f_lineno

    def __getattr__(self, attr: str) -> Any:
        """Delegate attribute access to wrapped frame.

        Args:
            attr: Attribute name.

        Returns:
            Attribute value from wrapped frame.
        """
        return getattr(self.frame, attr)        

def get_active_dialog(applet: Dialog) -> Dialog:
    """Get active child dialog, stopping at InspectionDialog.

    Args:
        applet: Starting dialog/applet.

    Returns:
        Active dialog (last child before InspectionDialog or end).
    """
    from .inspection import InspectionDialog
    d = applet
    while d.child and not isinstance(d.child, InspectionDialog):
        d = d.child
    return d
def where(frame: types.FrameType, applet: Dialog | None = None, lineno: int | None = None) -> str:
    """
    lineno is usually None, causing the line number to be extracted from the frame.
    However, in the case of a frame where an exception is caught, f_lineno points as usual to the line being executed in that frame,
    but that is in this case the exception handler and not the call that initiated the stack of calls leading to the eventual exception
    So when an exception traceback stack is being formatted we pass in tb_lineno as the optional lineno parameter to where, thereby causing the correct lineno to be displayed
    """
    if lineno is None:
        lineno = frame.f_lineno
    qualified_name=frame.f_code.co_qualname
    f_name = frame.f_code.co_filename
    if os.path.exists(f_name):
        file_or_cell  = os.path.basename(f_name)
    else:
        if applet and hasattr(applet,'convert_pseudo_filename') and (f_name.lower() not in  ('<module>','<string>','<frozen ')):
            try:
                file_or_cell = applet.convert_pseudo_filename(f_name)
            except Exception as e:
                file_or_cell = f'{f_name} unresolved due to exception {e} in converter'
        else:
            file_or_cell = f_name
    return f"{lineno},,,{source_line(frame,lineno)},,,in {qualified_name}, in {file_or_cell}:"    
def format_stack(start_frame: types.FrameType, end_frame: types.FrameType | None = None, applet: Dialog | None = None) -> list[tuple[str, types.FrameType | FrameAndLineno]]:
    stack=[]
    for f in get_stack_shot(start_frame,end_frame):
        lineno = f.lineno if hasattr(f,'lineno') else f.f_lineno
        stack.append((where(f,applet=applet,lineno=lineno),f))  # type: ignore[arg-type]
    return stack  # type: ignore[return-value]
    #return [(where(i,applet=applet),i) for i in get_stack(start_frame,end_frame)] 

def get_methods_with_key_attribute(cls: type) -> dict[str, tuple[type, Any]]:
    """Return methods with _associated_key attribute from the class and its superclasses."""
    methods = {}
    for klass in inspect.getmro(cls):
        for name, member in inspect.getmembers(klass, predicate=inspect.isfunction):
            if hasattr(member, "_associated_key") and name not in methods:
                methods[name] = (klass, member)
    return methods
def get_keys(object: Any) -> dict[str, Callable[[Any, str], Any]]:
    return object._key_methods
def get_first_frame(applet: Dialog) -> types.FrameType:
    frame = sys._current_frames()[applet.thread.ident]  # type: ignore[attr-defined]
    while frame.f_back:
        frame= frame.f_back
        if frame.f_code.co_name=='run':
            break
    return frame 
def inspect_object(object: Any, context: Context, dialog: Dialog, name: str | None = None, current: int = 0, parent_obj: Any = None, attr_name: str | None = None) -> None:
    from .inspection import Inspector, InspectionDialog
    from .applets.editor import Source
    from .applets.pycoder import PycodeDialog
    class_name = object.__class__.__name__
    if is_primitive(class_name):
        dialog.say_strings('Leaf not inspectable')
    elif (class_name=='Source') and os.path.exists(object.filename):
        #note PycodeDialog is the superclass of PycoderDialog and is not itself an Applet instance so runs on this thread
        PycodeDialog(dialog,element_type=TextElement,name_seed=object.filename,filler=object).run(speak=['%name','%current'])
        dialog.say_current()
    else:
        filler = Inspector(object,name,parent_obj=parent_obj,attr_name=attr_name)
        dialog_name=f"{name} Inspection"
        if isinstance(dialog,InspectionDialog):
            dialog.push_inspector()
            dialog.name=dialog_name
            dialog.refill(filler,position=(current,0))
        else:
            i_dialog = InspectionDialog(dialog,dialog_name,filler=filler,context=context)
            i_dialog.cursor = current
            #print('after creating inspection dialog', *[c.__class__.__name__ for c in i_dialog],'end of contents',sep='\n')
            i_dialog.run(speak="%current")
            #upon return from inspecting this object speak the line we were previously on unless this is an exception, in which case we are about to kill this dialog, so don't speak current line
            if not context.exc:
                dialog.say_current()
def get_source_lines(method: Any) -> list[str]:
    """Return source lines of the given method."""
    lines, _ = inspect.getsourcelines(method)
    return lines
def get_stack_shot(start_frame: types.FrameType, end_frame: types.FrameType | None = None) -> list[FrameAndLineno]:
    """
    adds the current lineno in the frame to the object which will be used in future inspection in preference to the f_lineno value giving the correct position of where the line pointer was at the time the snapshot was taken
    used when logging a frame stack for future inspection while not pausing execution
    note that locals and globals in the snapshot frame are not reliable as they may have been modified since the snapshot was taken
    """
    stack_shot = []
    for f in get_stack(start_frame,end_frame):
        stack_shot.append(FrameAndLineno(f,f.f_lineno))
    return stack_shot
def get_stack(start_frame: types.FrameType, end_frame: types.FrameType | None = None) -> list[types.FrameType]:
    stack = []
    frame = start_frame
    while frame and not frame is end_frame:
        stack.append(frame)
        frame=frame.f_back  # type: ignore[assignment]
    return stack
def syntax_exception_location_and_source(e: SyntaxError) -> tuple[str, str]:
    filename=os.path.basename(getattr(e,'filename',''))
    lineno=getattr(e,'lineno','')
    offset = getattr(e, 'offset','')
    #text is the source line
    text = getattr(e, 'text','').strip('\n')
    offset_label = '' if offset=='' else ' offset '
    location =   f"{filename} {lineno}{offset_label}{offset} "
    return (f'{location}',text) #tuple of location and source line
def exception_msg(e: BaseException) -> str:
    if hasattr(e, 'msg'):
        msg =  f'{e.__class__.__name__} {e.msg}'
    elif e.args:
        msg =  f'{e.__class__.__name__} {e.args[0]}'
    else:
        msg =  f'{e.__class__.__name__} {e}'
    msg = msg.split(' at line')[0] #get rid of confusing zero indexed line no. which is in some messages, and instead extract and adjust line number elsewhere
    msg = msg.split(' on line')[0] #get rid of confusing zero indexed line no. which is in some messages, and instead extract and adjust line number elsewhere
    return msg
def get_first_argument(frame: types.FrameType) -> Any:
    first_arg_name = frame.f_code.co_varnames[0]
    return frame.f_locals[first_arg_name]
def get_bound_method_from_frame(frame: types.FrameType) -> Any:
    first_arg = get_first_argument(frame)
        # Check if the first argument is an instance of some class
    if not isinstance(first_arg, type):
        method = getattr(first_arg, frame.f_code.co_name, None)
        if method:
            return method
    return None
def get_self_from_frame(frame: types.FrameType) -> Any:
    """
    Given a frame, return the __self__ attribute if the frame corresponds to a bound method.
    Otherwise, return None.
    """
    return frame.f_locals.get('self', None)
def get_exc_frame(tb: types.TracebackType) -> types.FrameType:
    """
    Given a traceback, navigate to the frame (where the exception occurred).
    """
    while tb.tb_next:
        tb = tb.tb_next
    return  tb.tb_frame
def get_self_from_tb(tb: types.TracebackType) -> Any:
    frame = get_exc_frame(tb)
    return get_self_from_frame(frame)
class TracebackAndException:
    """Wrapper passing traceback and exception together to inspection.

    Allows inspect_traceback_and_exception to display exception message
    along with stack frames inline.

    Attributes:
        traceback: Exception traceback object.
        exception: Exception instance.
    """

    def __init__(self, traceback: types.TracebackType, exception: BaseException) -> None:
        """Initialize wrapper.

        Args:
            traceback: Traceback from exception.
            exception: Exception instance.
        """
        self.traceback = traceback
        self.exception = exception


class ValueObjectList(list[tuple[str, Any]]):
    """List of (description, object) tuples for InspectionDialog.

    Used for filling InspectionDialog with displayable items.
    Has custom inspector that yields tuples unmodified.

    Attributes:
        name: Name of this list for display purposes.
    """

    def __init__(self, contents: list[tuple[str, Any]] | None = None, name: str = 'unnamed') -> None:
        """Initialize value-object list.

        Args:
            contents: Initial list of (description, object) tuples.
            name: Display name for this list.
        """
        super().__init__(contents or [])
        self.name = name


class WinStack(ValueObjectList):
    """Stack of (description, frame) tuples for exception/call stack display.

    For exceptions, first entries are exception message lines, followed by
    stack frames. For non-exception contexts, contains only stack frames.

    Built from traceback chain when exception present, otherwise from
    live frame snapshot.
    """

    def __init__(self, context: Context) -> None:
        """Build stack from debug context.

        Args:
            context: Debug context with frame and optional exception.
        """
        exc_tb = getattr(context.exc, "__traceback__", None)
        if exc_tb is not None:
            # Build from traceback chain using tb_lineno for accuracy
            stack: list[tuple[str, FrameAndLineno]] = []
            tb_node: types.TracebackType | None = exc_tb
            while tb_node is not None:
                f = tb_node.tb_frame
                lineno = tb_node.tb_lineno
                stack.append(
                    (where(f, applet=context.applet, lineno=lineno),
                     FrameAndLineno(f, lineno))
                )
                tb_node = tb_node.tb_next
            stack.reverse()  # Show throwing frame first
            super().__init__(stack, name='stack')
        else:
            # No exception: use live frame snapshot
            super().__init__(
                format_stack(
                    start_frame=context.frame,
                    end_frame=None,
                    applet=context.applet
                ),
                name='stack'
            )

        # Prepend exception header lines
        if isinstance(context.exc, SyntaxError):
            location, source = syntax_exception_location_and_source(context.exc)
            self.insert(0, (f'{location} {source}', context.exc))
        elif context.exc:
            exc_message = exception_msg(context.exc)
            # Split multi-line exception messages
            exc_lines = exc_message.split('\n')
            for i, line in enumerate(reversed(exc_lines)):
                if line.strip():  # Skip empty lines
                    self.insert(0, (line, context.exc))
def source_line(frame: types.FrameType, lineno: int | None = None) -> str:
    if lineno is None:
        lineno = frame.f_lineno
    linecache.clearcache()
    return linecache.getline(frame.f_code.co_filename, lineno).rstrip() #remove \n from end of source line
def cls_name(frame: types.FrameType) -> str | None:
    # Get the code object associated with the frame
    code = frame.f_code
    # Get the first argument's name from the code object
    arguments = inspect.getargs(code)
    if len(arguments.args) < 1:
                return None
    first_argument_name = arguments.args[0]
    # try to Get the object that defines the code (i.e.  a class instance)
    defining_object = frame.f_locals.get(first_argument_name, None)
    method = getattr(defining_object, code.co_name,None)
    if inspect.ismethod(method) and method.__code__ is code:
        return defining_object.__class__.__name__
    return None

def exc_writer(exc_type: type[BaseException], exc: BaseException, exc_tb: types.TracebackType, exc_applet: Dialog | None = None) -> None:
    """
    prints and usually also invokes inspect_object() to launch an InspectionDialog object
    do not pass exceptions where no exc_applet argument was supplied  to debugger_ui (i.e. exceptions in clem.py or in critical_keys[x]() invoked directly from clem.on_key_down() rather than passing a key to the active applet thread
    """
    from .inspection import Inspector
    frame = get_exc_frame(exc_tb)
    context = Context('exception',exc_applet,frame,exc)  # type: ignore[arg-type]
    stamp = f"exception at {dt.now().strftime('%H:%M:%S')} in {exc_applet}"
    stack_items = [item[0] for item in WinStack(context)]
    # WinStack already splits multi-line exceptions into separate items
    if stack_items:
        output_lines = stack_items
    else:
        output_lines = [stamp]
    print('\n'.join(output_lines), file=sys.stderr)
    sys.stderr.flush() 
    """
    exc_applet is set in except clause of applet dialog so all exceptions in applets should launch an inspection dialog 
    """
    if exc_applet is not None:
        #print(f'running inspection dialog on {exc_applet} thread')
        sys.stderr.flush() 
        try: 
            dialog = exc_applet
            while dialog.child:
                dialog = dialog.child
            inspect_object(dialog=dialog,object=context,context=context,name=Inspector.title(context,exc_type))  # type: ignore[arg-type]
        except bdb.BdbQuit:  
            raise bdb.BdbQuit('re-raised to percolate out to {exc_applet.name}  inner run exception handler where applet terminates')
        except Exception as e:
            try:
                print(f'while inspecting, encountered another exception: {e} in exc_writer for {exc_applet.name}',file=sys.stderr)                
                tb.print_exc()
                sys.stderr.flush() 
            except Exception as e_2:
                #give up on trying to format the stack and just print the exception 
                print(f'A second  exception occurred in exc_writer: {e_2} after {e}',file=sys.stderr)
                sys.stderr.flush() 
    else:
        print(f'exc_applet is {exc_applet} in exc_writer so debugger ui not launched')                    
        sys.stderr.flush()         
class LogDialog(Dialog):
    """Simple log file viewer dialog.

    Displays log file contents with each line as an Element.
    Closes with Alt+Left or Alt+Space.
    """

    def generate_content(
        self,
        filler: str,
        element_type: type[Element] | None = None
    ) -> Generator[Element, None, None]:
        """Generate elements from log file.

        Args:
            filler: Path to log file.
            element_type: Unused, kept for compatibility.

        Yields:
            Element for each line in log file.
        """
        with open(filler, encoding='utf-8', errors='ignore') as f:
            for l in f:
                yield Element(self, 'blank line', l.rstrip('\n'))

    def run(self, speak: list[str] | None = None) -> Any:
        """Run dialog starting at last line.

        Args:
            speak: Speech directives for initial announcement.

        Returns:
            Dialog result.
        """
        self.cursor = len(self) - 1
        return super().run(speak=speak)

    @KeyHandler.hotkey("Alt+Left")
    def close_dialog(self, modified_name: str) -> Any:
        """Close dialog (alternative to Escape).

        Args:
            modified_name: Full key name including modifiers.

        Returns:
            Result from cancel().
        """
        return self.cancel(modified_name)

    @KeyHandler.hotkey("Alt+Space")
    def toggle(self, modified_name: str) -> Any:
        """Toggle/close dialog.

        Args:
            modified_name: Full key name including modifiers.

        Returns:
            Result from cancel().
        """
        return self.cancel(modified_name)



class KeyTimer:
    """Utility for timing and counting keyboard events.

    Tracks keystroke timing and prints statistics every 60 keystrokes.

    Attributes:
        start: Start time for current batch.
        end: End time for current batch.
        count: Total keystroke count.
        tag: Label for timing reports.
        word: Current word being typed (unused).
    """

    def __init__(self, tag: str) -> None:
        """Initialize key timer.

        Args:
            tag: Label for timing reports.
        """
        self.start = self.end = self.count = 0
        self.tag = tag
        self.word = ""

    def log(self, event: Any) -> None:
        """Log keyboard event.

        Args:
            event: Keyboard event with Key and Ascii attributes.
        """
        print(f'{event.Key}: {event.Ascii}')

    def add(self, event: Any) -> None:
        """Add keystroke and print stats every 60 keys.

        Args:
            event: Keyboard event.
        """
        from .framework import beep
        if self.start == 0:
            self.start = time.time()  # type: ignore[assignment]
        elif self.count % 60 == 0:
            self.end = time.time()  # type: ignore[assignment]
            duration = (self.end - self.start)
            print(
                f'{self.tag} time {round(duration, 2)} seconds, '
                f'average time {round(duration * 1000 / 60, 1)}ms, '
                f'total count: {self.count}'
            )
            self.start = self.end = 0
            beep(750, 750)
        self.count += 1


class PythonInterpreter(code.InteractiveConsole):
    """Interactive Python console for Winston.

    Delegates syntax error and traceback display to custom sys.excepthook.
    """

    def showsyntaxerror(self, filename: str | None = None) -> None:
        """Show syntax error.

        Delegates to parent - actual handling in sys.excepthook.

        Args:
            filename: Optional filename where error occurred.
        """
        super().showsyntaxerror(filename=filename)

    def showtraceback(self) -> None:
        """Show traceback.

        Delegates to parent - actual handling in sys.excepthook.
        """
        super().showtraceback()        

class Tracer:
    """Function call tracer for Winston debugging.

    Traces function calls and returns using sys.setprofile().
    Logs to data/logs/trace.log with call depth and arguments.

    Attributes:
        app: Winston application instance.
        call_depth: Current call stack depth.
        trace_exclusions: List of function names to exclude from tracing.
        thread_commencing_trace: Name of thread that started tracing.
        log: Open file handle for trace.log.
    """

    def __init__(self, app: Any) -> None:
        """Initialize tracer.

        Args:
            app: Winston application instance with config and tts.
        """
        self.app = app
        self.call_depth = 0
        exclusions = self.app.config['application'].get('trace_exclusions', '').split(',')
        self.trace_exclusions = [e.strip() for e in exclusions]
        self.thread_commencing_trace: str | None = None
        os.makedirs('data/logs', exist_ok=True)
        self.log = open('data/logs/trace.log', 'w')
        self.log.write('Initialized trace: ' + dt.now().strftime('%H:%M:%S') + '\n')

    def toggle(self) -> None:
        """Toggle tracing on/off."""
        if sys.getprofile() is None:
            self.flush()
            self.report('enabled')
            sys.setprofile(self.trace)
            self.thread_commencing_trace = threading.current_thread().name
        else:
            sys.setprofile(None)
            self.report('disabled')
            self.flush()

    def report(self, action: str) -> None:
        """Report trace toggle action.

        Args:
            action: 'enabled' or 'disabled'.
        """
        toggle_action = f'{action} call logging in {threading.current_thread().name}'
        self.log.write(' '.join([toggle_action, dt.now().strftime('%H:%M:%S') + '\n']))
        self.app.tts.speak(toggle_action)    
    def trace(self, frame: types.FrameType, event: str, arg: Any = None) -> None:
        """Trace function calls and returns.

        Determines if frame represents class method or function by checking
        if first argument's method code matches frame code. Logs call depth,
        thread, function name, and arguments.

        Skips C calls/returns and functions in trace_exclusions.

        Args:
            frame: Current stack frame.
            event: Event type ('call', 'return', 'c_call', 'c_return', 'exception').
            arg: Return value for 'return' events, exception for 'exception' events.
        """
        # Get the code object associated with the frame
        code = frame.f_code
        #get the name of  the callable implemented  by this code 
        name= code.co_name
        if event in ['c_call','c_return']:  
            return #ignore  if this is a call/return event to a C  function as these clutter trace   
        elif event in ('call','return'): 
            thread_name = threading.current_thread().name 
            if thread_name == self.thread_commencing_trace:
                #do not log the returns of the stack that called toggle until we get to a fresh call  
                if event =='return':
                    return 
                else:
                    self.thread_commencing_trace = None
            if name in self.trace_exclusions:
                return 
            # Get the first argument's name from the code object
            arguments = inspect.getargs(code)
            method = None 
            if len(arguments.args):
                object_name = arguments.args[0]
                defining_object = frame.f_locals.get(object_name, None)
                method = getattr(defining_object, name,None)
                if inspect.ismethod(method) and method.__code__ is code:
                    name =  '.'.join([defining_object.__class__.__name__,name])
            if name not in self.trace_exclusions:
                if ret:=(event =='return'):
                    self.call_depth=max(self.call_depth-1,0) #do not go below zero when unwinding any initial unmatched returns after starting profiler 
                else:
                    self.call_depth+=1
                #the level for a return event is the same as the corresponding call so we adjust the depth back up one for the log statement below on a return 
                txt = f"{self.call_depth+1*ret} {thread_name} {name}"
                if ret:
                    try: #ignore exception in inspect 
                        txt += f' return {arg}' 
                    except Exception as e:
                        txt += f' return'
                else:
                    names, _, _, values = inspect.getargvalues(frame)
                    #think this needs upgrading to handle cases where params are passed in as *args and **kwargs which are the 2  members of the 4-tuple ignored in the above code 
                    for arg_name  in names:
                        value = values[arg_name]
                        if hasattr(value, '__class__') and str(value).startswith('<'):
                            value = value.__class__.__name__
                        value = str(value)[:30] #truncate to 30 characters in case value is e.g. a long list
                        txt+=    f", {arg_name}={value}"
        elif 'exception' in event: #handles exceptions passed by sys.settprofile as well as ones passed by DebuggerDialog
#assume that the second item in arg is the actual exception object.
            txt = "\n".join([event] + [stack_item[0] for stack_item in WinStack(frame,arg(1))])  # type: ignore[call-arg, arg-type] 
        else: #this is a debugging event, other than an exception,  provided by DebuggerDialog 
            txt = ' '.join([event, self.app.debugger.where(frame)])
        self.log.write(txt+'\n')
    #all the below can probably be deleted - it is from gpt and don't understand why this is useful - also don't think it actually works 
    #func_name=name.split('.')[-1]
    #print('func name: ',func_name)
    #func_obj = method if method else eval(name) #if we did not find a method object then this is a function name which should eval to a function object 
    #sig = inspect.signature(func_obj)
    #bound_args = sig.bind(*args, **kwargs)
    #bound_args.apply_defaults()
    # Display the parameter values
    #for name, value in bound_args.arguments.items():
        #self.log.write(f"        {name}: {value}\n")
    def flush(self) -> None:
        """Flush trace log to disk."""
        self.log.flush()

    def close(self) -> None:
        """Close trace log file."""
        self.log.close()