
from qtpy.QtWidgets import QMainWindow, QWidget, QFrame, QLabel, QHBoxLayout, QDialog, QToolButton
from qtpy.QtCore import QEvent, QSize, Qt
from qtpy.QtGui import QIcon, QShowEvent, QResizeEvent, QCursor, QMouseEvent

from pathlib import Path
from typing import Literal
from enum import IntEnum

import ctypes
import ctypes.wintypes
import win32api
import win32con
import win32gui


class MARGINS(ctypes.Structure):
    _fields_ = [
        ("cxLeftWidth",     ctypes.c_int),
        ("cxRightWidth",    ctypes.c_int),
        ("cyTopHeight",     ctypes.c_int),
        ("cyBottomHeight",  ctypes.c_int),
    ]


class PWINDOWPOS(ctypes.Structure):
    _fields_ = [
        ('hWnd',            ctypes.wintypes.HWND),
        ('hwndInsertAfter', ctypes.wintypes.HWND),
        ('x',               ctypes.c_int),
        ('y',               ctypes.c_int),
        ('cx',              ctypes.c_int),
        ('cy',              ctypes.c_int),
        ('flags',           ctypes.wintypes.UINT)
    ]


class NCCALCSIZE_PARAMS(ctypes.Structure):
    _fields_ = [
        ('rgrc',  ctypes.wintypes.RECT * 3),
        ('lppos', ctypes.POINTER(PWINDOWPOS))
    ]


LPNCCALCSIZE_PARAMS = ctypes.POINTER(NCCALCSIZE_PARAMS)

def nt_add_shadow_effect(window: QWidget):
    hWnd = int(window.winId())
    margins = MARGINS(0,0,1,0)
    dwmapi = ctypes.cdll.LoadLibrary("dwmapi")
    dwmapi.DwmExtendFrameIntoClientArea(hWnd, ctypes.byref(margins))



# HAS NO EFFECT ON WIN10 or WIN11
# def add_window_animation(window: QWidget):
#     hWnd = int(window.winId())
#     style = win32gui.GetWindowLong(hWnd, win32con.GWL_STYLE)
#     win32gui.SetWindowLong(
#         hWnd,
#         win32con.GWL_STYLE,
#         style
#         | win32con.WS_MINIMIZEBOX
#         | win32con.WS_MAXIMIZEBOX
#         | win32con.WS_CAPTION
#         | win32con.CS_DBLCLKS
#         | win32con.WS_THICKFRAME,
#     )


class WindowButton(IntEnum):
    Close = 0x00000001
    Minimize = 0x00000010
    Maximize = 0x00000100


class TitleBarButton(QToolButton):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setStyleSheet(
            "TitleBarButton {"
            "  border: none;"
            "  border-radius: 0px;"
            "  outline: none;"
            "}"
        )
        self.setAutoRaise(True)
        self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
        self.setFixedSize(40, 28)
        self.setIconSize(QSize(16, 16))


