"""Reference implementation of the candidate personalised temporal risk score (synthetic verification only)."""
import numpy as np, time, json
from collections import deque
FPS=15
P=dict(T_cal=60, W=30, alpha=0.2, blink_min=0.05, blink_max=0.5, yawn_thr=0.6, yawn_min=1.5,
       droop_deg=15, d_max=5.0, w=dict(perclos=0.5,blink=0.2,yawn=0.15,droop=0.15),
       floor=dict(perclos=0.03,blink=4.0,yawn=1.0,droop=0.05), q_min=0.7, q_win=10,
       ema=0.3, bands=(30,55), Tp=3)
def events(mask,fps):
    """return list of (start_idx,length_s) of True runs"""
    out=[];i=0;n=len(mask)
    while i<n:
        if mask[i]:
            j=i
            while j<n and mask[j]: j+=1
            out.append((i,(j-i)/fps)); i=j
        else: i+=1
    return out
class Engine:
    def __init__(s,p=P):
        s.p=p; s.N=int(p['W']*FPS); s.buf=deque(maxlen=s.N); s.cal=[]; s.base=None; s.R=0.; s.cnt={'MILD':0,'DROWSY':0}; s.state='Calibrating'; s.t=0; s.qbuf=deque(maxlen=int(p['q_win']*FPS))
    def feats(s,arr):
        ear,mar,pit=arr[:,0],arr[:,1],arr[:,2]; p=s.p; b=s.base
        closed=ear<p['alpha']*b['E']; per=closed.mean()
        ev=events(closed,FPS); blinks=sum(1 for _,d in ev if p['blink_min']<=d<=p['blink_max']); bpm=blinks*60.0/p['W']
        yw=sum(1 for _,d in events(mar>p['yawn_thr'],FPS) if d>=p['yawn_min'])
        dr=(np.abs(pit-b['pit'])>p['droop_deg']).mean()
        return dict(perclos=per,blink=abs(bpm-b['bpm']),yawn=float(yw),droop=dr)
    def step(s,ear,mar,pit,valid):
        s.t+=1; s.qbuf.append(1 if valid else 0)
        if s.base is None:
            if valid: s.cal.append((ear,mar,pit))
            if s.t>=s.p['T_cal']*FPS:
                c=np.array(s.cal); E=np.median(c[:,0]); cl=c[:,0]<s.p['alpha']*E
                n_bl=sum(1 for _,d in events(cl,FPS) if s.p['blink_min']<=d<=s.p['blink_max'])
                s.base=dict(E=E,pit=np.median(c[:,2]),bpm=n_bl*60.0/(len(c)/FPS)); s.state='Alert'
                for r in c[-s.N:]: s.buf.append(tuple(r))
            return s.state,None
        if valid: s.buf.append((ear,mar,pit))
        else: s.buf.append((np.nan,np.nan,np.nan))
        if s.t%FPS: return s.state,None
        q=np.mean(s.qbuf)
        if q<s.p['q_min']:
            s.cnt={'MILD':0,'DROWSY':0}; s.state='UNRELIABLE'; return s.state,None
        arr=np.array(s.buf); arr=arr[~np.isnan(arr[:,0])]
        if len(arr)<s.N*0.5: s.state='UNRELIABLE'; return s.state,None
        f=s.feats(arr); p=s.p; num=0;den=0
        for k,w in p['w'].items():
            sig=p['floor'][k]; d=max(0,f[k])/sig  # baseline mu_k=0 for perclos/yawn/droop; blink uses |dev|
            sk=min(1.0,d/p['d_max']); num+=w*sk; den+=w
        Rraw=100*num/den; s.R=p['ema']*Rraw+(1-p['ema'])*s.R
        lo,hi=p['bands']
        s.cnt['MILD']=s.cnt['MILD']+1 if s.R>=lo else 0; s.cnt['DROWSY']=s.cnt['DROWSY']+1 if s.R>=hi else 0
        s.state='DROWSY' if s.cnt['DROWSY']>=p['Tp'] else ('MILD' if s.cnt['MILD']>=p['Tp'] else 'ALERT')
        return s.state,s.R
