#!/usr/bin/env python3
"""
cromakga3d - WASM Build & Run
Place this script in the project root (alongside SDL3.dll).
"""

import os
import re
import sys
import subprocess
import threading
import http.server
import multiprocessing
import msvcrt
import time

# ── Config ───────────────────────────────────────────────────────
EMSDK_DIR         = r"C:\workspace\sdk\emsdk"                                   # <- emsdk 설치 경로에 맞게 수정
PORT              = 58080
EXEC_CHROME       = r"%ProgramFiles%\Google\Chrome\Application\chrome.exe"
EXEC_EDGE         = r"%ProgramFiles(X86)%\Microsoft\Edge\Application\msedge.exe"
DEPLOY_DIR        = r"C:\workspace\webroot"                                     # <- SVN working copy 경로에 맞게 수정
DEPLOY_COMMIT_MSG = "Deployed via __build_wasm.py script."                      # <- SVN 커밋 메시지

# ── Paths ────────────────────────────────────────────────────────
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
BUILD_DIR   = os.path.join(PROJECT_DIR, ".build", "wasm")
EMSDK_BAT   = os.path.join(EMSDK_DIR, "emsdk_env.bat")
SHELL_DIR   = os.path.join(PROJECT_DIR, "wasm_shell")
SHELL_ICON  = os.path.join(SHELL_DIR, "fav_icon_64.png")

GAME_HEADER = os.path.join(PROJECT_DIR, "src", "game", "Game.h")

def read_game_header(pattern, what):
    """src/game/Game.h 에서 값을 하나 읽는다. CMakeLists.txt 도 같은 줄들을 파싱한다."""
    with open(GAME_HEADER, "r", encoding="utf-8") as f:
        match = re.search(pattern, f.read())

    if match is None:
        raise SystemExit("  x  " + what + " not found in " + GAME_HEADER)

    return match.group(1)

# 출력물 이름은 GAME_NAME 에서 유도하지 않는다 — 배포 슬롯이라 교체 사이에도 고정이다.
# 둘이 어긋날 수 있으므로 배너에 나란히 찍는다.
OUTPUT_NAME = read_game_header(r'#define\s+GAME_WASM_OUTPUT_NAME\s+"([^"]+)"', "GAME_WASM_OUTPUT_NAME")
GAME_NAME   = read_game_header(r"#define\s+GAME_NAME\s+([A-Za-z0-9_]+)",       "GAME_NAME")

# ── ANSI colors (Windows 10+ terminal) ───────────────────────────
os.system("")   # enable VT100 on Windows

CYAN   = "\033[96m"
GREEN  = "\033[92m"
RED    = "\033[91m"
YELLOW = "\033[93m"
GRAY   = "\033[90m"
WHITE  = "\033[97m"
RESET  = "\033[0m"

def col(color, text):
    return color + text + RESET

# ── Print helpers ─────────────────────────────────────────────────
def print_header():
    os.system("cls")
    print()
    print(col(CYAN, "  +==========================================+"))
    print(col(CYAN, "  |    cromakga3d  .  WASM Build and Run    |"))
    print(col(CYAN, "  +==========================================+"))
    print()
    print(col(GRAY, "  Project  :  " + PROJECT_DIR))
    print(col(GRAY, "  Output   :  " + BUILD_DIR))
    print(col(GRAY, "  emsdk    :  " + EMSDK_DIR))
    # 어느 게임이 어느 산출물로 들어갔는지 매 빌드가 말해준다 — 이름이 게임을 따라가지 않으므로
    print(col(GRAY, "  Game     :  " + GAME_NAME + "   ->   " + OUTPUT_NAME))
    print(col(GRAY, "  Server   :  http://localhost:" + str(PORT) + "/" + OUTPUT_NAME + ".html"))

def print_section(title):
    print()
    print(col(CYAN, "  +------------------------------------------+"))
    print(col(CYAN, "  |  {:<40}|".format(title)))
    print(col(CYAN, "  +------------------------------------------+"))

