"""Object inspection system for Winston debugging.

This module provides the Inspector class and InspectionDialog for interactive
object introspection and navigation.

Key Classes:
    Inspector: Core inspection engine with custom inspect_* methods for different types
    InspectionElement: Element that displays inspectable objects
    InspectionDialog: Interactive dialog for navigating object hierarchies

The inspection system supports Control+Return to drill into objects and
Control+Back to navigate back through inspection history.
"""

from __future__ import annotations
from typing import Any, TYPE_CHECKING, cast
from collections.abc import Callable, Generator, Iterable
from collections import deque
import sys
import os
import types
import inspect
import re
import importlib
import textwrap
import linecache
from .key_handler import KeyHandler
from .subdialogs import InputPromptDialog
from .elements import Element
from .form_elements import SingleTextElement,InputPromptElement
from .debugging import Context, FrameAndLineno,AdultContent, StatementSuccessfullyExecuted,get_active_dialog, get_first_frame, get_keys, inspect_object, exception_msg, source_line

if TYPE_CHECKING:
    from .dialog import Dialog


def filter_globals(g_dict: dict[str, Any]) -> dict[str, Any]:
    select = lambda key: (key not in getattr(__builtins__,'__dict__',__builtins__)) and not key.startswith("__")  # type: ignore[operator]  # __builtins__ can be module or dict
    keys = filter(select,g_dict.keys())
    return {k:g_dict[k] for k in keys}
def where(frame: types.FrameType, applet: Dialog | None = None, lineno: int | None = None) -> str:
    from  .debugging  import where as debugging_where
    return debugging_where(frame,applet,lineno)
def make_snake(cls: type, prefix: str | None = None, suffix: str | None = None) -> str:
    """Convert CamelCase class name to snake_case with optional prefix/suffix.

    Args:
        cls: Class whose name to convert.
        prefix: Optional prefix to prepend.
        suffix: Optional suffix to append.

    Returns:
        Snake case name with optional prefix and suffix.

    Example:
        >>> make_snake(MyClass, prefix='inspect', suffix='method')
        'inspect_my_class_method'
    """
    class_name = cls.__name__
    snake_case_name = re.sub('(?<!^)(?=[A-Z])', '_', class_name).lower()
    name_parts = []
    if prefix is not None:
        name_parts.append(prefix)
    name_parts.append(snake_case_name)
    if suffix is not None:
        name_parts.append(suffix)
    return '_'.join(name_parts)

class InspectionElement(Element):
    """Element displaying an inspectable object.

    Pressing Control+Return drills into the object. Supports re-fetching
    for generators/iterators that may be exhausted.

    Attributes:
        object: The inspectable object.
        parent_obj: Parent object for re-fetching attributes.
        attr_name: Attribute name for re-fetching from parent.
    """

    def __init__(
        self,
        owner: Dialog,
        name: str,
        value: str,
        obj: Any,
        parent_obj: Any = None,
        attr_name: str | None = None
    ) -> None:
        """Initialize inspection element.

        Args:
            owner: Parent dialog.
            name: Element class name.
            value: Display string for element.
            obj: Object to inspect on Control+Return.
            parent_obj: Parent object for re-fetching (generators/properties).
            attr_name: Attribute name for re-fetching.
        """
        super().__init__(owner, name, value)
        self.object = obj
        self.parent_obj = parent_obj
        self.attr_name = attr_name

    @KeyHandler.hotkey("Control+Return")
    def forward(self, modified_name: str) -> None:
        """Drill into object with Control+Return.

        Re-fetches attribute from parent if needed for exhausted generators.

        Args:
            modified_name: Full key name including modifiers.
        """
        from .applets.editor import Source
        current = 0
        obj = self.object
        # Re-fetch attribute if needed (for generators/iterators)
        if self.parent_obj and self.attr_name:
            try:
                refreshed_obj = getattr(self.parent_obj, self.attr_name)
                # Only use refreshed object for iterators/generators or properties
                if ((hasattr(refreshed_obj, '__iter__') and hasattr(refreshed_obj, '__next__'))
                        or isinstance(refreshed_obj, property)):
                    obj = refreshed_obj
            except:
                pass  # Use original if re-fetch fails
        if isinstance(obj, Source):
            current = obj.line
        context = self.dialog.context
        # Update context frame if inspecting a frame
        if isinstance(obj, (types.FrameType, FrameAndLineno)):
            context.frame = obj  # type: ignore[assignment]
        inspect_object(
            dialog=self.dialog,
            object=obj,
            context=context,
            current=current,
            name=self.value,
            parent_obj=self.parent_obj,
            attr_name=self.attr_name
        )   

