#!/usr/bin/env python3
"""
cromakga3d - ADB Tool

빌드 없이 adb 작업만 하는 도구. 설치 / 제거 / 실행 / logcat.
__build_android.py 가 기기 선택과 설치를 여기서 가져다 쓴다.
"""

import os
import re
import sys
import datetime
import subprocess
import msvcrt

# ── Config ───────────────────────────────────────────────────────
# 경로를 박아둔다. PATH 탐색을 하면 에뮬레이터가 자기 번들 adb(구버전)를 PATH 앞에 끼워넣은
# 상황에서 어느 쪽이 잡혔는지 알 수 없고, 버전이 다른 adb 는 서로의 서버를 죽인다.
ADB_EXE = r"C:\Users\maile\AppData\Local\Android\Sdk\platform-tools\adb.exe"   # <- adb 경로에 맞게 수정

# SDL_Log 는 "SDL/<카테고리>" 로 태그를 붙인다. DEBUG/libc 는 네이티브 크래시가 나가는 태그.
LOG_TAGS = ["SDL", "SDL/APP", "SDL/CUSTOM", "SDL/GPU", "SDL/ERROR", "SDL/VIDEO",
            "SDL/AUDIO", "SDL/INPUT", "SDL/RENDER", "SDL/SYSTEM", "DEBUG", "libc"]

# ── Paths ────────────────────────────────────────────────────────
PROJECT_DIR  = os.path.dirname(os.path.abspath(__file__))
ANDROID_DIR  = os.path.join(PROJECT_DIR, "android")
BUILD_GRADLE = os.path.join(ANDROID_DIR, "app", "build.gradle")
OUTPUT_DIR   = os.path.join(ANDROID_DIR, "app", "build", "outputs")

APK = {
    "Release": os.path.join(OUTPUT_DIR, "apk", "release", "app-release.apk"),
    "Debug":   os.path.join(OUTPUT_DIR, "apk", "debug",   "app-debug.apk"),
}
AAB = os.path.join(OUTPUT_DIR, "bundle", "release", "app-release.aab")

# ── 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_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_warn(text):
    print(col(YELLOW, "  ! " + text))

def print_error(text):
    print(col(RED, "  x " + text))

def print_hint(text):
    """무엇을 하면 되는지까지 말해준다. 원인만 찍고 끝내면 매번 검색하게 된다."""
    print(col(GRAY, "    -> " + text))

