# -*- coding: utf-8 -*-
"""
yjm_capture_sources.py

캡처 대상 선택 전용 모듈.
- window  : 선택된 HWND 영역
- display : 특정 모니터 1개
- desktop : 전체 가상 바탕화면

수정 포인트(v2.3):
- 실제 캡처 기준은 초기 yjm_window_cast와 동일하게 mss.monitors[1:] 원본 순서를 사용한다.
- Win32 병합/정렬은 하지 않는다. Win32 정보는 yjm_monitor_lab.py에서 별도 진단한다.
- 프로그램 실행 후 디스플레이가 바뀌어도 refresh_displays()로 다시 읽을 수 있다.

mss.monitors 규칙:
- monitors[0] : 전체 가상 바탕화면
- monitors[1] : 첫 번째 물리/논리 디스플레이
- monitors[2] : 두 번째 디스플레이
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Callable, Optional, Tuple

import mss
import win32gui

from yjm_monitor_probe import list_mss_physical_displays, set_dpi_awareness


Rect = Tuple[int, int, int, int]


@dataclass(frozen=True)
class CaptureTarget:
    mode: str
    rect: Rect
    title: str

    @property
    def monitor_dict(self) -> dict:
        left, top, right, bottom = self.rect
        return {
            "left": int(left),
            "top": int(top),
            "width": max(1, int(right - left)),
            "height": max(1, int(bottom - top)),
        }


class CaptureSourceManager:
    def __init__(self, get_selected_hwnd: Callable[[], Optional[int]]):
        set_dpi_awareness()
        self.get_selected_hwnd = get_selected_hwnd
        self.mode = "window"       # window | display | desktop
        self.display_index = 0      # UI 기준 0
        self.displays = self.list_displays()

    def set_window(self) -> None:
        self.mode = "window"

    def set_display(self, display_index: int) -> None:
        self.mode = "display"
        self.display_index = max(0, int(display_index))
        if self.displays:
            self.display_index = min(self.display_index, len(self.displays) - 1)

    def set_desktop(self) -> None:
        self.mode = "desktop"

    def refresh_displays(self) -> list[dict]:
        self.displays = self.list_displays()
        if self.displays:
            self.display_index = min(max(0, self.display_index), len(self.displays) - 1)
        else:
            self.display_index = 0
        return self.displays

    def get_displays(self) -> list[dict]:
        if not self.displays:
            self.refresh_displays()
        return self.displays

    @staticmethod
    def _normalize_display(left: int, top: int, width: int, height: int, *, source: str, index: int, device: str = "", primary: bool = False) -> dict:
        left = int(left)
        top = int(top)
        width = int(width)
        height = int(height)
        return {
            "left": left,
            "top": top,
            "width": max(1, width),
            "height": max(1, height),
            "right": left + max(1, width),
            "bottom": top + max(1, height),
            "source": source,
            "index": index,
            "device": device,
            "primary": bool(primary),
        }

    @staticmethod
    def list_displays() -> list[dict]:
        """실제 캡처 대상은 mss.monitors[1:] 원본 순서를 그대로 사용한다.

        초기 yjm_window_cast.py도 self.sct.monitors[1:]를 그대로 사용했다.
        그래서 여기서는 Win32 EnumDisplayMonitors와 병합하거나 top/left로 정렬하지 않는다.
        """
        displays: list[dict] = []
        for i, mon in enumerate(list_mss_physical_displays()):
            displays.append(
                CaptureSourceManager._normalize_display(
                    mon.left,
                    mon.top,
                    mon.width,
                    mon.height,
                    source="mss",
                    index=mon.index,   # 실제 mss index. 보통 1, 2, ...
                    device=mon.device,
                    primary=mon.primary,
                )
            )
        return displays

    @staticmethod
    def get_virtual_desktop() -> dict:
        with mss.mss() as sct:
            m = sct.monitors[0]
            return CaptureSourceManager._normalize_display(
                m["left"],
                m["top"],
                m["width"],
                m["height"],
                source="mss",
                index=0,
            )

    def get_target(self) -> Optional[CaptureTarget]:
        if self.mode == "window":
            return self._get_window_target()
        if self.mode == "display":
            return self._get_display_target()
        if self.mode == "desktop":
            return self._get_desktop_target()
        return None

    def _get_window_target(self) -> Optional[CaptureTarget]:
        hwnd = self.get_selected_hwnd()
        if not hwnd or not win32gui.IsWindow(hwnd):
            return None

        left, top, right, bottom = win32gui.GetWindowRect(hwnd)
        if right - left <= 2 or bottom - top <= 2:
            return None

        title = win32gui.GetWindowText(hwnd) or f"HWND {hwnd}"
        return CaptureTarget("window", (left, top, right, bottom), title)

    def _get_display_target(self) -> Optional[CaptureTarget]:
        displays = self.get_displays()
        if not displays:
            return None
        idx = min(max(0, self.display_index), len(displays) - 1)
        m = displays[idx]
        left = int(m["left"])
        top = int(m["top"])
        right = left + int(m["width"])
        bottom = top + int(m["height"])
        src = m.get("source", "")
        dev = m.get("device", "")
        title = f"디스플레이 {idx} {int(m['width'])}x{int(m['height'])} @ {left},{top}"
        if src:
            title += f" [{src}]"
        if dev:
            title += f" {dev}"
        return CaptureTarget("display", (left, top, right, bottom), title)

    def _get_desktop_target(self) -> Optional[CaptureTarget]:
        m = self.get_virtual_desktop()
        left = int(m["left"])
        top = int(m["top"])
        right = left + int(m["width"])
        bottom = top + int(m["height"])
        return CaptureTarget("desktop", (left, top, right, bottom), "전체 바탕화면")
