"""Qt helper widgets for the desktop viewer."""

from __future__ import annotations

import copy
from typing import Any

from PySide6 import QtCore, QtGui, QtWidgets

from .theme import ThemeSpec, theme_spec_for_mode

NUMERIC_INPUT_DECIMALS = 6
PROPERTY_LABEL_WIDTH = 124
PROPERTY_CONTROL_HEIGHT = 24
PROPERTY_RESET_WIDTH = 24
RESET_ICON_SIZE = 14


def _tint_icon_pixmap(
    pixmap: QtGui.QPixmap,
    color: QtGui.QColor,
) -> QtGui.QPixmap:
    image = pixmap.toImage().convertToFormat(QtGui.QImage.Format.Format_ARGB32)
    painter = QtGui.QPainter(image)
    painter.setCompositionMode(QtGui.QPainter.CompositionMode.CompositionMode_SourceIn)
    painter.fillRect(image.rect(), color)
    painter.end()
    return QtGui.QPixmap.fromImage(image)


class CollapsibleSection(QtWidgets.QWidget):
    """A lightweight collapsible group container."""

    toggled = QtCore.Signal(bool)

    def __init__(self, title: str, parent: QtWidgets.QWidget | None = None):
        super().__init__(parent)
        self._toggle = QtWidgets.QToolButton(text=title)
        self._toggle.setProperty("inspectorRole", "section-toggle")
        self._toggle.setCheckable(True)
        self._toggle.setChecked(False)
        self._toggle.setToolButtonStyle(
            QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon
        )
        self._toggle.setArrowType(QtCore.Qt.ArrowType.RightArrow)
        self._toggle.toggled.connect(self._on_toggled)

        self._content = QtWidgets.QWidget()
        self._content_layout = QtWidgets.QVBoxLayout()
        self._content_layout.setContentsMargins(6, 4, 0, 4)
        self._content_layout.setSpacing(4)
        self._content.setLayout(self._content_layout)

        layout = QtWidgets.QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.addWidget(self._toggle)
        layout.addWidget(self._content)
        self.setLayout(layout)
        self._on_toggled(False)

    def _on_toggled(self, checked: bool) -> None:
        self._toggle.setArrowType(
            QtCore.Qt.ArrowType.DownArrow
            if checked
            else QtCore.Qt.ArrowType.RightArrow
        )
        self._content.setVisible(bool(checked))
        self.toggled.emit(bool(checked))

    def add_content_widget(self, widget: QtWidgets.QWidget) -> None:
        self._content_layout.addWidget(widget)

    def set_expanded(self, expanded: bool) -> None:
        self._toggle.setChecked(bool(expanded))

    def is_expanded(self) -> bool:
        return bool(self._toggle.isChecked())


class PropertyNumericSpinBox(QtWidgets.QDoubleSpinBox):
    """Spin box that commits stepped changes immediately and typed edits on finish."""

    value_committed = QtCore.Signal(float)

    def __init__(self, parent: QtWidgets.QWidget | None = None):
        super().__init__(parent)
        self._last_committed_value: float | None = None
        self.editingFinished.connect(self._commit_pending_edit)

    def sync_committed_value(self, value: float | None = None) -> None:
        current = self.value() if value is None else float(value)
        self._last_committed_value = float(current)

    def _commit_value(self, value: float) -> None:
        committed = float(value)
        if (
            self._last_committed_value is not None
            and committed == self._last_committed_value
        ):
            return
        self._last_committed_value = committed
        self.value_committed.emit(committed)

    def _commit_pending_edit(self) -> None:
        self._commit_value(self.value())

    def stepBy(self, steps: int) -> None:
        before = float(self.value())
        super().stepBy(int(steps))
        after = float(self.value())
        if after != before:
            self._commit_value(after)


