"""
_face_emotion.py — Geometric face emotion classifier (no ROS2, no ML deps).

Classifies facial expressions using geometric rules derived from facial
landmark positions.  Input is a set of key 2-D landmark coordinates in
normalised image space (x, y ∈ [0, 1]) as produced by MediaPipe Face Mesh.

Emotions
--------
  neutral   — baseline, no strong geometric signal
  happy     — lip corners raised relative to lip midpoint (smile)
  surprised — raised eyebrows + wide eyes + open mouth
  angry     — furrowed inner brows (inner brow below outer brow level)
  sad       — lip corners depressed relative to lip midpoint (frown)

Classification priority: surprised > happy > angry > sad > neutral.

Coordinate convention
---------------------
  x : 0 = left edge of image,  1 = right edge
  y : 0 = top  edge of image,  1 = bottom edge   (y increases downward)

MediaPipe Face Mesh key indices used
-------------------------------------
  MOUTH_UPPER  = 13   inner upper-lip centre
  MOUTH_LOWER  = 14   inner lower-lip centre
  MOUTH_LEFT   = 61   left mouth corner
  MOUTH_RIGHT  = 291  right mouth corner
  L_EYE_TOP    = 159  left eye upper lid centre
  L_EYE_BOT    = 145  left eye lower lid centre
  R_EYE_TOP    = 386  right eye upper lid centre
  R_EYE_BOT    = 374  right eye lower lid centre
  L_BROW_INNER = 107  left inner eyebrow
  L_BROW_OUTER = 46   left outer eyebrow
  R_BROW_INNER = 336  right inner eyebrow
  R_BROW_OUTER = 276  right outer eyebrow
  CHIN         = 152  chin tip
  FOREHEAD     = 10   midpoint between eyes (forehead anchor)

Public API
----------
  EMOTION_LABELS                   tuple[str, ...]
  FaceLandmarks                    dataclass — key points only
  EmotionFeatures                  NamedTuple — 5 normalised geometric scores
  EmotionResult                    NamedTuple — (emotion, confidence, features)

  from_mediapipe(lms)              → FaceLandmarks
  compute_features(fl)             → EmotionFeatures
  classify_emotion(features)       → (emotion, confidence)
  detect_emotion(fl)               → EmotionResult
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import NamedTuple, Tuple


# ── Public constants ──────────────────────────────────────────────────────────

EMOTION_LABELS: Tuple[str, ...] = (
    'neutral', 'happy', 'surprised', 'angry', 'sad'
)

# MediaPipe Face Mesh 468-vertex indices for key facial geometry
MOUTH_UPPER  = 13
MOUTH_LOWER  = 14
MOUTH_LEFT   = 61
MOUTH_RIGHT  = 291
L_EYE_TOP    = 159
L_EYE_BOT    = 145
R_EYE_TOP    = 386
R_EYE_BOT    = 374
L_EYE_LEFT   = 33
L_EYE_RIGHT  = 133
R_EYE_LEFT   = 362
R_EYE_RIGHT  = 263
L_BROW_INNER = 107
L_BROW_OUTER = 46
R_BROW_INNER = 336
R_BROW_OUTER = 276
CHIN         = 152
FOREHEAD     = 10


# ── Data types ────────────────────────────────────────────────────────────────

@dataclass
class FaceLandmarks:
    """
    Key geometric points extracted from a MediaPipe Face Mesh result.

    All coordinates are normalised to [0, 1] image space:
      x = 0 (left) … 1 (right)
      y = 0 (top)  … 1 (bottom)
    """
    mouth_upper:   Tuple[float, float]
    mouth_lower:   Tuple[float, float]
    mouth_left:    Tuple[float, float]   # left corner (image-left = face-right)
    mouth_right:   Tuple[float, float]
    l_eye_top:     Tuple[float, float]
    l_eye_bot:     Tuple[float, float]
    r_eye_top:     Tuple[float, float]
    r_eye_bot:     Tuple[float, float]
    l_eye_left:    Tuple[float, float]
    l_eye_right:   Tuple[float, float]
    r_eye_left:    Tuple[float, float]
    r_eye_right:   Tuple[float, float]
    l_brow_inner:  Tuple[float, float]
    l_brow_outer:  Tuple[float, float]
    r_brow_inner:  Tuple[float, float]
    r_brow_outer:  Tuple[float, float]
    chin:          Tuple[float, float]
    forehead:      Tuple[float, float]


class EmotionFeatures(NamedTuple):
    """
    Five normalised geometric features derived from FaceLandmarks.

    All features are relative to face_height; positive = expressive direction.
    """
    mouth_open:  float   # mouth height / face_height  (≥ 0)
    smile:       float   # (lip-mid y − avg corner y) / face_h  (+ = smile)
    brow_raise:  float   # (eye-top y − inner-brow y) / face_h  (+ = raised)
    brow_furl:   float   # (inner-brow y − outer-brow y) / face_h  (+ = angry)
    eye_open:    float   # eye height / face_height  (≥ 0)
    face_height: float   # forehead-to-chin distance in image coordinates


class EmotionResult(NamedTuple):
    """Result of detect_emotion()."""
    emotion:    str            # one of EMOTION_LABELS
    confidence: float          # 0.0–1.0
    features:   EmotionFeatures


# ── MediaPipe adapter ─────────────────────────────────────────────────────────

def from_mediapipe(lms) -> FaceLandmarks:
    """
    Build a FaceLandmarks from a single MediaPipe NormalizedLandmarkList.

    Parameters
    ----------
    lms : mediapipe.framework.formats.landmark_pb2.NormalizedLandmarkList
          (element of FaceMeshResults.multi_face_landmarks)

    Returns
    -------
    FaceLandmarks with normalised (x, y) coordinates.
    """
    def _pt(idx: int) -> Tuple[float, float]:
        lm = lms.landmark[idx]
        return float(lm.x), float(lm.y)

    return FaceLandmarks(
        mouth_upper  = _pt(MOUTH_UPPER),
        mouth_lower  = _pt(MOUTH_LOWER),
        mouth_left   = _pt(MOUTH_LEFT),
        mouth_right  = _pt(MOUTH_RIGHT),
        l_eye_top    = _pt(L_EYE_TOP),
        l_eye_bot    = _pt(L_EYE_BOT),
        r_eye_top    = _pt(R_EYE_TOP),
        r_eye_bot    = _pt(R_EYE_BOT),
        l_eye_left   = _pt(L_EYE_LEFT),
        l_eye_right  = _pt(L_EYE_RIGHT),
        r_eye_left   = _pt(R_EYE_LEFT),
        r_eye_right  = _pt(R_EYE_RIGHT),
        l_brow_inner = _pt(L_BROW_INNER),
        l_brow_outer = _pt(L_BROW_OUTER),
        r_brow_inner = _pt(R_BROW_INNER),
        r_brow_outer = _pt(R_BROW_OUTER),
        chin         = _pt(CHIN),
        forehead     = _pt(FOREHEAD),
    )


# ── Feature extraction ────────────────────────────────────────────────────────

def compute_features(fl: FaceLandmarks) -> EmotionFeatures:
    """
    Derive five normalised geometric features from a FaceLandmarks object.

    Coordinate notes
    ----------------
    Image y increases downward, so:
      • mouth_open > 0  when lower lip is below upper lip (mouth open)
      • smile > 0       when corners are above the lip midpoint (raised)
      • brow_raise > 0  when inner brow is above the eye-top (raised brow)
      • brow_furl > 0   when inner brow is lower than outer brow (furrowed)
      • eye_open > 0    when lower lid is below upper lid (open eye)
    """
    face_h = max(fl.chin[1] - fl.forehead[1], 1e-4)

    # ── Mouth openness ────────────────────────────────────────────────────
    mouth_h   = fl.mouth_lower[1] - fl.mouth_upper[1]
    mouth_open = max(0.0, mouth_h) / face_h

    # ── Smile / frown ─────────────────────────────────────────────────────
    # lip midpoint y
    mid_y    = (fl.mouth_upper[1] + fl.mouth_lower[1]) / 2.0
    # average corner y
    corner_y = (fl.mouth_left[1] + fl.mouth_right[1]) / 2.0
    # positive when corners are above midpoint (y_corner < y_mid)
    smile = (mid_y - corner_y) / face_h

    # ── Eyebrow raise ─────────────────────────────────────────────────────
    # Distance from inner brow (above) to eye top (below) — larger = more raised
    l_raise = fl.l_eye_top[1] - fl.l_brow_inner[1]
    r_raise = fl.r_eye_top[1] - fl.r_brow_inner[1]
    brow_raise = max(0.0, (l_raise + r_raise) / 2.0) / face_h

    # ── Brow furl (angry) ─────────────────────────────────────────────────
    # Inner brow below outer brow (in image y) → inner y > outer y → positive
    l_furl = (fl.l_brow_inner[1] - fl.l_brow_outer[1]) / face_h
    r_furl = (fl.r_brow_inner[1] - fl.r_brow_outer[1]) / face_h
    brow_furl = (l_furl + r_furl) / 2.0

    # ── Eye openness ──────────────────────────────────────────────────────
    l_eye_h = fl.l_eye_bot[1] - fl.l_eye_top[1]
    r_eye_h = fl.r_eye_bot[1] - fl.r_eye_top[1]
    eye_open = max(0.0, (l_eye_h + r_eye_h) / 2.0) / face_h

    return EmotionFeatures(
        mouth_open  = float(mouth_open),
        smile       = float(smile),
        brow_raise  = float(brow_raise),
        brow_furl   = float(brow_furl),
        eye_open    = float(eye_open),
        face_height = float(face_h),
    )


# ── Classification rules ──────────────────────────────────────────────────────

#  Thresholds are relative to face_height (all features normalised by face_h).
#  Chosen to match realistic facial proportions:
#    brow_raise:  0.06–0.10 neutral,  > 0.12 raised
#    eye_open:    0.04–0.06 normal,   > 0.07 wide
#    mouth_open:  0.0–0.02 closed,    > 0.07 open
#    smile:       -0.02–0.02 neutral, > 0.025 smile, < -0.025 frown
#    brow_furl:   -0.02–0.01 neutral, > 0.02 furrowed

_T_SURPRISED_BROW  = 0.12    # brow_raise threshold for surprised
_T_SURPRISED_EYE   = 0.07    # eye_open   threshold for surprised
_T_SURPRISED_MOUTH = 0.07    # mouth_open threshold for surprised
_T_HAPPY_SMILE     = 0.025   # smile      threshold for happy
_T_ANGRY_FURL      = 0.02    # brow_furl  threshold for angry
_T_ANGRY_NO_SMILE  = 0.01    # smile must be below this for angry
_T_SAD_FROWN       = 0.025   # -smile     threshold for sad
_T_SAD_NO_FURL     = 0.015   # brow_furl must be below this for sad


def classify_emotion(features: EmotionFeatures) -> Tuple[str, float]:
    """
    Apply geometric rules to classify emotion from EmotionFeatures.

    Priority order: surprised → happy → angry → sad → neutral.

    Returns
    -------
    (emotion, confidence)  where confidence ∈ (0, 1].
    """
    f = features

    # ── Surprised ────────────────────────────────────────────────────────
    if (f.brow_raise > _T_SURPRISED_BROW
            and f.eye_open   > _T_SURPRISED_EYE
            and f.mouth_open > _T_SURPRISED_MOUTH):
        # Confidence from excess over each threshold
        br_exc  = (f.brow_raise  - _T_SURPRISED_BROW)  / 0.05
        ey_exc  = (f.eye_open    - _T_SURPRISED_EYE)   / 0.03
        mo_exc  = (f.mouth_open  - _T_SURPRISED_MOUTH) / 0.06
        conf = min(1.0, 0.5 + (br_exc + ey_exc + mo_exc) / 6.0)
        return 'surprised', conf

    # ── Happy ─────────────────────────────────────────────────────────────
    if f.smile > _T_HAPPY_SMILE:
        conf = min(1.0, 0.5 + (f.smile - _T_HAPPY_SMILE) / 0.05)
        return 'happy', conf

    # ── Angry ─────────────────────────────────────────────────────────────
    if f.brow_furl > _T_ANGRY_FURL and f.smile < _T_ANGRY_NO_SMILE:
        conf = min(1.0, 0.5 + (f.brow_furl - _T_ANGRY_FURL) / 0.04)
        return 'angry', conf

    # ── Sad ───────────────────────────────────────────────────────────────
    if f.smile < -_T_SAD_FROWN and f.brow_furl < _T_SAD_NO_FURL:
        conf = min(1.0, 0.5 + (-f.smile - _T_SAD_FROWN) / 0.04)
        return 'sad', conf

    return 'neutral', 0.8


# ── Main entry point ──────────────────────────────────────────────────────────

def detect_emotion(fl: FaceLandmarks) -> EmotionResult:
    """
    Classify emotion from face landmark geometry.

    Parameters
    ----------
    fl : FaceLandmarks — normalised key point positions

    Returns
    -------
    EmotionResult(emotion, confidence, features)
    """
    features = compute_features(fl)
    emotion, confidence = classify_emotion(features)
    return EmotionResult(
        emotion    = emotion,
        confidence = float(confidence),
        features   = features,
    )