class InspectionDialog(InputPromptDialog):
    """Interactive object inspection dialog with navigation history.

    Provides object introspection with forward navigation (Control+Return)
    and backward navigation (Control+Back). Supports immediate code
    evaluation of input including single letter shortcuts for common objects
    Attributes:
        debugging_stack: Stack for debugging state (currently unused).
        inspector_stack: Stack of (Inspector, position) for back navigation.
        context: Debug context with frame and exception info.
    """
    def __init__(
        self,
        parent: Dialog,
        name_seed: str,
        element_type: type[Element] = Element,
        filler: list[Any] | Inspector | None = None,
        revert_to: Dialog | None = None,
        empty_value: str = 'empty element',
        context: Context | None = None
    ) -> None:
        """Initialize inspection dialog.

        Args:
            parent: Parent dialog.
            name_seed: Base name for dialog.
            element_type: Element type for body content (unused).
            filler: Inspector or list for content, or None for empty Inspector.
            revert_to: Dialog to revert to on close.
            empty_value: Speech text for empty elements.
            context: Debug context, or None to create default.
        """
        self.debugging_stack: list[Any] = []
        self.inspector_stack: list[tuple[Inspector, tuple[int, int]]] = []
        if not filler:
            filler = Inspector(None, None)
        if context is None:
            self.context = Context('at home', self, inspect.currentframe(), None)  # type: ignore[arg-type]
        else:
            self.context = context
        super().__init__(
            parent,
            name_seed,
            element_type=Element,
            filler=filler,  # type: ignore[arg-type]
            revert_to=revert_to,
            bottom_input=True,
            output_consumption=False,
            empty_value=empty_value
        )

    def element_attributes(self) -> set[str]:
        """Get required element attributes.

        Returns:
            Set of required attribute names.
        """
        return super().element_attributes()
    def content_elements(  # type: ignore[override]
        self,
        filler: Inspector | Any,
        element_type: type[Element] | None = None
    ) -> Generator[InspectionElement, None, None]: 
        """
        filler is either:
            - an Inspector object which has the object to be inspected and it's name as attributes and an inspect()  method for yielding tuples which this content_generation method uses below to yield the actual content elements the contents 
            - another object for which there is an Inspector 
        Inspectors for base iterable classes (list, tuple, dict and set) return a tuple for each item in the iterable 
                The default inspector for other classes returns an (value_string,object)  tuple for each non-callable variable attribute of an object. 
                It also returns a members and a 'dunders and under' tuple which can be entered to launch a further inspection dialog that displays these subsets of the callable attributes   
                if the object is iterable it then returns a 'contents' tuple which, when opened,  launches a further inspection dialog to display the object's content items
                the contents sub-dialog is achieved by co-ercing the object to the relevant base class (list, dict, etc) which will result in the base class inspector being used to fill the sub_dialog 
        Custom inspect methods  can be implemented for any type (e.g. Dialog or carousel)   which perform class specific inspection if the default inspection is not suitable 
        inspect  always appends an unfiltered 'all members' element that can be used to e.g. inspect methods of base iterables that are not displayed by the class specific inspect method 
        """
        if not isinstance(filler,Inspector):
            filler = Inspector(filler,f'object of {filler.__class__.__name__} class')
        for item in filler.inspect():
            if len(item) == 4:  # New format: (value_str, actual_obj, parent_obj, attr_name)
                value_string, obj, parent_obj, attr_name = item
                yield InspectionElement(self,name=obj.__class__.__name__,value=value_string,obj=obj,parent_obj=parent_obj,attr_name=attr_name)
            elif len(item) == 2:  # Old format: (value_str, obj)
                value_string, obj = item
                yield InspectionElement(self,name=obj.__class__.__name__,value=value_string,obj=obj)
    def input_element(self) -> tuple[bool, InputPromptElement]:
        """Get input prompt element for immediate code evaluation.

        Returns:
            Tuple of (bottom_position=True, InputPromptElement).
        """
        return (True, InputPromptElement(self, name='Input Prompt', value=""))

    def push_inspector(self) -> None:
        """Push current Inspector onto stack for back navigation."""
        self.inspector_stack.append((self.filler, (self.cursor, 0)))

    def pop_inspector(self) -> bool | None:
        if len(self.inspector_stack):
            inspector,position=self.inspector_stack.pop()
            # Re-fetch object if it's a generator/iterator that may be exhausted
            inspector.refresh_object()
            self.name=inspector.title(inspector.object,inspector.object_name)
            self.name = 'unnamed inspection dialog' if not self.name else self.name
            self.refill(inspector,position)
        else:
            if self.message(buttons=['Yes','No'],title='Exit inspection?')=='Yes':
                return True
            else:
                self.say_current()
    def ingest_input(self, what: str) -> None:
        #decide if this should really be here or if should put it in whichever subclass of InspectionDialog uses input_q (think it is ConsoleDialog)
        if hasattr(self,'input_to_q') and self.input_to_q:
            self.input_q.put(what)  # type: ignore[attr-defined]
            return
        self.set_output_consumption(True)
        outcome = self.interpret(what)
        self.set_output_consumption(False)
        c_name = outcome.__class__.__name__
        if (outcome is not StatementSuccessfullyExecuted):
            #use the value method of the Inspector class directly on the class as it does not make any use of instance data
            val= Inspector.display_value(outcome)
            f_what = f'{what}'
            f_val = f'{val}'
            if f_val==f_what:
                value_string = f_val
            else:
                value_string = f'{what}: {val}'
        else:
            value_string = what
        self.insert(len(self)-1,InspectionElement(self,name=c_name,value=value_string,obj=outcome))
        self.move(len(self)-2,0,False)
        self.say_current()
    def interpret(self, immediate_code: str) -> Any:
        """
        implements immediate code evaluation for the input prompt  
        the applet  and frame items in the context attribute of the dialog argument provide the context from which many of the other shortcut variables are reached 
        single letters   are convenience variables allowing strings such as c[0] for the first dialog in the carousel, a.app.quit_now, len(a) e.caret and simply v to all evaluate correctly 
        a is the applet in the context
        b is  app.debugger
        c is app. applets carousel
        d is the active dialog of the applet, excluding the inspection dialog itself. d.child is therefore the inspection dialog itself   
        e is the current element of d 
        f is the frame in the context
        i is this inspection dialog 
        k gets the key map for the provided object, so k(e), Return, Control+Return  fills the inspection dialog with the key map for the current element  
        n is the name of the object under inspection as provided in the name argument to inspect_object 
        o is the object being inspected in this inspection dialog. So o, Return, Control+Return leaves the dialog contents unchanged (although it does mangle the title)  but pushes a duplicate on to the stack. More usefully o.<attr>.<attr2> returns the attr2 attribute of the attr attribute of the object currently under inspection, which can be much easier than trying to find attr and attr2 via descending the inspection tree . 
        self evaluates in the current frame due to f_locals being placed in the super_dict prior to invoking exec/eval. This will often be the dialog or element under inspection, but treat carefully as unpredictable due to other self object's sometimes being in scope   
        v is the value attribute of e 
        w wraps the text object provided into a list of strings max_length of 150, so w(repr(o)) produces a list of strings which together contain the repr of the current object under inspection 
        the dialog.super_dict attribute is used to make variable assignments persistent between calls to this method 
        The input history stack on this  dialog is used by this method and is accessed via Control+Up/Down 
        """
        context = self.context
        super_dict = self.super_dict 
        a=context.applet
        b=a.app.debugger 
        c = a.app.applets
        d = get_active_dialog(a)
        e = d.current()
        f = context.frame if context.frame else         get_first_frame(a) 
        i = self
        k = get_keys 
        n = i.filler.object_name 
        o = i.filler.object 
        v = e.value 
        w = lambda s:textwrap.wrap(s,width=150,replace_whitespace=False)    
        super_dict.update(locals())
        super_dict.update(f.f_locals)    
        try: 
            outcome=eval(immediate_code,super_dict,f.f_globals)
        except SyntaxError:
            try:
                exec(immediate_code,super_dict,f.f_globals)
                outcome=StatementSuccessfullyExecuted 
            except Exception as e:
                outcome = str(e)
                #exc_writer(type(e),e,e.__traceback__)
        except Exception as e:
            outcome = str(e)                    
            #exc_writer(type(e),e,e.__traceback__)            
        return outcome 
    @KeyHandler.hotkey("Alt+C")
    def command_cmd(self, modified_name: str) -> None:
        self.set_output_consumption(True)
        self.run_process(title='shell run', prompt='command,  args  and flags',shell=True)
        self.set_output_consumption(False)
    @KeyHandler.hotkey("Alt+P")
    def command_install(self, modified_name: str) -> None:
        self.set_output_consumption(True)
        self.run_process(title='install', fixed_input=[sys.executable,'-m', 'pip','install'],prompt='package name and flags')
        self.set_output_consumption(False)
    def run_process(self, title: str = '', prompt: str = '', fixed_input: list[str] = [], shell: bool = False) -> None:
        import subprocess
        user_input   = self.input_text(title=title,prompt=prompt)
        if user_input in(False,''):
            self.say_strings(f'cancelled {title}')
            return
        # mypy doesn't track narrowing, so cast to str
        params = [p.strip() for p in cast(str, user_input).split(' ') if len(p) and not p.isspace()]
        args_list  = fixed_input+params
        self.say_strings('please wait')
        outcome =  f'{title} completed '
        result: Any = ''
        errors = False
        try:
            result=subprocess.run(args_list,shell=shell,capture_output=True)
        except subprocess.CalledProcessError as e:
            errors = True
            outcome = f"Error occurred in subprocess: {e} "
        outputs = []
        if hasattr(result,'stdout'):
            outputs.extend(result.stdout.decode('utf-8').split('\r\n'))
        if hasattr(result,'stderr') and len(result.stderr):
            errors = True
            outputs.append('*** error output ***')
            outputs.extend(result.stderr.decode('utf-8').split('\r\n'))  
        outcome = outcome + ('check output log for warnings and errors' if errors else 'successfully')
        #outputs.append(outcome)
        outputs =[line for line in outputs if len(line) and not line.isspace()] 
        print('\n'.join(outputs))
        if len(self)>1:
            self.move(len(self)-2,0,False)        
        self.say_current()             
    @KeyHandler.hotkey("Control+Back")
    def backward(self, modified_name: str) -> bool | None:
        return self.pop_inspector()