class PropertyPanel(QtWidgets.QScrollArea):
    """Scrollable auto-generated editor for scene property snapshots."""

    property_edited = QtCore.Signal(str, int, object, object, object)

    def __init__(self, parent: QtWidgets.QWidget | None = None):
        super().__init__(parent)
        self.setWidgetResizable(True)
        self.setFrameShape(QtWidgets.QFrame.Shape.NoFrame)
        self._container = QtWidgets.QWidget()
        self._container.setObjectName("propertyInspector")
        self._layout = QtWidgets.QVBoxLayout()
        self._layout.setContentsMargins(8, 8, 8, 8)
        self._layout.setSpacing(6)
        self._container.setLayout(self._layout)
        self.setWidget(self._container)
        self._structure_key = None
        self._bindings: dict[tuple[str, int], dict[str, Any]] = {}
        self._sections: list[CollapsibleSection] = []
        app = QtWidgets.QApplication.instance()
        self._theme_spec = theme_spec_for_mode("system", app) if app is not None else None
        self.apply_theme(self._theme_spec)
        self.set_snapshot(None)

    def _theme_stylesheet(self, spec: ThemeSpec | None) -> str:
        reset_background_enabled = (
            "rgba(216, 178, 110, 0.18)"
            if spec is None
            else spec.reset_background_enabled
        )
        reset_background_hover = (
            "rgba(216, 178, 110, 0.28)"
            if spec is None
            else spec.reset_background_hover
        )
        reset_border_enabled = (
            "rgba(216, 178, 110, 0.32)"
            if spec is None
            else spec.reset_border_enabled
        )
        reset_border_hover = (
            "rgba(216, 178, 110, 0.48)"
            if spec is None
            else spec.reset_border_hover
        )
        return f"""
            QScrollArea {{
                border: none;
            }}
            QToolButton[inspectorRole="section-toggle"] {{
                border: none;
                font-weight: 600;
                padding: 3px 0px;
                text-align: left;
            }}
            QWidget[inspectorRole="row"] {{
                background: transparent;
            }}
            QToolButton[inspectorRole="reset"] {{
                padding: 0px;
                border: 1px solid transparent;
                border-radius: 4px;
                qproperty-toolButtonStyle: ToolButtonIconOnly;
            }}
            QToolButton[inspectorRole="reset"]:enabled {{
                background: {reset_background_enabled};
                border-color: {reset_border_enabled};
            }}
            QToolButton[inspectorRole="reset"]:enabled:hover {{
                background: {reset_background_hover};
                border-color: {reset_border_hover};
            }}
            QComboBox, QDoubleSpinBox, QCheckBox, QToolButton[inspectorRole="reset"] {{
                min-height: 24px;
                max-height: 24px;
            }}
        """

    def apply_theme(self, spec: ThemeSpec | None) -> None:
        self._theme_spec = spec
        self.setStyleSheet(self._theme_stylesheet(spec))
        for binding in self._bindings.values():
            reset = binding.get("reset")
            if isinstance(reset, QtWidgets.QToolButton):
                reset.setIcon(self._reset_icon())

    def _reset_icon(self) -> QtGui.QIcon:
        base = self.style().standardIcon(
            QtWidgets.QStyle.StandardPixmap.SP_DialogResetButton
        )
        size = QtCore.QSize(RESET_ICON_SIZE, RESET_ICON_SIZE)
        enabled = (
            QtGui.QColor("#d8b26e")
            if self._theme_spec is None
            else self._theme_spec.reset_enabled
        )
        active = (
            QtGui.QColor("#f0c880")
            if self._theme_spec is None
            else self._theme_spec.reset_active
        )
        disabled = (
            QtGui.QColor("#596270")
            if self._theme_spec is None
            else self._theme_spec.reset_disabled
        )
        icon = QtGui.QIcon()
        icon.addPixmap(
            _tint_icon_pixmap(
                base.pixmap(size, QtGui.QIcon.Mode.Normal),
                enabled,
            ),
            QtGui.QIcon.Mode.Normal,
        )
        icon.addPixmap(
            _tint_icon_pixmap(
                base.pixmap(size, QtGui.QIcon.Mode.Active),
                active,
            ),
            QtGui.QIcon.Mode.Active,
        )
        icon.addPixmap(
            _tint_icon_pixmap(
                base.pixmap(size, QtGui.QIcon.Mode.Disabled),
                disabled,
            ),
            QtGui.QIcon.Mode.Disabled,
        )
        return icon

    def _clear_snapshot_widgets(self) -> None:
        while self._layout.count():
            item = self._layout.takeAt(0)
            widget = item.widget()
            if widget is not None:
                widget.deleteLater()
        self._bindings = {}
        self._sections = []
        self._structure_key = None

    def _on_section_toggled(
        self,
        section: CollapsibleSection,
        expanded: bool,
    ) -> None:
        if not expanded:
            return
        for other in self._sections:
            if other is section or not other.is_expanded():
                continue
            other.set_expanded(False)

    def _structure_signature(
        self,
        groups: list[dict[str, Any]],
    ) -> tuple[tuple[Any, ...], ...]:
        signature = []
        for group in groups:
            props_signature = []
            for prop in list(group.get("properties") or []):
                props_signature.append(
                    (
                        int(prop["index"]),
                        str(prop.get("kind", "")),
                        str(prop.get("field", "")),
                        str(prop.get("title", "")),
                        str(prop.get("input_kind", "")),
                        str(prop.get("row_kind", "")),
                    )
                )
            signature.append(
                (
                    str(group.get("id") or ""),
                    str(group.get("title") or ""),
                    tuple(props_signature),
                )
            )
        return tuple(signature)

    def set_snapshot(
        self,
        snapshot: dict[str, Any] | None,
        *,
        rebuild: bool = True,
    ) -> None:
        groups = [] if snapshot is None else list(snapshot.get("property_groups") or [])
        structure_key = self._structure_signature(groups)
        if rebuild or structure_key != self._structure_key:
            self._rebuild_snapshot(groups, structure_key)
            return
        self._update_snapshot(groups)

    def _rebuild_snapshot(
        self,
        groups: list[dict[str, Any]],
        structure_key: tuple[tuple[Any, ...], ...],
    ) -> None:
        self._clear_snapshot_widgets()

        if not groups:
            label = QtWidgets.QLabel("No editable properties.")
            label.setWordWrap(True)
            self._layout.addWidget(label)
            self._layout.addStretch(1)
            self._structure_key = structure_key
            return

        for group in groups:
            section = CollapsibleSection(str(group.get("title") or group.get("id") or ""))
            section.toggled.connect(
                lambda checked, current=section: self._on_section_toggled(
                    current, bool(checked)
                )
            )
            self._sections.append(section)
            for prop in list(group.get("properties") or []):
                widget, binding = self._build_property_widget(str(group["id"]), prop)
                if binding is not None:
                    self._bindings[(str(group["id"]), int(prop["index"]))] = binding
                section.add_content_widget(widget)
            self._layout.addWidget(section)
        self._layout.addStretch(1)
        self._structure_key = structure_key

    def _update_snapshot(self, groups: list[dict[str, Any]]) -> None:
        if not groups:
            return
        for group in groups:
            group_id = str(group["id"])
            for prop in list(group.get("properties") or []):
                binding = self._bindings.get((group_id, int(prop["index"])))
                if binding is None:
                    continue
                self._update_property_widget(binding, prop)

    def _emit_property(
        self,
        group_id: str,
        prop: dict[str, Any],
        value: Any,
    ) -> None:
        self.property_edited.emit(
            group_id,
            int(prop["index"]),
            value,
            prop.get("field"),
            prop.get("title"),
        )

    def _emit_reset_property(self, group_id: str, prop: dict[str, Any]) -> None:
        if not bool(prop.get("can_reset")):
            return
        self._emit_property(group_id, prop, copy.deepcopy(prop.get("default_value")))

    def _build_row_widget(
        self,
        group_id: str,
        prop: dict[str, Any],
        *,
        title: str,
        control: QtWidgets.QWidget,
    ) -> tuple[QtWidgets.QWidget, dict[str, Any]]:
        prop_state = dict(prop)
        row = QtWidgets.QWidget()
        row.setProperty("inspectorRole", "row")

        layout = QtWidgets.QGridLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setHorizontalSpacing(8)
        layout.setVerticalSpacing(0)

        name = QtWidgets.QLabel(str(title))
        name.setProperty("inspectorRole", "name")
        name.setFixedWidth(PROPERTY_LABEL_WIDTH)
        name.setMinimumHeight(PROPERTY_CONTROL_HEIGHT)

        reset = QtWidgets.QToolButton()
        reset.setProperty("inspectorRole", "reset")
        reset.setFixedWidth(PROPERTY_RESET_WIDTH)
        reset.setIcon(self._reset_icon())
        reset.setIconSize(QtCore.QSize(RESET_ICON_SIZE, RESET_ICON_SIZE))
        reset.setMinimumHeight(PROPERTY_CONTROL_HEIGHT)
        reset.setToolTip("Reset to default")
        reset.setAutoRaise(True)
        reset.setEnabled(
            bool(prop_state.get("can_reset")) and not bool(prop_state.get("is_default"))
        )
        reset.clicked.connect(
            lambda _checked=False, gid=group_id, ps=prop_state: self._emit_reset_property(
                gid, ps
            )
        )

        layout.addWidget(name, 0, 0)
        layout.addWidget(control, 0, 1)
        layout.addWidget(reset, 0, 2)
        layout.setColumnStretch(1, 1)
        row.setLayout(layout)
        return row, {
            "prop": prop_state,
            "row": row,
            "label": name,
            "control": control,
            "reset": reset,
        }

    def _build_property_widget(
        self,
        group_id: str,
        prop: dict[str, Any],
    ) -> tuple[QtWidgets.QWidget, dict[str, Any] | None]:
        kind = str(prop.get("kind", ""))
        if kind == "numeric":
            return self._build_numeric_widget(group_id, prop)
        if kind == "boolean":
            box = QtWidgets.QCheckBox()
            box.setText("")
            box.setChecked(bool(prop.get("value")))
            box.setMinimumHeight(PROPERTY_CONTROL_HEIGHT)
            box.toggled.connect(
                lambda checked, gid=group_id, ps=prop: self._emit_property(
                    gid, ps, bool(checked)
                )
            )
            row, binding = self._build_row_widget(
                group_id,
                prop,
                title=str(prop.get("title") or ""),
                control=box,
            )
            binding["kind"] = "boolean"
            binding["checkbox"] = box
            return row, binding
        if kind == "list":
            combo = QtWidgets.QComboBox()
            combo.setMinimumHeight(PROPERTY_CONTROL_HEIGHT)
            for value in list(prop.get("values") or []):
                combo.addItem(str(value), userData=value)
            combo.setCurrentIndex(int(prop.get("selected_index") or 0))
            combo.currentIndexChanged.connect(
                lambda index, gid=group_id, ps=prop, box=combo: self._emit_property(
                    gid,
                    ps,
                    box.itemData(index),
                )
            )
            row, binding = self._build_row_widget(
                group_id,
                prop,
                title=str(prop.get("title") or ""),
                control=combo,
            )
            binding["kind"] = "list"
            binding["combo"] = combo
            return row, binding

        value_label = QtWidgets.QLabel(str(prop.get("value") or ""))
        value_label.setMinimumHeight(PROPERTY_CONTROL_HEIGHT)
        row, binding = self._build_row_widget(
            group_id,
            prop,
            title=str(prop.get("title") or prop.get("field") or ""),
            control=value_label,
        )
        binding["kind"] = "display"
        binding["value_label"] = value_label
        return row, binding

    def _build_numeric_widget(
        self,
        group_id: str,
        prop: dict[str, Any],
    ) -> tuple[QtWidgets.QWidget, dict[str, Any]]:
        value = float(prop.get("value", 0.0))
        minimum = float(prop.get("min", value))
        maximum = float(prop.get("max", value))
        step = float(prop.get("step") or 0.001)

        spin = PropertyNumericSpinBox()
        spin.setDecimals(NUMERIC_INPUT_DECIMALS)
        spin.setRange(minimum, maximum)
        spin.setSingleStep(step)
        spin.setValue(value)
        spin.sync_committed_value(value)
        spin.setMinimumHeight(PROPERTY_CONTROL_HEIGHT)
        spin.value_committed.connect(
            lambda _value, gid=group_id, ps=prop, box=spin: self._emit_property(
                gid,
                ps,
                float(box.value()),
            )
        )
        row, binding = self._build_row_widget(
            group_id,
            prop,
            title=str(prop.get("title") or ""),
            control=spin,
        )
        binding["kind"] = "numeric-spin"
        binding["spin"] = spin
        return row, binding

    def _update_property_widget(
        self,
        binding: dict[str, Any],
        prop: dict[str, Any],
    ) -> None:
        prop_state = binding["prop"]
        prop_state.clear()
        prop_state.update(prop)

        binding["label"].setText(str(prop.get("title") or ""))
        binding["reset"].setEnabled(
            bool(prop.get("can_reset")) and not bool(prop.get("is_default"))
        )

        kind = binding["kind"]
        if kind == "boolean":
            checkbox = binding["checkbox"]
            with QtCore.QSignalBlocker(checkbox):
                checkbox.setChecked(bool(prop.get("value")))
            return

        if kind == "list":
            combo = binding["combo"]
            values = list(prop.get("values") or [])
            current_values = [combo.itemData(index) for index in range(combo.count())]
            with QtCore.QSignalBlocker(combo):
                if current_values != values:
                    combo.clear()
                    for value in values:
                        combo.addItem(str(value), userData=value)
                selected_index = int(prop.get("selected_index") or 0)
                selected_index = max(0, min(combo.count() - 1, selected_index))
                if combo.count():
                    combo.setCurrentIndex(selected_index)
            return

        if kind == "numeric-spin":
            spin = binding["spin"]
            minimum = float(prop.get("min", prop.get("value", 0.0)))
            maximum = float(prop.get("max", prop.get("value", 0.0)))
            step = float(prop.get("step") or 0.001)
            value = float(prop.get("value", minimum))
            with QtCore.QSignalBlocker(spin):
                spin.setDecimals(NUMERIC_INPUT_DECIMALS)
                spin.setRange(minimum, maximum)
                spin.setSingleStep(step)
                spin.setValue(value)
                spin.sync_committed_value(value)
            return

        if kind == "display":
            binding["value_label"].setText(str(prop.get("value") or ""))


