import argparse
import multiprocessing as mp
import sys
import threading
import time

import cv2
import numpy as np


def _fix_qt_plugin_path():
    try:
        from PyQt5.QtCore import QLibraryInfo

        import os

        os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = QLibraryInfo.location(
            QLibraryInfo.PluginsPath
        )
    except Exception:
        pass


_fix_qt_plugin_path()

_QT_IMPORT_ERROR = None
try:
    from PyQt5.QtCore import QEasingCurve, QPropertyAnimation, Qt, QTimer
    from PyQt5.QtGui import QFont, QImage, QPixmap
    from PyQt5.QtWidgets import (
        QApplication,
        QFrame,
        QGraphicsOpacityEffect,
        QGridLayout,
        QHBoxLayout,
        QLabel,
        QMainWindow,
        QPushButton,
        QSizePolicy,
        QVBoxLayout,
        QWidget,
    )
except Exception as exc:
    _QT_IMPORT_ERROR = exc
    Qt = QTimer = QPropertyAnimation = QEasingCurve = QFont = QImage = QPixmap = QApplication = None
    QGridLayout = QHBoxLayout = QLabel = QPushButton = QSizePolicy = None
    QVBoxLayout = QWidget = None
    QFrame = QGraphicsOpacityEffect = QMainWindow = object

try:
    import pyqtgraph as pg
except Exception as exc:
    pg = None
    _PYQTGRAPH_IMPORT_ERROR = exc
else:
    _PYQTGRAPH_IMPORT_ERROR = None

from .main import build_controller


PLOT_POINTS = 180
PLOT_FORCE_Y_MIN_N = 0.0
PLOT_FORCE_Y_MAX_N = 3.0
PLOT_SHEAR_Y_MIN_N = -3.0
PLOT_SHEAR_Y_MAX_N = 3.0
FORCE_DISPLAY_ZERO_THRESHOLD_N = 0.05
CURVE_COLOR = "#00E5FF"


def cv2_to_pixmap(img_bgr):
    if img_bgr is None:
        return QPixmap()
    if len(img_bgr.shape) == 2:
        img_bgr = cv2.cvtColor(img_bgr, cv2.COLOR_GRAY2BGR)
    rgb = cv2.cvtColor(np.ascontiguousarray(img_bgr), cv2.COLOR_BGR2RGB)
    h, w, ch = rgb.shape
    qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy()
    return QPixmap.fromImage(qimg)


def force_display_value(value):
    value = float(value)
    if abs(value) < FORCE_DISPLAY_ZERO_THRESHOLD_N:
        return 0.0
    return value


def fmt(value, unit="", width=0, precision=3):
    if value is None:
        text = "--"
    else:
        value = force_display_value(value)
        text = "0" if value == 0.0 else f"{value:.{precision}f}"
    if width:
        text = text.rjust(width)
    return f"{text}{unit}"


STYLE = """
QMainWindow {
    background-color: #000000;
}
QWidget {
    background-color: #000000;
    color: #9a9a9a;
    font-family: "Microsoft YaHei", "SimHei";
}
QLabel#Title {
    color: #d7d7d7;
    font-size: 18px;
    font-weight: 600;
}
QLabel#Subtle {
    color: #777777;
    font-size: 12px;
}
QLabel#MetricLabel {
    color: #888888;
    font-size: 12px;
}
QLabel#MetricValue {
    color: #f2f2f2;
    font-family: "Cascadia Code", "Consolas";
    font-size: 22px;
    font-weight: 600;
}
QLabel#SmallValue {
    color: #d8d8d8;
    font-family: "Cascadia Code", "Consolas";
    font-size: 15px;
}
QLabel#StatusChip {
    background-color: #101010;
    border: 1px solid #303030;
    border-radius: 4px;
    color: #00e5ff;
    font-family: "Cascadia Code", "Consolas";
    font-size: 18px;
    font-weight: 600;
    padding: 8px 12px;
}
QLabel#ActionChip {
    background-color: #0b0b0b;
    border: 1px solid #262626;
    border-radius: 4px;
    color: #c8c8c8;
    font-family: "Cascadia Code", "Consolas";
    font-size: 13px;
    padding: 7px 10px;
}
QLabel#ImagePanel {
    background-color: #0b0b0b;
    border: 1px solid #262626;
    border-radius: 4px;
}
QFrame#Panel {
    background-color: #050505;
    border: 1px solid #222222;
    border-radius: 4px;
}
QPushButton {
    background-color: #171717;
    color: #d8d8d8;
    border: 1px solid #363636;
    border-radius: 4px;
    padding: 7px 16px;
    font-size: 13px;
}
QPushButton:hover {
    background-color: #242424;
    color: #ffffff;
    border: 1px solid #5a5a5a;
}
QPushButton:pressed {
    background-color: #0f0f0f;
    border: 1px solid #8a8a8a;
    padding-top: 8px;
    padding-bottom: 6px;
}
QPushButton:disabled {
    background-color: #101010;
    color: #606060;
    border: 1px solid #242424;
}
QPushButton#StartButton {
    background-color: #171717;
    color: #d8d8d8;
    border: 1px solid #363636;
}
QPushButton#StopButton {
    background-color: #171717;
    color: #d8d8d8;
    border: 1px solid #363636;
}
QPushButton#RestartButton {
    background-color: #171717;
    color: #d8d8d8;
    border: 1px solid #363636;
}
QPushButton#ManualButton {
    background-color: #171717;
    color: #d8d8d8;
    border: 1px solid #363636;
}
QPushButton#HoldButton {
    background-color: #171717;
    color: #d8d8d8;
    border: 1px solid #363636;
}
"""


