# FILE: _St/Shared/windows_tray.py | ROLE: Auto8700·WS8771 공용 Windows 시스템 Tray 아이콘
# -*- coding: utf-8 -*-
"""표준 라이브러리(ctypes)만 사용하는 최소 Windows system-tray helper.

Tk UI는 main thread에서 유지하고, tray thread는 복원 요청 Event만 올린다.
호출 측은 poll_restore()를 Tk after()에서 주기적으로 확인한다.
"""
from __future__ import annotations

import ctypes
import os
import threading
from ctypes import wintypes
from typing import Optional
from pathlib import Path


class WindowsTrayIcon:
    """Windows notification area icon with click-to-restore semantics.

    - 외부 패키지를 사용하지 않는다.
    - 아이콘은 hide_to_tray 시에만 표시하고 복원 시 제거한다.
    - tray thread에서 Tk를 직접 호출하지 않는다.
    """

    def __init__(self, app_id: str, tooltip: str, icon_path: str | os.PathLike[str] | None = None):
        self.app_id = str(app_id or "_StApp")
        self.tooltip = str(tooltip or self.app_id)[:127]
        self.icon_path = str(Path(icon_path).resolve()) if icon_path else ""
        self._restore_event = threading.Event()
        self._ready_event = threading.Event()
        self._show_event = threading.Event()
        self._last_show_ok = False
        self._thread: Optional[threading.Thread] = None
        self._hwnd = 0
        self._visible = False
        self._supported = os.name == "nt"

    @property
    def supported(self) -> bool:
        return bool(self._supported)

    @property
    def visible(self) -> bool:
        return bool(self._visible)

    def start(self) -> bool:
        if not self._supported:
            return False
        if self._thread and self._thread.is_alive():
            return True
        self._ready_event.clear()
        self._thread = threading.Thread(target=self._thread_main, name=f"{self.app_id}-tray", daemon=True)
        self._thread.start()
        self._ready_event.wait(timeout=2.0)
        return bool(self._hwnd)

    def show(self) -> bool:
        if not self.start():
            return False
        try:
            self._show_event.clear()
            self._last_show_ok = False
            ctypes.windll.user32.PostMessageW(self._hwnd, self._wm_show, 0, 0)
            self._show_event.wait(timeout=1.0)
            return bool(self._last_show_ok)
        except Exception:
            return False

    def hide(self) -> None:
        if not self._supported or not self._hwnd:
            self._visible = False
            return
        try:
            ctypes.windll.user32.PostMessageW(self._hwnd, self._wm_hide, 0, 0)
        except Exception:
            self._visible = False

    def stop(self) -> None:
        if not self._supported:
            return
        hwnd = int(self._hwnd or 0)
        if hwnd:
            try:
                ctypes.windll.user32.PostMessageW(hwnd, 0x0010, 0, 0)  # WM_CLOSE
            except Exception:
                pass
        t = self._thread
        if t and t.is_alive() and t is not threading.current_thread():
            t.join(timeout=1.0)
        self._visible = False
        self._hwnd = 0

    def poll_restore(self) -> bool:
        if not self._restore_event.is_set():
            return False
        self._restore_event.clear()
        return True

    def _thread_main(self) -> None:
        user32 = ctypes.windll.user32
        shell32 = ctypes.windll.shell32
        kernel32 = ctypes.windll.kernel32

        WM_APP = 0x8000
        self._wm_tray = WM_APP + 0x51
        self._wm_show = WM_APP + 0x52
        self._wm_hide = WM_APP + 0x53
        WM_DESTROY = 0x0002
        WM_CLOSE = 0x0010
        WM_LBUTTONUP = 0x0202
        WM_LBUTTONDBLCLK = 0x0203
        WM_RBUTTONUP = 0x0205
        NIM_ADD = 0x00000000
        NIM_DELETE = 0x00000002
        NIF_MESSAGE = 0x00000001
        NIF_ICON = 0x00000002
        NIF_TIP = 0x00000004
        IDI_APPLICATION = 32512
        IMAGE_ICON = 1
        LR_LOADFROMFILE = 0x0010
        LR_DEFAULTSIZE = 0x0040

        if ctypes.sizeof(ctypes.c_void_p) == 8:
            LRESULT = ctypes.c_longlong
        else:
            LRESULT = ctypes.c_long
        WNDPROC = ctypes.WINFUNCTYPE(LRESULT, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM)

        class WNDCLASSW(ctypes.Structure):
            _fields_ = [
                ("style", wintypes.UINT),
                ("lpfnWndProc", WNDPROC),
                ("cbClsExtra", ctypes.c_int),
                ("cbWndExtra", ctypes.c_int),
                ("hInstance", wintypes.HINSTANCE),
                ("hIcon", wintypes.HICON),
                ("hCursor", wintypes.HANDLE),
                ("hbrBackground", wintypes.HBRUSH),
                ("lpszMenuName", wintypes.LPCWSTR),
                ("lpszClassName", wintypes.LPCWSTR),
            ]

        class GUID(ctypes.Structure):
            _fields_ = [
                ("Data1", ctypes.c_ulong),
                ("Data2", ctypes.c_ushort),
                ("Data3", ctypes.c_ushort),
                ("Data4", ctypes.c_ubyte * 8),
            ]

        class NOTIFYICONDATAW(ctypes.Structure):
            _fields_ = [
                ("cbSize", wintypes.DWORD),
                ("hWnd", wintypes.HWND),
                ("uID", wintypes.UINT),
                ("uFlags", wintypes.UINT),
                ("uCallbackMessage", wintypes.UINT),
                ("hIcon", wintypes.HICON),
                ("szTip", wintypes.WCHAR * 128),
                ("dwState", wintypes.DWORD),
                ("dwStateMask", wintypes.DWORD),
                ("szInfo", wintypes.WCHAR * 256),
                ("uTimeoutOrVersion", wintypes.UINT),
                ("szInfoTitle", wintypes.WCHAR * 64),
                ("dwInfoFlags", wintypes.DWORD),
                ("guidItem", GUID),
                ("hBalloonIcon", wintypes.HICON),
            ]

        kernel32.GetModuleHandleW.restype = wintypes.HMODULE
        user32.CreateWindowExW.restype = wintypes.HWND
        user32.CreateWindowExW.argtypes = [
            wintypes.DWORD, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD,
            ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int,
            wintypes.HWND, wintypes.HMENU, wintypes.HINSTANCE, wintypes.LPVOID,
        ]
        user32.DefWindowProcW.restype = LRESULT
        user32.DefWindowProcW.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM]
        user32.PostMessageW.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM]
        user32.LoadIconW.restype = wintypes.HICON
        user32.LoadImageW.restype = wintypes.HANDLE
        user32.LoadImageW.argtypes = [wintypes.HINSTANCE, wintypes.LPCWSTR, wintypes.UINT, ctypes.c_int, ctypes.c_int, wintypes.UINT]
        user32.DestroyIcon.argtypes = [wintypes.HICON]
        shell32.Shell_NotifyIconW.argtypes = [wintypes.DWORD, ctypes.POINTER(NOTIFYICONDATAW)]

        hinst = kernel32.GetModuleHandleW(None)
        class_name = f"_StTray_{self.app_id}_{os.getpid()}_{id(self)}"
        nid = NOTIFYICONDATAW()
        icon_added = False
        loaded_icon = wintypes.HICON()

        def remove_icon() -> None:
            nonlocal icon_added
            if icon_added:
                try:
                    shell32.Shell_NotifyIconW(NIM_DELETE, ctypes.byref(nid))
                except Exception:
                    pass
            icon_added = False
            self._visible = False

        def add_icon(hwnd: int) -> bool:
            nonlocal icon_added
            if icon_added:
                self._visible = True
                return True
            nid.cbSize = ctypes.sizeof(NOTIFYICONDATAW)
            nid.hWnd = hwnd
            nid.uID = 1
            nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP
            nid.uCallbackMessage = self._wm_tray
            nonlocal loaded_icon
            loaded_icon = wintypes.HICON()
            if self.icon_path and Path(self.icon_path).is_file():
                try:
                    loaded = user32.LoadImageW(None, self.icon_path, IMAGE_ICON, 0, 0, LR_LOADFROMFILE | LR_DEFAULTSIZE)
                    if loaded:
                        loaded_icon = wintypes.HICON(loaded)
                except Exception:
                    loaded_icon = wintypes.HICON()
            if loaded_icon:
                nid.hIcon = loaded_icon
            else:
                icon_resource = ctypes.cast(ctypes.c_void_p(IDI_APPLICATION), wintypes.LPCWSTR)
                nid.hIcon = user32.LoadIconW(None, icon_resource)
            nid.szTip = self.tooltip
            try:
                ok = bool(shell32.Shell_NotifyIconW(NIM_ADD, ctypes.byref(nid)))
            except Exception:
                ok = False
            icon_added = ok
            self._visible = ok
            return ok

        @WNDPROC
        def wndproc(hwnd, msg, wparam, lparam):
            if msg == self._wm_show:
                self._last_show_ok = add_icon(hwnd)
                self._show_event.set()
                return 0
            if msg == self._wm_hide:
                remove_icon()
                return 0
            if msg == self._wm_tray:
                event = int(lparam)
                if event in (WM_LBUTTONUP, WM_LBUTTONDBLCLK, WM_RBUTTONUP):
                    self._restore_event.set()
                return 0
            if msg == WM_CLOSE:
                remove_icon()
                user32.DestroyWindow(hwnd)
                return 0
            if msg == WM_DESTROY:
                remove_icon()
                user32.PostQuitMessage(0)
                return 0
            return user32.DefWindowProcW(hwnd, msg, wparam, lparam)

        wc = WNDCLASSW()
        wc.lpfnWndProc = wndproc
        wc.hInstance = hinst
        wc.lpszClassName = class_name
        atom = user32.RegisterClassW(ctypes.byref(wc))
        if not atom:
            self._ready_event.set()
            return

        hwnd = user32.CreateWindowExW(0, class_name, class_name, 0, 0, 0, 0, 0, None, None, hinst, None)
        if not hwnd:
            self._ready_event.set()
            try:
                user32.UnregisterClassW(class_name, hinst)
            except Exception:
                pass
            return

        self._hwnd = int(hwnd)
        self._ready_event.set()
        msg = wintypes.MSG()
        try:
            while user32.GetMessageW(ctypes.byref(msg), None, 0, 0) > 0:
                user32.TranslateMessage(ctypes.byref(msg))
                user32.DispatchMessageW(ctypes.byref(msg))
        finally:
            remove_icon()
            self._hwnd = 0
            if loaded_icon:
                try:
                    user32.DestroyIcon(loaded_icon)
                except Exception:
                    pass
            try:
                user32.UnregisterClassW(class_name, hinst)
            except Exception:
                pass
