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

별도 실행 가능한 모니터/캡처 진단 도구.

실행:
    python yjm_monitor_lab.py
    python yjm_monitor_lab.py --dump
    python yjm_monitor_lab.py --capture

기능:
- mss 원본 모니터 목록 확인
- Win32 모니터 목록 확인
- DPI awareness 상태 확인
- 각 모니터에 테스트/마커 창 띄우기
- mss[0], mss[1], mss[2]... 캡처 저장
- Tk 창 HWND의 실제 GetWindowRect 확인
"""

from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
import time
from pathlib import Path

import tkinter as tk
from tkinter import messagebox, ttk

import mss
from PIL import Image, ImageDraw

try:
    import win32gui
except Exception:
    win32gui = None

from yjm_monitor_probe import (
    MonitorInfo,
    format_report_text,
    get_virtual_bounds_from_monitors,
    list_mss_monitors,
    list_mss_physical_displays,
    list_win32_monitors,
    make_probe_report,
    save_probe_report,
    set_dpi_awareness,
)


OUT_DIR = Path("_monitor_probe_output")


def monitor_from_dict(d: dict) -> MonitorInfo:
    return MonitorInfo(
        api=str(d.get("api", "")),
        index=int(d.get("index", 0)),
        left=int(d.get("left", 0)),
        top=int(d.get("top", 0)),
        width=int(d.get("width", 0)),
        height=int(d.get("height", 0)),
        right=int(d.get("right", 0)),
        bottom=int(d.get("bottom", 0)),
        primary=bool(d.get("primary", False)),
        device=str(d.get("device", "")),
        name=str(d.get("name", "")),
        extra=d.get("extra"),
    )


def capture_all_mss(out_dir: Path = OUT_DIR) -> list[Path]:
    """mss.monitors 전체를 PNG로 저장한다."""
    out_dir.mkdir(parents=True, exist_ok=True)
    saved: list[Path] = []
    set_dpi_awareness()
    with mss.mss() as sct:
        for idx, mon in enumerate(sct.monitors):
            shot = sct.grab(mon)
            img = Image.frombytes("RGB", shot.size, shot.bgra, "raw", "BGRX")
            draw = ImageDraw.Draw(img)
            label = f"mss[{idx}] {mon['width']}x{mon['height']} @ {mon['left']},{mon['top']}"
            draw.rectangle((0, 0, min(img.width - 1, 700), 44), fill=(0, 0, 0))
            draw.text((12, 12), label, fill=(255, 255, 255))
            path = out_dir / f"mss_{idx}_{mon['width']}x{mon['height']}_{mon['left']}_{mon['top']}.png"
            img.save(path)
            saved.append(path)
    return saved


class MonitorLabApp:
    def __init__(self, root: tk.Tk):
        set_dpi_awareness()
        self.root = root
        self.root.title("yjm_monitor_lab - 다중 모니터 진단")
        self.root.geometry("1050x760")
        self.marker_windows: list[tk.Toplevel] = []
        self.test_windows: list[tk.Toplevel] = []
        self.report: dict = {}

        self.status_var = tk.StringVar(value="대기 중")
        self.setup_ui()
        self.refresh()
        self.root.protocol("WM_DELETE_WINDOW", self.on_close)

    def setup_ui(self):
        root_frame = ttk.Frame(self.root, padding=10)
        root_frame.pack(fill=tk.BOTH, expand=True)

        btns = ttk.Frame(root_frame)
        btns.pack(fill=tk.X, pady=(0, 8))
        ttk.Button(btns, text="새로고침", command=self.refresh).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns, text="마커 창 띄우기", command=self.show_marker_windows).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns, text="테스트 창 띄우기", command=self.show_test_windows).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns, text="창 닫기", command=self.close_probe_windows).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns, text="mss 캡처 저장", command=self.capture_and_save).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns, text="리포트 저장", command=self.save_report).pack(side=tk.LEFT, padx=(0, 6))
        ttk.Button(btns, text="결과 폴더 열기", command=self.open_output_dir).pack(side=tk.LEFT, padx=(0, 6))

        top = ttk.PanedWindow(root_frame, orient=tk.HORIZONTAL)
        top.pack(fill=tk.BOTH, expand=True)

        left = ttk.Frame(top)
        right = ttk.Frame(top)
        top.add(left, weight=1)
        top.add(right, weight=2)

        table_box = ttk.LabelFrame(left, text="모니터 목록")
        table_box.pack(fill=tk.BOTH, expand=True, padx=(0, 8))
        columns = ("api", "idx", "size", "pos", "rightbottom", "primary", "device")
        self.tree = ttk.Treeview(table_box, columns=columns, show="headings", height=13)
        headings = {
            "api": "API",
            "idx": "idx",
            "size": "size",
            "pos": "left,top",
            "rightbottom": "right,bottom",
            "primary": "primary",
            "device": "device",
        }
        widths = {
            "api": 65,
            "idx": 45,
            "size": 105,
            "pos": 115,
            "rightbottom": 115,
            "primary": 70,
            "device": 130,
        }
        for col in columns:
            self.tree.heading(col, text=headings[col])
            self.tree.column(col, width=widths[col], anchor="w")
        self.tree.pack(fill=tk.BOTH, expand=True)

        canvas_box = ttk.LabelFrame(left, text="가상 바탕화면 배치도")
        canvas_box.pack(fill=tk.BOTH, expand=True, padx=(0, 8), pady=(8, 0))
        self.canvas = tk.Canvas(canvas_box, height=260, bg="#f7f7f7", highlightthickness=1, highlightbackground="#bbbbbb")
        self.canvas.pack(fill=tk.BOTH, expand=True)

        text_box = ttk.LabelFrame(right, text="상세 리포트")
        text_box.pack(fill=tk.BOTH, expand=True)
        self.text = tk.Text(text_box, wrap="none", undo=False)
        self.text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        yscroll = ttk.Scrollbar(text_box, orient="vertical", command=self.text.yview)
        yscroll.pack(side=tk.RIGHT, fill=tk.Y)
        self.text.configure(yscrollcommand=yscroll.set)

        ttk.Label(root_frame, textvariable=self.status_var).pack(anchor="w", pady=(8, 0))
        ttk.Label(
            root_frame,
            text="판단 기준: yjm_win2rtc의 실제 디스플레이 캡처는 mss[1:], 전체 바탕화면은 mss[0]을 우선 사용해야 합니다. Win32는 비교/진단용입니다.",
            foreground="#aa0000",
            wraplength=1000,
        ).pack(anchor="w", pady=(4, 0))

    def refresh(self):
        try:
            self.report = make_probe_report()
            self.populate_table()
            self.draw_layout()
            self.text.delete("1.0", tk.END)
            self.text.insert(tk.END, format_report_text(self.report))
            mss_count = max(0, len(self.report.get("mss_monitors", [])) - 1)
            win32_count = len(self.report.get("win32_monitors", []))
            self.status_var.set(f"새로고침 완료: mss 물리 디스플레이 {mss_count}개 / win32 {win32_count}개")
        except Exception as exc:
            self.status_var.set(f"새로고침 실패: {exc}")
            messagebox.showerror("오류", str(exc))

    def populate_table(self):
        self.tree.delete(*self.tree.get_children())
        for raw in self.report.get("mss_monitors", []):
            m = monitor_from_dict(raw)
            self.tree.insert(
                "",
                tk.END,
                values=(
                    m.api,
                    m.index,
                    f"{m.width}x{m.height}",
                    f"{m.left},{m.top}",
                    f"{m.right},{m.bottom}",
                    "Y" if m.primary else "",
                    m.device,
                ),
            )
        for raw in self.report.get("win32_monitors", []):
            m = monitor_from_dict(raw)
            self.tree.insert(
                "",
                tk.END,
                values=(
                    m.api,
                    m.index,
                    f"{m.width}x{m.height}",
                    f"{m.left},{m.top}",
                    f"{m.right},{m.bottom}",
                    "Y" if m.primary else "",
                    m.device,
                ),
            )

    def draw_layout(self):
        self.canvas.delete("all")
        monitors = [monitor_from_dict(d) for d in self.report.get("mss_monitors", [])[1:]]
        if not monitors:
            self.canvas.create_text(20, 20, anchor="nw", text="mss 물리 모니터가 감지되지 않았습니다.", fill="#aa0000")
            return

        bounds = get_virtual_bounds_from_monitors(monitors)
        self.canvas.update_idletasks()
        cw = max(300, self.canvas.winfo_width())
        ch = max(220, self.canvas.winfo_height())
        margin = 24
        bw = max(1, bounds["width"])
        bh = max(1, bounds["height"])
        scale = min((cw - margin * 2) / bw, (ch - margin * 2) / bh)
        scale = max(0.02, scale)

        # 좌표축 원점 표시
        ox = margin + (0 - bounds["left"]) * scale
        oy = margin + (0 - bounds["top"]) * scale
        self.canvas.create_line(ox, margin, ox, ch - margin, fill="#999999", dash=(4, 3))
        self.canvas.create_line(margin, oy, cw - margin, oy, fill="#999999", dash=(4, 3))
        self.canvas.create_text(ox + 6, oy + 6, anchor="nw", text="0,0", fill="#555555")

        for i, m in enumerate(monitors):
            x1 = margin + (m.left - bounds["left"]) * scale
            y1 = margin + (m.top - bounds["top"]) * scale
            x2 = margin + (m.right - bounds["left"]) * scale
            y2 = margin + (m.bottom - bounds["top"]) * scale
            fill = "#d8ecff" if i % 2 == 0 else "#e7ffd8"
            outline = "#005bbb" if m.primary else "#008b3a"
            self.canvas.create_rectangle(x1, y1, x2, y2, fill=fill, outline=outline, width=3)
            label = f"mss[{m.index}]\n{m.width}x{m.height}\n@ {m.left},{m.top}"
            self.canvas.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=label, fill="#000000")

    def close_probe_windows(self):
        for w in self.marker_windows + self.test_windows:
            try:
                w.destroy()
            except Exception:
                pass
        self.marker_windows.clear()
        self.test_windows.clear()
        self.status_var.set("마커/테스트 창을 닫았습니다")

    def show_marker_windows(self):
        self.close_probe_windows()
        monitors = list_mss_physical_displays()
        if not monitors:
            messagebox.showwarning("알림", "mss 물리 모니터가 없습니다.")
            return
        for i, m in enumerate(monitors):
            w = tk.Toplevel(self.root)
            w.overrideredirect(True)
            w.attributes("-topmost", True)
            width = min(560, max(360, m.width // 2))
            height = 170
            x = m.left + 40
            y = m.top + 40
            w.geometry(f"{width}x{height}+{x}+{y}")
            bg = "#202020" if i % 2 == 0 else "#303050"
            w.configure(bg=bg)
            label = tk.Label(
                w,
                text=(
                    f"MSS[{m.index}] / yjm 디스플레이 {i}\n"
                    f"{m.width}x{m.height} @ left={m.left}, top={m.top}\n"
                    f"right={m.right}, bottom={m.bottom}\n"
                    "이 창이 어느 모니터에 뜨는지 확인하세요"
                ),
                bg=bg,
                fg="white",
                font=("Consolas", 14, "bold"),
                justify="left",
            )
            label.pack(fill=tk.BOTH, expand=True, padx=14, pady=14)
            self.marker_windows.append(w)
        self.status_var.set(f"마커 창 {len(monitors)}개 생성")

    def show_test_windows(self):
        # 기존 마커는 유지하지 않고 테스트 창만 새로 만든다.
        for w in self.test_windows:
            try:
                w.destroy()
            except Exception:
                pass
        self.test_windows.clear()

        monitors = list_mss_physical_displays()
        if not monitors:
            messagebox.showwarning("알림", "mss 물리 모니터가 없습니다.")
            return

        lines = ["## Tk 테스트 창 HWND rect"]
        for i, m in enumerate(monitors):
            w = tk.Toplevel(self.root)
            w.title(f"YJM_MONITOR_TEST_MSS{m.index}_DISPLAY{i}")
            ww = min(520, max(360, m.width // 2))
            wh = 260
            x = m.left + 100
            y = m.top + 230
            w.geometry(f"{ww}x{wh}+{x}+{y}")
            w.attributes("-topmost", True)
            text = tk.Text(w, wrap="word")
            text.pack(fill=tk.BOTH, expand=True)
            msg = (
                f"이 창은 mss[{m.index}] / yjm 디스플레이 {i}에 생성했습니다.\n\n"
                f"요청 geometry: {ww}x{wh}+{x}+{y}\n"
                f"모니터: {m.width}x{m.height} @ {m.left},{m.top}\n\n"
                "창을 다른 모니터로 옮긴 뒤 [새로고침]을 누르면 win32 GetWindowRect 결과를 비교할 수 있습니다."
            )
            text.insert("1.0", msg)
            self.test_windows.append(w)
            self.root.update_idletasks()
            if win32gui is not None:
                try:
                    hwnd = int(w.winfo_id())
                    rect = win32gui.GetWindowRect(hwnd)
                    lines.append(f"  hwnd={hwnd} title={w.title()} rect={rect}")
                except Exception as exc:
                    lines.append(f"  {w.title()} rect error: {exc}")

        self.text.insert(tk.END, "\n" + "\n".join(lines) + "\n")
        self.status_var.set(f"테스트 창 {len(self.test_windows)}개 생성")

    def capture_and_save(self):
        try:
            saved = capture_all_mss(OUT_DIR)
            self.status_var.set(f"캡처 저장 완료: {len(saved)}개 → {OUT_DIR.resolve()}")
            self.text.insert(tk.END, "\n## Saved captures\n")
            for p in saved:
                self.text.insert(tk.END, f"  {p.resolve()}\n")
        except Exception as exc:
            self.status_var.set(f"캡처 저장 실패: {exc}")
            messagebox.showerror("오류", str(exc))

    def save_report(self):
        try:
            json_path, txt_path = save_probe_report(OUT_DIR)
            self.status_var.set(f"리포트 저장 완료: {json_path}, {txt_path}")
        except Exception as exc:
            self.status_var.set(f"리포트 저장 실패: {exc}")
            messagebox.showerror("오류", str(exc))

    def open_output_dir(self):
        OUT_DIR.mkdir(parents=True, exist_ok=True)
        path = str(OUT_DIR.resolve())
        if os.name == "nt":
            os.startfile(path)  # type: ignore[attr-defined]
        else:
            subprocess.Popen(["xdg-open", path])

    def on_close(self):
        self.close_probe_windows()
        self.root.destroy()


def main(argv=None):
    parser = argparse.ArgumentParser(description="yjm monitor lab")
    parser.add_argument("--dump", action="store_true", help="텍스트 리포트만 출력")
    parser.add_argument("--capture", action="store_true", help="mss 전체/디스플레이 캡처 저장 후 종료")
    parser.add_argument("--save-report", action="store_true", help="리포트 저장 후 종료")
    args = parser.parse_args(argv)

    if args.dump:
        print(format_report_text(make_probe_report()))
        return 0

    if args.capture:
        paths = capture_all_mss(OUT_DIR)
        for p in paths:
            print(p.resolve())
        return 0

    if args.save_report:
        json_path, txt_path = save_probe_report(OUT_DIR)
        print(json_path.resolve())
        print(txt_path.resolve())
        return 0

    root = tk.Tk()
    MonitorLabApp(root)
    root.mainloop()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
