# -*- coding: utf-8 -*-
"""
yjm_save_dialog_auto_enter.py
Windows 10/11 전용: "다른 이름으로 저장" 창이 뜨면, 사용 중 상태에서만 자동으로 저장 버튼/Enter를 실행합니다.

외부 패키지 없음. Python 표준 라이브러리 + Windows API(ctypes)만 사용합니다.
"""

import ctypes
from ctypes import wintypes
import json
import os
import sys
import time
import tkinter as tk
from tkinter import messagebox


APP_NAME = "YJM Save Dialog Auto Enter"
CONFIG_FILE = "yjm_save_dialog_auto_enter_config.json"

DEFAULT_CONFIG = {
    "enabled": False,
    "alpha": 0.78,
    "x": 80,
    "y": 80,
    "poll_ms": 250,
    "cooldown_sec": 2.5,
    "match_contains": False,
    "target_titles": [
        "다른 이름으로 저장",
        "Save As"
    ],
    "button_keywords": [
        "저장",
        "Save"
    ],
    "try_idok_after_button_fail": True,
    "try_enter_after_idok": True
}


# -----------------------------
# Windows API
# -----------------------------
if sys.platform != "win32":
    raise RuntimeError("이 프로그램은 Windows 전용입니다.")

user32 = ctypes.WinDLL("user32", use_last_error=True)

EnumWindowsProc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
EnumChildProc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)

user32.EnumWindows.argtypes = [EnumWindowsProc, wintypes.LPARAM]
user32.EnumWindows.restype = wintypes.BOOL

user32.EnumChildWindows.argtypes = [wintypes.HWND, EnumChildProc, wintypes.LPARAM]
user32.EnumChildWindows.restype = wintypes.BOOL

user32.IsWindowVisible.argtypes = [wintypes.HWND]
user32.IsWindowVisible.restype = wintypes.BOOL

user32.IsWindow.argtypes = [wintypes.HWND]
user32.IsWindow.restype = wintypes.BOOL

user32.GetWindowTextLengthW.argtypes = [wintypes.HWND]
user32.GetWindowTextLengthW.restype = ctypes.c_int

user32.GetWindowTextW.argtypes = [wintypes.HWND, wintypes.LPWSTR, ctypes.c_int]
user32.GetWindowTextW.restype = ctypes.c_int

user32.GetClassNameW.argtypes = [wintypes.HWND, wintypes.LPWSTR, ctypes.c_int]
user32.GetClassNameW.restype = ctypes.c_int

user32.ShowWindow.argtypes = [wintypes.HWND, ctypes.c_int]
user32.ShowWindow.restype = wintypes.BOOL

user32.SetForegroundWindow.argtypes = [wintypes.HWND]
user32.SetForegroundWindow.restype = wintypes.BOOL

user32.BringWindowToTop.argtypes = [wintypes.HWND]
user32.BringWindowToTop.restype = wintypes.BOOL

user32.SetActiveWindow.argtypes = [wintypes.HWND]
user32.SetActiveWindow.restype = wintypes.HWND

user32.SendMessageW.argtypes = [wintypes.HWND, ctypes.c_uint, wintypes.WPARAM, wintypes.LPARAM]
user32.SendMessageW.restype = wintypes.LPARAM

user32.keybd_event.argtypes = [ctypes.c_ubyte, ctypes.c_ubyte, ctypes.c_ulong, ctypes.c_ulong]
user32.keybd_event.restype = None

SW_RESTORE = 9
BM_CLICK = 0x00F5
WM_COMMAND = 0x0111
IDOK = 1
VK_RETURN = 0x0D
KEYEVENTF_KEYUP = 0x0002


def get_window_text(hwnd: int) -> str:
    try:
        length = user32.GetWindowTextLengthW(hwnd)
        if length <= 0:
            return ""
        buf = ctypes.create_unicode_buffer(length + 1)
        user32.GetWindowTextW(hwnd, buf, length + 1)
        return buf.value
    except Exception:
        return ""


def get_class_name(hwnd: int) -> str:
    try:
        buf = ctypes.create_unicode_buffer(256)
        user32.GetClassNameW(hwnd, buf, 256)
        return buf.value
    except Exception:
        return ""


def normalize_list(values):
    if isinstance(values, str):
        values = values.replace("\n", ",").split(",")
    return [str(v).strip() for v in values if str(v).strip()]


