"""
Test frame providers for scripted frame provider functionality.

These providers demonstrate various merge strategies:
- Replace: Replace entire stack
- Prepend: Add frames before real stack
- Append: Add frames after real stack

It also shows the ability to mix a dictionary, a ScriptedFrame or an SBFrame
index to create stackframes
"""

import os
import lldb
from lldb.plugins.scripted_process import ScriptedFrame
from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider


class ReplaceFrameProvider(ScriptedFrameProvider):
    """Replace entire stack with custom frames."""

    def __init__(self, input_frames, args):
        super().__init__(input_frames, args)
        self.frames = [
            {
                "idx": 0,
                "pc": 0x1000,
            },
            0,
            {
                "idx": 2,
                "pc": 0x3000,
            },
        ]

    @staticmethod
    def get_description():
        """Return a description of this provider."""
        return "Replace entire stack with 3 custom frames"

    def get_frame_at_index(self, index):
        if index >= len(self.frames):
            return None
        return self.frames[index]


class PrependFrameProvider(ScriptedFrameProvider):
    """Prepend synthetic frames before real stack."""

    def __init__(self, input_frames, args):
        super().__init__(input_frames, args)

    @staticmethod
    def get_description():
        """Return a description of this provider."""
        return "Prepend 2 synthetic frames before real stack"

    def get_frame_at_index(self, index):
        if index == 0:
            return {"pc": 0x9000}
        elif index == 1:
            return {"pc": 0xA000}
        elif index - 2 < len(self.input_frames):
            return index - 2  # Return real frame index.
        return None


class AppendFrameProvider(ScriptedFrameProvider):
    """Append synthetic frames after real stack."""

    def __init__(self, input_frames, args):
        super().__init__(input_frames, args)

    @staticmethod
    def get_description():
        """Return a description of this provider."""
        return "Append 1 synthetic frame after real stack"

    def get_frame_at_index(self, index):
        if index < len(self.input_frames):
            return index  # Return real frame index.
        elif index == len(self.input_frames):
            return {
                "idx": 1,
                "pc": 0x10,
            }
        return None


class CustomScriptedFrame(ScriptedFrame):
    """Custom scripted frame with full control over frame behavior."""

    def __init__(self, thread, idx, pc, function_name):
        args = lldb.SBStructuredData()
        super().__init__(thread, args)

        self.idx = idx
        self.pc = pc
        self.function_name = function_name

    def get_id(self):
        """Return the frame index."""
        return self.idx

    def get_pc(self):
        """Return the program counter."""
        return self.pc

    def get_function_name(self):
        """Return the function name."""
        return self.function_name

    def is_artificial(self):
        """Mark as artificial frame."""
        return False

    def is_hidden(self):
        """Not hidden."""
        return False

    def get_register_context(self):
        """No register context for this test."""
        return None


class ScriptedFrameObjectProvider(ScriptedFrameProvider):
    """Provider that returns ScriptedFrame objects instead of dictionaries."""

    def __init__(self, input_frames, args):
        super().__init__(input_frames, args)

    @staticmethod
    def get_description():
        """Return a description of this provider."""
        return "Provider returning custom ScriptedFrame objects"

    def get_frame_at_index(self, index):
        """Return ScriptedFrame objects or dictionaries based on index."""
        if index == 0:
            return CustomScriptedFrame(
                self.thread, 0, 0x5000, "custom_scripted_frame_0"
            )
        elif index == 1:
            return {"pc": 0x6000}
        elif index == 2:
            return CustomScriptedFrame(
                self.thread, 2, 0x7000, "custom_scripted_frame_2"
            )
        elif index == 3:
            return len(self.input_frames) - 2  # Real frame index.
        elif index == 4:
            return len(self.input_frames) - 1  # Real frame index.
        return None


class ThreadFilterFrameProvider(ScriptedFrameProvider):
    """Provider that only applies to thread with ID 1."""

    @staticmethod
    def applies_to_thread(thread):
        """Only apply to thread with index ID 1."""
        return thread.GetIndexID() == 1

    def __init__(self, input_frames, args):
        super().__init__(input_frames, args)

    @staticmethod
    def get_description():
        """Return a description of this provider."""
        return "Provider that only applies to thread ID 1"

    def get_frame_at_index(self, index):
        """Return a single synthetic frame."""
        if index == 0:
            return {"pc": 0xFFFF}
        return None


