"""apply_blender_patch.py — scanvas_maker.py 에 Blender 렌더 통합 패치 적용.

패치 내용 (3곳):
  P1) `_open_structure_template_dialog` 안에 `_do_blender_render` 콜백 추가
       (기존 `_do_vlm_feedback` 함수 정의 바로 앞에 삽입)
  P2) "🤖 AI 검증" 버튼 다음 줄에 "🎨 Blender 렌더" 버튼 추가
  P3) 클래스 메서드로 `_show_structure_render(self, image_path)` 추가
       (`_show_rendered_result` 메서드 바로 앞에 삽입)

특징:
  - Idempotent: 이미 적용된 상태에서 다시 실행해도 변경 없음 (안전)
  - Anchor 기반: 원본의 정확한 텍스트를 찾아 그 위치에만 삽입
       anchor 못 찾으면 즉시 중단 (파일 망가뜨리지 않음)
  - 백업: 실행 전 scanvas_maker.py.bak_blender 자동 생성
  - 줄바꿈: 입력 파일이 CRLF면 출력도 CRLF, LF면 LF (보존)
  - AST parse 검증으로 결과 syntax 체크

사용법:
    cd D:\\2026\\PROGRAM\\1_S-CANVAS
    python apply_blender_patch.py

옵션:
    --dry-run        실제 쓰지 않고 어떤 변경이 일어날지 출력만
    --no-backup      백업 파일 생성 생략
    --check          이미 적용됐는지만 확인 (변경 없음)
"""

from __future__ import annotations

import argparse
import ast
import shutil
import sys
from pathlib import Path

TARGET = Path("scanvas_maker.py")
BACKUP = Path("scanvas_maker.py.bak_blender")


# ===========================================================================
# 패치 정의 (anchor + insert)
# ===========================================================================