if QPushButton is not None:
    class AnimatedButton(QPushButton):
        def __init__(self, text=""):
            super().__init__(text)
            self._opacity = QGraphicsOpacityEffect(self)
            self._opacity.setOpacity(1.0)
            self.setGraphicsEffect(self._opacity)
            self._animation = QPropertyAnimation(self._opacity, b"opacity", self)
            self._animation.setEasingCurve(QEasingCurve.OutCubic)

        def _animate_opacity(self, target, duration):
            self._animation.stop()
            self._animation.setDuration(duration)
            self._animation.setStartValue(self._opacity.opacity())
            self._animation.setEndValue(target)
            self._animation.start()

        def mousePressEvent(self, event):
            if self.isEnabled():
                self._animate_opacity(0.72, 70)
            super().mousePressEvent(event)

        def mouseReleaseEvent(self, event):
            if self.isEnabled():
                self._animate_opacity(1.0, 140)
            super().mouseReleaseEvent(event)
else:
    AnimatedButton = None


class MetricBox(QFrame):
    def __init__(self, title, value="--", sub=""):
        super().__init__()
        self.setObjectName("Panel")
        layout = QVBoxLayout(self)
        layout.setContentsMargins(10, 8, 10, 8)
        layout.setSpacing(3)
        self.title_label = QLabel(title)
        self.title_label.setObjectName("MetricLabel")
        self.value_label = QLabel(value)
        self.value_label.setObjectName("MetricValue")
        self.sub_label = QLabel(sub)
        self.sub_label.setObjectName("Subtle")
        layout.addWidget(self.title_label)
        layout.addWidget(self.value_label)
        layout.addWidget(self.sub_label)

    def set_value(self, value, sub=None):
        self.value_label.setText(value)
        if sub is not None:
            self.sub_label.setText(sub)


class ImageBox(QFrame):
    def __init__(self, title):
        super().__init__()
        self.setObjectName("Panel")
        layout = QVBoxLayout(self)
        layout.setContentsMargins(8, 8, 8, 8)
        layout.setSpacing(6)
        self.title_label = QLabel(title)
        self.title_label.setObjectName("MetricLabel")
        self.image_label = QLabel()
        self.image_label.setObjectName("ImagePanel")
        self.image_label.setAlignment(Qt.AlignCenter)
        self.image_label.setMinimumSize(180, 180)
        self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.image_label.setScaledContents(True)
        layout.addWidget(self.title_label)
        layout.addWidget(self.image_label, 1)

    def set_image(self, img):
        self.image_label.setPixmap(cv2_to_pixmap(img))