class CircularDependencyTestProvider(ScriptedFrameProvider):
    """
    Provider that tests the circular dependency fix.

    This provider accesses input_frames during __init__ and calls methods
    on those frames. Before the fix, this would cause a circular dependency:
    - Thread::GetStackFrameList() creates provider
    - Provider's __init__ accesses input_frames[0]
    - SBFrame::GetPC() tries to resolve ExecutionContextRef
    - ExecutionContextRef::GetFrameSP() calls Thread::GetStackFrameList()
    - Re-enters initialization -> circular dependency!

    With the fix, ExecutionContextRef remembers the frame list, so it doesn't
    re-enter Thread::GetStackFrameList().
    """

    def __init__(self, input_frames, args):
        super().__init__(input_frames, args)

        # This would cause circular dependency before the fix!
        # Accessing frames and calling methods on them during __init__
        self.original_frame_count = len(input_frames)
        self.original_pcs = []

        # Call GetPC() on each input frame - this triggers ExecutionContextRef resolution.
        for i in range(min(3, len(input_frames))):
            frame = input_frames[i]
            if frame.IsValid():
                pc = frame.GetPC()
                self.original_pcs.append(pc)

    @staticmethod
    def get_description():
        """Return a description of this provider."""
        return "Provider that tests circular dependency fix by accessing frames in __init__"

    def get_frame_at_index(self, index):
        """Prepend a synthetic frame, then pass through original frames."""
        if index == 0:
            # Synthetic frame at index 0.
            return {"pc": 0xDEADBEEF}
        elif index - 1 < self.original_frame_count:
            # Pass through original frames at indices 1, 2, 3, ...
            return index - 1
        return None


class PythonSourceFrame(ScriptedFrame):
    """Scripted frame that points to Python source code."""

    def __init__(self, thread, idx, function_name, python_file, line_number):
        args = lldb.SBStructuredData()
        super().__init__(thread, args)

        self.idx = idx
        self.function_name = function_name
        self.python_file = python_file
        self.line_number = line_number

    def get_id(self):
        """Return the frame index."""
        return self.idx

    def get_pc(self):
        """PC-less frame - return invalid address."""
        return lldb.LLDB_INVALID_ADDRESS

    def get_function_name(self):
        """Return the function name."""
        return self.function_name

    def get_symbol_context(self):
        """Return a symbol context with LineEntry pointing to Python source."""
        # Create a LineEntry pointing to the Python source file
        line_entry = lldb.SBLineEntry()
        line_entry.SetFileSpec(lldb.SBFileSpec(self.python_file, True))
        line_entry.SetLine(self.line_number)
        line_entry.SetColumn(0)

        # Create a symbol context with the line entry
        sym_ctx = lldb.SBSymbolContext()
        sym_ctx.SetLineEntry(line_entry)

        return sym_ctx

    def is_artificial(self):
        """Not artificial."""
        return False

    def is_hidden(self):
        """Not hidden."""
        return False

    def get_register_context(self):
        """No register context for PC-less frames."""
        return None


class PythonSourceFrameProvider(ScriptedFrameProvider):
    """
    Provider that demonstrates Python source display in scripted frames.

    This provider prepends frames pointing to Python source code, showing
    that PC-less frames can display Python source files with proper line
    numbers and module/compile unit information.
    """

    def __init__(self, input_frames, args):
        super().__init__(input_frames, args)

        # Find the python_helper.py file
        current_dir = os.path.dirname(os.path.abspath(__file__))
        self.python_file = os.path.join(current_dir, "python_helper.py")

    @staticmethod
    def get_description():
        """Return a description of this provider."""
        return "Provider that prepends frames pointing to Python source"

    def get_frame_at_index(self, index):
        """Return Python source frames followed by original frames."""
        if index == 0:
            # Frame pointing to compute_fibonacci function (line 7)
            return PythonSourceFrame(
                self.thread, 0, "compute_fibonacci", self.python_file, 7
            )
        elif index == 1:
            # Frame pointing to process_data function (line 16)
            return PythonSourceFrame(
                self.thread, 1, "process_data", self.python_file, 16
            )
        elif index == 2:
            # Frame pointing to main function (line 27)
            return PythonSourceFrame(self.thread, 2, "main", self.python_file, 27)
        elif index - 3 < len(self.input_frames):
            # Pass through original frames
            return index - 3
        return None