class SnippetPanel(QtWidgets.QWidget):
    """Read-only Python snippet viewer with a copy action."""

    def __init__(self, parent: QtWidgets.QWidget | None = None):
        super().__init__(parent)
        self._copy_button = QtWidgets.QPushButton("Copy")
        self._copy_button.clicked.connect(self.copy_to_clipboard)
        self._editor = QtWidgets.QPlainTextEdit()
        self._editor.setReadOnly(True)
        self._editor.setLineWrapMode(QtWidgets.QPlainTextEdit.LineWrapMode.NoWrap)

        header = QtWidgets.QHBoxLayout()
        header.setContentsMargins(0, 0, 0, 0)
        header.addStretch(1)
        header.addWidget(self._copy_button)

        layout = QtWidgets.QVBoxLayout()
        layout.setContentsMargins(8, 8, 8, 8)
        layout.addLayout(header)
        layout.addWidget(self._editor, 1)
        self.setLayout(layout)

    def set_text(self, text: str) -> None:
        self._editor.setPlainText(str(text))

    def copy_to_clipboard(self) -> None:
        clipboard = QtWidgets.QApplication.clipboard()
        clipboard.setText(self._editor.toPlainText())


class LogPanel(QtWidgets.QWidget):
    """Read-only bounded log viewer with copy and clear actions."""

    record_appended = QtCore.Signal(int)

    def __init__(
        self,
        parent: QtWidgets.QWidget | None = None,
        *,
        max_blocks: int = 2000,
    ):
        super().__init__(parent)
        self._clear_button = QtWidgets.QPushButton("Clear")
        self._clear_button.clicked.connect(self.clear)
        self._copy_button = QtWidgets.QPushButton("Copy")
        self._copy_button.clicked.connect(self.copy_to_clipboard)

        self._editor = QtWidgets.QPlainTextEdit()
        self._editor.setReadOnly(True)
        self._editor.setLineWrapMode(QtWidgets.QPlainTextEdit.LineWrapMode.NoWrap)
        self._editor.document().setMaximumBlockCount(int(max_blocks))

        header = QtWidgets.QHBoxLayout()
        header.setContentsMargins(0, 0, 0, 0)
        header.addStretch(1)
        header.addWidget(self._clear_button)
        header.addWidget(self._copy_button)

        layout = QtWidgets.QVBoxLayout()
        layout.setContentsMargins(8, 8, 8, 8)
        layout.addLayout(header)
        layout.addWidget(self._editor, 1)
        self.setLayout(layout)

    @QtCore.Slot(str, int)
    def append_message(self, text: str, levelno: int) -> None:
        self._editor.appendPlainText(str(text).rstrip())
        scrollbar = self._editor.verticalScrollBar()
        scrollbar.setValue(scrollbar.maximum())
        self.record_appended.emit(int(levelno))

    def clear(self) -> None:
        self._editor.clear()

    def text(self) -> str:
        return self._editor.toPlainText()

    def copy_to_clipboard(self) -> None:
        clipboard = QtWidgets.QApplication.clipboard()
        clipboard.setText(self._editor.toPlainText())