P1_GUARD = "def _do_blender_render():"
P1_ANCHOR = "        def _do_vlm_feedback():\n"
P1_INSERT = '''        def _do_blender_render():
            """Blender Cycles로 구조물 단독 고품질 렌더 (별도 트랙).

            AI 워크플로(Step 4)와 별개로 실행. 결과는 'structure_render.png'.
            transparent_bg=True 로 RGBA 출력 → 추후 지형 합성 입력으로도 사용 가능.
            """
            try:
                from blender_renderer import run_blender_render
                from gate_3d_builder_bpy import dump_params_to_json as _dump_gate
            except ImportError as e:
                messagebox.showerror(
                    "모듈 없음",
                    f"Blender 렌더 모듈을 찾을 수 없습니다:\\n{e}\\n\\n"
                    "blender_renderer.py / gate_3d_builder_bpy.py 가 "
                    "S-CANVAS 폴더에 있는지 확인하세요.",
                    parent=win,
                )
                return

            if state["params"] is None:
                messagebox.showwarning(
                    "안내", "파라미터가 비어있습니다. '미리보기 (빌드)' 먼저 실행하세요.",
                    parent=win,
                )
                return

            # 현재는 수문(spillway_gate)만 지원.
            if tid != "spillway_gate":
                messagebox.showinfo(
                    "지원 예정",
                    f"Blender 렌더는 현재 '여수로 수문(spillway_gate)' 템플릿만 "
                    f"지원합니다.\\n현재 템플릿: {tid}",
                    parent=win,
                )
                return

            # 옵션 다이얼로그 — 시간대 + 투명배경 + 샘플
            opt_win = ctk.CTkToplevel(win)
            opt_win.title("Blender Cycles 렌더 옵션")
            opt_win.geometry("420x340")
            opt_win.transient(win); opt_win.grab_set()

            ctk.CTkLabel(
                opt_win, text="Blender Cycles 렌더 옵션",
                font=ctk.CTkFont(size=14, weight="bold"),
            ).pack(pady=(15, 10))

            ctk.CTkLabel(opt_win, text="조명 / 시간대").pack(anchor="w", padx=20)
            time_var = ctk.StringVar(value="daytime")
            tf = ctk.CTkFrame(opt_win, fg_color="transparent")
            tf.pack(fill="x", padx=20, pady=(0, 10))
            for v, lbl in [("daytime", "주간"), ("sunset", "노을"), ("overcast", "흐림")]:
                ctk.CTkRadioButton(tf, text=lbl, variable=time_var, value=v).pack(side="left", padx=8)

            transparent_var = ctk.BooleanVar(value=False)
            ctk.CTkCheckBox(
                opt_win,
                text="투명 배경 (RGBA) — 지형 합성용",
                variable=transparent_var,
            ).pack(anchor="w", padx=20, pady=(0, 8))

            ctk.CTkLabel(opt_win, text="Cycles 샘플 (높을수록 깨끗·느림)").pack(anchor="w", padx=20)
            samples_var = ctk.StringVar(value="128")
            sf = ctk.CTkFrame(opt_win, fg_color="transparent")
            sf.pack(fill="x", padx=20, pady=(0, 8))
            for s in ("32", "64", "128", "256"):
                ctk.CTkRadioButton(sf, text=s, variable=samples_var, value=s).pack(side="left", padx=6)

            save_blend_var = ctk.BooleanVar(value=False)
            save_glb_var = ctk.BooleanVar(value=False)
            ctk.CTkCheckBox(opt_win, text=".blend 저장",
                            variable=save_blend_var).pack(anchor="w", padx=20)
            ctk.CTkCheckBox(opt_win, text=".glb 저장 (외부 뷰어/VR)",
                            variable=save_glb_var).pack(anchor="w", padx=20, pady=(0, 6))

            def _start():
                try:
                    samples = int(samples_var.get())
                except ValueError:
                    samples = 128
                t_preset = time_var.get()
                trans = bool(transparent_var.get())
                save_b = bool(save_blend_var.get())
                save_g = bool(save_glb_var.get())
                opt_win.destroy()

                try:
                    json_path = "gate_params.json"
                    _dump_gate(state["params"], json_path)
                    self.log(f"  [{name}] GateParams -> {json_path}")
                except Exception as e:
                    messagebox.showerror("JSON 저장 실패",
                        f"GateParams JSON 직렬화 실패:\\n{e}", parent=win)
                    return

                self.log(f"  [{name}] Blender 렌더 시작 "
                         f"(time={t_preset}, samples={samples}, "
                         f"bg={'투명' if trans else 'sky'})")
                threading.Thread(
                    target=run_blender_render,
                    args=(self, None, json_path),
                    kwargs=dict(
                        time_preset=t_preset,
                        engine="CYCLES",
                        samples=samples,
                        output_path="structure_render.png",
                        transparent_bg=trans,
                        save_blend=save_b,
                        save_glb=save_g,
                        structure_kind="gate",
                    ),
                    daemon=True,
                ).start()

            bf = ctk.CTkFrame(opt_win, fg_color="transparent")
            bf.pack(fill="x", pady=15, padx=20)
            ctk.CTkButton(bf, text="취소", width=80,
                          fg_color="transparent", border_width=1,
                          command=opt_win.destroy).pack(side="left")
            ctk.CTkButton(bf, text="🎨 렌더 시작", width=140,
                          fg_color="#16A085", hover_color="#117A65",
                          text_color="white",
                          command=_start).pack(side="right")

''' + P1_ANCHOR


P2_GUARD = '🎨 Blender 렌더'
P2_ANCHOR = (
    '        ctk.CTkButton(bottom, text="🤖 AI 검증", width=110,\n'
    '                      fg_color="#D35400", hover_color="#A04000",\n'
    '                      text_color="white",\n'
    '                      font=ctk.CTkFont(size=11, weight="bold"),\n'
    '                      command=_do_vlm_feedback).pack(side="right", padx=3)\n'
)
P2_INSERT = P2_ANCHOR + (
    '        ctk.CTkButton(bottom, text="🎨 Blender 렌더", width=140,\n'
    '                      fg_color="#16A085", hover_color="#117A65",\n'
    '                      text_color="white",\n'
    '                      font=ctk.CTkFont(size=11, weight="bold"),\n'
    '                      command=_do_blender_render).pack(side="right", padx=3)\n'
)


