import ctypes
import os
import sys
from ctypes import wintypes

from PyQt6.QtWidgets import QWidget, QApplication
from PyQt6.QtCore import Qt, QPoint, pyqtSignal, QPropertyAnimation, QEasingCurve, QRectF
from PyQt6.QtGui import (QPainter, QPixmap, QBrush, QColor,
                          QPainterPath, QRadialGradient, QLinearGradient,
                          QIcon, QRegion)

# 球体内容直径；外侧留一圈给投影
BALL_DIAM = 96
SHADOW_PAD = 5
# 窗口边长 = 圆直径（含投影环），main 里 show_near 用此尺寸
BALL_SIZE = BALL_DIAM + 1 * SHADOW_PAD
DRAG_THRESHOLD = 6


def _resource_path(*parts: str) -> str:
    """开发与 PyInstaller 打包后均能定位项目根目录资源。"""
    base = sys._MEIPASS if hasattr(sys, "_MEIPASS") else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base, *parts)


def _resolve_logo_path():
    for name in ("logo.png", "logo.ico"):
        p = _resource_path(name)
        if os.path.isfile(p):
            return p
    return None


def _load_logo_pixmap(path):
    """ICO 多尺寸时用 QIcon 取较大图，避免位图过小、放大后发糊或带边线感。"""
    if not path:
        return QPixmap()
    if os.path.splitext(path)[1].lower() == ".ico":
        pm = QIcon(path).pixmap(512, 512)
        if not pm.isNull():
            return pm
    return QPixmap(path)


def _win32_set_ellipse_window_rgn(hwnd, w, h):
    """用系统区域裁切 HWND，w/h 为逻辑像素，内部换算为物理像素。"""
    if not hwnd or w < 2 or h < 2:
        return
    gdi32 = ctypes.windll.gdi32
    user32 = ctypes.windll.user32
    # 获取窗口所在显示器的 DPI，换算为物理像素
    try:
        MONITOR_DEFAULTTONEAREST = 2
        hmon = ctypes.windll.user32.MonitorFromWindow(wintypes.HWND(hwnd), MONITOR_DEFAULTTONEAREST)
        dpi_x = ctypes.c_uint(0)
        dpi_y = ctypes.c_uint(0)
        MDT_EFFECTIVE_DPI = 0
        ctypes.windll.shcore.GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, ctypes.byref(dpi_x), ctypes.byref(dpi_y))
        scale = dpi_x.value / 96.0
    except Exception:
        scale = 1.0
    pw = max(2, int(w * scale))
    ph = max(2, int(h * scale))
    hrgn = gdi32.CreateEllipticRgn(0, 0, pw, ph)
    if not hrgn:
        return
    user32.SetWindowRgn(hwnd, hrgn, True)


def _win32_disable_dwm_rounded_shell(hwnd):
    """减弱 Win11 给无边框窗套的圆角矩形描边/阴影（与自绘圆叠在一起会像方框）。"""
    if not hwnd:
        return
    try:
        dwm = ctypes.windll.dwmapi
        DWMWA_WINDOW_CORNER_PREFERENCE = 33
        DWMWCP_DONOTROUND = 1
        pref = ctypes.c_uint(DWMWCP_DONOTROUND)
        dwm.DwmSetWindowAttribute(
            wintypes.HWND(hwnd),
            wintypes.DWORD(DWMWA_WINDOW_CORNER_PREFERENCE),
            ctypes.byref(pref),
            ctypes.sizeof(pref),
        )
    except Exception:
        pass


