"""
MediaPipe 얼굴 랜드마크 예제 (수업용) — 478개 점 + 홍채 + 표정 수치
──────────────────────────────────────────────────────────────
필요한 것 : 파이썬 3.10~3.12, 웹캠, 인터넷(첫 실행 때 모델 자동 다운로드)
설치      : pip install mediapipe opencv-python
실행      : IDLE에서 열고 F5  /  종료는 ESC 키
"""

import os
import time
import urllib.request

import cv2
import mediapipe as mp

# ──────────────────────────────────────────────
# 설정값 모음 — 이 블록만 바꿔도 동작이 달라집니다
# ──────────────────────────────────────────────
SHOW_ALL_DOTS = True         # [수정 지점] False로 하면 홍채만 표시
DOT_COLOR = (200, 160, 80)   # [수정 지점] 얼굴 점 색깔 (파랑, 초록, 빨강)
IRIS_COLOR = (0, 0, 255)     # [수정 지점] 홍채(눈동자) 점 색깔
CAM_INDEX = 0                # [수정 지점] 카메라가 안 켜지면 1, 2로

# [수정 지점] 관찰할 표정 수치 목록 (52가지 중 선택)
#   예) "browInnerUp"(눈썹 올림), "mouthPucker"(입 오므림), "eyeSquintLeft"(눈 찡그림)
WATCH_BLENDSHAPES = ["eyeBlinkLeft", "eyeBlinkRight", "jawOpen", "mouthSmileLeft"]

MODEL_URL = ("https://storage.googleapis.com/mediapipe-models/"
             "face_landmarker/face_landmarker/float16/1/face_landmarker.task")
MODEL_FILE = "face_landmarker.task"

# 특별한 랜드마크 번호 (전체 478개 중에서)
IRIS_A = list(range(468, 473))   # 홍채 5점 (중심 468) — 거울 화면에서 왼쪽 눈
IRIS_B = list(range(473, 478))   # 홍채 5점 (중심 473) — 거울 화면에서 오른쪽 눈
EYE_A_CORNERS = (33, 133)        # 왼쪽 눈의 양쪽 끝
EYE_B_CORNERS = (362, 263)       # 오른쪽 눈의 양쪽 끝


def download_model():
    """모델 파일이 없으면 인터넷에서 한 번만 받아 옵니다."""
    if os.path.exists(MODEL_FILE):
        return
    print("모델 내려받는 중... (처음 한 번만, 3MB 정도)")
    urllib.request.urlretrieve(MODEL_URL, MODEL_FILE)
    print("완료:", MODEL_FILE)


def draw_face(frame, landmarks, w, h):
    """얼굴 점 478개를 작게, 홍채 10개는 크고 눈에 띄게 그립니다."""
    if SHOW_ALL_DOTS:
        for p in landmarks:
            cv2.circle(frame, (int(p.x * w), int(p.y * h)), 1, DOT_COLOR, -1)
    for idx in IRIS_A + IRIS_B:
        p = landmarks[idx]
        cv2.circle(frame, (int(p.x * w), int(p.y * h)), 3, IRIS_COLOR, -1)


def gaze_x(landmarks):
    """시선의 좌우 위치를 -1(왼쪽 끝) ~ +1(오른쪽 끝)로 계산합니다.

    원리: 눈동자 중심이 눈의 양쪽 끝 사이에서 어디쯤 있는지 재는 것.
    [수정 지점] 위아래 시선(y좌표)도 같은 방법으로 만들 수 있어요.
    """
    total = 0.0
    for center_idx, (c1, c2) in [(468, EYE_A_CORNERS), (473, EYE_B_CORNERS)]:
        eye_l = min(landmarks[c1].x, landmarks[c2].x)
        eye_r = max(landmarks[c1].x, landmarks[c2].x)
        width = max(eye_r - eye_l, 1e-6)
        ratio = (landmarks[center_idx].x - eye_l) / width   # 0(왼쪽)~1(오른쪽)
        total += ratio * 2.0 - 1.0                          # -1~+1로 변환
    return total / 2.0                                      # 두 눈의 평균


def draw_blendshape_bars(frame, blendshapes):
    """관찰 목록에 있는 표정 수치를 게이지 막대로 그립니다."""
    scores = {b.category_name: b.score for b in blendshapes}
    y = 60
    for name in WATCH_BLENDSHAPES:
        score = scores.get(name, 0.0)
        cv2.rectangle(frame, (10, y), (10 + 150, y + 16), (60, 60, 60), -1)      # 배경
        cv2.rectangle(frame, (10, y), (10 + int(150 * score), y + 16),
                      (80, 220, 80), -1)                                          # 게이지
        cv2.putText(frame, f"{name} {score:.2f}", (170, y + 13),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1)
        y += 24
    return scores


def main():
    download_model()

    BaseOptions = mp.tasks.BaseOptions
    FaceLandmarker = mp.tasks.vision.FaceLandmarker
    FaceLandmarkerOptions = mp.tasks.vision.FaceLandmarkerOptions
    RunningMode = mp.tasks.vision.RunningMode

    options = FaceLandmarkerOptions(
        base_options=BaseOptions(model_asset_path=MODEL_FILE),
        running_mode=RunningMode.VIDEO,      # 웹캠 프레임을 순서대로 처리하는 모드
        output_face_blendshapes=True,        # 표정 수치 52가지도 함께 계산
        num_faces=1,
    )

    cap = cv2.VideoCapture(CAM_INDEX)
    if not cap.isOpened():
        print("카메라를 열 수 없어요. CAM_INDEX 값을 바꿔 보세요.")
        return

    fps = 0.0
    prev_t = time.time()

    with FaceLandmarker.create_from_options(options) as landmarker:
        while True:
            ok, frame = cap.read()
            if not ok:
                break
            frame = cv2.flip(frame, 1)  # 거울 모드
            h, w = frame.shape[:2]

            rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
            result = landmarker.detect_for_video(mp_image, int(time.monotonic() * 1000))

            if result.face_landmarks:
                landmarks = result.face_landmarks[0]
                draw_face(frame, landmarks, w, h)

                # 시선 방향 표시: -1(왼쪽) ~ +1(오른쪽)
                g = gaze_x(landmarks)
                direction = "LEFT" if g < -0.15 else "RIGHT" if g > 0.15 else "CENTER"
                cv2.putText(frame, f"Gaze: {direction} ({g:+.2f})", (10, h - 20),
                            cv2.FONT_HERSHEY_SIMPLEX, 0.8, IRIS_COLOR, 2)

                # 표정 수치 게이지
                if result.face_blendshapes:
                    scores = draw_blendshape_bars(frame, result.face_blendshapes[0])
                    # [수정 지점] 표정으로 동작을 만들어 보세요
                    #   (거울 모드에서는 Left/Right 라벨이 반대로 느껴질 수 있어요)
                    if scores.get("jawOpen", 0) > 0.5:
                        cv2.putText(frame, "Mouth Open!", (w // 2 - 100, 50),
                                    cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 0, 255), 3)

            now = time.time()
            fps = fps * 0.9 + (1.0 / max(now - prev_t, 1e-6)) * 0.1
            prev_t = now
            cv2.putText(frame, f"FPS: {fps:.0f}", (10, 30),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)

            cv2.imshow("Face Landmarks (ESC: quit)", frame)
            if cv2.waitKey(1) & 0xFF == 27:  # ESC 키
                break

    cap.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()