def print_step(text):
    print()
    print(col(WHITE, "  " + text))

def print_ok(text):
    print(col(GREEN, "  v " + text))

def print_fail(text):
    print()
    print(col(RED, "  x " + text))
    print()
    sys.exit(1)

def fmt_size(n):
    if n >= 1024 * 1024:
        return "{:.1f} MB".format(n / (1024 * 1024))
    if n >= 1024:
        return "{:.1f} KB".format(n / 1024)
    return str(n) + " B"

# ── Key input ─────────────────────────────────────────────────────
def read_key():
    key = msvcrt.getwch()
    # special keys (arrows, F-keys) send a two-byte sequence
    if key in ("\x00", "\xe0"):
        msvcrt.getwch()
        return ""
    return key.lower()

def select_build_type():
    print()
    print(col(CYAN, "  Select build type:"))
    print(col(GRAY, "  -----------------------------------------"))
    print(col(WHITE, "    R      ->  Release"))
    print(col(WHITE, "    D      ->  Debug"))
    print(col(WHITE, "    SPACE  ->  Skip build (run server only)"))
    print(col(GRAY,  "    other  ->  exit"))
    print()

    key = read_key()
    if key == "r":
        print(col(GREEN, "  Release"))
        print()
        return "Release"
    elif key == "d":
        print(col(YELLOW, "  Debug"))
        print()
        return "Debug"
    elif key == " ":
        print(col(YELLOW, "  Skip build"))
        print()
        return None
    else:
        print(col(YELLOW, "  cancelled."))
        print()
        sys.exit(0)

# ── Browser helpers ───────────────────────────────────────────────
def resolve_browser_path(raw_path):
    """환경 변수를 전개하고 파일 존재 여부를 반환한다."""
    if not raw_path:
        return None
    expanded = os.path.expandvars(raw_path)
    return expanded if os.path.isfile(expanded) else None

def select_browser():
    chrome_path = resolve_browser_path(EXEC_CHROME)
    edge_path   = resolve_browser_path(EXEC_EDGE)

    def browser_label(label, path):
        if path:
            return col(WHITE, "    " + label)
        return col(GRAY, "    " + label + "  (not found)")

    print()
    print(col(CYAN, "  Select browser:"))
    print(col(GRAY, "  -----------------------------------------"))
    print(browser_label("C  ->  Chrome", chrome_path))
    print(browser_label("E  ->  Edge",   edge_path))
    print(col(GRAY,    "    other  ->  exit"))
    print()

    while True:
        key = read_key()
        if key == "c":
            if not chrome_path:
                print(col(RED, "  Chrome not found. Choose another."))
                continue
            print(col(GREEN, "  Chrome"))
            print()
            return ("chrome", chrome_path)
        elif key == "e":
            if not edge_path:
                print(col(RED, "  Edge not found. Choose another."))
                continue
            print(col(GREEN, "  Edge"))
            print()
            return ("edge", edge_path)
        else:
            print(col(YELLOW, "  cancelled."))
            print()
            sys.exit(0)

def open_browser(browser_tuple, url):
    kind, path = browser_tuple
    subprocess.Popen([path, "--new-window", url], stdin=subprocess.DEVNULL)

# ── Build steps ───────────────────────────────────────────────────
def load_emsdk_env():
    import shutil

    print_step("[1/3] Activating emsdk")

    if not os.path.exists(EMSDK_BAT):
        print_fail("emsdk_env.bat not found: " + EMSDK_BAT)

    # 경로는 별도 인자로 넘긴다. `cmd /c "..."` 한 덩어리 안에 따옴표로 넣으면 cmd 가 이해 못 하는
    # 이스케이프가 섞여 배치가 아예 실행되지 않는데, 뒤의 set 은 그대로 성공하므로 실패가 조용히
    # 지나가고 나중에 emcmake 없음으로만 드러난다.
    result = subprocess.run(["cmd", "/c", "call", EMSDK_BAT, "&&", "set"],
                            capture_output=True, text=True)

    for line in result.stdout.splitlines():
        key, sep, value = line.partition("=")
        if sep and key and " " not in key:   # 배치가 찍는 배너 줄을 걸러낸다
            os.environ[key] = value

    if shutil.which("emcc") is None:
        print_fail("emcc not on PATH after activating emsdk")

    print_ok("emsdk activated")


