"""Raw serial terminal showing every line one selected sensor sends."""

from __future__ import annotations

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
    QComboBox,
    QDialog,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QPlainTextEdit,
    QPushButton,
    QVBoxLayout,
    QWidget,
)


class TerminalWindow(QDialog):
    """Live view of the ASCII lines arriving from one sensor's serial port.

    An optional "starts with" filter limits the display to lines beginning
    with any of the given characters — ``!`` for scalar data lines or ``#``
    for firmware status lines, for example.
    """

    MAX_LINES = 2_000

    def __init__(self, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self.setObjectName("terminalWindow")
        self.setWindowTitle("QTFM Sensor Terminal")
        self.setModal(False)
        # A plain window frame so it can be minimized and left open beside
        # the main window.
        self.setWindowFlags(Qt.Window)
        self.resize(860, 540)
        self._received = 0
        self._shown = 0

        layout = QVBoxLayout(self)
        layout.setContentsMargins(14, 12, 14, 12)
        layout.setSpacing(8)

        toolbar = QHBoxLayout()
        toolbar.setSpacing(9)

        title = QLabel("SENSOR TERMINAL")
        title.setObjectName("plotTitle")
        toolbar.addWidget(title)

        sensor_label = QLabel("SENSOR")
        sensor_label.setObjectName("fieldLabel")
        toolbar.addWidget(sensor_label)

        self.sensor_combo = QComboBox()
        self.sensor_combo.setMinimumWidth(110)
        self.sensor_combo.setToolTip(
            "Which connected sensor's serial data to display."
        )
        self.sensor_combo.currentIndexChanged.connect(self._on_sensor_changed)
        toolbar.addWidget(self.sensor_combo)

        filter_label = QLabel("STARTS WITH")
        filter_label.setObjectName("fieldLabel")
        toolbar.addWidget(filter_label)

        self.filter_edit = QLineEdit()
        self.filter_edit.setMaxLength(8)
        self.filter_edit.setFixedWidth(80)
        self.filter_edit.setPlaceholderText("! or #")
        self.filter_edit.setToolTip(
            "Show only lines that begin with one of these characters — "
            "for example ! for scalar data lines or # for status lines. "
            "Leave empty to show every line. The filter applies to newly "
            "received lines."
        )
        self.filter_edit.textChanged.connect(self._render_status)
        toolbar.addWidget(self.filter_edit)
        toolbar.addStretch()

        self.pause_button = QPushButton("PAUSE")
        self.pause_button.setObjectName("deviceButton")
        self.pause_button.setCheckable(True)
        self.pause_button.setToolTip(
            "Stop appending new lines. Data received while paused is not "
            "shown afterwards."
        )
        toolbar.addWidget(self.pause_button)

        clear_button = QPushButton("CLEAR")
        clear_button.setObjectName("secondaryButton")
        clear_button.clicked.connect(self.clear)
        toolbar.addWidget(clear_button)
        layout.addLayout(toolbar)

        self.view = QPlainTextEdit()
        self.view.setObjectName("terminalView")
        self.view.setReadOnly(True)
        self.view.setMaximumBlockCount(self.MAX_LINES)
        self.view.setLineWrapMode(QPlainTextEdit.NoWrap)
        layout.addWidget(self.view, 1)

        self.status_label = QLabel("Waiting for data.")
        self.status_label.setObjectName("controlHint")
        layout.addWidget(self.status_label)

    # ------------------------------------------------------------- selection
    @property
    def selected_sensor(self) -> str | None:
        data = self.sensor_combo.currentData()
        return data if isinstance(data, str) else None

    def set_sensors(self, keys: tuple[str, ...]) -> None:
        """Offer the active sensors, keeping the current choice when possible."""
        previous = self.selected_sensor
        self.sensor_combo.blockSignals(True)
        self.sensor_combo.clear()
        for key in keys:
            self.sensor_combo.addItem(f"Sensor {key}", key)
        index = self.sensor_combo.findData(previous)
        self.sensor_combo.setCurrentIndex(max(0, index))
        self.sensor_combo.blockSignals(False)
        if self.selected_sensor != previous:
            self.clear()

    def _on_sensor_changed(self, *_args) -> None:
        # Lines from the previous sensor would be misleading under a new title.
        self.clear()

    # ------------------------------------------------------------------ data
    def clear(self) -> None:
        self.view.clear()
        self._received = 0
        self._shown = 0
        self.status_label.setText("Waiting for data.")

    def append_lines(self, sensor: str, lines: list[str]) -> None:
        """Show freshly received lines belonging to the selected sensor."""
        if (
            self.isHidden()
            or sensor != self.selected_sensor
            or self.pause_button.isChecked()
            or not lines
        ):
            return

        self._received += len(lines)
        allowed = self.filter_edit.text()
        if allowed:
            shown = [line for line in lines if line and line[0] in allowed]
        else:
            shown = lines
        if shown:
            self._shown += len(shown)
            self.view.appendPlainText("\n".join(shown))
            scrollbar = self.view.verticalScrollBar()
            scrollbar.setValue(scrollbar.maximum())
        self._render_status()

    def _render_status(self, *_args) -> None:
        if self._received == 0:
            self.status_label.setText("Waiting for data.")
            return
        allowed = self.filter_edit.text()
        filter_note = (
            f"  •  showing lines starting with {allowed!r}" if allowed else ""
        )
        self.status_label.setText(
            f"{self._received:,} lines received  •  {self._shown:,} shown"
            f"{filter_note}  •  display keeps the last "
            f"{self.MAX_LINES:,} lines"
        )
