# エイイチ（ドット絵）が話している間、口と目を動かす部品（2026-10-28 エイイチの指示「話している時はおれの口や目が動くように」）
# 口は3段（閉じる・いつもの笑顔・大きく開く）を、声の大きさで 0.1 秒ごとに選ぶ（1段ずつしか変えない）。
# 目は、話し始めと、話している間ときどきまばたきする。
# どれも 100 ドットの絵の「その部分だけ」を描いた透明な PNG を、時間で重ねていく（motion_kit の data-a="fade"）。
import os, io, base64, wave
import numpy as np
from PIL import Image

HERE = os.path.dirname(os.path.abspath(__file__))
INK = (1, 1, 1, 255)
TEETH = (250, 250, 246, 255)
INSIDE = (70, 22, 18, 255)
TONGUE = (177, 67, 33, 255)

# 口の両端（左x, 右x, 上y）。100 ドットの絵の座標
MOUTH = {'udegumi': (39, 52, 41), 'yubisasu': (39, 52, 41), 'unazuku': (36, 47, 43), 'hirameku': (38, 49, 42)}


def _load(pose, b=False):
    return np.array(Image.open(os.path.join(HERE, f"src/ei/{pose}{'_b' if b else ''}.png")).convert('RGBA'))


def fill_teeth(a):
    """顔の中の透明な穴（歯の所）を白で埋める。背景に映像があると透けて見えるため"""
    from scipy import ndimage
    t = a[..., 3] < 128
    lab, n = ndimage.label(t)
    edge = set(np.unique(np.concatenate([lab[0], lab[-1], lab[:, 0], lab[:, -1]])))
    for k in range(1, n + 1):
        if k in edge:
            continue
        m = lab == k
        if m.sum() < 80:
            a[m] = TEETH
    return a


def _uri(a):
    b = io.BytesIO(); Image.fromarray(a.astype(np.uint8)).save(b, 'PNG')
    return 'data:image/png;base64,' + base64.b64encode(b.getvalue()).decode()


def _skin(a, xl, xr, yt):
    """顔のいちばん多い肌の色（口のまわりの、明るい色）"""
    from collections import Counter
    reg = a[yt - 6:yt + 6, xl - 3:xr + 4].reshape(-1, 4)
    c = Counter(tuple(int(v) for v in px) for px in reg if px[3] > 200 and px[0] > 220 and 120 < px[1] < 200)
    return c.most_common(1)[0][0]


def mouth_patches(pose):
    """口だけを描いた透明な絵 3枚（0 閉じる・1 いつもの・2 開く）"""
    a = fill_teeth(_load(pose))
    xl, xr, yt = MOUTH[pose]
    sk = _skin(a, xl, xr, yt)
    y0, y1 = yt, yt + 5
    out = []
    for lv in range(3):
        p = np.zeros_like(a)
        if lv == 1:
            p[y0:y1 + 1, xl:xr + 1] = a[y0:y1 + 1, xl:xr + 1]
        else:
            # 元の口の線と歯だけを肌の色で消す（まわりの影は残す）
            reg = a[y0:y1 + 1, xl:xr + 1].copy()
            rg = reg[..., :3].astype(int)
            ink = (rg.max(2) < 90) | (abs(rg[..., 0] - rg[..., 1]) < 40)   # 黒い線・白や灰の歯
            reg[ink] = sk
            p[y0:y1 + 1, xl:xr + 1] = reg
            p[yt, xl] = p[yt, xr] = INK
            p[yt + 1, xl + 1:xr] = INK
            if lv == 2:
                p[yt + 2, xl + 1] = p[yt + 2, xr - 1] = INK
                p[yt + 2, xl + 2:xr - 1] = TEETH
                p[yt + 3, xl + 2] = p[yt + 3, xr - 2] = INK
                p[yt + 3, xl + 3:xr - 2] = INSIDE
                p[yt + 4, xl + 3] = p[yt + 4, xr - 3] = INK
                p[yt + 4, xl + 4:xr - 3] = TONGUE
                p[yt + 5, xl + 4:xr - 3] = INK
        out.append(_uri(p))
    return out