class ValidPCNoModuleFrame(ScriptedFrame):
    """Scripted frame with a valid PC but no associated module."""

    def __init__(self, thread, idx, pc, function_name):
        args = lldb.SBStructuredData()
        super().__init__(thread, args)

        self.idx = idx
        self.pc = pc
        self.function_name = function_name

    def get_id(self):
        """Return the frame index."""
        return self.idx

    def get_pc(self):
        """Return the program counter."""
        return self.pc

    def get_function_name(self):
        """Return the function name."""
        return self.function_name

    def is_artificial(self):
        """Not artificial."""
        return False

    def is_hidden(self):
        """Not hidden."""
        return False

    def get_register_context(self):
        """No register context."""
        return None


class ValidPCNoModuleFrameProvider(ScriptedFrameProvider):
    """
    Provider that demonstrates frames with valid PC but no module.

    This tests that backtrace output handles frames that have a valid
    program counter but cannot be resolved to any loaded module.
    """

    def __init__(self, input_frames, args):
        super().__init__(input_frames, args)

    @staticmethod
    def get_description():
        """Return a description of this provider."""
        return "Provider that prepends frames with valid PC but no module"

    def get_frame_at_index(self, index):
        """Return frames with valid PCs but no module information."""
        if index == 0:
            # Frame with valid PC (0x1234000) but no module
            return ValidPCNoModuleFrame(self.thread, 0, 0x1234000, "unknown_function_1")
        elif index == 1:
            # Another frame with valid PC (0x5678000) but no module
            return ValidPCNoModuleFrame(self.thread, 1, 0x5678000, "unknown_function_2")
        elif index - 2 < len(self.input_frames):
            # Pass through original frames
            return index - 2
        return None


class AddFooFrameProvider(ScriptedFrameProvider):
    """Add a single 'foo' frame at the beginning."""

    def __init__(self, input_frames, args):
        super().__init__(input_frames, args)

    @staticmethod
    def get_description():
        """Return a description of this provider."""
        return "Add 'foo' frame at beginning"

    @staticmethod
    def get_priority():
        """Return priority 10 (runs first in chain)."""
        return 10

    def get_frame_at_index(self, index):
        if index == 0:
            # Return synthetic "foo" frame
            return CustomScriptedFrame(self.thread, 0, 0xF00, "foo")
        elif index - 1 < len(self.input_frames):
            # Pass through input frames (shifted by 1)
            return index - 1
        return None


class AddBarFrameProvider(ScriptedFrameProvider):
    """Add a single 'bar' frame at the beginning."""

    def __init__(self, input_frames, args):
        super().__init__(input_frames, args)

    @staticmethod
    def get_description():
        """Return a description of this provider."""
        return "Add 'bar' frame at beginning"

    @staticmethod
    def get_priority():
        """Return priority 20 (runs second in chain)."""
        return 20

    def get_frame_at_index(self, index):
        if index == 0:
            # Return synthetic "bar" frame
            return CustomScriptedFrame(self.thread, 0, 0xBAA, "bar")
        elif index - 1 < len(self.input_frames):
            # Pass through input frames (shifted by 1)
            return index - 1
        return None


class AddBazFrameProvider(ScriptedFrameProvider):
    """Add a single 'baz' frame at the beginning."""

    def __init__(self, input_frames, args):
        super().__init__(input_frames, args)

    @staticmethod
    def get_description():
        """Return a description of this provider."""
        return "Add 'baz' frame at beginning"

    @staticmethod
    def get_priority():
        """Return priority 30 (runs last in chain)."""
        return 30

    def get_frame_at_index(self, index):
        if index == 0:
            # Return synthetic "baz" frame
            return CustomScriptedFrame(self.thread, 0, 0xBAC, "baz")
        elif index - 1 < len(self.input_frames):
            # Pass through input frames (shifted by 1)
            return index - 1
        return None


