#!/usr/bin/env python3
"""
UART Widget (PyQt6)
===================
GUI for UART port control with 4 modes:
- Request-Response: Send command, get response (packet-based)
- Polling: Continuous packet detection with grace period
- Listening: Raw stream, no stop condition
- PGKomm2: V-ZUG PGKomm2 protocol (DD 22 framed, length-delimited)
- **Listening with Packet Detection (optional)**

Features:
- Port configuration (device, baud, etc.)
- Mode selection (fixed after connect)
- Packet detection configuration (optional, for listening mode)
- Buffer status (embedded compact widget)
- Packet history (configurable)
- Send panel with hex validation
- Console logging for all events
- Idle check (can't send while busy)
- Theme-ready (global theme controlled)

Author: Kynsight
Version: 2.3.0 - Added PGKomm2 protocol support
"""

from PyQt6.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
    QLabel, QComboBox, QPushButton, QLineEdit,
    QGroupBox, QTextEdit, QSpinBox, QCheckBox
)
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal
from PyQt6.QtGui import QFont

# Import UART core and buffer widget
from uart.uart_kit.uart_core import *
from buffer_kit.buffer_widget_compact import CompactBufferWidget
from buffer_kit.circular_buffer import cb_overflows, cb_debug_info, cb_fill_bytes, cb_capacity


class UARTCommandWorker(QThread):
    """
    Worker thread for non-blocking UART send/receive operations.

    Prevents GUI freezing when waiting for UART responses.
    """

    # Signals
    finished = pyqtSignal(object, object)  # (status, packet) - Status enum, PacketInfo

    def __init__(self, port, data):
        super().__init__()
        self.port = port
        self.data = data

    def run(self):
        """Execute UART send/receive in background thread."""
        try:
            status, packet = uart_send_and_receive(self.port, self.data)
            self.finished.emit(status, packet)
        except Exception as e:
            # Emit error status - ALWAYS emit to ensure cleanup
            self.finished.emit(Status.IO_ERROR, None)


# UARTPGKomm2Worker removed - PGKomm2 now calls function directly (blocking is OK)
# Simplified threading: only one reader thread, all modes read from circular buffer