def eye_patches(pose):
    """目の所だけの絵（開いた目・閉じた目）"""
    o, c = _load(pose), _load(pose, True)
    d = np.argwhere((o != c).any(2))
    if not len(d):
        return None
    y0, y1, x0, x1 = d[:, 0].min() - 1, d[:, 0].max() + 1, d[:, 1].min() - 1, d[:, 1].max() + 1
    po, pc = np.zeros_like(o), np.zeros_like(o)
    po[y0:y1 + 1, x0:x1 + 1] = o[y0:y1 + 1, x0:x1 + 1]
    pc[y0:y1 + 1, x0:x1 + 1] = c[y0:y1 + 1, x0:x1 + 1]
    return _uri(po), _uri(pc)


def base_uri(pose, b=False):
    return _uri(fill_teeth(_load(pose, b)))


def envelope(i, step=0.1):
    """tts/{i:03d}.wav の声の大きさ（step 秒ごと・0〜1）"""
    w = wave.open(os.path.join(HERE, f'tts/{i:03d}.wav'))
    sr, n, ch, sw = w.getframerate(), w.getnframes(), w.getnchannels(), w.getsampwidth()
    x = np.frombuffer(w.readframes(n), dtype=np.int16 if sw == 2 else np.int32).astype(np.float32)
    if ch > 1:
        x = x.reshape(-1, ch).mean(1)
    x /= np.abs(x).max() + 1e-9
    h = int(sr * step)
    r = np.array([np.sqrt(np.mean(x[k:k + h] ** 2)) for k in range(0, len(x) - h, h)])
    return r / (np.percentile(r, 95) + 1e-9)


def talk_layers(pose, i, t0, step=0.1):
    """場面の頭から t0 秒にエイイチの文 i が始まるときの、口と目の重ね（img の並び）"""
    m = mouth_patches(pose)
    env = envelope(i, step)
    seq, lv = [], 1
    for k, v in enumerate(env):
        want = 0 if v < 0.18 else (1 if v < 0.55 else 2)
        if want != lv:
            lv += 1 if want > lv else -1
            seq.append((round(t0 + k * step, 2), lv))
    end = round(t0 + len(env) * step, 2)
    seq.append((end, 1))   # 話し終わったら、いつもの笑顔に戻す
    h = ''
    for t, l in seq:
        h += f'<img src="{m[l]}" data-a="fade" data-t="{t}" data-d="0.01">'
    e = eye_patches(pose)
    if e:
        o, c = e
        bt, last = t0 + 0.05, end
        while bt < last - 0.3:
            h += f'<img src="{c}" data-a="fade" data-t="{round(bt, 2)}" data-d="0.01"><img src="{o}" data-a="fade" data-t="{round(bt + 0.13, 2)}" data-d="0.01">'
            bt += 1.6 + (int(bt * 7) % 5) * 0.25
    return h, (t0, end)


from functools import lru_cache
mouth_patches = lru_cache(None)(mouth_patches)
eye_patches = lru_cache(None)(eye_patches)
base_uri = lru_cache(None)(base_uri)


def mouth_seq(i, t0, step=0.1):
    """口の段の切り替え [(秒, 段)] と、話し終わりの秒"""
    env = envelope(i, step)
    seq, lv = [], 1
    for k, v in enumerate(env):
        want = 0 if v < 0.18 else (1 if v < 0.55 else 2)
        if want != lv:
            lv += 1 if want > lv else -1
            seq.append((round(t0 + k * step, 2), lv))
    end = round(t0 + len(env) * step, 2)
    seq.append((end, 1))
    return seq, end


def blink_times(t0, end):
    out, bt = [], t0 + 0.05
    while bt < end - 0.3:
        out.append(round(bt, 2)); bt += 1.6 + (int(bt * 7) % 5) * 0.25
    return out