def window_title_matches(title: str, targets, contains: bool) -> bool:
    title = (title or "").strip()
    if not title:
        return False
    targets = normalize_list(targets)
    if contains:
        return any(t.lower() in title.lower() for t in targets)
    return any(title.lower() == t.lower() for t in targets)


def enum_matching_dialogs(target_titles, match_contains=False):
    found = []

    @EnumWindowsProc
    def callback(hwnd, lparam):
        if user32.IsWindowVisible(hwnd):
            title = get_window_text(hwnd)
            if window_title_matches(title, target_titles, match_contains):
                found.append(int(hwnd))
        return True

    user32.EnumWindows(callback, 0)
    return found


def find_child_button(parent_hwnd: int, keywords):
    keywords = [k.lower() for k in normalize_list(keywords)]
    if not keywords:
        return None

    result = {"hwnd": None}

    @EnumChildProc
    def callback(hwnd, lparam):
        text = get_window_text(hwnd)
        cls = get_class_name(hwnd)
        if text and cls.lower() == "button":
            low = text.lower()
            if any(k in low for k in keywords):
                result["hwnd"] = int(hwnd)
                return False
        return True

    user32.EnumChildWindows(parent_hwnd, callback, 0)
    return result["hwnd"]


def press_enter_key():
    # foreground 창에 Enter 입력
    user32.keybd_event(VK_RETURN, 0, 0, 0)
    time.sleep(0.03)
    user32.keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0)


def activate_window(hwnd: int):
    try:
        user32.ShowWindow(hwnd, SW_RESTORE)
        user32.BringWindowToTop(hwnd)
        user32.SetActiveWindow(hwnd)
        user32.SetForegroundWindow(hwnd)
    except Exception:
        pass


def auto_accept_save_dialog(hwnd: int, cfg: dict):
    """
    1순위: '저장' 버튼 직접 클릭
    2순위: WM_COMMAND / IDOK
    3순위: 포커스 후 Enter
    """
    if not user32.IsWindow(hwnd):
        return False, "window_gone"

    activate_window(hwnd)
    time.sleep(0.05)

    button_hwnd = find_child_button(hwnd, cfg.get("button_keywords", []))
    if button_hwnd and user32.IsWindow(button_hwnd):
        user32.SendMessageW(button_hwnd, BM_CLICK, 0, 0)
        return True, "button_click"

    if cfg.get("try_idok_after_button_fail", True):
        user32.SendMessageW(hwnd, WM_COMMAND, IDOK, 0)
        time.sleep(0.05)
        return True, "idok"

    if cfg.get("try_enter_after_idok", True):
        press_enter_key()
        return True, "enter"

    return False, "no_action"


def load_config():
    path = os.path.join(os.path.dirname(os.path.abspath(__file__)), CONFIG_FILE)
    cfg = dict(DEFAULT_CONFIG)
    if os.path.exists(path):
        try:
            with open(path, "r", encoding="utf-8") as f:
                old = json.load(f)
            if isinstance(old, dict):
                cfg.update(old)
        except Exception:
            pass
    return cfg


def save_config(cfg):
    path = os.path.join(os.path.dirname(os.path.abspath(__file__)), CONFIG_FILE)
    try:
        with open(path, "w", encoding="utf-8") as f:
            json.dump(cfg, f, ensure_ascii=False, indent=2)
    except Exception:
        pass


