"""Widget classes for the live profiling collector UI."""

import curses
import time
from abc import ABC, abstractmethod

from .constants import (
    TOP_FUNCTIONS_DISPLAY_COUNT,
    MIN_FUNC_NAME_WIDTH,
    MAX_FUNC_NAME_WIDTH,
    WIDTH_THRESHOLD_SAMPLE_PCT,
    WIDTH_THRESHOLD_TOTTIME,
    WIDTH_THRESHOLD_CUMUL_PCT,
    WIDTH_THRESHOLD_CUMTIME,
    MICROSECONDS_PER_SECOND,
    DISPLAY_UPDATE_INTERVAL_SEC,
    MIN_BAR_WIDTH,
    MAX_SAMPLE_RATE_BAR_WIDTH,
    MAX_EFFICIENCY_BAR_WIDTH,
    MIN_SAMPLE_RATE_FOR_SCALING,
    FOOTER_LINES,
    FINISHED_BANNER_EXTRA_LINES,
    OPCODE_PANEL_HEIGHT,
)
from ..constants import (
    THREAD_STATUS_HAS_GIL,
    THREAD_STATUS_ON_CPU,
    THREAD_STATUS_UNKNOWN,
    THREAD_STATUS_GIL_REQUESTED,
    PROFILING_MODE_CPU,
    PROFILING_MODE_GIL,
    PROFILING_MODE_WALL,
)
from ..opcode_utils import get_opcode_info, format_opcode


class Widget(ABC):
    """Base class for UI widgets."""

    def __init__(self, display, colors):
        """
        Initialize widget.

        Args:
            display: DisplayInterface implementation
            colors: Dictionary of color attributes
        """
        self.display = display
        self.colors = colors

    @abstractmethod
    def render(self, line, width, **kwargs):
        """
        Render the widget starting at the given line.

        Args:
            line: Starting line number
            width: Available width
            **kwargs: Additional rendering parameters

        Returns:
            Next available line number after rendering
        """
        pass

    def add_str(self, line, col, text, attr=0):
        """Add a string to the display at the specified position."""
        self.display.add_str(line, col, text, attr)


class ProgressBarWidget(Widget):
    """Reusable progress bar widget."""

    def render(self, line, width, **kwargs):
        """Render is not used for progress bars - use render_bar instead."""
        raise NotImplementedError("Use render_bar method instead")

    def render_bar(
        self, filled, total, max_width, fill_char="█", empty_char="░"
    ):
        """
        Render a progress bar and return the bar string and its length.

        Args:
            filled: Current filled amount
            total: Total amount (max value)
            max_width: Maximum width for the bar
            fill_char: Character to use for filled portion
            empty_char: Character to use for empty portion

        Returns:
            Tuple of (bar_string, bar_length)
        """
        bar_width = min(max_width, max_width)
        normalized = min(filled / max(total, 1), 1.0)
        bar_fill = int(normalized * bar_width)

        bar = "["
        for i in range(bar_width):
            if i < bar_fill:
                bar += fill_char
            else:
                bar += empty_char
        bar += "]"
        return bar, len(bar)