def cmake_configure(build_type):
    print_step("[2/3] CMake configure  (emcmake)  [" + build_type + "]")

    import shutil
    if os.path.exists(BUILD_DIR):
        shutil.rmtree(BUILD_DIR)
    os.makedirs(BUILD_DIR)

    result = subprocess.run(
        "emcmake cmake -S \"" + PROJECT_DIR + "\" -B \"" + BUILD_DIR + "\" -DCMAKE_BUILD_TYPE=" + build_type,
        shell=True,
        cwd=PROJECT_DIR
    )

    if result.returncode != 0:
        print_fail("CMake configure failed.")

    print_ok("CMake configure done")


def cmake_build():
    jobs = multiprocessing.cpu_count()
    print_step("[3/3] Building  (-j" + str(jobs) + ")")

    result = subprocess.run(
        "cmake --build \"" + BUILD_DIR + "\" -j " + str(jobs),
        shell=True,
        cwd=PROJECT_DIR
    )

    if result.returncode != 0:
        print_fail("Build failed.")

    print()
    print(col(GREEN, "  +==========================================+"))
    print(col(GREEN, "  |  v  Build succeeded!                    |"))
    print(col(GREEN, "  +==========================================+"))


def copy_shell_assets():
    """셸 아이콘을 출력물 이름에 맞춰 빌드 폴더로 복사한다 (fav_icon_64.png -> <OUTPUT_NAME>.png)."""
    import shutil

    if not os.path.isdir(BUILD_DIR):
        return

    if not os.path.isfile(SHELL_ICON):
        print(col(YELLOW, "  !  shell icon not found: " + SHELL_ICON))
        return

    shutil.copy2(SHELL_ICON, os.path.join(BUILD_DIR, OUTPUT_NAME + ".png"))


# ── Local HTTP server ─────────────────────────────────────────────
class QuietHandler(http.server.SimpleHTTPRequestHandler):
    """Suppress per-request log noise + COOP/COEP headers (SharedArrayBuffer for WASM threads)."""
    extensions_map = { **http.server.SimpleHTTPRequestHandler.extensions_map,
                       ".wasm": "application/wasm" }

    def end_headers(self):
        self.send_header("Cross-Origin-Opener-Policy", "same-origin")
        self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
        super().end_headers()

    def log_message(self, format, *args):
        pass


def run_server(browser_tuple):
    url = "http://localhost:" + str(PORT) + "/" + OUTPUT_NAME + ".html"

    print_section("RUN")
    print()
    print(col(CYAN, "  URL   :  " + url))
    print(col(GRAY,  "  Stop  :  Q"))
    print()

    os.chdir(BUILD_DIR)
    httpd = http.server.HTTPServer(("", PORT), QuietHandler)

    t = threading.Thread(target=httpd.serve_forever, daemon=True)
    t.start()

    time.sleep(0.6)
    open_browser(browser_tuple, url)

    print(col(GRAY, "  Press Q to stop server."))
    print()

    while True:
        key = read_key()
        if key == "q":
            break

    httpd.shutdown()
    httpd.server_close()
    print()
    print(col(YELLOW, "  Server stopped."))
    print()