class Inspector:
    """Core inspection engine for object introspection.

    Provides custom inspect_* methods for different types and generates
    (description, object) tuples for InspectionDialog display.

    Attributes:
        object: Object being inspected.
        object_name: Display name for object.
        parent_obj: Parent object for re-fetching (generators).
        attr_name: Attribute name for re-fetching.
    """

    def __init__(
        self,
        object: Any,
        object_name: str | None,
        parent_obj: Any = None,
        attr_name: str | None = None
    ) -> None:
        """Initialize Inspector.

        Args:
            object: Object to inspect.
            object_name: Display name, or None to use class name.
            parent_obj: Parent for re-fetching generators/properties.
            attr_name: Attribute name for re-fetching.
        """
        self.object = object
        self.parent_obj = parent_obj
        self.attr_name = attr_name
        if object_name is None:
            self.object_name = object.__class__.__name__
        else:
            self.object_name = object_name

    def refresh_object(self) -> bool:
        """Re-fetch object if it's a generator/iterator.

        Returns:
            True if object was refreshed, False otherwise.
        """
        if self.parent_obj and self.attr_name:
            try:
                refreshed_obj = getattr(self.parent_obj, self.attr_name)
                # Only refresh for iterators/generators
                if hasattr(refreshed_obj, '__iter__') and hasattr(refreshed_obj, '__next__'):
                    self.object = refreshed_obj
                    return True
            except:
                pass
        return False

    @staticmethod
    def title(obj: Any, obj_name: str) -> str:
        """
        returns string to use as dialog name and also as first element in the dialog
        this is a static method that uses explicit obj and obj_name arguments rather than self.object/self.object_name as can be called on the class to return a title for an object before the Inspector object for that object has been instantiated 
        Consider changing to def title(obj,obj_name):; return Inspector.description(obj,obj_name) + ' inspection', and then calling Inspector.ddescription from self.display_value not Inspector.title and also change all custom title methods to be custom_description methods that never have the suffix'inspection' 
        """
        if custom_title:= Inspector.custom_title_method(obj): 
            return custom_title(obj,obj_name)
        elif obj_name==obj.__class__.__name__: 
            return f'{obj_name} object inspection' 
        else:
            class_or_instance = 'class' if isinstance(obj,type) else 'instance'
            return f'{obj_name} {class_or_instance} inspection' 
    @staticmethod
    def custom_title_method(obj: Any) -> Callable[[Any, str], str] | None:
        custom_title_method_name = make_snake(obj.__class__,suffix='title')
        if hasattr(Inspector,custom_title_method_name):
            custom_title=getattr(Inspector,custom_title_method_name)
            return custom_title
        return None
    @classmethod
    def display_value(cls, obj: Any) -> str:
        from .debugging import is_primitive 
        # For class objects, use __name__ directly to avoid getting property descriptors
        if isinstance(obj, type):
            obj_name = obj.__name__
        else:
            obj_name = getattr(obj,'name',getattr(obj,'__name__',''))
        c = obj if isinstance(obj,type) else obj.__class__
        class_name = c.__name__
        if is_primitive(class_name) or (class_name=='str'):
            val = obj
            if isinstance(val,str) and (not len(val)):
                val = 'empty string'
            elif val is None:
                val='None'
        elif isinstance(obj,dict) and 'name' in obj:
            val =            f"{class_name},  {obj.get('name','')}, {obj.get('value','')}"
        elif isinstance(obj, property):
            # Display property objects with access mode info
            access_modes = []
            if obj.fget: access_modes.append('get')
            if obj.fset: access_modes.append('set')
            if obj.fdel: access_modes.append('del')
            access_str = ', '.join(access_modes) if access_modes else 'no access'
            val = f'property ({access_str})'
        elif isinstance(obj, type):
            # For class objects, check early before checking for 'value' attribute
            if class_name == obj_name:
                val = f'{class_name} class'
            else:
                val = f'{class_name},  {obj_name}'
        elif hasattr(obj,'value') and (obj.value is not None):
            #val = obj.value
            val = f'{class_name},  {obj.value}'
        elif custom_title:=cls.custom_title_method(obj):
            val = custom_title(obj,obj_name)
        else:
            import types
            # Avoid redundancy for modules, generators, descriptors, and other types where name matches class
            # or where obj_name is not useful (like <genexpr>, <lambda>)
            # For modules, generators, and descriptors, just show the type without the name
            if isinstance(obj, types.ModuleType):
                val = 'module'
            elif isinstance(obj, types.GeneratorType):
                val = 'generator'
            elif class_name.endswith('_descriptor'):
                # Descriptor types: member_descriptor, getset_descriptor, method_descriptor, wrapper_descriptor
                val = class_name
            elif class_name == obj_name or obj_name.startswith('<'):
                val = class_name
            else:
                val = f'{class_name},  {obj_name}'
        return f'{val}'
    def inspect(self, obj: Any | None = None) -> Generator[tuple[str, Any] | tuple[str, Any, Any, str], None, None]:
        """
        yield a sequence of tuples that are used to build the elements for the inspection dialog for self.object
        firstly yield a title string
        secondly, if a class specific inspection method is available for this object, yield from  it
        finally  yield from the default inspection method unless this is an object for which we just want to see the contents
        """
        from .debugging import Context, ValueObjectList, TracebackAndException
        obj=obj if obj else self.object
        try:
            title = Inspector.title(obj,self.object_name)
        except Exception as e:
            print(e)
            title='Inspection title unavailable'
        if title and len(title):
            yield (title,'')
        yield from self.inspect_by_class(obj)
        #append the default inspection elements unless this object's class is in the skip list
        if not (isinstance(obj,ValueObjectList) or  obj.__class__ in (Context,list,dict,set,tuple,TracebackAndException)):
            yield from self.inspect_default(obj)
    def inspect_by_class(self, obj: Any) -> Generator[tuple[str, Any] | tuple[str, Any, Any, str], None, None]:
        """
        If this is a class object or if this object's class does not have an __inspection_class__ method then simply use the class as input to the specific inspection method name builder
        otherwise use the class returned by the object's __inspection_class__ method as input to the specific inspection method name builder
        if the Inspector class has a method with the built name then yield from it, otherwise simply return
        """
        if hasattr(obj,'__inspection_class__') and isinstance(obj,obj.__inspection_class__()):
            cls = obj.__inspection_class__()
        else:
            cls = obj.__class__
        specific_inspect_name = make_snake(cls,prefix='inspect')
        if hasattr(self,specific_inspect_name):
            yield from getattr(self,specific_inspect_name)(obj)
    def inspect_default(self, obj: Any) -> Generator[tuple[str, Any] | tuple[str, Any, Any, str], None, None]:
        from .applets.editor import Source
        from .debugging import ValueObjectList 
        #Now co-erce subclasses of list to list type so that list inspector is used to display the members when  the contents element below is opened 
        if isinstance(obj,list) :
            yield ('contents',list(obj))
        #do similar for other iterables 
        elif isinstance(obj,dict):
            yield ('contents',dict(obj))
        elif isinstance(obj,tuple):
            yield ('contents',tuple(obj))
        elif isinstance(obj,set):
            yield ('contents',set(obj))
        elif isinstance(obj,deque):
            yield ('contents',list(obj))
        members = inspect.getmembers(obj)
        members.sort()
        #m_f_r() is not the same as callable() because m_f_r does not capture e.g. BeautifulSoup elements which are actually callable even though normal usage is as a property 
        m_f_r = lambda x: inspect.ismethod(x) or inspect.isfunction(x) or inspect.isroutine(x)
        for v,o in filter(lambda x:(not m_f_r(x[1]) and not x[0].startswith('_')),members):
            display_val = self.display_value(o)
            # If display_value already starts with the attribute name, don't prepend it again
            # This handles "Applet class", etc.
            # For generic types (module, generator, function, descriptors), always prepend the attribute name
            generic_types = ('module', 'generator', 'function') + tuple(t for t in [display_val] if t.endswith('_descriptor'))
            if (display_val.startswith(f'{v},') or display_val.startswith(f'{v} ')) and display_val not in generic_types:
                yield (display_val, o, obj, v)  # 4-tuple: (value_str, actual_obj, parent_obj, attr_name)
            else:
                yield (f'{v}: {display_val}', o, obj, v)  
        for v,filename in filter(lambda x: (x[0] == '__file__'),members):            
            yield (f'file: {filename}',Source(filename,0,0))        
        for v,mod_name in filter(lambda x: (x[0] == '__module__'),members):            
            try:
                mod = importlib.import_module(mod_name)
                yield (f'module: {mod_name}',mod)
            except Exception as e:
                print( f'unable to import {mod_name}, exception {e}')
        methods = list(filter(lambda x: (m_f_r(x[1]) and not x[0].startswith('_')),members))
        if len(methods):
            methods = ValueObjectList([(f"{m[0]}: {getattr(m[1],'_associated_key','')}: {m[1].__class__.__name__}",m[1]) for m in methods],name='primary methods')
            yield ('Primary methods', methods)
        interesting_private_properties = lambda x: (
            not m_f_r(x[1]) and 
            x[0].startswith('_') and 
            (x[0] not in ['__dict__','__weakref__','__hash__'])
            )
        for v,o in filter(interesting_private_properties,members):
            yield (f'{v}: {self.display_value(o)}', o, obj, v)
        yield ('dunder and under methods',ValueObjectList(list(filter(lambda x: (m_f_r(x[1]) and x[0].startswith('_')),members)),name='dunder and under methods'))
        formatted_members = [(f'{v}: {self.display_value(o)}', o) for v,o in members]
        yield ('all members',ValueObjectList(formatted_members,name='all members'))
    def inspect_module(self, obj: Any) -> Generator[tuple[str, Any] | tuple[str, str], None, None]:
        from .applets.editor import Source
        yield (f'inspection for module: {obj}','')
        if hasattr(obj,'__file__') and obj.__file__ and os.path.exists(obj.__file__):
            yield ('module source',Source(obj.__file__,0,0))
        else:
            yield (f'no source file available for this module','')
    def inspect_element(self, obj: Any) -> Generator[tuple[str, Any], None, None]:
        yield ('key methods', obj._key_methods)
    def inspect_carousel(self, obj: Any) -> Generator[tuple[str, Any], None, None]:
        for item  in obj:
            yield(item.name,item)
    def inspect_dialog(self, obj: Any) -> Generator[tuple[str, Any], None, None]:
        from .applets.editor import Source
        from .debugging import WinStack 
        if hasattr(obj, 'context'):
            if obj.context.frame:
                formatted_trace = WinStack(obj.context)
                if len(formatted_trace):
                    #below is the current frame - could re-factor code so call frame formatter directly rather than getting from stack
                    yield formatted_trace[0]
                yield ('stack',formatted_trace)
                yield ('Edit source',Source(obj.context.frame.f_code.co_filename,obj.context.frame.f_lineno-1,0))
            else:
                # Alt+Right inspection with no frame - add current frame for source/stack inspection
                current_frame = inspect.currentframe()
                if current_frame:
                    # Skip frames until we get out of the debugging module
                    caller_frame = current_frame.f_back
                    while caller_frame and 'debugging.py' in caller_frame.f_code.co_filename:
                        caller_frame = caller_frame.f_back
                    if caller_frame:
                        formatted_trace = WinStack(Context('pseudo_context', obj.context.applet, caller_frame, None))
                        if len(formatted_trace):
                            yield formatted_trace[0]
                        yield ('stack', formatted_trace)
                        yield ('Edit source', Source(caller_frame.f_code.co_filename, caller_frame.f_lineno-1, 0))
        yield ('key methods', obj._key_methods)
    def inspect_source(self, obj: Any) -> Generator[tuple[str, str], None, None]:
        #only arrive here if Source object is not successfully put into an editor in inspect_object()
        linecache.clearcache()
        for line in linecache.getlines(obj.filename):
            yield (line,'')
    @staticmethod
    def block_node_title(obj: Any, obj_name: str) -> str:
        try:
            title  =  f'level {obj.level} block, {obj.owner}'
        except:
            title = 'partially initialized block without level value'
        return title
    def inspect_block_node(self, obj: Any) -> Generator[tuple[str, str] | tuple[str, Any], None, None]:
        yield (f'owner: {obj.owner}','')
        yield (f'length: {obj.length}','')
        yield (f'{len(obj.children)} children','')
        for child  in obj.children:
            yield (f'{self.display_value(child)}',child)
    @staticmethod
    def context_title(obj: Any, obj_name: str) -> str:
        #the below hack is a bit confusing; relies on the exception being spoken
        if obj.exc:
            return "" #results in no title line being yielded in Inspector.inspect().
        else:
            location = where(obj.frame,obj.applet) if obj.frame else obj_name
            return f'{obj.trap_type}, {location}'
    def inspect_position_stack(self, obj: Any) -> Generator[tuple[str, Any], None, None]:
        for position in obj:
            yield(f'cursor: {position.cursor}, caret: {position.caret}',position)
    def inspect_context(self, obj: Any) -> Generator[tuple[str, Any], None, None]:
        from .applets.editor import Source
        from .debugging import WinStack, TracebackAndException 
        context = obj
        #add logic for return_value and arguments_list
        if context.frame:
            formatted_trace = WinStack(context)
            if context.exc:
                # Yield all items from the start where the second element is an Exception object
                # These represent multi-line exception descriptions
                for item in formatted_trace:
                    if isinstance(item[1], BaseException):
                        yield item
                    else:
                        # Stop when we reach non-exception items (stack frames)
                        break

                # Add the failing line (top frame from traceback) between exception and traceback
                tb = context.exc.__traceback__
                if tb:
                    # Get the topmost (most recent) frame from traceback
                    while tb.tb_next is not None:
                        tb = tb.tb_next
                    failing_line = where(tb.tb_frame, lineno=tb.tb_lineno)
                    yield (f'Failing line: {failing_line}', FrameAndLineno(tb.tb_frame, tb.tb_lineno))

                yield ('traceback', TracebackAndException(context.exc.__traceback__, context.exc))
            else:
                if len(formatted_trace):
                    #below is the current frame - could re-factor code so call frame formatter directly rather than getting from stack
                    yield formatted_trace[0]
                yield ('stack',formatted_trace)
            yield ('Edit source',Source(context.frame.f_code.co_filename,context.frame.f_lineno-1,0))
        else:
            # Alt+Right inspection with no frame - add current frame for source/stack inspection
            current_frame = inspect.currentframe()
            if current_frame:
                # Skip this frame and go to the caller's frame
                caller_frame = current_frame.f_back
                if caller_frame:
                    formatted_trace = WinStack(Context('pseudo_context', context.applet, caller_frame, None))
                    if len(formatted_trace):
                        yield formatted_trace[0]
                    yield ('stack', formatted_trace)
                    yield ('Edit source', Source(caller_frame.f_code.co_filename, caller_frame.f_lineno-1, 0))
        #inject the context into the applet for ease of further inspection
        context.applet.context = context
        yield (f'applet: {context.applet}',context.applet)
        active_dialog = get_active_dialog(context.applet)
        active_dialog.context = context
        yield (f'active dialog: {active_dialog}',active_dialog)
        if context.frame:
            yield from self.inspect_dict(context.frame.f_locals,empty_message='locals dictionary is empty')
            yield ('selected globals',filter_globals(context.frame.f_globals))
    def inspect_dict(self, obj: dict[str, Any], empty_message: str = 'empty dictionary') -> Generator[tuple[str, Any] | tuple[str, str], None, None]:
        sorted_items = sorted(list(obj.items()))
        if sorted_items:
            for k,v in sorted_items:
                #only put first 160 chars of value in Element. value in case this is e.g. a list or a dict within a dict
                yield (f'{k}: {self.display_value(v)}'[:160],v)
        else:
            yield (empty_message,empty_message)
    def inspect_traceback_and_exception(self, obj: Any) -> Generator[tuple[str, Any], None, None]:
        """
        Custom inspector for TracebackAndException wrapper
        Displays exception message followed by inline stack frames only
        Does not yield from inspect_default to prevent redundant exception/traceback elements
        """
        from .debugging import WinStack
        tb = obj.traceback
        exc = obj.exception

        # Yield exception message using the same formatting as WinStack
        exc_message = exception_msg(exc)
        exc_lines = exc_message.split('\n')
        for line in exc_lines:
            if line.strip():  # Skip empty lines
                yield (line, exc)

        # Build and yield the stack frames inline
        tb_current = tb
        stack = []
        while True:
            stack.append((where(tb_current.tb_frame, lineno=tb_current.tb_lineno), FrameAndLineno(tb_current.tb_frame, tb_current.tb_lineno)))
            if tb_current.tb_next is None:
                break
            else:
                tb_current = tb_current.tb_next
        # Turn the stack upside down so the frame throwing the exception is at the top as per WinStack
        stack.reverse()
        # Yield frames inline
        for f in stack:
            yield f
        # IMPORTANT: Do not yield from inspect_default or inspect_by_class will add redundant elements

    def inspect_traceback(self, obj: Any) -> Generator[tuple[str, Any], None, None]:
        # Handle both raw traceback objects and TracebackAndException wrappers
        from .debugging import TracebackAndException
        if isinstance(obj, TracebackAndException):
            # Delegate to the custom inspector
            yield from self.inspect_traceback_and_exception(obj)
        else:
            # Original traceback inspection without exception
            tb = obj
            tb_current = tb
            stack = []
            while True:
                stack.append((where(tb_current.tb_frame, lineno=tb_current.tb_lineno), FrameAndLineno(tb_current.tb_frame, tb_current.tb_lineno)))
                if tb_current.tb_next is None:
                    break
                else:
                    tb_current = tb_current.tb_next
            # Turn the stack upside down so the frame throwing the exception is at the top
            stack.reverse()
            for f in stack:
                yield f
    def inspect_value_object_list(self, obj: Any) -> Generator[tuple[str, Any], None, None]:
        for v,o in obj:
            yield (v,o)
    def inspect_win_stack(self, obj: Any) -> Generator[tuple[str, Any], None, None]:
        for item in obj:
            # Split multi-line strings for easier navigation
            # This handles both exception messages and any other multi-line items
            if isinstance(item[0], str) and '\n' in item[0]:
                for line in item[0].split('\n'):
                    if line.strip():  # Skip empty lines
                        yield (line, item[1])
            else:
                yield item
    @staticmethod
    def frame_title(obj: Any, obj_name: str) -> str:
        return  f'frame for {obj.f_code.co_qualname} on line {obj.f_lineno} in {obj.f_code.co_filename}'
    def inspect_frame(self, obj: Any) -> Generator[tuple[str, Any] | tuple[str, str], None, None]:
        yield from self.inspect_frame_and_lineno(obj)
    @staticmethod
    def frame_and_lineno_title(obj: Any, obj_name: str) -> str:
        return  f'FrameAndLineNO for {obj.f_code.co_qualname} on line {obj.lineno} in {obj.f_code.co_filename}'
    def inspect_frame_and_lineno(self, obj: Any) -> Generator[tuple[str, Any] | tuple[str, str], None, None]:
        from .applets.editor import Source
        from .debugging import WinStack
        lineno = getattr(obj,'lineno',obj.f_lineno)
        yield (source_line(obj,lineno=lineno),'')
        yield ('stack',WinStack(Context('pseudo_context',applet=None,frame=obj,exc=None)))  # type: ignore[arg-type]               
        yield ('Edit source',Source(obj.f_code.co_filename,lineno-1,0))
        yield from self.inspect_dict(obj.f_locals)
    @staticmethod
    def method_title(obj: Any, obj_name: str) -> str:
        return f'method inspection for {obj.__func__.__code__.co_qualname}'
    def inspect_method(self, obj: Any) -> Generator[tuple[str, Any], None, None]:
        from .applets.editor import Source
        yield (f'func object', obj.__func__)
        yield (f'code object',obj.__func__.__code__)
        yield (f'source file: {obj.__func__.__code__.co_filename}', Source(obj.__func__.__code__.co_filename,obj.__func__.__code__.co_firstlineno,0))
        yield (f'starts at line: {obj.__func__.__code__.co_firstlineno}', obj.__func__.__code__.co_firstlineno)
    def inspect_tuple(self, obj: Any) -> Generator[tuple[str, Any] | tuple[str, str], None, None]:
        yield from self.inspect_list(obj)
    def inspect_set(self, obj: Any) -> Generator[tuple[str, Any] | tuple[str, str], None, None]:
        yield from self.inspect_list(obj)
    def inspect_generator(self, obj: Any) -> Generator[tuple[str, Any] | tuple[str, str], None, None]:
        #handle like any iterator
        yield from self.inspect_list_iterator(obj)
    def inspect_list_iterator(self, obj: Any) -> Generator[tuple[str, Any] | tuple[str, str], None, None]:
        yield from self.inspect_list(obj)
    def inspect_list(self, obj: Any) -> Generator[tuple[str, Any] | tuple[str, str], None, None]:
        """
        firstly coerce into a list as this method is called from methods for other iterables such as set and tuple
        """
        obj = list(obj)
        if len(obj):
            for i in range(len(obj)):
                yield  (f'{self.display_value(obj[i])}'[:160],obj[i])
        else:
            yield ('empty list, tuple or set','empty list, tuple or set')
    def inspect_str(self, obj: Any) -> Generator[tuple[str, None] | tuple[str, str], None, None]:
        if len(obj):
            for line  in textwrap.wrap(obj,width=150,replace_whitespace=False):
                yield  (line,None)
        else:
            yield ('empty string','empty string')