P3_GUARD = "def _show_structure_render(self,"
P3_ANCHOR = "    def _show_rendered_result(self, image_path):\n"
P3_INSERT = '''    def _show_structure_render(self, image_path):
        """Blender 구조물 렌더 결과를 별도 창에 표시 (AI 결과와 분리).

        blender_renderer.py 가 호출. 투명 PNG도 정상 표시(체커보드 배경).
        """
        try:
            from PIL import ImageTk, Image as _PILImage
            try:
                pil_img = _PILImage.open(image_path)
            except Exception as e:
                messagebox.showerror("이미지 열기 실패",
                    f"렌더 결과 PNG를 열 수 없습니다:\\n{image_path}\\n\\n{e}")
                return

            # 투명 PNG는 체커보드 배경 위에 합성 표시
            display_img = pil_img
            if pil_img.mode == "RGBA":
                from PIL import ImageDraw
                tile = _PILImage.new("RGB", (16, 16), (200, 200, 200))
                d = ImageDraw.Draw(tile)
                d.rectangle((0, 0, 7, 7), fill=(170, 170, 170))
                d.rectangle((8, 8, 15, 15), fill=(170, 170, 170))
                bg = _PILImage.new("RGB", pil_img.size, (200, 200, 200))
                for y in range(0, pil_img.size[1], 16):
                    for x in range(0, pil_img.size[0], 16):
                        bg.paste(tile, (x, y))
                display_img = _PILImage.alpha_composite(
                    bg.convert("RGBA"), pil_img
                ).convert("RGB")

            win = ctk.CTkToplevel(self)
            win.title(f"🎨 Blender 렌더 결과 - {Path(image_path).name}")

            sw = self.winfo_screenwidth(); sh = self.winfo_screenheight()
            max_w, max_h = int(sw * 0.7), int(sh * 0.75)
            iw, ih = display_img.size
            scale = min(max_w / iw, max_h / ih, 1.0)
            disp_w, disp_h = int(iw * scale), int(ih * scale)
            disp = display_img.resize((disp_w, disp_h), _PILImage.LANCZOS)

            tk_img = ImageTk.PhotoImage(disp)
            lbl = ctk.CTkLabel(win, text="", image=tk_img)
            lbl.image = tk_img
            lbl.pack(padx=10, pady=10)

            info = ctk.CTkFrame(win, fg_color="transparent")
            info.pack(fill="x", padx=10, pady=(0, 5))
            mode_str = "투명 배경 (RGBA, 합성용)" if pil_img.mode == "RGBA" else "Sky 배경 (RGB)"
            ctk.CTkLabel(
                info,
                text=f"파일: {image_path}  ·  원본 {iw}×{ih}  ·  {mode_str}",
                font=ctk.CTkFont(size=11),
            ).pack(side="left", padx=5)

            btnf = ctk.CTkFrame(win, fg_color="transparent")
            btnf.pack(fill="x", padx=10, pady=(0, 10))

            def _open_external():
                try:
                    if sys.platform == "win32":
                        os.startfile(image_path)
                    elif sys.platform == "darwin":
                        import subprocess
                        subprocess.Popen(["open", image_path])
                    else:
                        import subprocess
                        subprocess.Popen(["xdg-open", image_path])
                except Exception as e:
                    messagebox.showerror("열기 실패",
                        f"기본 뷰어 실행 실패:\\n{e}", parent=win)

            def _open_folder():
                try:
                    folder = str(Path(image_path).resolve().parent)
                    if sys.platform == "win32":
                        os.startfile(folder)
                    elif sys.platform == "darwin":
                        import subprocess
                        subprocess.Popen(["open", folder])
                    else:
                        import subprocess
                        subprocess.Popen(["xdg-open", folder])
                except Exception as e:
                    messagebox.showerror("폴더 열기 실패", f"{e}", parent=win)

            ctk.CTkButton(btnf, text="📂 폴더 열기", width=110,
                          command=_open_folder).pack(side="right", padx=3)
            ctk.CTkButton(btnf, text="🖼 외부 뷰어로 열기", width=160,
                          command=_open_external).pack(side="right", padx=3)
            ctk.CTkButton(btnf, text="닫기", width=80,
                          fg_color="transparent", border_width=1,
                          command=win.destroy).pack(side="left", padx=3)

        except Exception as e:
            import traceback
            traceback.print_exc()
            messagebox.showerror("결과 창 오류", f"렌더 결과 창 생성 실패:\\n{e}")

''' + P3_ANCHOR


# ===========================================================================
# 메인
# ===========================================================================