class HeaderWidget(Widget):
    """Widget for rendering the header section (lines 0-8)."""

    def __init__(self, display, colors, collector):
        """
        Initialize header widget.

        Args:
            display: DisplayInterface implementation
            colors: Dictionary of color attributes
            collector: Reference to LiveStatsCollector for accessing stats
        """
        super().__init__(display, colors)
        self.collector = collector
        self.progress_bar = ProgressBarWidget(display, colors)

    def render(self, line, width, **kwargs):
        """
        Render the complete header section.

        Args:
            line: Starting line number
            width: Available width
            kwargs: Must contain 'elapsed' key

        Returns:
            Next available line number
        """
        elapsed = kwargs["elapsed"]

        line = self.draw_header_info(line, width, elapsed)
        line = self.draw_sample_stats(line, width, elapsed)
        line = self.draw_efficiency_bar(line, width)
        line = self.draw_thread_status(line, width)
        line = self.draw_function_stats(
            line, width, kwargs.get("stats_list", [])
        )
        line = self.draw_top_functions(
            line, width, kwargs.get("stats_list", [])
        )

        # Show prominent finished banner if profiling is complete
        if self.collector.finished:
            line = self.draw_finished_banner(line, width)

        # Separator
        A_DIM = self.display.get_attr("A_DIM")
        separator = "─" * (width - 1)
        self.add_str(line, 0, separator[: width - 1], A_DIM)
        line += 1

        return line

    def format_uptime(self, elapsed):
        """Format elapsed time as uptime string."""
        uptime_sec = int(elapsed)
        hours = uptime_sec // 3600
        minutes = (uptime_sec % 3600) // 60
        seconds = uptime_sec % 60
        if hours > 0:
            return f"{hours}h{minutes:02d}m{seconds:02d}s"
        else:
            return f"{minutes}m{seconds:02d}s"

    def draw_header_info(self, line, width, elapsed):
        """Draw the header information line with PID, uptime, time, and interval."""
        # Draw title
        A_BOLD = self.display.get_attr("A_BOLD")
        title = "Tachyon Profiler"
        self.add_str(line, 0, title, A_BOLD | self.colors["cyan"])
        line += 1

        current_time = self.collector.current_time_display
        uptime = self.format_uptime(elapsed)

        # Calculate display refresh rate
        refresh_hz = (
            1.0 / self.collector.display_update_interval_sec if self.collector.display_update_interval_sec > 0 else 0
        )

        # Get current view mode and thread display
        if self.collector.view_mode == "ALL":
            thread_name = "ALL"
            thread_color = self.colors["green"]
        else:
            # PER_THREAD mode
            if self.collector.current_thread_index < len(
                self.collector.thread_ids
            ):
                thread_id = self.collector.thread_ids[
                    self.collector.current_thread_index
                ]
                num_threads = len(self.collector.thread_ids)
                thread_name = f"{thread_id} ({self.collector.current_thread_index + 1}/{num_threads})"
                thread_color = self.colors["magenta"]
            else:
                thread_name = "ALL"
                thread_color = self.colors["green"]

        header_parts = [
            ("PID: ", curses.A_BOLD),
            (f"{self.collector.pid}", self.colors["cyan"]),
            (" │ ", curses.A_DIM),
            ("Thread: ", curses.A_BOLD),
            (thread_name, thread_color),
            (" │ ", curses.A_DIM),
            ("Uptime: ", curses.A_BOLD),
            (uptime, self.colors["green"]),
            (" │ ", curses.A_DIM),
            ("Time: ", curses.A_BOLD),
            (current_time, self.colors["yellow"]),
            (" │ ", curses.A_DIM),
            ("Interval: ", curses.A_BOLD),
            (
                f"{self.collector.sample_interval_usec}µs",
                self.colors["magenta"],
            ),
            (" │ ", curses.A_DIM),
            ("Display: ", curses.A_BOLD),
            (f"{refresh_hz:.1f}Hz", self.colors["cyan"]),
        ]

        col = 0
        for text, attr in header_parts:
            if col < width - 1:
                self.add_str(line, col, text, attr)
                col += len(text)
        return line + 1

    def format_rate_with_units(self, rate_hz):
        """Format a rate in Hz with appropriate units (Hz, KHz, MHz)."""
        if rate_hz >= MICROSECONDS_PER_SECOND:
            return f"{rate_hz / MICROSECONDS_PER_SECOND:.1f}MHz"
        elif rate_hz >= 1_000:
            return f"{rate_hz / 1_000:.1f}KHz"
        else:
            return f"{rate_hz:.1f}Hz"

    def draw_sample_stats(self, line, width, elapsed):
        """Draw sample statistics with visual progress bar."""
        sample_rate = (
            self.collector.total_samples / elapsed if elapsed > 0 else 0
        )

        # Update max sample rate
        if sample_rate > self.collector.max_sample_rate:
            self.collector.max_sample_rate = sample_rate

        col = 0
        self.add_str(line, col, "Samples: ", curses.A_BOLD)
        col += 9
        self.add_str(
            line,
            col,
            f"{self.collector.total_samples:>8}",
            self.colors["cyan"],
        )
        col += 8
        self.add_str(
            line, col, f" total ({sample_rate:>7.1f}/s) ", curses.A_NORMAL
        )
        col += 23

        # Draw sample rate bar
        target_rate = (
            MICROSECONDS_PER_SECOND / self.collector.sample_interval_usec
        )

        # Show current/target ratio with percentage
        if sample_rate > 0 and target_rate > 0:
            percentage = min((sample_rate / target_rate) * 100, 100)
            current_formatted = self.format_rate_with_units(sample_rate)
            target_formatted = self.format_rate_with_units(target_rate)

            if percentage >= 99.5:  # Show 100% when very close
                rate_label = f" {current_formatted}/{target_formatted} (100%)"
            else:
                rate_label = f" {current_formatted}/{target_formatted} ({percentage:>4.1f}%)"
        else:
            target_formatted = self.format_rate_with_units(target_rate)
            rate_label = f" target: {target_formatted}"

        available_width = width - col - len(rate_label) - 3

        if available_width >= MIN_BAR_WIDTH:
            bar_width = min(MAX_SAMPLE_RATE_BAR_WIDTH, available_width)
            # Use target rate as the reference, with a minimum for scaling
            reference_rate = max(target_rate, MIN_SAMPLE_RATE_FOR_SCALING)
            normalized_rate = min(sample_rate / reference_rate, 1.0)
            bar_fill = int(normalized_rate * bar_width)

            bar = "["
            for i in range(bar_width):
                bar += "█" if i < bar_fill else "░"
            bar += "]"
            self.add_str(line, col, bar, self.colors["green"])
            col += len(bar)

            if col + len(rate_label) < width - 1:
                self.add_str(line, col + 1, rate_label, curses.A_DIM)
        return line + 1

    def draw_efficiency_bar(self, line, width):
        """Draw sample efficiency bar showing success/failure rates."""
        # total_samples = successful_samples + failed_samples, so percentages add to 100%
        total = max(1, self.collector.total_samples)
        success_pct = (self.collector.successful_samples / total) * 100
        failed_pct = (self.collector.failed_samples / total) * 100

        col = 0
        self.add_str(line, col, "Efficiency:", curses.A_BOLD)
        col += 11

        label = f" {success_pct:>5.2f}% good, {failed_pct:>5.2f}% failed"
        available_width = width - col - len(label) - 3

        if available_width >= MIN_BAR_WIDTH:
            bar_width = min(MAX_EFFICIENCY_BAR_WIDTH, available_width)
            success_fill = int((self.collector.successful_samples / total) * bar_width)
            failed_fill = bar_width - success_fill

            self.add_str(line, col, "[", curses.A_NORMAL)
            col += 1
            if success_fill > 0:
                self.add_str(
                    line, col, "█" * success_fill, self.colors["green"]
                )
                col += success_fill
            if failed_fill > 0:
                self.add_str(line, col, "█" * failed_fill, self.colors["red"])
                col += failed_fill
            self.add_str(line, col, "]", curses.A_NORMAL)
            col += 1

            self.add_str(line, col + 1, label, curses.A_NORMAL)
        return line + 1

    def _add_percentage_stat(
        self, line, col, value, label, color, add_separator=False
    ):
        """Add a percentage stat to the display.

        Args:
            line: Line number
            col: Starting column
            value: Percentage value
            label: Label text
            color: Color attribute
            add_separator: Whether to add separator before the stat

        Returns:
            Updated column position
        """
        if add_separator:
            self.add_str(line, col, " │ ", curses.A_DIM)
            col += 3

        self.add_str(line, col, f"{value:>4.1f}", color)
        col += 4
        self.add_str(line, col, f"% {label}", curses.A_NORMAL)
        col += len(label) + 2

        return col

    def draw_thread_status(self, line, width):
        """Draw thread status statistics and GC information."""
        # Get status counts for current view mode
        thread_data = self.collector._get_current_thread_data()
        status_counts = thread_data.as_status_dict() if thread_data else self.collector.thread_status_counts

        # Calculate percentages
        total_threads = max(1, status_counts["total"])
        pct_on_gil = (status_counts["has_gil"] / total_threads) * 100
        pct_off_gil = 100.0 - pct_on_gil
        pct_gil_requested = (status_counts["gil_requested"] / total_threads) * 100
        pct_exception = (status_counts.get("has_exception", 0) / total_threads) * 100

        # Get GC percentage based on view mode
        if thread_data:
            total_samples = max(1, thread_data.sample_count)
            pct_gc = (thread_data.gc_frame_samples / total_samples) * 100
        else:
            # Use total_samples for GC percentage since gc_frame_samples is tracked
            # across ALL samples (via thread status), not just successful ones
            total_samples = max(1, self.collector.total_samples)
            pct_gc = (self.collector.gc_frame_samples / total_samples) * 100

        col = 0
        self.add_str(line, col, "Threads:   ", curses.A_BOLD)
        col += 11

        # Show GIL stats only if mode is not GIL (GIL mode filters to only GIL holders)
        if self.collector.mode != PROFILING_MODE_GIL:
            col = self._add_percentage_stat(
                line, col, pct_on_gil, "on gil", self.colors["green"]
            )
            col = self._add_percentage_stat(
                line,
                col,
                pct_off_gil,
                "off gil",
                self.colors["red"],
                add_separator=True,
            )

        # Show "waiting for gil" only if mode is not GIL
        if self.collector.mode != PROFILING_MODE_GIL and col < width - 30:
            col = self._add_percentage_stat(
                line,
                col,
                pct_gil_requested,
                "waiting for gil",
                self.colors["yellow"],
                add_separator=True,
            )

        # Show exception stats
        if col < width - 15:
            col = self._add_percentage_stat(
                line,
                col,
                pct_exception,
                "exc",
                self.colors["red"],
                add_separator=(col > 11),
            )

        # Always show GC stats
        if col < width - 15:
            col = self._add_percentage_stat(
                line,
                col,
                pct_gc,
                "GC",
                self.colors["magenta"],
                add_separator=(col > 11),
            )

        return line + 1

    def draw_function_stats(self, line, width, stats_list):
        """Draw function statistics summary."""
        result_set = self.collector._get_current_result_source()
        total_funcs = len(result_set)
        funcs_shown = len(stats_list)
        executing_funcs = sum(
            1 for f in result_set.values() if f.get("direct_calls", 0) > 0
        )
        stack_only = total_funcs - executing_funcs

        col = 0
        self.add_str(line, col, "Functions: ", curses.A_BOLD)
        col += 11
        self.add_str(line, col, f"{total_funcs:>5}", self.colors["cyan"])
        col += 5
        self.add_str(line, col, " total", curses.A_NORMAL)
        col += 6

        if col < width - 25:
            self.add_str(line, col, " │ ", curses.A_DIM)
            col += 3
            self.add_str(
                line, col, f"{executing_funcs:>5}", self.colors["green"]
            )
            col += 5
            self.add_str(line, col, " exec", curses.A_NORMAL)
            col += 5

        if col < width - 25:
            self.add_str(line, col, " │ ", curses.A_DIM)
            col += 3
            self.add_str(line, col, f"{stack_only:>5}", self.colors["yellow"])
            col += 5
            self.add_str(line, col, " stack", curses.A_NORMAL)
            col += 6

        if col < width - 20:
            self.add_str(line, col, " │ ", curses.A_DIM)
            col += 3
            self.add_str(
                line, col, f"{funcs_shown:>5}", self.colors["magenta"]
            )
            col += 5
            self.add_str(line, col, " shown", curses.A_NORMAL)
        return line + 1

    def draw_top_functions(self, line, width, stats_list):
        """Draw top N hottest functions."""
        col = 0
        self.add_str(
            line,
            col,
            f"Top {TOP_FUNCTIONS_DISPLAY_COUNT}:     ",
            curses.A_BOLD,
        )
        col += 11

        top_by_samples = sorted(
            stats_list, key=lambda x: x["direct_calls"], reverse=True
        )
        emojis = ["🥇", "🥈", "🥉"]
        medal_colors = [
            self.colors["red"],
            self.colors["yellow"],
            self.colors["green"],
        ]

        displayed = 0
        for func_data in top_by_samples:
            if displayed >= TOP_FUNCTIONS_DISPLAY_COUNT:
                break
            if col >= width - 20:
                break
            if func_data["direct_calls"] == 0:
                continue

            func_name = func_data["func"][2]
            func_pct = func_data["sample_pct"]

            # Medal emoji
            if col + 3 < width - 15:
                self.add_str(
                    line, col, emojis[displayed] + " ", medal_colors[displayed]
                )
                col += 3

            # Function name (truncate to fit)
            available_for_name = width - col - 15
            max_name_len = min(25, max(5, available_for_name))
            if len(func_name) > max_name_len:
                func_name = func_name[: max_name_len - 3] + "..."

            if col + len(func_name) < width - 10:
                self.add_str(line, col, func_name, medal_colors[displayed])
                col += len(func_name)

                pct_str = (
                    f" ({func_pct:.1f}%)"
                    if func_pct >= 0.1
                    else f" ({func_data['direct_calls']})"
                )
                self.add_str(line, col, pct_str, curses.A_DIM)
                col += len(pct_str)

                displayed += 1

                if displayed < 3 and col < width - 30:
                    self.add_str(line, col, " │ ", curses.A_DIM)
                    col += 3

        if displayed == 0 and col < width - 25:
            self.add_str(line, col, "(collecting samples...)", curses.A_DIM)

        return line + 1

    def draw_finished_banner(self, line, width):
        """Draw a prominent banner when profiling is finished."""
        A_REVERSE = self.display.get_attr("A_REVERSE")
        A_BOLD = self.display.get_attr("A_BOLD")

        # Add blank line for separation
        line += 1

        # Create the banner message
        message = "  ✓ PROFILING COMPLETE - Final Results Below - Press 'q' to Quit  "

        # Center the message and fill the width with reverse video
        if len(message) < width - 1:
            padding_total = width - len(message) - 1
            padding_left = padding_total // 2
            padding_right = padding_total - padding_left
            full_message = " " * padding_left + message + " " * padding_right
        else:
            full_message = message[: width - 1]

        # Draw the banner with reverse video and bold
        self.add_str(
            line, 0, full_message, A_REVERSE | A_BOLD | self.colors["green"]
        )
        line += 1

        # Add blank line for separation
        line += 1

        return line