class QModernTitleBar(QFrame):
    def __init__(self, window: QWidget) -> None:
        super().__init__(window)
        self.__window = window
        self.setStyleSheet(
            "QModernTitleBar, QModernTitleBar QLabel {"
            "  background-color: transparent;"
            "}"
        )

        self.setFixedHeight(28)
        # load icons
        iconFolder = Path(__file__).parent / "icons"
        self.__closeIcon = QIcon(str(iconFolder / "close.svg"))
        self.__closeDarkIcon = QIcon(str(iconFolder / "close_dark.svg"))
        self.__minimizeIcon = QIcon(str(iconFolder / "minimize.svg"))
        self.__minimizeDarkIcon = QIcon(str(iconFolder / "minimize_dark.svg"))
        self.__maximizeIcon = QIcon(str(iconFolder / "maximize.svg"))
        self.__maximizeDarkIcon = QIcon(str(iconFolder / "maximize_dark.svg"))
        self.__restoreIcon = QIcon(str(iconFolder / "restore.svg"))
        self.__restoreDarkIcon = QIcon(str(iconFolder / "restore_dark.svg"))

        self._minimizeButton = TitleBarButton()
        self._maximizeButton = TitleBarButton()
        self._closeButton = TitleBarButton()
        self._closeButton.setStyleSheet(
            "TitleBarButton {"
            "  background-position: center;"
            "  background-repeat: no-repeat;"
            "  border: none;"
            "  border-radius: 0px;"
            "  outline: none;"
            "  background-color: transparent;"
            "}"
            "TitleBarButton:hover {"
            "  background-color: #b43a29;"
            "}"
            "TitleBarButton:pressed {"
            "  background-color: #c50f1f;"
            "}"
        )

        self._closeButton.clicked.connect(self.__window.close)
        self._minimizeButton.clicked.connect(self.__window.showMinimized)
        self._maximizeButton.clicked.connect(self.__toggleMaximized)

        self.__buttonBox = QWidget(self)
        self.__buttonBox.setObjectName('button-box')
        self.__buttonBox.setStyleSheet(
            '#button-box {background-color: transparent;}'
        )
        self.__layoutButtonBox()

        self.__label = QLabel()
        font = self.__label.font()
        font.setBold(True)
        self.__label.setFont(font)
        self.__label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.__layoutTitleBar()
        self.__theme: str = ''
        self.setTheme('light')
        self.__window.installEventFilter(self)

    def setText(self, text: str):
        self.__label.setText(text)

    def text(self) -> str:
        return self.__label.text()

    def setTheme(self, theme: Literal['light', 'dark']):
        assert theme in ('light', 'dark')
        if theme != self.__theme:
            if theme == 'light':
                self._minimizeButton.setIcon(self.__minimizeIcon)
                self._maximizeButton.setIcon(self.__restoreIcon if self.__window.isMaximized() else self.__maximizeIcon)
                self._closeButton.setIcon(self.__closeIcon)
            elif theme == 'dark':
                self._minimizeButton.setIcon(self.__minimizeDarkIcon)
                self._maximizeButton.setIcon(self.__restoreDarkIcon if self.__window.isMaximized() else self.__maximizeDarkIcon)
                self._closeButton.setIcon(self.__closeDarkIcon)
            self.__theme = theme

    def theme(self) -> Literal['light', 'dark']:
        return self.__theme  # noqa

    def mouseMoveEvent(self, event: QMouseEvent):
        win32gui.ReleaseCapture()
        win32api.SendMessage(
            int(self.__window.winId()),
            win32con.WM_SYSCOMMAND,
            win32con.SC_MOVE | win32con.HTCAPTION,
            0
        )

    def eventFilter(self, obj, e):
        if obj is self.__window:
            if e.type() == QEvent.Type.WindowStateChange:
                if self.__window.isMaximized():
                    if self.__theme == 'light':
                        self._maximizeButton.setIcon(self.__restoreIcon)
                    elif self.__theme == 'dark':
                        self._maximizeButton.setIcon(self.__restoreDarkIcon)
                else:
                    if self.__theme == 'light':
                        self._maximizeButton.setIcon(self.__maximizeIcon)
                    elif self.__theme == 'dark':
                        self._maximizeButton.setIcon(self.__maximizeDarkIcon)

        return super().eventFilter(obj, e)

    def __toggleMaximized(self):
        if self.__window.isMaximized():
            self.__window.showNormal()
        else:
            self.__window.showMaximized()

    def __layoutButtonBox(self):
        layout = QHBoxLayout(self.__buttonBox)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.addWidget(self._minimizeButton)
        layout.addWidget(self._maximizeButton)
        layout.addWidget(self._closeButton)

    def __layoutTitleBar(self):
        layout = QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.addWidget(self.__label)
        layout.addWidget(self.__buttonBox)
        layout.setStretch(0, 1000)