class ExportImageDialog(QtWidgets.QDialog):
    """Small dialog for export resolution selection."""

    def __init__(
        self,
        *,
        width: int,
        height: int,
        parent: QtWidgets.QWidget | None = None,
    ):
        super().__init__(parent)
        self.setWindowTitle("Export Image")
        self._ratio = float(width) / max(float(height), 1.0)
        self._syncing = False

        self._width = QtWidgets.QSpinBox()
        self._width.setRange(1, 16384)
        self._width.setValue(int(width))

        self._height = QtWidgets.QSpinBox()
        self._height.setRange(1, 16384)
        self._height.setValue(int(height))

        self._lock = QtWidgets.QCheckBox("Keep aspect ratio")
        self._lock.setChecked(True)

        self._width.valueChanged.connect(lambda value: self._sync_dimension("width", value))
        self._height.valueChanged.connect(lambda value: self._sync_dimension("height", value))

        form = QtWidgets.QFormLayout()
        form.addRow("Width", self._width)
        form.addRow("Height", self._height)
        form.addRow("", self._lock)

        buttons = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.StandardButton.Ok
            | QtWidgets.QDialogButtonBox.StandardButton.Cancel
        )
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)

        layout = QtWidgets.QVBoxLayout()
        layout.addLayout(form)
        layout.addWidget(buttons)
        self.setLayout(layout)

    def _sync_dimension(self, axis: str, value: int) -> None:
        if self._syncing or not self._lock.isChecked():
            return
        self._syncing = True
        try:
            if axis == "width":
                self._height.setValue(max(1, int(round(float(value) / self._ratio))))
            else:
                self._width.setValue(max(1, int(round(float(value) * self._ratio))))
        finally:
            self._syncing = False

    def selected_size(self) -> tuple[int, int]:
        return int(self._width.value()), int(self._height.value())