# -----------------------------
# Tkinter UI
# -----------------------------
class FloatingApp:
    def __init__(self):
        self.cfg = load_config()
        self.acted = {}  # hwnd -> last action time
        self.drag_start_x = 0
        self.drag_start_y = 0

        self.root = tk.Tk()
        self.root.title(APP_NAME)
        self.root.overrideredirect(True)
        self.root.attributes("-topmost", True)
        self.root.attributes("-alpha", float(self.cfg.get("alpha", 0.78)))
        self.root.configure(bg="#20242a")

        x = int(self.cfg.get("x", 80))
        y = int(self.cfg.get("y", 80))
        self.root.geometry(f"+{x}+{y}")

        self.frame = tk.Frame(self.root, bg="#20242a", bd=1, relief="solid")
        self.frame.pack(fill="both", expand=True)

        self.status = tk.Label(
            self.frame,
            text="",
            fg="white",
            bg="#20242a",
            font=("맑은 고딕", 11, "bold"),
            padx=12,
            pady=8
        )
        self.status.grid(row=0, column=0, sticky="w")

        self.toggle_btn = tk.Button(
            self.frame,
            text="ON/OFF",
            command=self.toggle_enabled,
            width=7,
            bd=0,
            bg="#3b82f6",
            fg="white",
            activebackground="#2563eb",
            activeforeground="white"
        )
        self.toggle_btn.grid(row=0, column=1, padx=(4, 2), pady=5)

        self.setting_btn = tk.Button(
            self.frame,
            text="설정",
            command=self.open_settings,
            width=5,
            bd=0,
            bg="#4b5563",
            fg="white",
            activebackground="#374151",
            activeforeground="white"
        )
        self.setting_btn.grid(row=0, column=2, padx=2, pady=5)

        self.close_btn = tk.Button(
            self.frame,
            text="×",
            command=self.quit_app,
            width=3,
            bd=0,
            bg="#7f1d1d",
            fg="white",
            activebackground="#991b1b",
            activeforeground="white"
        )
        self.close_btn.grid(row=0, column=3, padx=(2, 6), pady=5)

        self.last_label = tk.Label(
            self.frame,
            text="대기 중",
            fg="#cbd5e1",
            bg="#20242a",
            font=("맑은 고딕", 8),
            padx=12,
            pady=3
        )
        self.last_label.grid(row=1, column=0, columnspan=4, sticky="w")

        for w in (self.frame, self.status, self.last_label):
            w.bind("<ButtonPress-1>", self.start_move)
            w.bind("<B1-Motion>", self.do_move)
            w.bind("<ButtonRelease-1>", self.end_move)

        self.root.bind("<F8>", lambda e: self.toggle_enabled())
        self.root.bind("<Escape>", lambda e: self.quit_app())

        self.update_status()
        self.root.after(int(self.cfg.get("poll_ms", 250)), self.scan_loop)

    def update_status(self):
        enabled = bool(self.cfg.get("enabled", False))
        if enabled:
            self.status.config(text="체크 사용중", fg="#86efac")
            self.toggle_btn.config(text="끄기", bg="#166534", activebackground="#14532d")
        else:
            self.status.config(text="체크 미사용중", fg="#fca5a5")
            self.toggle_btn.config(text="켜기", bg="#6b7280", activebackground="#4b5563")

    def toggle_enabled(self):
        self.cfg["enabled"] = not bool(self.cfg.get("enabled", False))
        save_config(self.cfg)
        self.update_status()

    def start_move(self, event):
        self.drag_start_x = event.x
        self.drag_start_y = event.y

    def do_move(self, event):
        x = self.root.winfo_pointerx() - self.drag_start_x
        y = self.root.winfo_pointery() - self.drag_start_y
        self.root.geometry(f"+{x}+{y}")

    def end_move(self, event):
        try:
            self.cfg["x"] = self.root.winfo_x()
            self.cfg["y"] = self.root.winfo_y()
            save_config(self.cfg)
        except Exception:
            pass

    def scan_loop(self):
        try:
            self.scan_once()
        finally:
            poll_ms = max(100, int(self.cfg.get("poll_ms", 250)))
            self.root.after(poll_ms, self.scan_loop)

    def scan_once(self):
        now = time.time()

        # 닫힌 창 기록 정리
        for hwnd in list(self.acted.keys()):
            if not user32.IsWindow(hwnd):
                self.acted.pop(hwnd, None)

        if not self.cfg.get("enabled", False):
            return

        hwnds = enum_matching_dialogs(
            self.cfg.get("target_titles", []),
            bool(self.cfg.get("match_contains", False))
        )
        if not hwnds:
            return

        cooldown = float(self.cfg.get("cooldown_sec", 2.5))
        for hwnd in hwnds:
            last = self.acted.get(hwnd, 0)
            if now - last < cooldown:
                continue

            title = get_window_text(hwnd)
            ok, method = auto_accept_save_dialog(hwnd, self.cfg)
            self.acted[hwnd] = now

            if ok:
                self.last_label.config(text=f"처리: {title} / {method}")
            else:
                self.last_label.config(text=f"실패: {title} / {method}")

    def open_settings(self):
        win = tk.Toplevel(self.root)
        win.title("설정")
        win.attributes("-topmost", True)
        win.configure(bg="#f3f4f6")
        win.resizable(False, False)

        tk.Label(win, text="투명도", bg="#f3f4f6").grid(row=0, column=0, sticky="w", padx=10, pady=(10, 4))
        alpha_var = tk.DoubleVar(value=float(self.cfg.get("alpha", 0.78)) * 100)
        alpha_scale = tk.Scale(win, from_=35, to=100, orient="horizontal", variable=alpha_var, length=260)
        alpha_scale.grid(row=0, column=1, padx=10, pady=(10, 4))

        tk.Label(win, text="검사 간격(ms)", bg="#f3f4f6").grid(row=1, column=0, sticky="w", padx=10, pady=4)
        poll_var = tk.StringVar(value=str(self.cfg.get("poll_ms", 250)))
        tk.Entry(win, textvariable=poll_var, width=32).grid(row=1, column=1, padx=10, pady=4)

        tk.Label(win, text="대상 창 제목", bg="#f3f4f6").grid(row=2, column=0, sticky="nw", padx=10, pady=4)
        titles_text = tk.Text(win, width=36, height=4)
        titles_text.insert("1.0", "\n".join(normalize_list(self.cfg.get("target_titles", []))))
        titles_text.grid(row=2, column=1, padx=10, pady=4)

        contains_var = tk.BooleanVar(value=bool(self.cfg.get("match_contains", False)))
        tk.Checkbutton(
            win,
            text="제목 전체 일치가 아니라 포함으로 찾기",
            variable=contains_var,
            bg="#f3f4f6"
        ).grid(row=3, column=1, sticky="w", padx=10, pady=4)

        tk.Label(win, text="버튼 글자", bg="#f3f4f6").grid(row=4, column=0, sticky="nw", padx=10, pady=4)
        btn_text = tk.Text(win, width=36, height=3)
        btn_text.insert("1.0", "\n".join(normalize_list(self.cfg.get("button_keywords", []))))
        btn_text.grid(row=4, column=1, padx=10, pady=4)

        idok_var = tk.BooleanVar(value=bool(self.cfg.get("try_idok_after_button_fail", True)))
        enter_var = tk.BooleanVar(value=bool(self.cfg.get("try_enter_after_idok", True)))

        tk.Checkbutton(win, text="버튼 못 찾으면 IDOK 명령 시도", variable=idok_var, bg="#f3f4f6").grid(
            row=5, column=1, sticky="w", padx=10, pady=2
        )
        tk.Checkbutton(win, text="마지막에 Enter 키 시도", variable=enter_var, bg="#f3f4f6").grid(
            row=6, column=1, sticky="w", padx=10, pady=2
        )

        hint = (
            "F8: 켜기/끄기, ESC: 종료\n"
            "주의: 대상 제목이 맞으면 자동 저장됩니다. 필요한 창 제목만 넣으세요."
        )
        tk.Label(win, text=hint, bg="#f3f4f6", fg="#374151", justify="left").grid(
            row=7, column=0, columnspan=2, sticky="w", padx=10, pady=(8, 4)
        )

        def apply():
            try:
                alpha = max(0.35, min(1.0, float(alpha_var.get()) / 100.0))
                poll = max(100, int(str(poll_var.get()).strip()))
                titles = normalize_list(titles_text.get("1.0", "end"))
                btns = normalize_list(btn_text.get("1.0", "end"))

                if not titles:
                    messagebox.showwarning("확인", "대상 창 제목이 비어 있으면 안 됩니다.")
                    return
                if not btns:
                    messagebox.showwarning("확인", "버튼 글자가 비어 있으면 안 됩니다.")
                    return

                self.cfg["alpha"] = alpha
                self.cfg["poll_ms"] = poll
                self.cfg["target_titles"] = titles
                self.cfg["match_contains"] = bool(contains_var.get())
                self.cfg["button_keywords"] = btns
                self.cfg["try_idok_after_button_fail"] = bool(idok_var.get())
                self.cfg["try_enter_after_idok"] = bool(enter_var.get())
                save_config(self.cfg)
                self.root.attributes("-alpha", alpha)
                win.destroy()
            except Exception as e:
                messagebox.showerror("오류", str(e))

        bottom = tk.Frame(win, bg="#f3f4f6")
        bottom.grid(row=8, column=0, columnspan=2, sticky="e", padx=10, pady=10)

        tk.Button(bottom, text="저장", command=apply, width=10).pack(side="left", padx=4)
        tk.Button(bottom, text="닫기", command=win.destroy, width=10).pack(side="left", padx=4)

    def quit_app(self):
        save_config(self.cfg)
        self.root.destroy()

    def run(self):
        self.root.mainloop()


def main():
    app = FloatingApp()
    app.run()


if __name__ == "__main__":
    main()