class ModernWindowMixin:

    WindowButton = WindowButton

    def initialize(self):
        self.__titleBar = QModernTitleBar(self)
        nt_add_shadow_effect(self.window())

    def titleBar(self) -> QModernTitleBar:
        return self.__titleBar

    def setWindowButtonVisible(self, button: WindowButton, visible: bool):
        if button == WindowButton.Close:
            self.__titleBar._closeButton.setVisible(visible)
        elif button == WindowButton.Minimize:
            self.__titleBar._minimizeButton.setVisible(visible)
        elif button == WindowButton.Maximize:
            self.__titleBar._maximizeButton.setVisible(visible)

    def setWindowButtonEnabled(self, button: WindowButton, enabled: bool):
        if button == WindowButton.Close:
            self.__titleBar._closeButton.setEnabled(enabled)
        elif button == WindowButton.Minimize:
            self.__titleBar._minimizeButton.setEnabled(enabled)
        elif button == WindowButton.Maximize:
            self.__titleBar._maximizeButton.setEnabled(enabled)

    def visibleWindowButtons(self) -> int:
        flags = 0x000000
        if self.__titleBar._closeButton.isVisible():
            flags |= WindowButton.Close
        elif self.__titleBar._minimizeButton.isVisible():
            flags |= WindowButton.Minimize
        elif self.__titleBar._maximizeButton.isVisible():
            flags |= WindowButton.Maximize
        return flags

    def enabledWindowButtons(self) -> int:
        flags = 0x000000
        if self.__titleBar._closeButton.isEnabled():
            flags |= WindowButton.Close
        elif self.__titleBar._minimizeButton.isEnabled():
            flags |= WindowButton.Minimize
        elif self.__titleBar._maximizeButton.isEnabled():
            flags |= WindowButton.Maximize
        return flags

    def setWindowTitle(self, title: str):
        self.__titleBar.setText(title)

    def windowTitle(self) -> str:
        return self.__titleBar.text()

    def showEvent(self, event: QShowEvent):
        self.__titleBar.raise_()
        super().showEvent(event)

    def resizeEvent(self, event: QResizeEvent):
        super().resizeEvent(event)
        if hasattr(self, 'menuBar'):
            menuBarWidth = self.getMenuBarVisibleWidth()
            self.__titleBar.move(menuBarWidth, 0)
            self.__titleBar.resize(self.width() - menuBarWidth, self.__titleBar.height())
        else:
            self.__titleBar.move(0, 0)
            self.__titleBar.resize(self.width(), self.__titleBar.height())

    def getMenuBarVisibleWidth(self):
        total_width = sum(self.menuBar().actionGeometry(act).width() for act in self.menuBar().actions())
        return total_width

    def nativeEvent(self, eventType, message):
        """
        Native Windows event handler used to implement a frameless window that
        still behaves like a normal Win32 window.

        This function mainly handles several Win32 messages:

            WM_NCHITTEST     -> tell Windows which part of the window the cursor is on
                                (resize border / caption / client)
            WM_NCCALCSIZE    -> adjust client area when window is maximized
            WM_MOVE          -> force Windows to recompute frame
            WM_MOUSELEAVE    -> reset button hover state
            WM_NCLBUTTON*    -> forward maximize button events

        Qt frameless windows remove the OS frame, so we must emulate parts of it
        to restore native behaviour such as:

            • window resize borders
            • maximize padding
            • hit testing
            • caption buttons
        """

        # Convert Qt native message pointer to Win32 MSG struct
        msg = ctypes.wintypes.MSG.from_address(message.__int__())

        user32 = ctypes.windll.user32

        # ------------------------------------------------------------------
        # Get DPI of the current window
        # Windows uses DPI-aware metrics for frame sizes
        # ------------------------------------------------------------------
        dpi = user32.GetDpiForWindow(msg.hWnd)

        # ------------------------------------------------------------------
        # Native resize border thickness
        #
        # Windows frame width =
        #       SM_CXSIZEFRAME + SM_CXPADDEDBORDER
        #
        # SIZEFRAME      -> actual resize border
        # PADDEDBORDER   -> invisible padding added by Windows
        #
        # Value 92 = SM_CXPADDEDBORDER (not exported by pywin32)
        # ------------------------------------------------------------------
        SM_CXPADDEDBORDER = 92

        frameW = user32.GetSystemMetricsForDpi(win32con.SM_CXSIZEFRAME, dpi)
        borderW = (frameW + user32.GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi))
        frameH = user32.GetSystemMetricsForDpi(win32con.SM_CYSIZEFRAME, dpi)
        borderH = (frameH + user32.GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi))

        # ------------------------------------------------------------------
        # Cursor position in global coordinates
        # ------------------------------------------------------------------
        cursor_pos = QCursor.pos()

        # Convert to widget-local coordinates
        geo = self.geometry()
        x = cursor_pos.x() - geo.x()
        y = cursor_pos.y() - geo.y()

        # ================================================================
        # WM_NCHITTEST
        #
        # Windows asks: "what part of the window is under the cursor?"
        #
        # We return values such as:
        #
        #   HTLEFT
        #   HTTOPRIGHT
        #   HTCAPTION
        #
        # so the OS knows how to behave.
        # ================================================================
        if msg.message == win32con.WM_NCHITTEST:

            if not self.isMaximized():
                w = geo.width()
                h = geo.height()

                left = x < frameW
                right = x >= w - frameW
                top = y < frameH
                bottom = y >= h - frameH

                if left and top:
                    return True, win32con.HTTOPLEFT

                if right and bottom:
                    return True, win32con.HTBOTTOMRIGHT

                if right and top:
                    return True, win32con.HTTOPRIGHT

                if left and bottom:
                    return True, win32con.HTBOTTOMLEFT

                if top:
                    return True, win32con.HTTOP

                if bottom:
                    return True, win32con.HTBOTTOM

                if left:
                    return True, win32con.HTLEFT

                if right:
                    return True, win32con.HTRIGHT
            # Otherwise let Qt handle normally
            else:
                return False, 0

        # ================================================================
        # WM_MOVE
        #
        # When window moves we notify Windows that frame changed.
        # This forces recalculation of shadow / borders.
        # ================================================================
        elif msg.message == win32con.WM_MOVE:

            win32gui.SetWindowPos(
                msg.hWnd,
                None,
                0,
                0,
                0,
                0,
                win32con.SWP_NOMOVE
                | win32con.SWP_NOSIZE
                | win32con.SWP_FRAMECHANGED,
            )

        # ================================================================
        # WM_NCCALCSIZE
        #
        # Adjust client area when window is maximized.
        #
        # Without this, frameless windows will:
        #
        #   • overlap taskbar
        #   • exceed monitor bounds
        #
        # Windows normally shrinks maximized windows by the resize border.
        # We must reproduce that manually.
        # ================================================================
        elif msg.message == win32con.WM_NCCALCSIZE:

            rect = ctypes.cast(msg.lParam, LPNCCALCSIZE_PARAMS).contents.rgrc[0]

            if self.isMaximized() and not self.isFullScreen():
                rect.top += borderH
                rect.left += borderW
                rect.right -= borderW
                rect.bottom -= borderH

            return True, win32con.WVR_REDRAW

        return False, 0


class QModernMainWindow(ModernWindowMixin, QMainWindow):
    def __init__(self):
        super().__init__()
        ModernWindowMixin.initialize(self)


class QModernDialog(ModernWindowMixin, QDialog):

    def __init__(self) -> None:
        super().__init__()
        ModernWindowMixin.initialize(self)
        self.setWindowButtonVisible(WindowButton.Minimize, False)
        self.setWindowButtonVisible(WindowButton.Maximize, False)


class QModernWindow(ModernWindowMixin, QWidget):

    def __init__(self) -> None:
        super().__init__()
        ModernWindowMixin.initialize(self)