def detect_eol(raw_bytes: bytes) -> str:
    """원본 파일의 줄바꿈 형식 감지. 패치 후 보존."""
    crlf = raw_bytes.count(b"\r\n")
    lf = raw_bytes.count(b"\n") - crlf
    if crlf > lf:
        return "\r\n"
    return "\n"


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--dry-run", action="store_true",
                    help="실제 쓰지 않고 변경 사항만 출력")
    ap.add_argument("--no-backup", action="store_true",
                    help="백업 파일 생성 생략")
    ap.add_argument("--check", action="store_true",
                    help="패치 적용 상태만 확인 (변경 없음)")
    args = ap.parse_args()

    if not TARGET.is_file():
        sys.exit(f"[ERR] {TARGET} 가 현재 폴더에 없습니다. "
                 f"D:\\2026\\PROGRAM\\1_S-CANVAS 에서 실행하세요.")

    raw = TARGET.read_bytes()
    eol = detect_eol(raw)
    print(f"파일: {TARGET} ({len(raw):,} bytes, EOL={'CRLF' if eol == chr(13) + chr(10) else 'LF'})")

    # 항상 LF로 정규화해서 작업 (anchor 매칭 일관성)
    src = raw.decode("utf-8").replace("\r\n", "\n")
    original_src = src

    # 적용 상태 점검
    states = {
        "P1 (callback _do_blender_render)":          P1_GUARD in src,
        "P2 (button '🎨 Blender 렌더')":              P2_GUARD in src,
        "P3 (method _show_structure_render)":        P3_GUARD in src,
    }
    all_applied = all(states.values())
    none_applied = not any(states.values())

    print("\n현재 상태:")
    for name, applied in states.items():
        mark = "✓ 적용됨" if applied else "  미적용"
        print(f"  {mark}  {name}")

    if args.check:
        if all_applied:
            print("\n[CHECK] 모든 패치 적용됨")
            sys.exit(0)
        elif none_applied:
            print("\n[CHECK] 패치 미적용")
            sys.exit(1)
        else:
            print("\n[CHECK] 부분 적용 — 권장: --dry-run 으로 확인 후 정상 실행")
            sys.exit(2)

    if all_applied:
        print("\n→ 모든 패치 이미 적용됨. 추가 작업 없음.")
        return 0

    # 적용
    changes = []
    if P1_GUARD not in src:
        if P1_ANCHOR not in src:
            sys.exit("[ERR] P1 anchor 'def _do_vlm_feedback():' 못 찾음. "
                     "scanvas_maker.py 구조가 변경되었을 수 있습니다.")
        src = src.replace(P1_ANCHOR, P1_INSERT, 1)
        changes.append("P1: callback _do_blender_render 추가")

    if P2_GUARD not in src:
        if P2_ANCHOR not in src:
            sys.exit("[ERR] P2 anchor (🤖 AI 검증 버튼 블록) 못 찾음.")
        src = src.replace(P2_ANCHOR, P2_INSERT, 1)
        changes.append("P2: 🎨 Blender 렌더 버튼 추가")

    if P3_GUARD not in src:
        if P3_ANCHOR not in src:
            sys.exit("[ERR] P3 anchor 'def _show_rendered_result' 못 찾음.")
        src = src.replace(P3_ANCHOR, P3_INSERT, 1)
        changes.append("P3: method _show_structure_render 추가")

    print("\n적용할 변경:")
    for c in changes:
        print(f"  + {c}")
    print(f"\n  파일 크기: {len(original_src):,} → {len(src):,} chars "
          f"({len(src) - len(original_src):+,})")

    # AST parse 검증
    try:
        ast.parse(src)
        print(f"  AST parse: OK ({len(src.splitlines()):,} lines)")
    except SyntaxError as e:
        sys.exit(f"\n[ERR] 패치 후 syntax error 발생: {e}\n"
                 f"파일 변경 안 함. 원본 유지.")

    if args.dry_run:
        print("\n[DRY RUN] 실제 파일은 변경하지 않았습니다. 적용하려면 --dry-run 빼고 재실행.")
        return 0

    # 백업
    if not args.no_backup:
        shutil.copy2(TARGET, BACKUP)
        print(f"\n  백업: {BACKUP}")

    # 원본 EOL로 복원해서 쓰기
    final_bytes = src.encode("utf-8")
    if eol == "\r\n":
        final_bytes = final_bytes.replace(b"\r\n", b"\n").replace(b"\n", b"\r\n")
    TARGET.write_bytes(final_bytes)
    print(f"\n✓ 패치 적용 완료: {TARGET} ({len(final_bytes):,} bytes)")
    print("\n다음 단계:")
    print("  1) S-CANVAS 실행: python scanvas_maker.py")
    print("  2) Step 2에서 도면 로드 → 구조물 식별")
    print("  3) 사이드바 '구조물 상세 빌드' 다이얼로그 → 수문 선택")
    print("  4) 파라미터 조정 후 '🗔 미리보기' 또는 '🎨 Blender 렌더'")


if __name__ == "__main__":
    sys.exit(main() or 0)
