# edited by glg

import threading
import weakref
from typing import Callable, Optional, Tuple

from PySide6.QtCore import QEvent, QMargins, QObject, QSize, QTimer
from PySide6.QtGui import QGuiApplication
from PySide6.QtWidgets import QAbstractButton, QGridLayout, QLayout, QWidget


_RUNTIME_LOCK = threading.RLock()
_RUNTIME_SINGLETON = None


class UiScaleRuntime(QObject):
    # edited by glg
    # Runtime scaler global:
    # - hitung faktor skala dari resolusi + DPI + override config
    # - patch API ukuran widget/layout agar nilai piksel statis otomatis terskala
    # - re-apply ukuran tersimpan saat monitor/DPI berubah.
    def __init__(self, app, config: Optional[dict] = None):
        super().__init__(app)
        self._app = app
        self._cfg = config if isinstance(config, dict) else {}
        self._current_scale = 1.0
        self._recompute_scheduled = False
        self._listeners = []
        self._widget_bases = weakref.WeakKeyDictionary()
        self._layout_bases = weakref.WeakKeyDictionary()
        self._button_bases = weakref.WeakKeyDictionary()
        self._ensure_patched()
        self._bind_app_events()
        self.refresh_scale(force=True)

    def _cfg_int(self, key: str, default_value: int) -> int:
        try:
            return int(self._cfg.get(key, default_value))
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError):
            return int(default_value)

    def _cfg_float(self, key: str, default_value: float) -> float:
        try:
            return float(self._cfg.get(key, default_value))
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError):
            return float(default_value)

    def _get_screen_scale(self) -> float:
        screen = QGuiApplication.primaryScreen()
        if screen is None:
            return 1.0
        geo = screen.availableGeometry()
        base_width = max(1, self._cfg_int("ui_base_width", 1900))
        base_height = max(1, self._cfg_int("ui_base_height", 1000))
        ui_scale_max = max(0.1, self._cfg_float("ui_scale_max", 1.0))
        ui_scale_min = max(0.1, self._cfg_float("ui_scale_min", 0.75))
        override = self._cfg_float("ui_scale_override", 0.0)
        dpi = float(screen.logicalDotsPerInch() or 96.0)
        dpi_scale = max(0.75, min(1.5, dpi / 96.0))
        resolution_scale = min(
            float(geo.width()) / float(base_width),
            float(geo.height()) / float(base_height),
        )
        if override > 0:
            raw_scale = override
        else:
            raw_scale = resolution_scale * dpi_scale
        return max(ui_scale_min, min(ui_scale_max, raw_scale))

    def get_scale(self) -> float:
        return float(self._current_scale or 1.0)

    def scale_px(self, value, min_value: int = 1) -> int:
        try:
            base = float(value or 0.0)
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError):
            base = 0.0
        scaled = int(round(base * self.get_scale()))
        if base == 0:
            return 0
        return max(int(min_value), scaled)

    def scale_qsize(self, size: QSize) -> QSize:
        if not isinstance(size, QSize):
            return QSize(0, 0)
        return QSize(
            self.scale_px(size.width(), min_value=0),
            self.scale_px(size.height(), min_value=0),
        )

    def register_listener(self, callback: Callable[[float], None]):
        if not callable(callback):
            return
        owner = getattr(callback, "__self__", None)
        fn = getattr(callback, "__func__", None)
        if owner is not None and fn is not None:
            self._listeners.append(("weak_method", weakref.WeakMethod(callback)))
            return
        self._listeners.append(("callable", callback))

    def refresh_scale(self, force: bool = False):
        new_scale = self._get_screen_scale()
        if (not force) and abs(new_scale - self._current_scale) < 0.005:
            return
        self._current_scale = float(new_scale)
        self._reapply_widget_bases()
        self._reapply_layout_bases()
        self._reapply_button_bases()
        alive_callbacks = []
        for mode, payload in list(self._listeners):
            callback = None
            if mode == "weak_method":
                callback = payload()
            elif mode == "callable":
                callback = payload
            if callback is None:
                continue
            try:
                callback(self._current_scale)
                alive_callbacks.append((mode, payload))
            except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError):
                continue
        self._listeners = alive_callbacks

    def schedule_refresh_scale(self):
        if self._recompute_scheduled:
            return
        self._recompute_scheduled = True

        def _run():
            self._recompute_scheduled = False
            self.refresh_scale(force=False)

        QTimer.singleShot(0, _run)

    def eventFilter(self, _obj, event):
        event_type = event.type() if event is not None else None
        screen_change = getattr(QEvent.Type, "ScreenChange", None)
        screen_change_internal = getattr(QEvent.Type, "ScreenChangeInternal", None)
        dpi_change = getattr(QEvent.Type, "DpiChange", None)
        dpi_change_internal = getattr(QEvent.Type, "DpiChangeInternal", None)
        watched_types = {
            event_code
            for event_code in (
                screen_change,
                screen_change_internal,
                dpi_change,
                dpi_change_internal,
            )
            if event_code is not None
        }
        if event_type in watched_types:
            self.schedule_refresh_scale()
        return super().eventFilter(_obj, event)

    def _bind_app_events(self):
        app = self._app
        if app is not None:
            app.installEventFilter(self)
        app_instance = app if app is not None else QGuiApplication.instance()
        if app_instance is None:
            return
        try:
            if hasattr(app_instance, "screenAdded"):
                app_instance.screenAdded.connect(
                    lambda _screen: self.schedule_refresh_scale()
                )
            if hasattr(app_instance, "screenRemoved"):
                app_instance.screenRemoved.connect(
                    lambda _screen: self.schedule_refresh_scale()
                )
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError):
            return

    def _ensure_patched(self):
        self._patch_widget_size_methods()
        self._patch_layout_methods()
        self._patch_button_methods()

    def _parse_size_pair(self, width_or_size, height=None) -> Tuple[int, int]:
        if isinstance(width_or_size, QSize):
            return int(width_or_size.width()), int(width_or_size.height())
        return int(width_or_size), int(height)

    def _remember_widget_base(self, widget, key: str, value):
        if widget is None:
            return
        data = self._widget_bases.get(widget)
        if data is None:
            data = {}
            self._widget_bases[widget] = data
        data[key] = value

    def _remember_layout_base(self, layout, key: str, value):
        if layout is None:
            return
        data = self._layout_bases.get(layout)
        if data is None:
            data = {}
            self._layout_bases[layout] = data
        data[key] = value

    def _remember_button_base(self, button, key: str, value):
        if button is None:
            return
        data = self._button_bases.get(button)
        if data is None:
            data = {}
            self._button_bases[button] = data
        data[key] = value

    def _patch_widget_size_methods(self):
        if getattr(QWidget, "_ui_scale_runtime_patched_sizes", False):
            return
        QWidget._ui_scale_runtime_patched_sizes = True

        QWidget._ui_scale_runtime_orig_setFixedSize = QWidget.setFixedSize
        QWidget._ui_scale_runtime_orig_setMinimumSize = QWidget.setMinimumSize
        QWidget._ui_scale_runtime_orig_setMaximumSize = QWidget.setMaximumSize
        QWidget._ui_scale_runtime_orig_setFixedWidth = QWidget.setFixedWidth
        QWidget._ui_scale_runtime_orig_setFixedHeight = QWidget.setFixedHeight
        QWidget._ui_scale_runtime_orig_setMinimumWidth = QWidget.setMinimumWidth
        QWidget._ui_scale_runtime_orig_setMinimumHeight = QWidget.setMinimumHeight
        QWidget._ui_scale_runtime_orig_setMaximumWidth = QWidget.setMaximumWidth
        QWidget._ui_scale_runtime_orig_setMaximumHeight = QWidget.setMaximumHeight

        def _runtime():
            return get_ui_scale_runtime()

        def _set_fixed_size(widget, width_or_size, height=None):
            runtime = _runtime()
            if runtime is None:
                if isinstance(width_or_size, QSize):
                    return QWidget._ui_scale_runtime_orig_setFixedSize(widget, width_or_size)
                return QWidget._ui_scale_runtime_orig_setFixedSize(widget, width_or_size, height)
            base_w, base_h = runtime._parse_size_pair(width_or_size, height)
            runtime._remember_widget_base(widget, "fixed_size", (base_w, base_h))
            return QWidget._ui_scale_runtime_orig_setFixedSize(
                widget,
                runtime.scale_px(base_w),
                runtime.scale_px(base_h),
            )

        def _set_minimum_size(widget, width_or_size, height=None):
            runtime = _runtime()
            if runtime is None:
                if isinstance(width_or_size, QSize):
                    return QWidget._ui_scale_runtime_orig_setMinimumSize(widget, width_or_size)
                return QWidget._ui_scale_runtime_orig_setMinimumSize(widget, width_or_size, height)
            base_w, base_h = runtime._parse_size_pair(width_or_size, height)
            runtime._remember_widget_base(widget, "minimum_size", (base_w, base_h))
            return QWidget._ui_scale_runtime_orig_setMinimumSize(
                widget,
                runtime.scale_px(base_w),
                runtime.scale_px(base_h),
            )

        def _set_maximum_size(widget, width_or_size, height=None):
            runtime = _runtime()
            if runtime is None:
                if isinstance(width_or_size, QSize):
                    return QWidget._ui_scale_runtime_orig_setMaximumSize(widget, width_or_size)
                return QWidget._ui_scale_runtime_orig_setMaximumSize(widget, width_or_size, height)
            base_w, base_h = runtime._parse_size_pair(width_or_size, height)
            runtime._remember_widget_base(widget, "maximum_size", (base_w, base_h))
            return QWidget._ui_scale_runtime_orig_setMaximumSize(
                widget,
                runtime.scale_px(base_w),
                runtime.scale_px(base_h),
            )

        def _set_fixed_width(widget, width):
            runtime = _runtime()
            if runtime is None:
                return QWidget._ui_scale_runtime_orig_setFixedWidth(widget, width)
            base_value = int(width)
            runtime._remember_widget_base(widget, "fixed_width", base_value)
            return QWidget._ui_scale_runtime_orig_setFixedWidth(widget, runtime.scale_px(base_value))

        def _set_fixed_height(widget, height):
            runtime = _runtime()
            if runtime is None:
                return QWidget._ui_scale_runtime_orig_setFixedHeight(widget, height)
            base_value = int(height)
            runtime._remember_widget_base(widget, "fixed_height", base_value)
            return QWidget._ui_scale_runtime_orig_setFixedHeight(widget, runtime.scale_px(base_value))

        def _set_minimum_width(widget, width):
            runtime = _runtime()
            if runtime is None:
                return QWidget._ui_scale_runtime_orig_setMinimumWidth(widget, width)
            base_value = int(width)
            runtime._remember_widget_base(widget, "minimum_width", base_value)
            return QWidget._ui_scale_runtime_orig_setMinimumWidth(widget, runtime.scale_px(base_value))

        def _set_minimum_height(widget, height):
            runtime = _runtime()
            if runtime is None:
                return QWidget._ui_scale_runtime_orig_setMinimumHeight(widget, height)
            base_value = int(height)
            runtime._remember_widget_base(widget, "minimum_height", base_value)
            return QWidget._ui_scale_runtime_orig_setMinimumHeight(widget, runtime.scale_px(base_value))

        def _set_maximum_width(widget, width):
            runtime = _runtime()
            if runtime is None:
                return QWidget._ui_scale_runtime_orig_setMaximumWidth(widget, width)
            base_value = int(width)
            runtime._remember_widget_base(widget, "maximum_width", base_value)
            return QWidget._ui_scale_runtime_orig_setMaximumWidth(widget, runtime.scale_px(base_value))

        def _set_maximum_height(widget, height):
            runtime = _runtime()
            if runtime is None:
                return QWidget._ui_scale_runtime_orig_setMaximumHeight(widget, height)
            base_value = int(height)
            runtime._remember_widget_base(widget, "maximum_height", base_value)
            return QWidget._ui_scale_runtime_orig_setMaximumHeight(widget, runtime.scale_px(base_value))

        QWidget.setFixedSize = _set_fixed_size
        QWidget.setMinimumSize = _set_minimum_size
        QWidget.setMaximumSize = _set_maximum_size
        QWidget.setFixedWidth = _set_fixed_width
        QWidget.setFixedHeight = _set_fixed_height
        QWidget.setMinimumWidth = _set_minimum_width
        QWidget.setMinimumHeight = _set_minimum_height
        QWidget.setMaximumWidth = _set_maximum_width
        QWidget.setMaximumHeight = _set_maximum_height

    def _patch_layout_methods(self):
        if getattr(QLayout, "_ui_scale_runtime_patched_layout", False):
            return
        QLayout._ui_scale_runtime_patched_layout = True

        QLayout._ui_scale_runtime_orig_setSpacing = QLayout.setSpacing
        QLayout._ui_scale_runtime_orig_setContentsMargins = QLayout.setContentsMargins

        def _runtime():
            return get_ui_scale_runtime()

        def _set_spacing(layout, spacing):
            runtime = _runtime()
            if runtime is None:
                return QLayout._ui_scale_runtime_orig_setSpacing(layout, spacing)
            base_spacing = int(spacing)
            runtime._remember_layout_base(layout, "spacing", base_spacing)
            return QLayout._ui_scale_runtime_orig_setSpacing(layout, runtime.scale_px(base_spacing, min_value=0))

        def _set_contents_margins(layout, left_or_margins, top=None, right=None, bottom=None):
            runtime = _runtime()
            if runtime is None:
                if isinstance(left_or_margins, QMargins):
                    return QLayout._ui_scale_runtime_orig_setContentsMargins(layout, left_or_margins)
                return QLayout._ui_scale_runtime_orig_setContentsMargins(
                    layout,
                    left_or_margins,
                    top,
                    right,
                    bottom,
                )
            if isinstance(left_or_margins, QMargins):
                base_margins = (
                    int(left_or_margins.left()),
                    int(left_or_margins.top()),
                    int(left_or_margins.right()),
                    int(left_or_margins.bottom()),
                )
            else:
                base_margins = (
                    int(left_or_margins),
                    int(top),
                    int(right),
                    int(bottom),
                )
            runtime._remember_layout_base(layout, "contents_margins", base_margins)
            scaled = tuple(runtime.scale_px(v, min_value=0) for v in base_margins)
            return QLayout._ui_scale_runtime_orig_setContentsMargins(layout, *scaled)

        QLayout.setSpacing = _set_spacing
        QLayout.setContentsMargins = _set_contents_margins

        if hasattr(QGridLayout, "setHorizontalSpacing") and not hasattr(
            QGridLayout, "_ui_scale_runtime_orig_setHorizontalSpacing"
        ):
            QGridLayout._ui_scale_runtime_orig_setHorizontalSpacing = QGridLayout.setHorizontalSpacing

            def _set_horizontal_spacing(layout, spacing):
                runtime = _runtime()
                if runtime is None:
                    return QGridLayout._ui_scale_runtime_orig_setHorizontalSpacing(layout, spacing)
                base_spacing = int(spacing)
                runtime._remember_layout_base(layout, "horizontal_spacing", base_spacing)
                return QGridLayout._ui_scale_runtime_orig_setHorizontalSpacing(
                    layout,
                    runtime.scale_px(base_spacing, min_value=0),
                )

            QGridLayout.setHorizontalSpacing = _set_horizontal_spacing

        if hasattr(QGridLayout, "setVerticalSpacing") and not hasattr(
            QGridLayout, "_ui_scale_runtime_orig_setVerticalSpacing"
        ):
            QGridLayout._ui_scale_runtime_orig_setVerticalSpacing = QGridLayout.setVerticalSpacing

            def _set_vertical_spacing(layout, spacing):
                runtime = _runtime()
                if runtime is None:
                    return QGridLayout._ui_scale_runtime_orig_setVerticalSpacing(layout, spacing)
                base_spacing = int(spacing)
                runtime._remember_layout_base(layout, "vertical_spacing", base_spacing)
                return QGridLayout._ui_scale_runtime_orig_setVerticalSpacing(
                    layout,
                    runtime.scale_px(base_spacing, min_value=0),
                )

            QGridLayout.setVerticalSpacing = _set_vertical_spacing

    def _patch_button_methods(self):
        if getattr(QAbstractButton, "_ui_scale_runtime_patched_button", False):
            return
        QAbstractButton._ui_scale_runtime_patched_button = True
        QAbstractButton._ui_scale_runtime_orig_setIconSize = QAbstractButton.setIconSize

        def _runtime():
            return get_ui_scale_runtime()

        def _set_icon_size(button, size):
            runtime = _runtime()
            if runtime is None:
                return QAbstractButton._ui_scale_runtime_orig_setIconSize(button, size)
            base_size = QSize(size) if isinstance(size, QSize) else QSize(0, 0)
            runtime._remember_button_base(button, "icon_size", base_size)
            return QAbstractButton._ui_scale_runtime_orig_setIconSize(button, runtime.scale_qsize(base_size))

        QAbstractButton.setIconSize = _set_icon_size

    def _reapply_widget_bases(self):
        for widget, data in list(self._widget_bases.items()):
            if widget is None or not isinstance(data, dict):
                continue
            fixed_size = data.get("fixed_size")
            if fixed_size:
                QWidget._ui_scale_runtime_orig_setFixedSize(
                    widget,
                    self.scale_px(fixed_size[0]),
                    self.scale_px(fixed_size[1]),
                )
            min_size = data.get("minimum_size")
            if min_size:
                QWidget._ui_scale_runtime_orig_setMinimumSize(
                    widget,
                    self.scale_px(min_size[0]),
                    self.scale_px(min_size[1]),
                )
            max_size = data.get("maximum_size")
            if max_size:
                QWidget._ui_scale_runtime_orig_setMaximumSize(
                    widget,
                    self.scale_px(max_size[0]),
                    self.scale_px(max_size[1]),
                )
            fixed_width = data.get("fixed_width")
            if fixed_width is not None:
                QWidget._ui_scale_runtime_orig_setFixedWidth(widget, self.scale_px(fixed_width))
            fixed_height = data.get("fixed_height")
            if fixed_height is not None:
                QWidget._ui_scale_runtime_orig_setFixedHeight(widget, self.scale_px(fixed_height))
            minimum_width = data.get("minimum_width")
            if minimum_width is not None:
                QWidget._ui_scale_runtime_orig_setMinimumWidth(widget, self.scale_px(minimum_width))
            minimum_height = data.get("minimum_height")
            if minimum_height is not None:
                QWidget._ui_scale_runtime_orig_setMinimumHeight(widget, self.scale_px(minimum_height))
            maximum_width = data.get("maximum_width")
            if maximum_width is not None:
                QWidget._ui_scale_runtime_orig_setMaximumWidth(widget, self.scale_px(maximum_width))
            maximum_height = data.get("maximum_height")
            if maximum_height is not None:
                QWidget._ui_scale_runtime_orig_setMaximumHeight(widget, self.scale_px(maximum_height))

    def _reapply_layout_bases(self):
        for layout, data in list(self._layout_bases.items()):
            if layout is None or not isinstance(data, dict):
                continue
            spacing = data.get("spacing")
            if spacing is not None:
                QLayout._ui_scale_runtime_orig_setSpacing(layout, self.scale_px(spacing, min_value=0))
            margins = data.get("contents_margins")
            if margins:
                scaled = tuple(self.scale_px(v, min_value=0) for v in margins)
                QLayout._ui_scale_runtime_orig_setContentsMargins(layout, *scaled)
            horizontal_spacing = data.get("horizontal_spacing")
            if (
                horizontal_spacing is not None
                and hasattr(QGridLayout, "_ui_scale_runtime_orig_setHorizontalSpacing")
                and isinstance(layout, QGridLayout)
            ):
                QGridLayout._ui_scale_runtime_orig_setHorizontalSpacing(
                    layout,
                    self.scale_px(horizontal_spacing, min_value=0),
                )
            vertical_spacing = data.get("vertical_spacing")
            if (
                vertical_spacing is not None
                and hasattr(QGridLayout, "_ui_scale_runtime_orig_setVerticalSpacing")
                and isinstance(layout, QGridLayout)
            ):
                QGridLayout._ui_scale_runtime_orig_setVerticalSpacing(
                    layout,
                    self.scale_px(vertical_spacing, min_value=0),
                )

    def _reapply_button_bases(self):
        for button, data in list(self._button_bases.items()):
            if button is None or not isinstance(data, dict):
                continue
            icon_size = data.get("icon_size")
            if icon_size is not None:
                QAbstractButton._ui_scale_runtime_orig_setIconSize(button, self.scale_qsize(icon_size))