class GripperDemo02Viewer(QMainWindow):
    def __init__(self, argv=None):
        super().__init__()
        if _QT_IMPORT_ERROR is not None:
            raise RuntimeError(f"PyQt5 import failed: {_QT_IMPORT_ERROR}")
        if pg is None:
            raise RuntimeError(f"pyqtgraph import failed: {_PYQTGRAPH_IMPORT_ERROR}")

        self.setWindowTitle("Gripper Demo 02")
        self.setStyleSheet(STYLE)
        self.controller = None
        self.control_thread = None
        self.latest_status = None
        self.status_lock = threading.Lock()
        self.controller_finished = False
        self.restart_pending = False
        self.button_state = "idle"
        self.start_time = time.time()
        self.force_history = []
        self.argv = argv

        self._build_ui()
        self._configure_plot()

        self.timer = QTimer(self)
        self.timer.timeout.connect(self._refresh)
        self.timer.start(33)

    def _build_ui(self):
        root = QWidget()
        self.setCentralWidget(root)
        outer = QVBoxLayout(root)
        outer.setContentsMargins(8, 8, 8, 8)
        outer.setSpacing(8)

        top = QHBoxLayout()
        top.setSpacing(8)
        title = QLabel("Orisys 夹爪")
        title.setObjectName("Title")
        self.status_chip = QLabel("state=--")
        self.status_chip.setObjectName("StatusChip")
        self.action_chip = QLabel("action=--")
        self.action_chip.setObjectName("ActionChip")
        self.start_button = AnimatedButton("启动")
        self.start_button.setObjectName("StartButton")
        self.start_button.clicked.connect(self.start_control)
        self.stop_button = AnimatedButton("停止")
        self.stop_button.setObjectName("StopButton")
        self.stop_button.clicked.connect(self.stop_control)
        self.stop_button.setEnabled(False)
        self.restart_button = AnimatedButton("重启")
        self.restart_button.setObjectName("RestartButton")
        self.restart_button.clicked.connect(self.restart_control)
        self.manual_open_button = AnimatedButton("夹爪张开")
        self.manual_open_button.setObjectName("ManualButton")
        self.manual_open_button.clicked.connect(lambda: self.send_manual_command("open"))
        self.manual_close_button = AnimatedButton("夹爪闭合")
        self.manual_close_button.setObjectName("ManualButton")
        self.manual_close_button.clicked.connect(lambda: self.send_manual_command("close"))
        self.manual_hold_button = AnimatedButton("保持")
        self.manual_hold_button.setObjectName("HoldButton")
        self.manual_hold_button.clicked.connect(self.toggle_manual_hold)
        self._set_button_state("idle")
        top.addWidget(title)
        top.addWidget(self.status_chip)
        top.addWidget(self.action_chip, 1)
        top.addWidget(self.start_button)
        top.addWidget(self.stop_button)
        top.addWidget(self.restart_button)
        top.addWidget(self.manual_open_button)
        top.addWidget(self.manual_close_button)
        top.addWidget(self.manual_hold_button)
        outer.addLayout(top)

        main = QGridLayout()
        main.setSpacing(8)
        outer.addLayout(main, 1)

        self.left_flow = ImageBox("左侧光流")
        self.right_flow = ImageBox("右侧光流")
        main.addWidget(self.left_flow, 0, 0)
        main.addWidget(self.right_flow, 1, 0)

        side = QVBoxLayout()
        side.setSpacing(8)
        main.addLayout(side, 0, 1, 2, 1)

        metrics_grid = QGridLayout()
        metrics_grid.setSpacing(8)
        side.addLayout(metrics_grid)
        self.left_force = MetricBox("左法向力", "--")
        self.right_force = MetricBox("右法向力", "--")
        self.left_shear_box = MetricBox("左切向力", "--")
        self.right_shear_box = MetricBox("右切向力", "--")
        self.force_pct_box = MetricBox("夹爪当前力比例（%）", "--")
        self.target_force_box = MetricBox("夹爪目标力比例（%）", "--")
        self.speed_box = MetricBox("速度", "--")
        self.trigger_box = MetricBox("是否触发/系统是否允许触发", "--")
        self.contact_box = MetricBox("接触", "--")
        self.release_box = MetricBox("释放检测", "--")
        boxes = [
            self.left_force,
            self.right_force,
            self.left_shear_box,
            self.right_shear_box,
            self.force_pct_box,
            self.target_force_box,
            self.speed_box,
            self.trigger_box,
            self.contact_box,
            self.release_box,
        ]
        for idx, box in enumerate(boxes):
            metrics_grid.addWidget(box, idx // 2, idx % 2)

        pg.setConfigOption("background", "#000000")
        pg.setConfigOption("foreground", "#777777")
        pg.setConfigOptions(antialias=True)
        self.plot_widget = pg.GraphicsLayoutWidget()
        self.plot_widget.setMinimumHeight(240)
        side.addWidget(self.plot_widget, 1)

        self.info_box = QFrame()
        self.info_box.setObjectName("Panel")
        info_layout = QVBoxLayout(self.info_box)
        info_layout.setContentsMargins(10, 8, 10, 8)
        self.info_label = QLabel("等待启动")
        self.info_label.setObjectName("SmallValue")
        self.info_label.setWordWrap(True)
        info_layout.addWidget(self.info_label)
        side.addWidget(self.info_box)

        main.setColumnStretch(0, 2)
        main.setColumnStretch(1, 4)
        main.setRowStretch(0, 1)
        main.setRowStretch(1, 1)

    def _configure_plot(self):
        self.plots = []
        self.curves = {}
        plot_specs = [
            ("left_normal", "左法向力", "#00E5FF", PLOT_FORCE_Y_MIN_N, PLOT_FORCE_Y_MAX_N),
            ("right_normal", "右法向力", "#ffb000", PLOT_FORCE_Y_MIN_N, PLOT_FORCE_Y_MAX_N),
            ("left_shear_x", "左切向 X", "#61d394", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N),
            ("left_shear_y", "左切向 Y", "#f07178", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N),
            ("right_shear_x", "右切向 X", "#8ab4f8", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N),
            ("right_shear_y", "右切向 Y", "#d19a66", PLOT_SHEAR_Y_MIN_N, PLOT_SHEAR_Y_MAX_N),
        ]
        for idx, (key, title, color, y_min, y_max) in enumerate(plot_specs):
            row = idx // 2
            col = idx % 2
            plot = self.plot_widget.addPlot(row=row, col=col)
            plot.setTitle(title, color="#999999", size="11pt")
            plot.titleLabel.setFont(QFont("SimHei", 11))
            plot.showAxis("top", False)
            plot.showAxis("right", False)
            plot.showGrid(x=False, y=True, alpha=0.12)
            plot.setLabel("bottom", "时间", units="s")
            plot.setLabel("left", "力", units="N")
            plot.setYRange(y_min, y_max, padding=0)
            plot.setLimits(yMin=y_min, yMax=y_max)
            plot.enableAutoRange(axis=pg.ViewBox.YAxis, enable=False)
            self.plots.append(plot)
            self.curves[key] = plot.plot(pen=pg.mkPen(color, width=2))

    def _set_button_state(self, state):
        labels = {
            "idle": ("启动", "停止", "重启"),
            "starting": ("启动中", "停止", "重启"),
            "running": ("启动", "停止", "重启"),
            "stopping": ("启动", "停止中", "重启"),
            "restarting": ("启动", "停止", "重启中"),
        }
        enabled = {
            "idle": (True, False, True),
            "starting": (False, True, False),
            "running": (False, True, True),
            "stopping": (False, False, False),
            "restarting": (False, False, False),
        }
        self.button_state = state
        start_text, stop_text, restart_text = labels[state]
        start_enabled, stop_enabled, restart_enabled = enabled[state]
        self.start_button.setText(start_text)
        self.stop_button.setText(stop_text)
        self.restart_button.setText(restart_text)
        self.start_button.setEnabled(start_enabled)
        self.stop_button.setEnabled(stop_enabled)
        self.restart_button.setEnabled(restart_enabled)
        manual_enabled = state == "running"
        self.manual_open_button.setEnabled(manual_enabled)
        self.manual_close_button.setEnabled(manual_enabled)
        self.manual_hold_button.setEnabled(manual_enabled)

    def send_manual_command(self, command):
        if self.controller is None or self.control_thread is None or not self.control_thread.is_alive():
            self.info_label.setText("请先启动控制器")
            return
        try:
            self.controller.request_manual_command(command)
        except Exception as exc:
            self.info_label.setText(f"手动命令失败：{exc}")

    def toggle_manual_hold(self):
        command = "release" if self.manual_hold_button.text() == "释放" else "hold"
        self.send_manual_command(command)

    def start_control(self, from_restart=False):
        if self.control_thread is not None and self.control_thread.is_alive():
            return
        self.force_history.clear()
        self.start_time = time.time()
        self._set_button_state("restarting" if from_restart else "starting")
        self.controller_finished = False
        if from_restart:
            self.info_label.setText("重启中：正在重新连接夹爪与两个触觉传感器")
        else:
            self.info_label.setText("启动中：正在连接夹爪与两个触觉传感器")

        self.controller = build_controller(
            self.argv,
            status_callback=self._on_status,
            include_visuals=True,
            visual_size=320,
        )
        self.control_thread = threading.Thread(
            target=self._run_controller,
            daemon=True,
        )
        self.control_thread.start()

    def stop_control(self):
        if self.controller is not None:
            self.controller.stop()
        self._set_button_state("stopping")
        self.info_label.setText("停止中：正在释放相机与夹爪")

    def restart_control(self):
        if self.control_thread is not None and self.control_thread.is_alive():
            self.restart_pending = True
            self._set_button_state("restarting")
            if self.controller is not None:
                self.controller.stop()
            self.info_label.setText("重启中：正在停止当前控制线程")
            return
        self.restart_pending = False
        self.start_control(from_restart=True)

    def _run_controller(self):
        try:
            self.controller.run()
        except Exception as exc:
            self._on_status({"state": "error", "action": str(exc), "timestamp": time.time()})
        finally:
            self.controller_finished = True

    def _on_status(self, status):
        with self.status_lock:
            self.latest_status = status

    def _refresh(self):
        with self.status_lock:
            status = self.latest_status
        if not status:
            self._update_thread_buttons()
            return
        self._update_thread_buttons()
        if (
            self.control_thread is not None
            and self.control_thread.is_alive()
            and not self.restart_pending
            and self.button_state in ("starting", "restarting")
        ):
            self._set_button_state("running")

        state = status.get("state", "--")
        action = status.get("action", "--")
        unit = status.get("normal_unit", "N")
        shear_unit = status.get("shear_unit", "N")
        self.status_chip.setText(f"state={state}")
        self.action_chip.setText(f"action={action}")
        self.manual_hold_button.setText(
            "释放" if bool(status.get("manual_hold_active")) else "保持"
        )

        self.left_force.set_value(
            fmt(status.get("left_normal"), unit),
            f"raw FPS {self._sample_fps(status, 'left_sample')}",
        )
        self.right_force.set_value(
            fmt(status.get("right_normal"), unit),
            f"raw FPS {self._sample_fps(status, 'right_sample')}",
        )
        self.left_shear_box.set_value(
            fmt(status.get("left_shear"), shear_unit),
            f"x={fmt(status.get('left_shear_x'), shear_unit)} y={fmt(status.get('left_shear_y'), shear_unit)}",
        )
        self.right_shear_box.set_value(
            fmt(status.get("right_shear"), shear_unit),
            f"x={fmt(status.get('right_shear_x'), shear_unit)} y={fmt(status.get('right_shear_y'), shear_unit)}",
        )
        self.force_pct_box.set_value(
            str(status.get("force_pct", "--")),
            f"hold_pos={status.get('hold_pos')}",
        )
        self.target_force_box.set_value(
            str(status.get("adaptive_target_force", "--")),
            f"adaptive={int(bool(status.get('adaptive_grip_enabled')))}",
        )
        self.speed_box.set_value(
            str(status.get("speed_pct", "--")),
            "speed_pct",
        )
        self.trigger_box.set_value(
            f"{int(bool(status.get('trigger')))} / {int(bool(status.get('trigger_armed')))}",
            "trigger / armed",
        )
        self.contact_box.set_value(
            str(int(bool(status.get("grip_contact")))),
            self._contact_text(status),
        )
        self.release_box.set_value(
            str(int(bool(status.get("release_armed")))),
            "release_armed",
        )

        left_sample = status.get("left_sample") or {}
        right_sample = status.get("right_sample") or {}
        self.left_flow.set_image(left_sample.get("flow_view"))
        self.right_flow.set_image(right_sample.get("flow_view"))

        elapsed = time.time() - self.start_time
        self.force_history.append(
            (
                elapsed,
                force_display_value(status.get("left_normal", 0.0) or 0.0),
                force_display_value(status.get("right_normal", 0.0) or 0.0),
                force_display_value(status.get("left_shear_x", 0.0) or 0.0),
                force_display_value(status.get("left_shear_y", 0.0) or 0.0),
                force_display_value(status.get("right_shear_x", 0.0) or 0.0),
                force_display_value(status.get("right_shear_y", 0.0) or 0.0),
            )
        )
        if len(self.force_history) > PLOT_POINTS:
            self.force_history = self.force_history[-PLOT_POINTS:]
        self._update_plot()

        self.info_label.setText(
            " | ".join(
                [
                    f"min={fmt(status.get('min_normal'), unit)}",
                    f"max={fmt(status.get('max_normal'), unit)}",
                    f"L_shear={fmt(status.get('left_shear'), shear_unit)}",
                    f"R_shear={fmt(status.get('right_shear'), shear_unit)}",
                    f"target={status.get('adaptive_target_force', '--')}",
                    f"release_armed={int(bool(status.get('release_armed')))}",
                    f"flow L/R={self._flow_text(left_sample)} / {self._flow_text(right_sample)}",
                ]
            )
        )

    def _update_plot(self):
        if not self.force_history:
            return
        data = np.asarray(self.force_history, dtype=float)
        x = data[:, 0]
        keys = [
            "left_normal",
            "right_normal",
            "left_shear_x",
            "left_shear_y",
            "right_shear_x",
            "right_shear_y",
        ]
        for idx, key in enumerate(keys, start=1):
            self.curves[key].setData(x, data[:, idx])
        if len(x) >= 2:
            x_min = max(0.0, x[-1] - 6.0)
            x_max = max(6.0, x[-1])
            for plot in self.plots:
                plot.setXRange(x_min, x_max, padding=0)

    def _update_thread_buttons(self):
        if not self.controller_finished:
            return
        self.controller_finished = False
        if self.control_thread is not None:
            self.control_thread.join(timeout=0.1)
            self.control_thread = None
        if self.restart_pending:
            self.restart_pending = False
            self.info_label.setText("重启中：正在重新启动")
            self.start_control(from_restart=True)
            return
        self._set_button_state("idle")
        if self.controller is not None:
            self.info_label.setText("已停止")

    def _sample_fps(self, status, key):
        sample = status.get(key) or {}
        fps = sample.get("fps")
        if fps is None:
            return "--"
        return f"{fps:.1f}"

    def _contact_text(self, status):
        left = status.get("left_sample") or {}
        right = status.get("right_sample") or {}
        return f"L={int(bool(left.get('is_contact')))} R={int(bool(right.get('is_contact')))}"

    def _flow_text(self, sample):
        if not sample:
            return "--"
        return f"{sample.get('flow_mean', 0.0):.2f}/{sample.get('flow_max', 0.0):.2f}"

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Q:
            self.close()
            return
        if event.key() == Qt.Key_Escape:
            if self.isFullScreen():
                self.showNormal()
            else:
                self.showFullScreen()
            return
        super().keyPressEvent(event)

    def closeEvent(self, event):
        self.timer.stop()
        self.restart_pending = False
        self.stop_control()
        if self.control_thread is not None:
            self.control_thread.join(timeout=3.0)
        event.accept()


def main(argv=None):
    parser = argparse.ArgumentParser()
    parser.add_argument("--windowed", action="store_true")
    ui_args, controller_argv = parser.parse_known_args(argv)

    if _QT_IMPORT_ERROR is not None:
        print(
            "PyQt5 is required for the visualizer. "
            "Install it in the active environment, for example: pip install PyQt5 pyqtgraph"
        )
        print(f"Import error: {_QT_IMPORT_ERROR}")
        return 1
    if pg is None:
        print(
            "pyqtgraph is required for the visualizer. "
            "Install it in the active environment, for example: pip install pyqtgraph"
        )
        print(f"Import error: {_PYQTGRAPH_IMPORT_ERROR}")
        return 1

    QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
    QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
    app = QApplication(sys.argv[:1])
    window = GripperDemo02Viewer(controller_argv)
    if ui_args.windowed:
        window.resize(1500, 900)
        window.show()
    else:
        window.showFullScreen()
    window.start_control()
    return app.exec_()


if __name__ == "__main__":
    mp.freeze_support()
    raise SystemExit(main(sys.argv[1:]))