def scenario(name,seed=0,dur=210):
    rng=np.random.default_rng(seed); n=dur*FPS; t=np.arange(n)/FPS
    E=0.30; ear=E+0.01*rng.standard_normal(n); mar=0.25+0.03*rng.standard_normal(n); pit=0+1.5*rng.standard_normal(n); valid=np.ones(n,bool)
    def blink_at(tt,d=0.2):
        m=(t>=tt)&(t<tt+d); ear[m]=0.05
    def blinks(rate,start=0,end=dur):
        x=start
        while True:
            x+=rng.exponential(60.0/rate)
            if x>=end: break
            blink_at(x,0.15+0.1*rng.random())
    onset=None
    if name=='narrow_eyed': E=0.19; ear[:]=E+0.008*rng.standard_normal(n); blinks(15)
    elif name=='sustained_closure':
        blinks(15,0,90); onset=90; x=90
        while x<dur:
            d=1.0+rng.random(); m=(t>=x)&(t<x+d); ear[m]=0.05; x+=d+0.5+0.5*rng.random()
    elif name=='gradual_fatigue':
        blinks(15,0,90); onset=90; x=90.
        while x<dur:
            prog=(x-90)/(dur-90); d=0.2+1.8*prog; m=(t>=x)&(t<x+d); ear[m]=0.05; x+=d+max(0.4,3.0*(1-prog))
    else: blinks(15)
    if name=='talking': mar+=0.15*np.abs(np.sin(2*np.pi*1.2*t))+0.1*(np.sin(2*np.pi*0.3*t)>0.9)
    if name=='brief_head':
        for k in range(70,dur,20): m=(t>=k)&(t<k+1.0); pit[m]+=25
    if name=='single_yawn': m=(t>=100)&(t<104); mar[m]=0.9
    if name=='repeated_yawns':
        for k in (80,100,120,140): m=(t>=k)&(t<k+4); mar[m]=0.9
    if name=='occlusion': m=(t>=90); valid[m]=False
    if name=='narrow_eyed': pass
    return t,ear,mar,pit,valid,onset
def run(name):
    t,ear,mar,pit,valid,onset=scenario(name); e=Engine(); states=[];Rs=[];tt=[]
    t0=time.perf_counter()
    for i in range(len(t)):
        st,R=e.step(ear[i],mar[i],pit[i],valid[i]);
        if R is not None or (i%FPS==0): states.append(st);Rs.append(R if R is not None else np.nan);tt.append(t[i])
    el=time.perf_counter()-t0
    states=np.array(states);Rs=np.array(Rs,float);tt=np.array(tt)
    after=tt>=60
    res=dict(scenario=name,maxR=float(np.nanmax(Rs[after])) if np.any(~np.isnan(Rs[after])) else None,
      drowsy_frac=float(np.mean(states[after]=='DROWSY')),mild_frac=float(np.mean(states[after]=='MILD')),unrel_frac=float(np.mean(states[after]=='UNRELIABLE')),
      us_per_frame=el/len(t)*1e6)
    idx=np.where((states=='DROWSY')&(tt>=(onset or 1e9)))[0]
    res['t_to_drowsy_s']=float(tt[idx[0]]-onset) if (onset and len(idx)) else None
    # B0: fixed per-frame EAR<0.2 alarm (no personalisation, no temporal)
    res['B0_alarm_frac']=float(np.mean(ear[int(60*FPS):]<0.2)) if True else None
    # B0 fraction measured against onset for fatigue scenarios
    return res,(tt,Rs,states)
if __name__=="__main__":
    names=['normal','talking','brief_head','single_yawn','repeated_yawns','occlusion','narrow_eyed','sustained_closure','gradual_fatigue']
    out=[];traces={}
    for n in names:
        r,tr=run(n); out.append(r); traces[n]=tr
    json.dump(out,open('sim_results.json','w'),indent=1)
    for r in out: print(r)
    import pickle; pickle.dump(traces,open('traces.pkl','wb'))