def init_ui_scale_runtime(app, config: Optional[dict] = None) -> UiScaleRuntime:
    global _RUNTIME_SINGLETON
    with _RUNTIME_LOCK:
        if _RUNTIME_SINGLETON is None:
            _RUNTIME_SINGLETON = UiScaleRuntime(app=app, config=config)
        else:
            _RUNTIME_SINGLETON._cfg = config if isinstance(config, dict) else {}
            _RUNTIME_SINGLETON.refresh_scale(force=True)
        return _RUNTIME_SINGLETON


def get_ui_scale_runtime() -> Optional[UiScaleRuntime]:
    return _RUNTIME_SINGLETON


def get_ui_scale_factor(default_value: float = 1.0) -> float:
    runtime = get_ui_scale_runtime()
    if runtime is None:
        return float(default_value)
    return runtime.get_scale()


def scale_ui_px(value, min_value: int = 1) -> int:
    runtime = get_ui_scale_runtime()
    if runtime is None:
        try:
            return int(value)
        except (TypeError, ValueError, KeyError, AttributeError, RuntimeError, OSError):
            return 0
    return runtime.scale_px(value, min_value=min_value)


def register_ui_scale_listener(callback: Callable[[float], None]):
    runtime = get_ui_scale_runtime()
    if runtime is None:
        return
    runtime.register_listener(callback)