def print_fail(text):
    print()
    print_error(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"

def menu_line(key, label, detail="", enabled=True, accent=WHITE):
    if not enabled:
        return col(GRAY, "    " + key + "  ->  " + label + ("   " + detail if detail else ""))
    line = "    " + col(CYAN, key) + col(GRAY, "  ->  ") + col(accent, label)
    if detail:
        line += col(GRAY, "   " + detail)
    return line

# ── 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()

# ── Project facts ─────────────────────────────────────────────────
def read_application_id():
    """applicationId 는 build.gradle 만 갖는다. 여기 상수로 복사해두면 언젠가 어긋난다."""
    with open(BUILD_GRADLE, "r", encoding="utf-8") as f:
        match = re.search(r'applicationId\s*=\s*"([^"]+)"', f.read())

    if match is None:
        print_fail("applicationId 를 찾을 수 없습니다: " + BUILD_GRADLE)

    return match.group(1)

# ── adb ───────────────────────────────────────────────────────────
STATE_HINT = {
    "unauthorized": "기기 화면의 'USB 디버깅 허용' 대화상자를 확인하세요",
    "offline":      "기기가 응답하지 않습니다. 케이블을 다시 꽂거나 adb kill-server 후 재시도",
}

def require_adb():
    if not os.path.isfile(ADB_EXE):
        print_fail("adb 를 찾을 수 없습니다: " + ADB_EXE + "\n"
                   "    -> 이 파일 상단의 ADB_EXE 상수를 설치 경로에 맞게 고치세요")

def adb(args, serial=None, capture=True):
    command = [ADB_EXE]
    if serial:
        command += ["-s", serial]
    command += args

    if capture:
        return subprocess.run(command, capture_output=True, text=True, errors="replace")
    return subprocess.run(command)

def adb_version():
    result = adb(["version"])
    match = re.search(r"Version ([0-9][^\s]*)", result.stdout)
    return match.group(1) if match else "?"

def list_devices():
    """(serial, state, model) 목록. state 가 'device' 가 아닌 기기로는 아무것도 못 한다."""
    result = adb(["devices", "-l"])
    devices = []

    for line in result.stdout.splitlines()[1:]:
        parts = line.split()
        if len(parts) < 2:
            continue

        model = ""
        for part in parts[2:]:
            if part.startswith("model:"):
                model = part[len("model:"):]

        devices.append((parts[0], parts[1], model))

    return devices

def device_label(device):
    serial, state, model = device
    text = serial + ("   (" + model + ")" if model else "")
    return text if state == "device" else text + "   [" + state + "]"

def pick_device(devices):
    """한 대뿐이면 묻지 않는다. 여러 대일 때만 고르게 한다."""
    usable = [d for d in devices if d[1] == "device"]

    if not usable:
        return None
    if len(usable) == 1:
        return usable[0]

    print()
    print(col(CYAN, "  Select device:"))
    print(col(GRAY, "  -----------------------------------------"))
    for i, device in enumerate(usable[:9]):
        print(menu_line(str(i + 1), device_label(device)))
    print(col(GRAY, "    other  ->  cancel"))
    print()

    key = read_key()
    if key.isdigit() and 1 <= int(key) <= len(usable[:9]):
        return usable[int(key) - 1]
    return None

def report_devices(devices):
    """상태가 나쁜 기기는 이유와 함께 보여준다. 오늘의 unauthorized 를 놓치지 않기 위해서."""
    if not devices:
        print_warn("연결된 기기가 없습니다")
        print_hint("USB 디버깅이 켜져 있는지 확인하세요. 에뮬레이터라면 adb connect localhost:6520")
        return

    for device in devices:
        serial, state, model = device
        if state == "device":
            print_ok(device_label(device))
        else:
            print_error(device_label(device))
            if state in STATE_HINT:
                print_hint(STATE_HINT[state])

# ── Actions ───────────────────────────────────────────────────────
def artifact_detail(path):
    if not os.path.isfile(path):
        return None
    stat = os.stat(path)
    stamp = datetime.datetime.fromtimestamp(stat.st_mtime).strftime("%m-%d %H:%M")
    return fmt_size(stat.st_size).rjust(9) + "   " + stamp

def install(serial, build_type):
    path = APK[build_type]

    if not os.path.isfile(path):
        print_warn(os.path.basename(path) + " 가 없습니다")
        print_hint("빌드부터 하세요 — gradlew assemble" + build_type + " (또는 __build_android.py)")
        return

    print_step("Install  [" + build_type + "]")
    result = adb(["install", "-r", path], serial)
    output = result.stdout + result.stderr

    if "Success" in output:
        print_ok("설치 완료")
        return

    print_error(output.strip() or "설치 실패")

    # 강제 재설치 플래그는 없다. 서명이 다르면 지우는 것 말고 방법이 없다.
    if "INSTALL_FAILED_UPDATE_INCOMPATIBLE" in output:
        print_hint("다른 서명의 앱이 이미 설치돼 있습니다. U 로 제거한 뒤 다시 설치하세요")
    elif "INSTALL_FAILED_VERSION_DOWNGRADE" in output:
        print_hint("설치된 쪽이 더 높은 버전입니다. U 로 제거한 뒤 다시 설치하세요")
    elif "INSTALL_FAILED_INSUFFICIENT_STORAGE" in output:
        print_hint("기기 저장공간이 부족합니다")

def uninstall(serial, package):
    print_step("Uninstall")
    result = adb(["uninstall", package], serial)
    output = result.stdout + result.stderr

    if "Success" in output:
        print_ok("제거 완료")
        return

    print_error(output.strip() or "제거 실패")
    if "DELETE_FAILED_INTERNAL_ERROR" in output or "Unknown package" in output:
        print_hint("설치되어 있지 않습니다")

def app_pid(serial, package):
    """실행 중이면 pid, 아니면 None."""
    parts = adb(["shell", "pidof", package], serial).stdout.split()
    return parts[0] if parts else None

def stop_app(serial, package):
    if app_pid(serial, package) is None:
        print_warn("실행 중이 아닙니다")
        return

    print_step("Stop")
    adb(["shell", "am", "force-stop", package], serial)
    print_ok("정지: " + package)

def run_app(serial, package):
    result = adb(["shell", "cmd", "package", "resolve-activity", "--brief", package], serial)

    component = ""
    for line in result.stdout.splitlines():
        if "/" in line:
            component = line.strip()

    if not component:
        print_warn("실행할 액티비티를 찾지 못했습니다")
        print_hint("앱이 설치되어 있지 않습니다. I 또는 D 로 먼저 설치하세요")
        return False

    print_step("Run")
    # -S 는 띄우기 전에 force-stop 한다. 돌고 있으면 재시작, 아니면 그냥 시작이라 분기가 없다.
    adb(["shell", "am", "start", "-S", "-n", component], serial, capture=False)
    print_ok(component)
    return True

def run_with_logcat(serial, package):
    """버퍼를 먼저 비우고 띄운다. logcat 은 붙는 순간 이미 쌓여 있는 줄부터 뱉으므로, 이 순서면
    시작 직후의 초기화 로그도 놓치지 않는다."""
    adb(["logcat", "-c"], serial)
    if run_app(serial, package):
        logcat(serial, clear=False)

def logcat(serial, clear=True):
    if clear:
        adb(["logcat", "-c"], serial)   # 비우지 않으면 이전 실행의 로그가 섞여 나온다
    print_step("Logcat   (Ctrl+C 로 종료)")
    print(col(GRAY, "  tags: " + " ".join(LOG_TAGS)))
    print()

    try:
        adb(["logcat", "-v", "time", "-s"] + [tag + ":V" for tag in LOG_TAGS], serial, capture=False)
    except KeyboardInterrupt:
        print()
        print_ok("logcat 종료")

# ── Menu ──────────────────────────────────────────────────────────
def menu_loop(package, serial=None):
    """__build_android.py 도 빌드가 끝난 뒤 이 루프로 들어온다."""
    while True:
        devices = list_devices()
        usable  = [d for d in devices if d[1] == "device"]

        if serial not in [d[0] for d in usable]:
            picked = usable[0] if len(usable) == 1 else None
            serial = picked[0] if picked else None

        current = next((d for d in devices if d[0] == serial), None)

        print()
        print(col(GRAY, "  package  :  " + package))
        live = current is not None
        pid  = app_pid(serial, package) if live else None

        if current:
            print(col(GRAY, "  device   :  ") + col(WHITE, device_label(current)))
            print(col(GRAY, "  state    :  ") +
                  (col(GREEN, "running   (pid " + pid + ")") if pid else col(GRAY, "stopped")))
        elif len(usable) > 1:
            print(col(GRAY, "  device   :  ") + col(YELLOW, "선택 필요  (S)"))
        else:
            print(col(GRAY, "  device   :  ") + col(RED, "없음"))
            report_devices(devices)

        release_detail = artifact_detail(APK["Release"])
        debug_detail   = artifact_detail(APK["Debug"])

        print(col(GRAY, "  -----------------------------------------"))
        print(menu_line("S", "Select device", "(" + str(len(usable)) + " connected)", len(usable) > 1))
        print(col(GRAY, "  -----------------------------------------"))
        print(menu_line("I", "Install   [Release]", release_detail or "(not built)",
                        live and release_detail is not None))
        print(menu_line("D", "Install   [Debug]  ", debug_detail or "(not built)",
                        live and debug_detail is not None))
        print(menu_line("U", "Uninstall", "", live, YELLOW))
        print(col(GRAY, "  -----------------------------------------"))
        launch = "Restart" if pid else "Run"
        print(menu_line("R", launch, "", live))
        print(menu_line("G", launch + " + Logcat", "", live))
        print(menu_line("X", "Stop", "", live and pid is not None, YELLOW))
        print(menu_line("L", "Logcat", "", live))
        print(col(GRAY, "  -----------------------------------------"))
        print(menu_line("Q", "Quit"))
        print()

        key = read_key()

        if key == "q":
            print()
            return
        if key == "s":
            picked = pick_device(devices)
            if picked:
                serial = picked[0]
            continue

        if not live:
            print_warn("사용 가능한 기기가 없습니다")
            continue

        if key == "i":
            install(serial, "Release")
        elif key == "d":
            install(serial, "Debug")
        elif key == "u":
            uninstall(serial, package)
        elif key == "r":
            run_app(serial, package)
        elif key == "g":
            run_with_logcat(serial, package)
        elif key == "x":
            stop_app(serial, package)
        elif key == "l":
            logcat(serial)

# ── Main ──────────────────────────────────────────────────────────
def main():
    os.system("cls")
    print()
    print(col(CYAN, "  +==========================================+"))
    print(col(CYAN, "  |    cromakga3d  .  ADB Tool               |"))
    print(col(CYAN, "  +==========================================+"))
    print()

    require_adb()
    print(col(GRAY, "  adb      :  " + ADB_EXE + "   (" + adb_version() + ")"))

    menu_loop(read_application_id())

if __name__ == "__main__":
    main()