class UARTWidget(QWidget):
    """
    UART port control widget.
    
    Signals:
        packet_received: Emitted when packet detected (mode: request/poll)
        data_received: Emitted when data read (mode: listening)
        connection_changed: Emitted on connect/disconnect
    """
    
    # Qt signals (use 'object' for custom types)
    packet_received = pyqtSignal(object)  # PacketInfo object
    data_received = pyqtSignal(bytes)
    connection_changed = pyqtSignal(bool)  # True=connected
    data_sent = pyqtSignal(bytes)  # TX data (for data monitor)
    data_received_display = pyqtSignal(bytes, str)  # RX data (for data monitor), info string
    
    def __init__(self, parent=None):
        super().__init__(parent)
        
        # Port state
        self.port = None
        self.is_connected = False
        
        # Packet history
        self.packet_history = []
        self.max_packet_history = 200
        
        # Command execution state (failsafe)
        self._command_in_progress = False  # True when waiting for response
        self._command_worker = None  # Worker thread for non-blocking sends

        # Overflow tracking (for Data Monitor warnings)
        self._last_overflow_count = 0

        # Packet detection state (NEW)
        self._packet_detection_enabled = False
        self._detected_packet_count = 0

        # Polling mode timeout tracking (for auto-stop)
        self._last_packet_time = 0.0
        self._polling_start_time = 0.0
        
        # Build UI
        self._init_ui()
        self._setup_timers()
    
    def _init_ui(self):
        """Create user interface."""
        layout = QVBoxLayout()
        self.setLayout(layout)
        
        # Configuration section (compact, horizontal)
        config_group = self._create_config_section()
        layout.addWidget(config_group)
        
        # Status + Buffer on same line
        status_buffer_layout = QHBoxLayout()
        
        # Status label (left)
        self.lbl_status = QLabel("Status: Disconnected")
        status_buffer_layout.addWidget(self.lbl_status, stretch=2)
        
        # Buffer status - compact on right
        buffer_group = self._create_buffer_section()
        status_buffer_layout.addWidget(buffer_group, stretch=1)
        
        layout.addLayout(status_buffer_layout)
        
        # Data monitor (shows everything: packets, errors, info)
        display_group = self._create_display_section()
        layout.addWidget(display_group)
        
        # Send panel
        send_group = self._create_send_section()
        layout.addWidget(send_group)
        
        self.setMinimumWidth(800)  # Wider for horizontal layout
    
    # =========================================================================
    # UI Creation
    # =========================================================================
    
    def _create_config_section(self):
        """Port configuration controls - TWO ROWS, left-aligned."""
        group = QGroupBox("UART Configuration")
        
        # Vertical layout to hold 2 rows
        main_layout = QVBoxLayout()
        group.setLayout(main_layout)
        
        # =====================================================================
        # ROW 1: Port, Baud, Data, Stop, Parity
        # =====================================================================
        row1 = QHBoxLayout()
        row1.setAlignment(Qt.AlignmentFlag.AlignLeft)
        
        # Port
        row1.addWidget(QLabel("Port:"))
        self.combo_port = QComboBox()
        self.combo_port.setMinimumWidth(150)
        row1.addWidget(self.combo_port)
        
        self.btn_refresh = QPushButton("🔄")
        self.btn_refresh.setMaximumWidth(30)
        self.btn_refresh.clicked.connect(self._refresh_ports)
        row1.addWidget(self.btn_refresh)
        
        # Baud
        row1.addWidget(QLabel("Baud:"))
        self.combo_baud = QComboBox()
        self.combo_baud.addItems(["9600", "19200", "38400", "57600", "115200", "230400", "256000"])
        self.combo_baud.setCurrentText("115200")
        self.combo_baud.setMinimumWidth(90)
        row1.addWidget(self.combo_baud)
        
        # Data bits
        row1.addWidget(QLabel("Data:"))
        self.combo_databits = QComboBox()
        self.combo_databits.addItems(["7", "8"])
        self.combo_databits.setCurrentText("8")
        self.combo_databits.setMaximumWidth(50)
        row1.addWidget(self.combo_databits)
        
        # Stop bits
        row1.addWidget(QLabel("Stop:"))
        self.combo_stopbits = QComboBox()
        self.combo_stopbits.addItems(["1", "2"])
        self.combo_stopbits.setMaximumWidth(50)
        row1.addWidget(self.combo_stopbits)
        
        # Parity
        row1.addWidget(QLabel("Parity:"))
        self.combo_parity = QComboBox()
        self.combo_parity.addItems(["None", "Even", "Odd"])
        self.combo_parity.setCurrentIndex(1)  # Default: Even (for PGKomm2)
        self.combo_parity.setMaximumWidth(80)
        row1.addWidget(self.combo_parity)
        
        # Stretch at end
        row1.addStretch()
        
        main_layout.addLayout(row1)
        
        # =====================================================================
        # ROW 2: Mode, Stop, Term, Timeout, Grace, Connect/Disconnect
        # =====================================================================
        row2 = QHBoxLayout()
        row2.setAlignment(Qt.AlignmentFlag.AlignLeft)
        
        # Mode
        row2.addWidget(QLabel("Mode:"))
        self.combo_mode = QComboBox()
        self.combo_mode.addItems(["Request-Response", "Polling", "Listening", "PGKomm2"])
        self.combo_mode.setCurrentIndex(3)  # Default: PGKomm2
        self.combo_mode.currentIndexChanged.connect(self._on_mode_changed)
        self.combo_mode.setMinimumWidth(160)
        row2.addWidget(self.combo_mode)
        
        # Add spacing to separate from next section
        row2.addSpacing(65)
        
        # Stop condition
        row2.addWidget(QLabel("Stop:"))
        self.combo_stop = QComboBox()
        self.combo_stop.addItems(["Timeout", "Terminator"])
        self.combo_stop.setMaximumWidth(100)
        row2.addWidget(self.combo_stop)
        
        # Terminator
        row2.addWidget(QLabel("Term:"))
        self.edit_terminator = QLineEdit("0x0A")
        self.edit_terminator.setMaximumWidth(60)
        row2.addWidget(self.edit_terminator)
        
        # Timeout
        row2.addWidget(QLabel("Timeout:"))
        self.spin_timeout = QSpinBox()
        self.spin_timeout.setRange(10, 10000)
        self.spin_timeout.setValue(150)
        self.spin_timeout.setSuffix(" ms")
        self.spin_timeout.setMaximumWidth(90)
        row2.addWidget(self.spin_timeout)
        
        # Grace
        row2.addWidget(QLabel("Grace:"))
        self.spin_grace = QSpinBox()
        self.spin_grace.setRange(10, 10000)
        self.spin_grace.setValue(150)
        self.spin_grace.setSuffix(" ms")
        self.spin_grace.setMaximumWidth(90)
        row2.addWidget(self.spin_grace)

        # Capture window (for PGKomm2 mode) - HIDDEN, automatic detection
        # Total timeout for command/response cycle (stops on HP detection)
        self.spin_silence = QSpinBox()
        self.spin_silence.setRange(10, 200)
        self.spin_silence.setValue(30)  # 30ms timeout (spec <15ms + margin for real-world conditions)
        self.spin_silence.setVisible(False)  # Hidden - automatic

        # Connect/Disconnect
        self.btn_connect = QPushButton("Connect")
        self.btn_connect.clicked.connect(self._on_connect)
        row2.addWidget(self.btn_connect)
        
        self.btn_disconnect = QPushButton("Disconnect")
        self.btn_disconnect.clicked.connect(self._on_disconnect)
        self.btn_disconnect.setEnabled(False)
        row2.addWidget(self.btn_disconnect)
        
        # Stretch at end
        row2.addStretch()
        
        main_layout.addLayout(row2)
        
        # =====================================================================
        # ROW 3: Packet Detection (Optional, only for Listening mode)
        # =====================================================================
        row3 = QHBoxLayout()
        row3.setAlignment(Qt.AlignmentFlag.AlignLeft)
        
        # Packet detection enable
        self.check_packet_detect = QCheckBox("Enable Packet Detection")
        self.check_packet_detect.setToolTip("Detect packets in listening mode with timestamps")
        self.check_packet_detect.stateChanged.connect(self._on_packet_detect_toggled)
        row3.addWidget(self.check_packet_detect)
        
        # Start marker
        row3.addWidget(QLabel("Start:"))
        self.edit_packet_start = QLineEdit("EF FE")
        self.edit_packet_start.setMaximumWidth(80)
        self.edit_packet_start.setToolTip("Start marker (hex, e.g., EF FE)")
        row3.addWidget(self.edit_packet_start)
        
        # Packet length
        row3.addWidget(QLabel("Length:"))
        self.spin_packet_length = QSpinBox()
        self.spin_packet_length.setRange(1, 1024)
        self.spin_packet_length.setValue(14)  # Default: 14 bytes (EF FE + 11 data + EE)
        self.spin_packet_length.setSuffix(" B")
        self.spin_packet_length.setMaximumWidth(80)
        self.spin_packet_length.setToolTip("Total packet length in bytes (including start+data+end)")
        row3.addWidget(self.spin_packet_length)
        
        # End marker
        row3.addWidget(QLabel("End:"))
        self.edit_packet_end = QLineEdit("EE")
        self.edit_packet_end.setMaximumWidth(60)
        self.edit_packet_end.setToolTip("End marker (hex, e.g., EE)")
        row3.addWidget(self.edit_packet_end)
        
        # Packet count label (shows detected packets)
        self.lbl_packet_count = QLabel("Packets: 0")
        self.lbl_packet_count.setStyleSheet("color: gray; font-weight: bold;")
        row3.addWidget(self.lbl_packet_count)

        # Stretch at end
        row3.addStretch()

        # Initially disabled (only for Listening mode)
        self.check_packet_detect.setEnabled(False)
        self._on_packet_detect_toggled(0)  # Disable fields

        main_layout.addLayout(row3)
        
        # Update UI based on initial mode
        self._on_mode_changed(0)
        
        return group
    
    def _create_buffer_section(self):
        """Buffer status display."""
        group = QGroupBox("Buffer Status")
        layout = QVBoxLayout()
        group.setLayout(layout)
        
        # Placeholder (replaced with CompactBufferWidget when connected)
        self.buffer_widget = QLabel("Not connected")
        layout.addWidget(self.buffer_widget)
        
        # Clear buffer button (only visible when connected)
        self.btn_clear_buffer = QPushButton("Clear Buffer")
        self.btn_clear_buffer.clicked.connect(self._on_clear_buffer)
        self.btn_clear_buffer.setEnabled(False)
        self.btn_clear_buffer.setMaximumHeight(25)
        layout.addWidget(self.btn_clear_buffer)
        
        return group
    
    def _create_display_section(self):
        """Data monitor - shows packets (RX/TX), errors, info with colors."""
        group = QGroupBox("Data Monitor (TX=Green, RX=Blue, Errors=Red, Info=Gray)")
        layout = QVBoxLayout()
        group.setLayout(layout)
        
        # Display area FIRST (takes most vertical space)
        self.text_display = QTextEdit()
        self.text_display.setReadOnly(True)
        self.text_display.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)  # Enable word wrap
        font = QFont("Courier New", 9)
        self.text_display.setFont(font)
        layout.addWidget(self.text_display)
        
        # Controls at BOTTOM (single line, compact)
        ctrl_layout = QHBoxLayout()
        
        ctrl_layout.addWidget(QLabel("History:"))
        self.spin_history = QSpinBox()
        self.spin_history.setRange(10, 500)
        self.spin_history.setValue(200)
        self.spin_history.valueChanged.connect(self._on_history_changed)
        self.spin_history.setMaximumWidth(70)
        ctrl_layout.addWidget(self.spin_history)
        
        self.btn_clear_display = QPushButton("Clear")
        self.btn_clear_display.setMaximumWidth(80)
        self.btn_clear_display.clicked.connect(self._clear_display)
        ctrl_layout.addWidget(self.btn_clear_display)
        
        ctrl_layout.addStretch()
        
        layout.addLayout(ctrl_layout)
        
        return group
    
    def _create_send_section(self):
        """Send data controls with format selector."""
        group = QGroupBox("Send Data")
        layout = QHBoxLayout()
        group.setLayout(layout)
        
        layout.addWidget(QLabel("Data:"))
        
        self.edit_send = QLineEdit()
        self.edit_send.returnPressed.connect(self._on_send)  # Enter key sends
        layout.addWidget(self.edit_send)
        
        self.combo_format = QComboBox()
        self.combo_format.addItems(["Text", "Hex"])
        layout.addWidget(self.combo_format)
        
        self.btn_send = QPushButton("Send")
        self.btn_send.clicked.connect(self._on_send)
        self.btn_send.setEnabled(False)
        layout.addWidget(self.btn_send)
        
        return group
    
    def _setup_timers(self):
        """Setup update timers for different modes."""
        # Listening mode update timer
        self.listen_timer = QTimer()
        self.listen_timer.timeout.connect(self._update_listening_display)

        # Polling mode timer
        self.poll_timer = QTimer()
        self.poll_timer.timeout.connect(self._poll_next_packet)
    
    # =========================================================================
    # Port Management
    # =========================================================================
    
    def _refresh_ports(self):
        """Refresh available serial ports with device descriptions."""
        self.combo_port.clear()
        
        # Try to auto-detect ports with descriptions
        try:
            import serial.tools.list_ports
            ports = serial.tools.list_ports.comports()
            for port in ports:
                # Build a clean description from manufacturer and product
                desc_parts = []
                if port.manufacturer and port.manufacturer != "n/a":
                    desc_parts.append(port.manufacturer)
                if port.product and port.product != "n/a" and port.product != port.manufacturer:
                    desc_parts.append(port.product)
                
                # Format: "/dev/ttyUSB0 - FTDI FT232R USB UART" or just "/dev/ttyUSB0"
                if desc_parts:
                    display_text = f"{port.device} - {' '.join(desc_parts)}"
                else:
                    display_text = port.device
                
                self.combo_port.addItem(display_text, port.device)
        except:
            pass
        
        # Add common Linux ports (USB and ACM) without descriptions
        for i in range(4):
            device = f"/dev/ttyUSB{i}"
            # Only add if not already in list
            if self.combo_port.findData(device) == -1:
                self.combo_port.addItem(device, device)
            
            device = f"/dev/ttyACM{i}"
            if self.combo_port.findData(device) == -1:
                self.combo_port.addItem(device, device)
        
        # Add Raspberry Pi serial ports
        for device in ["/dev/serial0", "/dev/ttyAMA0"]:
            if self.combo_port.findData(device) == -1:
                self.combo_port.addItem(device, device)
    
    def _on_connect(self):
        """Connect to serial port with current configuration."""
        self._log_info("Attempting to connect...")
        
        # Get configuration from UI
        # Use currentData() to get actual device path (not display text)
        device = self.combo_port.currentData()
        if not device:  # Fallback to text if no data stored
            device = self.combo_port.currentText()
        baudrate = int(self.combo_baud.currentText())
        
        self._log_info(f"Device: {device}, Baud: {baudrate}")
        
        # Get mode
        mode_idx = self.combo_mode.currentIndex()
        polling_mode = (mode_idx == 1)  # Polling
        listening_mode = (mode_idx == 2)  # Listening
        pgkomm2_mode = (mode_idx == 3)  # PGKomm2

        mode_name = ["Request-Response", "Polling", "Listening", "PGKomm2"][mode_idx]
        self._log_info(f"Mode: {mode_name}")
        
        # Get stop condition
        if listening_mode or pgkomm2_mode:
            # No stop condition in listening/PGKomm2 mode (handled separately)
            stop_mode = StopConditionMode.TIMEOUT
        else:
            stop_mode = (StopConditionMode.TIMEOUT if self.combo_stop.currentIndex() == 0
                        else StopConditionMode.TERMINATOR)
        
        # Parse terminator (accept hex or decimal)
        term_text = self.edit_terminator.text().strip()
        try:
            if term_text.startswith("0x"):
                terminator = int(term_text, 16)
            else:
                terminator = int(term_text)
        except:
            terminator = 0x0A  # Default to newline
            self._log_info(f"Invalid terminator, using default 0x0A")
        
        # Create UART config
        config = UARTConfig(
            device=device,
            baudrate=baudrate,
            data_bits=int(self.combo_databits.currentText()),
            stop_bits=int(self.combo_stopbits.currentText()),
            parity='N' if self.combo_parity.currentText() == 'None' else self.combo_parity.currentText()[0],
            # buffer_size uses default from UARTConfig
            stop_mode=stop_mode,
            stop_timeout_ms=self.spin_timeout.value(),
            stop_terminator=terminator,
            polling_mode=polling_mode,
            grace_timeout_ms=self.spin_grace.value()
        )
        
        # Create port
        status, self.port = uart_create(config)
        if status != Status.OK:
            self._show_error(f"Failed to create port: {status}")
            return
        
        # Open port
        status = uart_open(self.port)
        if status != Status.OK:
            self._show_error(f"Failed to open port: {status}")
            return

        # Start reader thread (always - all modes use circular buffer)
        status = uart_start_reader(self.port)
        if status != Status.OK:
            uart_close(self.port)
            self._show_error(f"Failed to start reader: {status}")
            return

        # Settle delay: Allow device to stabilize after port opens
        # Device may reset on DTR/RTS change, needs time to boot/initialize
        import time
        self._log_info("Waiting 100ms for device to settle...")
        time.sleep(0.1)  # 100ms settle delay

        # Replace buffer label with actual widget
        self._update_buffer_widget()
        
        # Update UI state
        self.is_connected = True
        self._command_in_progress = False  # Reset on connect
        self._last_overflow_count = 0  # Reset overflow tracking
        self._update_ui_state()
        self.lbl_status.setText("Status: ● Connected")
        self.lbl_status.setStyleSheet("color: green;")
        
        self._log_info(f"✓ Connected successfully!")
        
        # Start mode-specific operation
        if listening_mode:
            # Listening mode: Raw data, print everything, NO packet detection
            uart_start_listening(self.port)
            self._packet_detection_enabled = False
            self.listen_timer.start(100)
            self._log_info("Listening mode started (raw data, prints to monitor)")
        elif polling_mode:
            # Polling mode: Packet detection, NO printing
            # Check if packet detection is enabled
            packet_detect_enabled = self.check_packet_detect.isChecked()
            
            if packet_detect_enabled:
                # Parse packet configuration
                try:
                    # Parse start marker
                    start_str = self.edit_packet_start.text().strip().replace(' ', '')
                    start_marker = bytes.fromhex(start_str)
                    
                    # Parse end marker
                    end_str = self.edit_packet_end.text().strip().replace(' ', '')
                    end_marker = bytes.fromhex(end_str) if end_str else None
                    
                    # Get packet length
                    packet_length = self.spin_packet_length.value()
                    
                    # Create packet config
                    packet_config = PacketConfig(
                        enable=True,
                        start_marker=start_marker,
                        packet_length=packet_length,
                        end_marker=end_marker
                    )
                    
                    # Clear previous packets
                    uart_clear_detected_packets(self.port)
                    self._detected_packet_count = 0
                    self.lbl_packet_count.setText("Packets: 0")
                    
                    # Start listening with packet detection
                    uart_start_listening_with_packets(self.port, packet_config)
                    self._packet_detection_enabled = True

                    # Initialize timeout tracking (0 = no packets yet, timeout not started)
                    self._last_packet_time = 0.0

                    self.poll_timer.start(50)  # Poll for packets
                    self._log_info(f"Polling mode started with packet detection")
                    self._log_info(f"  Start: {start_marker.hex(' ')}, Length: {packet_length}, End: {end_marker.hex(' ') if end_marker else 'None'}")
                    
                except Exception as e:
                    self._show_error(f"Invalid packet config: {e}")
                    # Fallback to regular polling
                    # Initialize timeout tracking (0 = no packets yet, timeout not started)
                    self._last_packet_time = 0.0

                    self.poll_timer.start(50)
                    self._packet_detection_enabled = False
                    self._log_warning("Fallback to polling mode without packet detection")
            else:
                # Regular polling mode (use uart_poll_packet)
                # Initialize timeout tracking (0 = no packets yet, timeout not started)
                self._last_packet_time = 0.0

                self.poll_timer.start(50)
                self._packet_detection_enabled = False
                self._log_info("Polling mode started (no packet detection)")
                self.text_display.append("<span style='color: gray;'>[INFO] Logger Started</span>")
        else:
            # Request-response mode (handled per-command)
            pass
        
        # Emit connection changed signal
        self.connection_changed.emit(True)
    
    def _on_disconnect(self):
        """Disconnect from serial port."""
        self._log_info("Disconnecting...")

        if not self.port:
            return

        # Wait for command worker to finish if running
        if hasattr(self, '_command_worker') and self._command_worker is not None:
            if self._command_worker.isRunning():
                self._log_info("Waiting for command to finish...")
                self._command_worker.wait(2000)  # Wait up to 2 seconds
                if self._command_worker.isRunning():
                    self._log_info("⚠️ Force terminating worker thread")
                    self._command_worker.terminate()
                    self._command_worker.wait()

        # Stop timers
        self.listen_timer.stop()
        self.poll_timer.stop()
        
        # Get mode
        mode_idx = self.combo_mode.currentIndex()
        
        # Stop listening/polling and show summary
        if mode_idx == 2:  # Listening mode
            uart_stop_listening(self.port)
            self._log_info("Listening stopped")
        elif mode_idx == 1:  # Polling mode
            # Show packet detection summary if enabled
            if self._packet_detection_enabled:
                uart_stop_listening(self.port)  # Stop packet detection
                packets = uart_get_detected_packets(self.port)
                packet_count = len(packets)
                self._log_info(f"✓ Run complete: {packet_count} packets detected")
                self.lbl_status.setText(f"Disconnected - {packet_count} packets")
                self.lbl_status.setStyleSheet("color: green;")
        
        # Buffer contents at disconnect (continuous stream data) - not logged anymore

        # Close port (always stop reader thread)
        uart_stop_reader(self.port)
        uart_close(self.port)
        
        # Reset state
        self.port = None
        self.is_connected = False
        self._command_in_progress = False
        self._packet_detection_enabled = False
        self._detected_packet_count = 0
        
        # Update UI
        self._update_ui_state()
        if not (mode_idx == 1 and self._packet_detection_enabled):
            self.lbl_status.setText("Status: Disconnected")
            self.lbl_status.setStyleSheet("")
        
        self._log_info("✓ Disconnected")
        
        # Replace buffer widget with placeholder
        if isinstance(self.buffer_widget, CompactBufferWidget):
            self.buffer_widget.setParent(None)
            self.buffer_widget = QLabel("Not connected")
            status_buffer_layout = self.layout().itemAt(1)
            buffer_group_widget = status_buffer_layout.itemAt(1).widget()
            buffer_group_widget.layout().addWidget(self.buffer_widget)
        
        # Emit connection changed signal
        self.connection_changed.emit(False)
    
    # =========================================================================
    # UI State Management
    # =========================================================================
    
    def _update_ui_state(self):
        """Enable/disable controls based on connection state."""
        connected = self.is_connected
        
        # Disable configuration when connected
        self.combo_port.setEnabled(not connected)
        self.btn_refresh.setEnabled(not connected)
        self.combo_baud.setEnabled(not connected)
        self.combo_databits.setEnabled(not connected)
        self.combo_stopbits.setEnabled(not connected)
        self.combo_parity.setEnabled(not connected)
        self.combo_mode.setEnabled(not connected)
        self.combo_stop.setEnabled(not connected)
        self.edit_terminator.setEnabled(not connected)
        self.spin_timeout.setEnabled(not connected)
        self.spin_grace.setEnabled(not connected)
        self.spin_silence.setEnabled(not connected)

        # Connect/disconnect buttons
        self.btn_connect.setEnabled(not connected)
        self.btn_disconnect.setEnabled(connected)
        
        # Send button (enabled when connected AND idle)
        self.btn_send.setEnabled(connected and not self._command_in_progress)
        
        # Clear buffer button (enabled when connected)
        self.btn_clear_buffer.setEnabled(connected)
    
    def _on_mode_changed(self, index):
        """Update UI when mode changes (enable/disable stop condition fields)."""
        # 0=Request-Response, 1=Polling, 2=Listening, 3=PGKomm2

        if index == 3:  # PGKomm2 - V-ZUG protocol with DD 22 framing (auto-detects response)
            self.combo_stop.setEnabled(False)  # Protocol has built-in framing
            self.edit_terminator.setEnabled(False)  # Not used (length-delimited)
            self.spin_timeout.setEnabled(False)  # Not used (automatic command matching)
            self.spin_grace.setEnabled(False)  # No grace period
            # spin_silence is hidden - automatic command matching, no user config needed
            # Disable packet detection (protocol handles framing)
            self.check_packet_detect.setEnabled(False)
            self.check_packet_detect.setChecked(False)
        elif index == 2:  # Listening - raw mode, no packet detection, prints to monitor
            self.combo_stop.setEnabled(False)
            self.edit_terminator.setEnabled(False)
            self.spin_timeout.setEnabled(False)
            self.spin_grace.setEnabled(False)
            self.spin_silence.setEnabled(False)
            # Disable packet detection (not for listening - it's for raw data)
            self.check_packet_detect.setEnabled(False)
            self.check_packet_detect.setChecked(False)
        elif index == 1:  # Polling - packet detection mode, no printing
            self.combo_stop.setEnabled(True)
            self.edit_terminator.setEnabled(True)
            self.spin_timeout.setEnabled(True)
            self.spin_grace.setEnabled(True)
            self.spin_silence.setEnabled(False)
            # Enable packet detection (THIS is the mode for packet detection)
            self.check_packet_detect.setEnabled(True)
        else:  # Request-Response - enable stop condition, disable grace
            self.combo_stop.setEnabled(True)
            self.edit_terminator.setEnabled(True)
            self.spin_timeout.setEnabled(True)
            self.spin_grace.setEnabled(False)
            self.spin_silence.setEnabled(False)
            # Disable packet detection (not for request-response)
            self.check_packet_detect.setEnabled(False)
            self.check_packet_detect.setChecked(False)
    
    def _on_packet_detect_toggled(self, state):
        """Enable/disable packet detection fields based on checkbox."""
        enabled = (state == Qt.CheckState.Checked.value)
        self.edit_packet_start.setEnabled(enabled)
        self.spin_packet_length.setEnabled(enabled)
        self.edit_packet_end.setEnabled(enabled)
    
    def _update_buffer_widget(self):
        """Replace placeholder label with actual buffer widget."""
        if not self.port or not isinstance(self.buffer_widget, QLabel):
            return
        
        # Remove old label
        self.buffer_widget.setParent(None)
        
        # Create compact buffer widget
        self.buffer_widget = CompactBufferWidget(self.port._rx_buffer)
        
        # Find buffer group (in horizontal layout at index 1, second widget)
        status_buffer_layout = self.layout().itemAt(1)
        buffer_group_widget = status_buffer_layout.itemAt(1).widget()
        buffer_group_widget.layout().addWidget(self.buffer_widget)
    
    # =========================================================================
    # Data Display
    # =========================================================================
    
    def _update_listening_display(self):
        """Update display in listening mode (continuous stream with printing)."""
        if not self.port:
            return
        
        # Update buffer widget
        if isinstance(self.buffer_widget, CompactBufferWidget):
            self.buffer_widget.update_display()
        
        # Read current buffer contents and PRINT to monitor (Listening mode behavior)
        status, data = uart_read_buffer(self.port, max_bytes=4096)
        
        if status == Status.OK and data:
            # Listening mode ALWAYS prints to data monitor
            self._log_rx(data, info="Listening")
            # Emit signal
            self.data_received.emit(data)
    
    def _poll_next_packet(self):
        """Poll for next packet in polling mode (NO printing to monitor)."""
        if not self.port:
            return

        # Check timeout (auto-stop if no packets for timeout duration)
        import time
        current_time = time.time()
        timeout_s = self.spin_timeout.value() / 1000.0  # Convert ms to seconds

        if self._last_packet_time > 0:  # Only check after first packet
            silence_duration = current_time - self._last_packet_time
            if silence_duration >= timeout_s:
                # Timeout expired - print buffer info, packet stats, and auto-disconnect
                if self.port and self.port._rx_buffer:
                    fill = cb_fill_bytes(self.port._rx_buffer)
                    cap = cb_capacity(self.port._rx_buffer)
                    fill_mb = fill / (1024 * 1024)
                    cap_mb = cap / (1024 * 1024)
                    self._log_info(f"Buffer status: {fill_mb:.2f}MB / {cap_mb:.1f}MB ({fill}/{cap} bytes)")

                # Show packet statistics
                if self.port:
                    total_packets = len(uart_get_detected_packets(self.port))
                    packet_errors = uart_get_packet_errors(self.port)
                    if packet_errors > 0:
                        self._log_error(f"⚠ Packet errors: {packet_errors} packets with end marker mismatch")
                    self._log_info(f"✓ Total valid packets: {total_packets}")

                self._log_info(f"Auto-stop: {timeout_s*1000:.0f}ms timeout expired (no new packets)")
                self._on_disconnect()
                return

        # Update buffer widget
        if isinstance(self.buffer_widget, CompactBufferWidget):
            self.buffer_widget.update_display()

        # If packet detection is enabled, update packet count (NO printing)
        if self._packet_detection_enabled:
            packets = uart_get_detected_packets(self.port)
            current_count = len(packets)

            if current_count != self._detected_packet_count:
                # New packets detected - update counter and timestamp
                self._detected_packet_count = current_count
                self._last_packet_time = current_time  # Update timestamp
                self.lbl_packet_count.setText(f"Packets: {current_count}")
                self.lbl_packet_count.setStyleSheet("color: green; font-weight: bold;")
            return
        
        # Regular polling mode (without packet detection)
        # Get current overflow count
        overflow_count = cb_overflows(self.port._rx_buffer)
        
        # Check if overflow increased (new overflow occurred)
        if overflow_count > self._last_overflow_count:
            overflow_delta = overflow_count - self._last_overflow_count
            # Display RED warning in Data Monitor
            overflow_warning = (
                f"<span style='color: red; font-weight: bold;'>"
                f"[WARNING] ⚠ BUFFER OVERFLOW! Lost data. "
                f"Overflow count: {overflow_count} (+{overflow_delta})"
                f"</span>"
            )
            self.text_display.append(overflow_warning)
            self._last_overflow_count = overflow_count
        
        # Poll for packet (non-blocking)
        status, packet = uart_poll_packet(self.port)

        if status == Status.OK:
            # Update packet timestamp
            self._last_packet_time = current_time

            # Display stop condition message in gray (logging mode only)
            stop_msg = (
                f"<span style='color: gray;'>"
                f"[INFO] Stop condition detected | "
                f"UART Status: IDLE | "
                f"Registered Bytes: {len(packet.data)} | "
                f"Overflows: {overflow_count}"
                f"</span>"
            )
            self.text_display.append(stop_msg)

            # Update buffer widget
            self.buffer_widget.update_display()

            # Emit packet signal
            self.packet_received.emit(packet)
    
    def _display_packet(self, packet):
        """Display received packet in monitor (blue for RX) - same format as TX."""
        # Add to history
        self.packet_history.append(packet)
        if len(self.packet_history) > self.max_packet_history:
            self.packet_history.pop(0)
        
        # Use same format as TX - just show data
        self._log_rx(packet.data)
    
    def _clear_display(self):
        """Clear display area and history."""
        self.text_display.clear()
        self.packet_history.clear()
    
    def _on_history_changed(self, value):
        """Update maximum packet history size."""
        self.max_packet_history = value
    
    def _on_clear_buffer(self):
        """Clear the circular buffer and reset overflow counter."""
        if not self.port or not self.port._rx_buffer:
            return
        
        from buffer_kit.circular_buffer import cb_reset
        
        # Reset buffer (clears data and overflow count)
        cb_reset(self.port._rx_buffer)
        
        # Reset our overflow tracking
        self._last_overflow_count = 0
        
        # Update buffer display
        if isinstance(self.buffer_widget, CompactBufferWidget):
            self.buffer_widget.update_display()
        
        # Log action
        self._log_info("Buffer cleared (data and overflow count reset)")
        self.lbl_status.setText("✓ Buffer cleared")
        self.lbl_status.setStyleSheet("color: green;")
    
    # =========================================================================
    # Send Operations
    # =========================================================================
    
    def _parse_hex_string(self, hex_str: str):
        """
        Parse hex string with validation.
        
        Accepts:
            "AF 9B FF 89 AA"  (space-separated)
            "AF9BFF89AA"      (no spaces)
            "af9bff89aa"      (lowercase OK)
        
        Returns:
            (success: bool, data: bytes, error_message: str)
        """
        hex_str = hex_str.strip()
        
        if not hex_str:
            return (False, b"", "Empty hex string")
        
        # Remove all whitespace
        hex_clean = ''.join(hex_str.split())
        
        # Validate hex characters only
        if not all(c in '0123456789ABCDEFabcdef' for c in hex_clean):
            return (False, b"", "Invalid hex characters (use 0-9, A-F)")
        
        # Must have even number of characters (pairs)
        if len(hex_clean) % 2 != 0:
            return (False, b"", "Odd number of hex digits (need pairs)")
        
        # Convert to bytes
        try:
            data = bytes.fromhex(hex_clean)
            return (True, data, "")
        except ValueError as e:
            return (False, b"", f"Hex parse error: {e}")
    
    def _on_send(self):
        """
        Send data with validation and idle check.
        
        Features:
        - Hex validation (AF 9B FF format)
        - Length limit (500 bytes max)
        - Idle check (can't send while busy)
        - Console logging
        - Auto-clear on success
        """
        MAX_SEND_LENGTH = 500  # Maximum bytes to send
        
        # Check if port is connected
        if not self.port:
            self._show_error("Port not connected")
            return
        
        # FAILSAFE: Check if port is idle (not waiting for response)
        if self._command_in_progress:
            self._show_error("UART busy! Wait for current command to complete")
            return

        # FAILSAFE: Check if worker thread is still running
        if hasattr(self, '_command_worker') and self._command_worker is not None:
            if self._command_worker.isRunning():
                self._show_error("Worker thread still running! Wait for completion")
                return
        
        # Get input text
        text = self.edit_send.text().strip()
        if not text:
            self._show_error("Empty send field")
            return
        
        # Parse data based on format (Text or Hex)
        if self.combo_format.currentText() == "Hex":
            # Parse hex with validation
            success, data, error = self._parse_hex_string(text)
            if not success:
                self._show_error(f"Hex format error: {error}")
                return
        else:
            # Text mode - encode as UTF-8
            data = text.encode('utf-8')
        
        # Check length limit
        if len(data) > MAX_SEND_LENGTH:
            self._show_error(f"Data too long! Max {MAX_SEND_LENGTH} bytes, got {len(data)}")
            return
        
        # Get current mode
        mode_idx = self.combo_mode.currentIndex()

        # =====================================================================
        # Request-Response Mode: Send and wait for response (NON-BLOCKING)
        # =====================================================================
        if mode_idx == 0:
            # Mark as busy (prevent multiple sends)
            self._command_in_progress = True
            self.btn_send.setEnabled(False)
            self.lbl_status.setText("Sending command...")

            # Log TX in green
            self._log_tx(data)

            # Create worker thread for non-blocking send
            self._command_worker = UARTCommandWorker(self.port, data)
            self._command_worker.finished.connect(self._on_command_finished)
            self._command_worker.start()

            # Safety timeout: Auto-reset busy flag after 30 seconds
            # (prevents permanent lockout if worker crashes)
            QTimer.singleShot(30000, self._reset_busy_flag_safety)

            # ===== GUI remains responsive while worker runs =====

        # =====================================================================
        # PGKomm2 Mode: Send and read PGKomm2 frames (NON-BLOCKING)
        # =====================================================================
        elif mode_idx == 3:
            # Get capture window (from "Capture" spinbox)
            capture_max_ms = self.spin_silence.value()

            # Mark as busy
            self._command_in_progress = True
            self.btn_send.setEnabled(False)
            self.lbl_status.setText("Sending PGKomm2 command...")

            # Log TX in green
            self._log_tx(data)

            # Call PGKomm2 function directly (blocking for ~20ms is OK)
            status, frames = uart_send_and_read_pgkomm2(self.port, data, capture_max_ms)

            # Handle response immediately
            self._on_pgkomm2_finished(status, frames)

            # Reset busy flag
            self._command_in_progress = False
            self.btn_send.setEnabled(True)

        # =====================================================================
        # Polling or Listening Mode: Just write data
        # =====================================================================
        else:
            # Log TX in green
            self._log_tx(data)

            status, written = uart_write(self.port, data)

            if status == Status.OK:
                self.edit_send.clear()  # Clear input on success
                self.lbl_status.setText(f"✓ Sent {written} bytes")
                self.lbl_status.setStyleSheet("color: green;")
                self._log_info(f"Write successful: {written} bytes")
            else:
                self._show_error(f"Write failed: {status}")

    def _reset_busy_flag_safety(self):
        """
        Safety mechanism: Reset busy flag if worker is stuck.

        Called 30 seconds after worker starts. Only resets if worker
        is still marked as in progress (which shouldn't happen normally).
        """
        if self._command_in_progress:
            self._command_in_progress = False
            self.btn_send.setEnabled(True)
            self._log_warning("Command timed out after 30 seconds")
            self.lbl_status.setText("Worker timeout - reset")
            self.lbl_status.setStyleSheet("color: orange;")

    def _on_command_finished(self, status, packet):
        """
        Handle UART command worker completion (non-blocking result handler).

        Called when background worker finishes send/receive operation.
        """
        # Cleanup worker
        if self._command_worker:
            self._command_worker.deleteLater()
            self._command_worker = None

        # Mark as idle (allow next send)
        self._command_in_progress = False
        self.btn_send.setEnabled(True)

        # Handle result
        if status == Status.OK and packet:
            # Success - display packet (will be blue RX)
            self._display_packet(packet)
            self.buffer_widget.update_display()
            self.packet_received.emit(packet)
            self.edit_send.clear()  # Clear input on success
            self.lbl_status.setText(f"✓ TX: sent, RX: {len(packet.data)}B")
            self.lbl_status.setStyleSheet("color: green;")
        elif status == Status.TIMEOUT or status == Status.TIMEOUT_NO_DATA:
            # Timeout - no response received
            self._log_warning("No response received (timeout)")
            self.lbl_status.setText("Timeout!")
            self.lbl_status.setStyleSheet("color: orange;")
        elif status == Status.PORT_CLOSED:
            # Port closed during execution
            self._log_warning("Port closed during command execution")
            self.lbl_status.setText("Port closed")
            self.lbl_status.setStyleSheet("color: red;")
        else:
            # Other error (IO_ERROR, etc.)
            self._show_error(f"Send failed: {status}")

    def _on_pgkomm2_finished(self, status, frames):
        """
        Handle PGKomm2 worker completion.

        Called when background worker finishes PGKomm2 send/receive operation.

        Args:
            status: Status enum
            frames: List of frame bytes (or None on error/timeout)
        """
        # Cleanup worker
        if self._command_worker:
            self._command_worker.deleteLater()
            self._command_worker = None

        # Mark as idle (allow next send)
        self._command_in_progress = False
        self.btn_send.setEnabled(True)

        # Handle result
        if status == Status.OK and frames:
            # Success - display all frames
            total_bytes = sum(len(frame) for frame in frames)
            self._log_info(f"✓ Received {len(frames)} PGKomm2 frame(s), {total_bytes} bytes total")

            # Log each frame with ASCII
            for i, frame in enumerate(frames, 1):
                # Convert frame to ASCII (printable chars only, others as '.')
                ascii_str = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in frame)

                # Parse frame address to identify type
                if len(frame) >= 5:
                    adr1 = frame[2]
                    adr2 = frame[3]
                    length = frame[4]

                    # Identify frame type
                    frame_type = "Unknown"
                    if adr1 == 0x50 and adr2 == 0x48:  # PH
                        frame_type = "Echo (PH)"
                    elif adr1 == 0x48 and adr2 == 0x50:  # HP
                        frame_type = "Response (HP)"
                    elif adr1 == 0x53 and adr2 == 0x42:  # SB
                        frame_type = "Status (SB)"

                    # Log with ASCII
                    self._log_rx(frame, f"Frame {i}/{len(frames)}: {frame_type} (LEN={length}) | '{ascii_str}'")
                else:
                    self._log_rx(frame, f"Frame {i}/{len(frames)}: Invalid | '{ascii_str}'")

                # Emit signal for external handlers
                self.data_received_display.emit(frame, f"PGKomm2 Frame {i}")

            self.edit_send.clear()  # Clear input on success
            self.lbl_status.setText(f"✓ TX: sent, RX: {len(frames)} frames ({total_bytes}B)")
            self.lbl_status.setStyleSheet("color: green;")

        elif status == Status.TIMEOUT:
            # Timeout - no response received
            self._log_warning("No PGKomm2 response (timeout)")
            self.lbl_status.setText("Timeout!")
            self.lbl_status.setStyleSheet("color: orange;")

        elif status == Status.PORT_CLOSED:
            # Port closed during execution
            self._log_warning("Port closed during command execution")
            self.lbl_status.setText("Port closed")
            self.lbl_status.setStyleSheet("color: red;")
        else:
            # Other error (IO_ERROR, etc.)
            self._show_error(f"PGKomm2 command failed: {status}")

    # =========================================================================
    # Logging & Error Handling
    # =========================================================================
    
    def _show_error(self, message: str):
        """Show error in status bar and data monitor (red)."""
        self.lbl_status.setText(f"Error: {message}")
        self.lbl_status.setStyleSheet("color: red;")
        self.text_display.append(f"<span style='color: red;'>[ERROR] {message}</span>")
    
    def _log_info(self, message: str):
        """Log info to data monitor (gray)."""
        self.text_display.append(f"<span style='color: gray;'>[INFO] {message}</span>")
    
    def _log_warning(self, message: str):
        """Log warning to data monitor (orange)."""
        self.text_display.append(f"<span style='color: orange;'>[WARNING] {message}</span>")
    
    def _log_tx(self, data: bytes):
        """Log transmitted data (green) - full data, no truncation."""
        hex_str = data.hex(' ')  # Show ALL data
        self.text_display.append(f"<span style='color: green;'>[TX] {len(data)}B: {hex_str}</span>")
        # Emit signal for external displays (e.g., Data Monitor)
        self.data_sent.emit(data)

    def _log_rx(self, data: bytes, info: str = ""):
        """Log received data (blue) - full data, no truncation."""
        hex_str = data.hex(' ')  # Show ALL data
        prefix = f"[RX] {info} " if info else "[RX] "
        self.text_display.append(f"<span style='color: blue;'>{prefix}{len(data)}B: {hex_str}</span>")
        # Emit signal for external displays (e.g., Data Monitor)
        self.data_received_display.emit(data, info)
    
    # =========================================================================
    # Public API
    # =========================================================================
    
    def get_port(self):
        """Get current port handle (for external access)."""
        return self.port
    
    def is_port_connected(self) -> bool:
        """Check if port is connected."""
        return self.is_connected
    
    def update_buffer_display(self):
        """Manually update buffer widget (call from external timer if needed)."""
        if isinstance(self.buffer_widget, CompactBufferWidget):
            self.buffer_widget.update_display()
    
    def get_detected_packets(self):
        """
        Get detected packets (if packet detection is enabled).
        
        Returns:
            List of PacketInfo objects, or empty list if not enabled
        """
        if self.port and self._packet_detection_enabled:
            return uart_get_detected_packets(self.port)
        return []
    
    def clear_detected_packets(self):
        """
        Clear detected packets list.
        
        Useful when starting a new run/session.
        """
        if self.port and self._packet_detection_enabled:
            uart_clear_detected_packets(self.port)
            self._detected_packet_count = 0
            self.lbl_packet_count.setText("Packets: 0")
            self._log_info("Packet list cleared")

    def closeEvent(self, event):
        """Handle widget close - clean up threads properly."""
        # Wait for command worker to finish if running
        if hasattr(self, '_command_worker') and self._command_worker is not None:
            if self._command_worker.isRunning():
                print("[UARTWidget] Waiting for worker thread to finish before closing...")
                self._command_worker.wait(2000)  # Wait up to 2 seconds
                if self._command_worker.isRunning():
                    print("[UARTWidget] Force terminating worker thread")
                    self._command_worker.terminate()
                    self._command_worker.wait()

        # Disconnect if connected
        if self.is_connected:
            self._on_disconnect()

        # Accept close event
        event.accept()


# =============================================================================
# Demo
# =============================================================================

if __name__ == "__main__":
    import sys
    from PyQt6.QtWidgets import QApplication
    
    app = QApplication(sys.argv)
    
    widget = UARTWidget()
    widget.setWindowTitle("UART Control")
    widget.show()
    
    # Auto-refresh ports on startup
    widget._refresh_ports()
    
    sys.exit(app.exec())