class ThreadValidatingFrame(ScriptedFrame):
    """Frame that validates thread member is correctly set after init.

    This catches the regression where ScriptedFrame.__init__ used
    GetThreadByIndexID(tid) instead of GetThreadByID(tid). The thread ID
    and index ID are different, so using the wrong API produces an invalid
    thread. The validation result is encoded in the function name so the
    test can verify it through the SB API.
    """

    def __init__(self, thread, idx):
        args = lldb.SBStructuredData()
        super().__init__(thread, args)
        self.idx = idx
        # Store validation results after base class init
        self.thread_valid = self.thread is not None and self.thread.IsValid()
        self.thread_id = self.thread.GetThreadID() if self.thread_valid else 0

    def get_id(self):
        return self.idx

    def get_function_name(self):
        # Encode validation in function name so test can verify via SB API
        if self.thread_valid:
            return f"thread_valid_id_{self.thread_id:#x}"
        return "thread_INVALID"

    def is_artificial(self):
        return False

    def is_hidden(self):
        return False

    def get_register_context(self):
        return None


class ThreadValidatingFrameProvider(ScriptedFrameProvider):
    """Provider that validates ScriptedFrame.thread member initialization.

    Uses ThreadValidatingFrame to verify that self.thread is correctly set
    via GetThreadByID (not GetThreadByIndexID) after ScriptedFrame.__init__.
    """

    def __init__(self, input_frames, args):
        super().__init__(input_frames, args)

    @staticmethod
    def get_description():
        return "Validate ScriptedFrame.thread member initialization"

    def get_frame_at_index(self, index):
        if index == 0:
            return ThreadValidatingFrame(self.thread, 0)
        elif index - 1 < len(self.input_frames):
            return index - 1
        return None


class ValueProvidingFrame(ScriptedFrame):
    """Scripted frame with a valid PC but no associated module."""

    def __init__(self, thread, idx, pc, function_name, variable):
        args = lldb.SBStructuredData()
        super().__init__(thread, args)

        self.idx = idx
        self.pc = pc
        self.function_name = function_name
        self.variable = variable

    def get_id(self):
        """Return the frame index."""
        return self.idx

    def get_pc(self):
        """Return the program counter."""
        return self.pc

    def get_function_name(self):
        """Return the function name."""
        return self.function_name

    def is_artificial(self):
        """Not artificial."""
        return False

    def is_hidden(self):
        """Not hidden."""
        return False

    def get_register_context(self):
        """No register context."""
        return None

    def get_variables(self):
        """"""
        out = lldb.SBValueList()
        out.Append(self.variable)
        return out

    def get_value_for_variable_expression(self, expr, options, error: lldb.SBError):
        out = lldb.SBValue()
        if expr == "_handler_one":
            out = self.variable.CreateValueFromExpression("_handler_one", "(uint32_t)1")
        elif self.variable.name in expr:
            out = self.variable.CreateValueFromExpression("_expr", expr)

        if out.IsValid():
            return out

        error.SetErrorString(f"expression {expr} failed")
        return None


class ValueProvidingFrameProvider(ScriptedFrameProvider):
    """Add a single 'value-provider' frame at the beginning."""

    def __init__(self, input_frames, args):
        super().__init__(input_frames, args)

    @staticmethod
    def get_description():
        """Return a description of this provider."""
        return "Add 'value-provider' frame at beginning"

    def get_frame_at_index(self, index):
        if index == 0:
            f = self.input_frames.GetFrameAtIndex(index)
            # Find some variable we can give to the frame.
            variable = f.FindVariable("variable_in_main")
            # Return synthetic "value-provider" frame
            return ValueProvidingFrame(
                self.thread, 0, 0xF00, "value-provider", variable
            )
        elif index - 1 < len(self.input_frames):
            # Pass through input frames (shifted by 1)
            return index - 1
        return None