# ── Deploy ───────────────────────────────────────────────────────
def run_deploy(output_files):
    import shutil

    print_section("DEPLOY")

    print_step("[1/6] SVN cleanup")
    result = subprocess.run([
        "svn", "cleanup", DEPLOY_DIR,
        "--remove-unversioned",
        "--remove-ignored",
        "--vacuum-pristines",
    ])
    if result.returncode != 0:
        print_fail("SVN cleanup failed.")
    print_ok("cleanup done")

    print_step("[2/6] SVN revert")
    result = subprocess.run([
        "svn", "revert", DEPLOY_DIR,
        "--recursive",
    ])
    if result.returncode != 0:
        print_fail("SVN revert failed.")
    print_ok("revert done")

    print_step("[3/6] SVN update")
    result = subprocess.run([
        "svn", "update", DEPLOY_DIR,
        "--force",
    ])
    if result.returncode != 0:
        print_fail("SVN update failed.")
    print_ok("update done")

    print_step("[4/6] Copying files  ->  " + DEPLOY_DIR)
    deployed_paths = []
    for p in output_files:
        dst = os.path.join(DEPLOY_DIR, os.path.basename(p))
        shutil.copy2(p, dst)
        deployed_paths.append(dst)
        print_ok(os.path.basename(p))

    print_step("[5/6] SVN add")
    result = subprocess.run(["svn", "add", "--force"] + deployed_paths)
    if result.returncode != 0:
        print_fail("SVN add failed.")
    print_ok("add done")

    print_step("[6/6] SVN commit")
    result = subprocess.run(["svn", "commit", DEPLOY_DIR, "-m", DEPLOY_COMMIT_MSG])
    if result.returncode != 0:
        print_fail("SVN commit failed.")
    print_ok("commit done")

    print()
    print(col(GREEN, "  +==========================================+"))
    print(col(GREEN, "  |  v  Deploy succeeded!                   |"))
    print(col(GREEN, "  +==========================================+"))


# ── Main ──────────────────────────────────────────────────────────
def main():
    print_header()
    build_type = select_build_type()

    if build_type is not None:
        print_section("BUILD  [" + build_type + "]")
        load_emsdk_env()
        cmake_configure(build_type)
        cmake_build()

    copy_shell_assets()

    output_exts  = [".html", ".js", ".wasm", ".data", ".png"]
    output_files = [os.path.join(BUILD_DIR, OUTPUT_NAME + ext) for ext in output_exts]
    all_exist     = all(os.path.isfile(p) for p in output_files)

    names     = [os.path.basename(p) for p in output_files]
    col_width = max(len(n) for n in names)
    total_bytes = 0

    print()
    print(col(CYAN, "  Select action:"))
    print(col(GRAY, "  -----------------------------------------"))
    for p, name in zip(output_files, names):
        padded = name.ljust(col_width)
        if os.path.isfile(p):
            size = os.path.getsize(p)
            total_bytes += size
            print(col(GREEN, "  v  " + padded + "  " + fmt_size(size).rjust(10)))
        else:
            print(col(RED,   "  x  " + padded + "  (not found)"))
    print(col(GRAY,  "  -----------------------------------------"))
    print(col(GRAY,  "  total  :  " + fmt_size(total_bytes)))
    print(col(GRAY,  "  -----------------------------------------"))
    if all_exist:
        print(col(WHITE, "    S  ->  Start local server"))
        print(col(WHITE, "    D  ->  Deploy  (copy -> SVN cleanup -> update -> commit)"))
    else:
        print(col(GRAY,  "    S  ->  Start local server  (output incomplete)"))
        print(col(GRAY,  "    D  ->  Deploy               (output incomplete)"))
    print(col(GRAY,  "    other  ->  exit"))
    print()

    key = read_key()
    if key == "s":
        if not all_exist:
            print(col(RED, "  x  Output incomplete. Build first."))
            print()
            sys.exit(1)
        browser = select_browser()
        run_server(browser)
    elif key == "d":
        if not all_exist:
            print(col(RED, "  x  Output incomplete. Build first."))
            print()
            sys.exit(1)
        run_deploy(output_files)
        print()
        print(col(CYAN,  "  Start local server?"))
        print(col(GRAY,  "  -----------------------------------------"))
        print(col(WHITE, "    ENTER  ->  continue"))
        print(col(GRAY,  "    other  ->  exit"))
        print()
        if read_key() == "\r":
            browser = select_browser()
            run_server(browser)
    else:
        print(col(YELLOW, "  cancelled."))
        print()
        sys.exit(0)


if __name__ == "__main__":
    main()