class TableWidget(Widget):
    """Widget for rendering column headers and data rows."""

    def __init__(self, display, colors, collector):
        """
        Initialize table widget.

        Args:
            display: DisplayInterface implementation
            colors: Dictionary of color attributes
            collector: Reference to LiveStatsCollector for accessing stats
        """
        super().__init__(display, colors)
        self.collector = collector

    def render(self, line, width, **kwargs):
        """
        Render column headers and data rows.

        Args:
            line: Starting line number
            width: Available width
            kwargs: Must contain 'height' and 'stats_list' keys

        Returns:
            Next available line number
        """
        height = kwargs["height"]
        stats_list = kwargs["stats_list"]

        # Draw column headers
        line, show_sample_pct, show_tottime, show_cumul_pct, show_cumtime = (
            self.draw_column_headers(line, width)
        )
        column_flags = (
            show_sample_pct,
            show_tottime,
            show_cumul_pct,
            show_cumtime,
        )

        # Draw data rows
        line = self.draw_stats_rows(
            line, height, width, stats_list, column_flags
        )

        return line

    def draw_column_headers(self, line, width):
        """Draw column headers with sort indicators."""
        # Determine which columns to show based on width
        show_sample_pct = width >= WIDTH_THRESHOLD_SAMPLE_PCT
        show_tottime = width >= WIDTH_THRESHOLD_TOTTIME
        show_cumul_pct = width >= WIDTH_THRESHOLD_CUMUL_PCT
        show_cumtime = width >= WIDTH_THRESHOLD_CUMTIME

        sorted_header = self.colors["sorted_header"]
        normal_header = self.colors["normal_header"]

        # Determine which column is sorted
        sort_col = {
            "nsamples": 0,
            "sample_pct": 1,
            "tottime": 2,
            "cumul_pct": 3,
            "cumtime": 4,
        }.get(self.collector.sort_by, -1)

        # Build the full header line first, then draw it
        # This avoids gaps between columns when using reverse video
        header_parts = []
        col = 0

        # Column 0: nsamples
        text = f"{'▼nsamples' if sort_col == 0 else 'nsamples':>13}  "
        header_parts.append((col, text, sorted_header if sort_col == 0 else normal_header))
        col += 15

        # Column 1: sample %
        if show_sample_pct:
            text = f"{'▼%' if sort_col == 1 else '%':>5}  "
            header_parts.append((col, text, sorted_header if sort_col == 1 else normal_header))
            col += 7

        # Column 2: tottime
        if show_tottime:
            text = f"{'▼tottime' if sort_col == 2 else 'tottime':>10}  "
            header_parts.append((col, text, sorted_header if sort_col == 2 else normal_header))
            col += 12

        # Column 3: cumul %
        if show_cumul_pct:
            text = f"{'▼%' if sort_col == 3 else '%':>5}  "
            header_parts.append((col, text, sorted_header if sort_col == 3 else normal_header))
            col += 7

        # Column 4: cumtime
        if show_cumtime:
            text = f"{'▼cumtime' if sort_col == 4 else 'cumtime':>10}  "
            header_parts.append((col, text, sorted_header if sort_col == 4 else normal_header))
            col += 12

        # Remaining headers
        if col < width - 15:
            remaining_space = width - col - 1
            func_width = min(
                MAX_FUNC_NAME_WIDTH,
                max(MIN_FUNC_NAME_WIDTH, remaining_space // 2),
            )
            text = f"{'function':<{func_width}}  "
            header_parts.append((col, text, normal_header))
            col += func_width + 2

            if col < width - 10:
                file_text = "file:line"
                padding = width - col - len(file_text)
                text = file_text + " " * max(0, padding)
                header_parts.append((col, text, normal_header))

        # Draw full-width background first
        self.add_str(line, 0, " " * (width - 1), normal_header)

        # Draw each header part on top
        for col_pos, text, attr in header_parts:
            self.add_str(line, col_pos, text.rstrip(), attr)

        return (
            line + 1,
            show_sample_pct,
            show_tottime,
            show_cumul_pct,
            show_cumtime,
        )

    def draw_stats_rows(self, line, height, width, stats_list, column_flags):
        """Draw the statistics data rows."""
        show_sample_pct, show_tottime, show_cumul_pct, show_cumtime = (
            column_flags
        )

        # Get color attributes
        color_file = self.colors.get("color_file", curses.A_NORMAL)
        color_func = self.colors.get("color_func", curses.A_NORMAL)

        # Get trend tracker for color decisions
        trend_tracker = self.collector._trend_tracker

        # Check if opcode mode is enabled for row selection highlighting
        show_opcodes = getattr(self.collector, 'show_opcodes', False)
        selected_row = getattr(self.collector, 'selected_row', 0)
        scroll_offset = getattr(self.collector, 'scroll_offset', 0) if show_opcodes else 0
        A_REVERSE = self.display.get_attr("A_REVERSE")
        A_BOLD = self.display.get_attr("A_BOLD")

        # Reserve space for opcode panel when enabled
        opcode_panel_height = OPCODE_PANEL_HEIGHT if show_opcodes else 0

        # Apply scroll offset when in opcode mode
        display_stats = stats_list[scroll_offset:] if show_opcodes else stats_list

        for row_idx, stat in enumerate(display_stats):
            if line >= height - FOOTER_LINES - opcode_panel_height:
                break

            func = stat["func"]
            direct_calls = stat["direct_calls"]
            cumulative_calls = stat["cumulative_calls"]
            total_time = stat["total_time"]
            cumulative_time = stat["cumulative_time"]
            sample_pct = stat["sample_pct"]
            cum_pct = stat["cumul_pct"]
            trends = stat.get("trends", {})

            # Check if this row is selected
            is_selected = show_opcodes and row_idx == selected_row

            # Helper function to get trend color
            def get_trend_color(column_name):
                if is_selected:
                    return A_REVERSE | A_BOLD
                trend = trends.get(column_name, "stable")
                if trend_tracker is not None and trend_tracker.enabled:
                    return trend_tracker.get_color(trend)
                return curses.A_NORMAL

            filename, lineno, funcname = func[0], func[1], func[2]
            samples_str = f"{direct_calls}/{cumulative_calls}"
            col = 0

            # Fill entire row with reverse video background for selected row
            if is_selected:
                self.add_str(line, 0, " " * (width - 1), A_REVERSE | A_BOLD)

            # Show selection indicator when opcode panel is enabled
            if show_opcodes:
                if is_selected:
                    self.add_str(line, col, "►", A_REVERSE | A_BOLD)
                else:
                    self.add_str(line, col, " ", curses.A_NORMAL)
                col += 2

            # Samples column - apply trend color based on nsamples trend
            nsamples_color = get_trend_color("nsamples")
            self.add_str(line, col, f"{samples_str:>13}  ", nsamples_color)
            col += 15

            # Sample % column
            if show_sample_pct:
                sample_pct_color = get_trend_color("sample_pct")
                self.add_str(line, col, f"{sample_pct:>5.1f}  ", sample_pct_color)
                col += 7

            # Total time column
            if show_tottime:
                tottime_color = get_trend_color("tottime")
                self.add_str(line, col, f"{total_time:>10.3f}  ", tottime_color)
                col += 12

            # Cumul % column
            if show_cumul_pct:
                cumul_pct_color = get_trend_color("cumul_pct")
                self.add_str(line, col, f"{cum_pct:>5.1f}  ", cumul_pct_color)
                col += 7

            # Cumul time column
            if show_cumtime:
                cumtime_color = get_trend_color("cumtime")
                self.add_str(line, col, f"{cumulative_time:>10.3f}  ", cumtime_color)
                col += 12

            # Function name column
            if col < width - 15:
                remaining_space = width - col - 1
                func_width = min(
                    MAX_FUNC_NAME_WIDTH,
                    max(MIN_FUNC_NAME_WIDTH, remaining_space // 2),
                )

                func_display = funcname
                if len(funcname) > func_width:
                    func_display = funcname[: func_width - 3] + "..."
                func_display = f"{func_display:<{func_width}}"
                func_color = A_REVERSE | A_BOLD if is_selected else color_func
                self.add_str(line, col, func_display, func_color)
                col += func_width + 2

                # File:line column
                if col < width - 10:
                    simplified_path = self.collector.simplify_path(filename)
                    file_line = f"{simplified_path}:{lineno}"
                    remaining_width = width - col - 1
                    file_color = A_REVERSE | A_BOLD if is_selected else color_file
                    self.add_str(
                        line, col, file_line[:remaining_width], file_color
                    )

            line += 1

        return line


class FooterWidget(Widget):
    """Widget for rendering the footer section (legend and controls)."""

    def __init__(self, display, colors, collector):
        """
        Initialize footer widget.

        Args:
            display: DisplayInterface implementation
            colors: Dictionary of color attributes
            collector: Reference to LiveStatsCollector for accessing state
        """
        super().__init__(display, colors)
        self.collector = collector

    def render(self, line, width, **kwargs):
        """
        Render the footer at the specified position.

        Args:
            line: Starting line number (should be height - 2)
            width: Available width

        Returns:
            Next available line number
        """
        A_DIM = self.display.get_attr("A_DIM")
        A_BOLD = self.display.get_attr("A_BOLD")

        # Legend line
        legend = "nsamples: direct/cumulative (direct=executing, cumulative=on stack)"
        self.add_str(line, 0, legend[: width - 1], A_DIM)
        line += 1

        # Controls line with status
        sort_names = {
            "tottime": "Total Time",
            "nsamples": "Direct Samples",
            "cumtime": "Cumulative Time",
            "sample_pct": "Sample %",
            "cumul_pct": "Cumulative %",
        }
        sort_display = sort_names.get(
            self.collector.sort_by, self.collector.sort_by
        )

        # Build status indicators
        status = []
        if self.collector.finished:
            status.append("[PROFILING FINISHED - Press 'q' to quit]")
        elif self.collector.paused:
            status.append("[PAUSED]")
        if self.collector.filter_pattern:
            status.append(
                f"[Filter: {self.collector.filter_pattern} (c to clear)]"
            )
        # Show trend colors status if disabled
        if self.collector._trend_tracker is not None and not self.collector._trend_tracker.enabled:
            status.append("[Trend colors: OFF]")
        status_str = " ".join(status) + " " if status else ""

        if self.collector.finished:
            footer = f"{status_str}"
        else:
            footer = f"{status_str}Sort: {sort_display} | 't':mode 'x':trends ←→:thread 'h':help 'q':quit"
        self.add_str(
            line,
            0,
            footer[: width - 1],
            A_BOLD
            if (self.collector.paused or self.collector.finished)
            else A_DIM,
        )

        return line + 1

    def render_filter_input_prompt(self, line, width):
        """Draw the filter input prompt at the bottom of the screen."""
        A_BOLD = self.display.get_attr("A_BOLD")
        A_REVERSE = self.display.get_attr("A_REVERSE")

        # Draw prompt on last line
        prompt = f"Function filter: {self.collector.filter_input_buffer}_"
        self.add_str(line, 0, prompt[: width - 1], A_REVERSE | A_BOLD)


class HelpWidget(Widget):
    """Widget for rendering the help screen overlay."""

    def render(self, line, width, **kwargs):
        """
        Render the help screen.

        Args:
            line: Starting line number (ignored, help is centered)
            width: Available width
            kwargs: Must contain 'height' key

        Returns:
            Next available line number (not used for overlays)
        """
        height = kwargs["height"]
        A_BOLD = self.display.get_attr("A_BOLD")
        A_NORMAL = self.display.get_attr("A_NORMAL")

        help_lines = [
            ("Tachyon Profiler - Interactive Commands", A_BOLD),
            ("", A_NORMAL),
            ("Navigation & Display:", A_BOLD),
            ("  s           - Cycle through sort modes (forward)", A_NORMAL),
            ("  S           - Cycle through sort modes (backward)", A_NORMAL),
            ("  t           - Toggle view mode (ALL / per-thread)", A_NORMAL),
            ("  x           - Toggle trend colors (on/off)", A_NORMAL),
            ("  j/k or ↑/↓  - Select next/previous function (--opcodes)", A_NORMAL),
            ("  ← / →       - Cycle through threads", A_NORMAL),
            ("  +           - Faster display refresh rate", A_NORMAL),
            ("  -           - Slower display refresh rate", A_NORMAL),
            ("", A_NORMAL),
            ("Control:", A_BOLD),
            ("  p           - Freeze display (snapshot)", A_NORMAL),
            ("  r           - Reset all statistics", A_NORMAL),
            ("", A_NORMAL),
            ("Filtering:", A_BOLD),
            ("  /           - Enter function filter (substring)", A_NORMAL),
            ("  c           - Clear filter", A_NORMAL),
            ("  ESC         - Cancel filter input", A_NORMAL),
            ("", A_NORMAL),
            ("Other:", A_BOLD),
            ("  h or ?      - Show/hide this help", A_NORMAL),
            ("  q           - Quit profiler", A_NORMAL),
            ("", A_NORMAL),
            ("Press any key to close this help screen", A_BOLD),
        ]

        start_line = (height - len(help_lines)) // 2
        for i, (text, attr) in enumerate(help_lines):
            if start_line + i < height - 1:
                col = 2  # Left-align with small margin
                self.add_str(start_line + i, col, text[: width - 3], attr)

        return line  # Not used for overlays


class OpcodePanel(Widget):
    """Widget for displaying opcode statistics for a selected function."""

    def __init__(self, display, colors, collector):
        super().__init__(display, colors)
        self.collector = collector

    def render(self, line, width, **kwargs):
        """Render opcode statistics panel.

        Args:
            line: Starting line number
            width: Available width
            kwargs: Must contain 'stats_list', 'height'

        Returns:
            Next available line number
        """
        stats_list = kwargs.get("stats_list", [])
        height = kwargs.get("height", 24)
        selected_row = self.collector.selected_row
        scroll_offset = getattr(self.collector, 'scroll_offset', 0)

        A_BOLD = self.display.get_attr("A_BOLD")
        A_NORMAL = self.display.get_attr("A_NORMAL")
        color_cyan = self.colors.get("color_cyan", A_NORMAL)
        color_yellow = self.colors.get("color_yellow", A_NORMAL)
        color_magenta = self.colors.get("color_magenta", A_NORMAL)

        # Get the selected function from stats_list (accounting for scroll)
        actual_index = scroll_offset + selected_row
        if not stats_list or actual_index >= len(stats_list):
            self.add_str(line, 0, "No function selected (use j/k to select)", A_NORMAL)
            return line + 1

        selected_stat = stats_list[actual_index]
        func = selected_stat["func"]
        filename, lineno, funcname = func

        # Get opcode stats for this function
        opcode_stats = self.collector.opcode_stats.get(func, {})

        if not opcode_stats:
            self.add_str(line, 0, f"No opcode data for {funcname}() (requires --opcodes)", A_NORMAL)
            return line + 1

        # Sort opcodes by count
        sorted_opcodes = sorted(opcode_stats.items(), key=lambda x: -x[1])
        total_opcode_samples = sum(opcode_stats.values())

        # Draw header
        header = f"─── Opcodes for {funcname}() "
        header += "─" * max(0, width - len(header) - 1)
        self.add_str(line, 0, header[:width-1], color_cyan | A_BOLD)
        line += 1

        # Calculate max samples for bar scaling
        max_count = sorted_opcodes[0][1] if sorted_opcodes else 1

        # Draw opcode rows (limit to available space)
        max_rows = min(8, height - line - 3)  # Leave room for footer
        bar_width = 20

        for i, (opcode_num, count) in enumerate(sorted_opcodes[:max_rows]):
            if line >= height - 3:
                break

            opcode_info = get_opcode_info(opcode_num)
            is_specialized = opcode_info["is_specialized"]
            name_display = format_opcode(opcode_num)

            pct = (count / total_opcode_samples * 100) if total_opcode_samples > 0 else 0

            # Draw bar
            bar_fill = int((count / max_count) * bar_width) if max_count > 0 else 0
            bar = "█" * bar_fill + "░" * (bar_width - bar_fill)

            # Format: [████████░░░░] LOAD_ATTR  45.2% (1234)
            # Specialized opcodes shown in magenta, base opcodes in yellow
            name_color = color_magenta if is_specialized else color_yellow

            row_text = f"[{bar}] {name_display:<35} {pct:>5.1f}% ({count:>6})"
            self.add_str(line, 2, row_text[:width-3], name_color)
            line += 1

        # Show "..." if more opcodes exist
        if len(sorted_opcodes) > max_rows:
            remaining = len(sorted_opcodes) - max_rows
            self.add_str(line, 2, f"... and {remaining} more opcodes", A_NORMAL)
            line += 1

        return line