class FloatBall(QWidget):
    clicked = pyqtSignal()
    right_clicked = pyqtSignal(QPoint)

    def __init__(self, parent=None):
        super().__init__(parent)
        self._stays_on_top = False
        self._apply_window_flags()
        self._logo_path = _resolve_logo_path()
        if self._logo_path:
            self.setWindowIcon(QIcon(self._logo_path))
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground)
        self.setAutoFillBackground(False)
        # 正方形窗口边长 = 圆直径，绘制也用满圆，避免出现「大圆环套小图」
        self.setFixedSize(BALL_SIZE, BALL_SIZE)
        self._apply_circular_window_shape()
        self.setCursor(Qt.CursorShape.PointingHandCursor)

        self._logo = _load_logo_pixmap(self._logo_path) if self._logo_path else QPixmap()
        self._drag_start = QPoint()
        self._dragging = False
        self._hovered = False
        self._glow = 0.0   # 0.0~1.0 光晕强度

        self._glow_anim = QPropertyAnimation(self, b"_glow_prop")
        self._glow_anim.setDuration(300)
        self._glow_anim.setEasingCurve(QEasingCurve.Type.OutCubic)

        self._place_default()

    def set_stays_on_top(self, on_top):
        """与主面板图钉联动：仅图钉开启时置顶，避免挡在其他程序前面。"""
        if on_top == self._stays_on_top:
            return
        self._stays_on_top = bool(on_top)
        was_visible = self.isVisible()
        geo = self.geometry()
        self._apply_window_flags()
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
        self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, True)
        if self._logo_path:
            self.setWindowIcon(QIcon(self._logo_path))
        self.setCursor(Qt.CursorShape.PointingHandCursor)
        self._apply_circular_window_shape()
        if was_visible:
            self.setGeometry(geo)
            self.show()

    def _apply_circular_window_shape(self):
        w, h = self.width(), self.height()
        self.setMask(QRegion(0, 0, w, h, QRegion.RegionType.Ellipse))
        if sys.platform == "win32":
            wid = int(self.winId())
            if wid:
                _win32_set_ellipse_window_rgn(wid, w, h)
                _win32_disable_dwm_rounded_shell(wid)

    def showEvent(self, event):
        super().showEvent(event)
        if sys.platform == "win32":
            wid = int(self.winId())
            if wid:
                _win32_set_ellipse_window_rgn(wid, self.width(), self.height())
                _win32_disable_dwm_rounded_shell(wid)

    def moveEvent(self, event):
        super().moveEvent(event)
        # 跨屏移动后 DPI 可能变化，重新设置物理像素 region
        if sys.platform == "win32":
            wid = int(self.winId())
            if wid:
                _win32_set_ellipse_window_rgn(wid, self.width(), self.height())

    def _apply_window_flags(self):
        flags = (
            Qt.WindowType.FramelessWindowHint
            | Qt.WindowType.Tool
            | Qt.WindowType.NoDropShadowWindowHint
            | Qt.WindowType.WindowStaysOnTopHint
        )
        self.setWindowFlags(flags)

    def _get_glow(self): return self._glow
    def _set_glow(self, v): self._glow = v; self.update()
    from PyQt6.QtCore import pyqtProperty
    _glow_prop = pyqtProperty(float, _get_glow, _set_glow)

    def _current_screen_geometry(self):
        """返回当前窗口所在屏幕的可用区域，找不到时回退到主屏幕。"""
        center = self.geometry().center()
        screen = QApplication.screenAt(center)
        if screen is None:
            screen = QApplication.primaryScreen()
        return screen.availableGeometry()

    def _place_default(self):
        screen = QApplication.primaryScreen().availableGeometry()
        self.move(
            screen.right() - self.width() - 20,
            screen.top() + screen.height() // 2 - self.height() // 2,
        )

    def place_on_screen_of(self, widget):
        """将悬浮球放到指定 widget 所在屏幕的右侧中央。"""
        # frameGeometry().center() 直接是全局坐标，对顶层窗口最可靠
        global_center = widget.frameGeometry().center()
        screen_obj = QApplication.screenAt(global_center)
        if screen_obj is None:
            screen_obj = QApplication.primaryScreen()
        screen = screen_obj.availableGeometry()
        self.move(
            screen.right() - self.width() - 20,
            screen.top() + screen.height() // 2 - self.height() // 2,
        )

    def _paint_radius_outer(self):
        w, h = self.width(), self.height()
        return min(w, h) * 0.5 - 0.5

    def _paint_radius_ball(self):
        return self._paint_radius_outer() - SHADOW_PAD

    def paintEvent(self, event):
        p = QPainter(self)
        p.setRenderHint(QPainter.RenderHint.Antialiasing)

        w, h = self.width(), self.height()
        cx = w * 0.5
        cy = h * 0.5
        r_out = self._paint_radius_outer()
        r = self._paint_radius_ball()
        g = self._glow
        glow_strength = 0.28 + 0.72 * g

        disc_out = QRectF(cx - r_out, cy - r_out, r_out * 2, r_out * 2)
        disc = QRectF(cx - r, cy - r, r * 2, r * 2)

        # 0. 外环投影（内容圆与窗口边之间的环形：下重上轻，模拟落地阴影）
        p.setPen(Qt.PenStyle.NoPen)
        outer_path = QPainterPath()
        outer_path.addEllipse(disc_out)
        inner_hole = QPainterPath()
        inner_hole.addEllipse(disc)
        ring = outer_path.subtracted(inner_hole)
        sh = QLinearGradient(0, cy - r_out, 0, cy + r_out)
        sh.setColorAt(0.0, QColor(0, 0, 0, 0))
        sh.setColorAt(0.45, QColor(0, 0, 0, 0))
        sh.setColorAt(0.62, QColor(0, 0, 0, int(18 + 22 * glow_strength)))
        sh.setColorAt(0.82, QColor(0, 0, 0, int(38 + 35 * g)))
        sh.setColorAt(1.0, QColor(0, 0, 0, int(52 + 40 * g)))
        p.fillPath(ring, QBrush(sh))

        clip = QPainterPath()
        clip.addEllipse(disc)
        p.setClipPath(clip)

        # 1. 毛玻璃底：半透明冷白渐变（磨砂基底）
        frost_bg = QLinearGradient(0, cy - r, 0, cy + r)
        frost_bg.setColorAt(0.0, QColor(255, 255, 255, int(175 + 35 * glow_strength)))
        frost_bg.setColorAt(0.42, QColor(244, 249, 255, int(130 + 40 * glow_strength)))
        frost_bg.setColorAt(1.0, QColor(228, 236, 248, int(155 + 45 * glow_strength)))
        p.setBrush(QBrush(frost_bg))
        p.drawEllipse(disc)

        # 2. 斜向磨砂高光（模拟玻璃反光）
        sheen = QLinearGradient(cx - r, cy - r, cx + r * 0.55, cy + r * 0.85)
        sheen.setColorAt(0.0, QColor(255, 255, 255, 0))
        sheen.setColorAt(0.38, QColor(255, 255, 255, int(38 + 45 * glow_strength)))
        sheen.setColorAt(0.52, QColor(255, 255, 255, int(14 + 18 * glow_strength)))
        sheen.setColorAt(1.0, QColor(255, 255, 255, 0))
        p.setCompositionMode(QPainter.CompositionMode.CompositionMode_SoftLight)
        p.setBrush(QBrush(sheen))
        p.drawEllipse(disc)
        p.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver)

        # 3. Logo（cover 铺满圆）
        if not self._logo.isNull():
            p.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, True)
            side = max(1, int(2 * r))
            scaled = self._logo.scaled(
                side, side,
                Qt.AspectRatioMode.KeepAspectRatioByExpanding,
                Qt.TransformationMode.SmoothTransformation
            )
            # 居中裁切到 side×side，避免 Expanding 后图片偏大导致偏移
            ox = (scaled.width() - side) // 2
            oy = (scaled.height() - side) // 2
            cropped = scaled.copy(ox, oy, side, side)
            p.drawPixmap(int(cx - side / 2), int(cy - side / 2), cropped)

        # 4. 覆在图上的雾面（毛玻璃「蒙在内容上」）
        veil = QLinearGradient(cx, cy - r, cx, cy + r)
        veil.setColorAt(0.0, QColor(255, 255, 255, int(52 + 38 * g)))
        veil.setColorAt(0.35, QColor(255, 255, 255, int(28 + 22 * g)))
        veil.setColorAt(0.75, QColor(248, 252, 255, int(18 + 15 * g)))
        veil.setColorAt(1.0, QColor(240, 245, 252, int(22 + 18 * g)))
        p.setBrush(QBrush(veil))
        p.drawEllipse(disc)

        # 5. 内缘光晕（边缘略亮，悬停时更柔）
        rim = QRadialGradient(cx, cy, r)
        rim.setColorAt(0.0, QColor(255, 255, 255, 0))
        rim.setColorAt(0.62, QColor(255, 255, 255, 0))
        rim.setColorAt(0.88, QColor(255, 255, 255, int(28 + 55 * glow_strength)))
        rim.setColorAt(0.97, QColor(255, 255, 255, int(45 + 85 * glow_strength)))
        rim.setColorAt(1.0, QColor(255, 255, 255, int(22 + 40 * glow_strength)))
        p.setBrush(QBrush(rim))
        p.drawEllipse(disc)

        # 6. 顶区高光（玻璃上沿反光）
        hi = QRadialGradient(cx, cy - r * 0.22, r * 1.02)
        hi.setColorAt(0.0, QColor(255, 255, 255, int(42 + 55 * glow_strength)))
        hi.setColorAt(0.45, QColor(255, 255, 255, int(12 + 18 * glow_strength)))
        hi.setColorAt(1.0, QColor(255, 255, 255, 0))
        p.setBrush(QBrush(hi))
        p.drawEllipse(disc)

        p.setClipping(False)
        p.end()

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self._drag_start = event.globalPosition().toPoint()
            self._dragging = False
        elif event.button() == Qt.MouseButton.RightButton:
            self.right_clicked.emit(event.globalPosition().toPoint())

    def mouseMoveEvent(self, event):
        if not (event.buttons() & Qt.MouseButton.LeftButton):
            return
        delta = event.globalPosition().toPoint() - self._drag_start
        if not self._dragging and delta.manhattanLength() > DRAG_THRESHOLD:
            self._dragging = True
        if self._dragging:
            new_pos = self.pos() + delta
            self._drag_start = event.globalPosition().toPoint()
            # 用鼠标当前位置所在屏幕来限制边界，支持多显示器拖动
            cursor_screen = QApplication.screenAt(event.globalPosition().toPoint())
            if cursor_screen is None:
                cursor_screen = QApplication.primaryScreen()
            screen = cursor_screen.availableGeometry()
            new_pos.setX(max(screen.left(), min(new_pos.x(), screen.right() - self.width())))
            new_pos.setY(max(screen.top(), min(new_pos.y(), screen.bottom() - self.height())))
            self.move(new_pos)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            if not self._dragging:
                self.clicked.emit()
            else:
                self._snap_to_edge()

    def enterEvent(self, event):
        self._hovered = True
        self._glow_anim.stop()
        self._glow_anim.setStartValue(self._glow)
        self._glow_anim.setEndValue(1.0)
        self._glow_anim.start()

    def leaveEvent(self, event):
        self._hovered = False
        self._glow_anim.stop()
        self._glow_anim.setStartValue(self._glow)
        self._glow_anim.setEndValue(0.0)
        self._glow_anim.start()

    def _snap_to_edge(self):
        screen = self._current_screen_geometry()
        cx = self.x() + self.width() // 2
        cy = self.y() + self.height() // 2
        dists = {
            "l": cx - screen.left(),
            "r": screen.right() - cx,
            "t": cy - screen.top(),
            "b": screen.bottom() - cy,
        }
        side = min(dists, key=dists.get)
        ends = {
            "l": QPoint(screen.left(), self.y()),
            "r": QPoint(screen.right() - self.width(), self.y()),
            "t": QPoint(self.x(), screen.top()),
            "b": QPoint(self.x(), screen.bottom() - self.height()),
        }
        anim = QPropertyAnimation(self, b"pos")
        anim.setDuration(200)
        anim.setEasingCurve(QEasingCurve.Type.OutCubic)
        anim.setStartValue(self.pos())
        anim.setEndValue(ends[side])
        self._snap_anim = anim
        anim.start()
