# Codey onboard blueprint controller generated by codeyx
# A = next blueprint, B = previous blueprint, C = return to idle

import codey
import rocky
import event
import time
import json

EYES = '00003c7e7e3c000000003c7e7e3c0000'
BLINK_WIDE = '0000183c3c1800000000183c3c180000'
BLINK_MID = '0000181c1c1800000000181c1c180000'
BLINK_THIN = '00001018181000000000101818100000'
BLINK_TINY = '00000808080800000000080808080000'
LOOK_L = '003c7e7e3c00000000003c7e7e3c000000'
LOOK_R = '0000003c7e7e3c00000000003c7e7e3c'
HAPPY = '000024247e7e3c00000024247e7e3c0000'
SAD = '00003c7e7e24000000003c7e7e24000000'
WARN = '0018181818180018000018181818180018'
OK = '00000002060c18000000180c0602000000'
THINK = '0000001818000000000018180000181800'
SMALL = '0000181c1c1800000000181c1c180000'
ANGRY = '00003c1e0e0400000000040e1e3c0000'
SHY = '000c18181c0c000000000c1c18180c00'

BLUEPRINTS = [
    'ack', 'hello', 'ready', 'think', 'curious', 'notify', 'success',
    'celebrate', 'wow', 'laugh', 'warn', 'error', 'angry', 'sad',
    'sleepy', 'bored', 'dizzy', 'screaming', 'fear', 'thank_you', 'bye'
]
SOUNDS = [
    'hello.wav', 'hi.wav', 'bye.wav', 'yeah.wav', 'wow.wav', 'laugh.wav',
    'hum.wav', 'sad.wav', 'sigh.wav', 'annoyed.wav', 'angry.wav',
    'surprised.wav', 'yummy.wav', 'curious.wav', 'embarrassed.wav',
    'ready.wav', 'sprint.wav', 'sleepy.wav', 'meow.wav', 'start.wav',
    'switch.wav', 'beeps.wav', 'buzzing.wav', 'exhaust.wav', 'explosion.wav',
    'gotcha.wav', 'hurt.wav', 'jump.wav', 'laser.wav', 'level up.wav',
    'low energy.wav', 'metal clash.wav', 'prompt tone.wav', 'right.wav',
    'wrong.wav', 'ring.wav', 'score.wav', 'shot.wav', 'step_1.wav',
    'step_2.wav', 'wake.wav', 'warning.wav'
]
CONFIG = json.loads('{"movement":true,"movementSpeed":0.55,"sounds":true,"blueprints":{},"sensors":{"enabled":true,"shake":{"enabled":true,"blueprint":"dizzy","threshold":25,"cooldownMs":5000},"sound":{"enabled":true,"blueprint":"screaming","threshold":15,"cooldownMs":5000},"lift":{"enabled":true,"fearBlueprint":"fear","putDownBlueprint":"thank_you","lowAccel":4,"highAccel":22,"deltaAccel":10,"useFloorIr":true,"floorIrThreshold":20,"floorIrStableThreshold":60,"offFloorStableSeconds":0.6,"onFloorStableSeconds":1.0,"stableMin":7,"stableMax":13,"cooldownMs":5000,"putDownCooldownMs":1000}}}')
index = 0
idle_i = 0
requested = None
requested_sound = None
seen_messages = {}
current_blueprint = None
last_sensor_at = {}
lifted = False
stable_since = 0
off_floor_since = 0
last_accel_total = 9.8

IDLE_FRAMES = [
    (EYES, 1.7), (BLINK_MID, 0.06), (BLINK_TINY, 0.14), (BLINK_WIDE, 0.06),
    (EYES, 1.4), (LOOK_L, 0.65), (EYES, 1.8), (BLINK_THIN, 0.10),
    (EYES, 1.1), (LOOK_R, 0.65),
]

def pressed_a(): return codey.button_a.is_pressed()
def pressed_b(): return codey.button_b.is_pressed()
def pressed_c(): return codey.button_c.is_pressed()

def wait_release():
    while pressed_a() or pressed_b() or pressed_c():
        time.sleep(0.05)

def interruptible_sleep(seconds):
    ticks = max(1, int(seconds / 0.05))
    for _ in range(ticks):
        if pressed_a() or pressed_b() or pressed_c(): return True
        if current_blueprint is None:
            check_sensors()
            if requested or requested_sound: return True
        time.sleep(0.05)
    return False

def led(r, g, b): codey.led.show(r, g, b)
def face(img): codey.display.show_image(img)

def notes(seq, dur=0.12):
    # pi-codey intentionally keeps reactions silent by default.
    # Preserve this helper as a no-op so older blueprint calls stay compatible.
    pass

def bp_config(name):
    try:
        return CONFIG.get('blueprints', {}).get(name, {}) or {}
    except Exception:
        return {}

def movement_enabled(name=None):
    if name is None: name = current_blueprint
    try:
        return bool(bp_config(name).get('movement', CONFIG.get('movement', True)))
    except Exception:
        return True

def sounds_enabled(name=None):
    if name is None: name = current_blueprint
    try:
        return bool(bp_config(name).get('sounds', CONFIG.get('sounds', True)))
    except Exception:
        return True

def movement_speed(name=None):
    if name is None: name = current_blueprint
    try:
        speed = float(bp_config(name).get('movementSpeed', CONFIG.get('movementSpeed', 0.55)))
        if speed < 0: speed = 0
        if speed > 1: speed = 1
        return speed
    except Exception:
        return 0.55

def scaled_speed(speed):
    return max(1, int(speed * movement_speed()))

def sound_name(name, default):
    value = bp_config(name).get('sound', default)
    if value is None or value is False or value == '': return None
    if value in SOUNDS or (value + '.wav') in SOUNDS: return value
    return default

def sensor_config(name):
    try:
        sensors = CONFIG.get('sensors', {}) or {}
        if not sensors.get('enabled', True): return None
        item = sensors.get(name, {}) or {}
        if not item.get('enabled', True): return None
        return item
    except Exception:
        return None

def sensor_ready(name, cooldown_ms):
    now = time.time()
    last = last_sensor_at.get(name, 0)
    if (now - last) * 1000 < cooldown_ms: return False
    last_sensor_at[name] = now
    return True

def accel_total():
    try:
        x = codey.motion_sensor.get_acceleration('x')
        y = codey.motion_sensor.get_acceleration('y')
        z = codey.motion_sensor.get_acceleration('z')
        return abs(x) + abs(y) + abs(z)
    except Exception:
        return 9.8

def floor_ir():
    try:
        return rocky.color_ir_sensor.get_reflected_infrared()
    except Exception:
        return None

def melody(name, fallback=None):
    # Use Codey's built-in wave files, but avoid synthetic beep/note fallbacks.
    if current_blueprint and not sounds_enabled(current_blueprint): return
    try:
        codey.speaker.play_melody(name)
    except Exception:
        pass

def blueprint_melody(blueprint, default):
    name = sound_name(blueprint, default)
    if name: melody(name)

def wiggle(speed=25, duration=0.18):
    if not movement_enabled(): return
    speed = scaled_speed(speed)
    try:
        rocky.forward(speed, duration); time.sleep(0.05)
        rocky.backward(speed, duration); time.sleep(0.05)
        rocky.stop()
    except Exception: pass

def rotate(direction=1, speed=28, duration=0.22):
    if not movement_enabled(): return
    speed = scaled_speed(speed)
    try:
        rocky.drive(speed if direction >= 0 else -speed, -speed if direction >= 0 else speed)
        time.sleep(duration); rocky.stop()
    except Exception:
        try:
            if direction >= 0: rocky.turn_right(speed, duration)
            else: rocky.turn_left(speed, duration)
            rocky.stop()
        except Exception: pass

def nod(speed=18):
    wiggle(speed, 0.10)

def shake(speed=20):
    rotate(1, speed, 0.10); rotate(-1, speed, 0.10)

def idle_once():
    global idle_i
    led(0, 60, 180)
    img, delay = IDLE_FRAMES[idle_i]
    idle_i = (idle_i + 1) % len(IDLE_FRAMES)
    face(img)
    interruptible_sleep(delay)

def idle_now():
    led(0, 60, 180)
    face(EYES)

def preview_name(name):
    led(0, 100, 255)
    face(EYES)
    time.sleep(0.18)

def request_blueprint(name):
    global requested
    requested = name

def prime_seen_messages():
    global seen_messages
    try:
        for name in BLUEPRINTS + ['idle', 'sound']:
            val = codey.upload_mode_message.get_info(name)
            if val:
                seen_messages[name] = val
    except Exception:
        pass

def check_sensors():
    global requested, lifted, stable_since, off_floor_since, last_accel_total
    if requested or requested_sound or current_blueprint: return
    shake = sensor_config('shake')
    if shake:
        try:
            if codey.motion_sensor.is_shaked() and codey.motion_sensor.get_shake_strength() >= shake.get('threshold', 20):
                if sensor_ready('shake', shake.get('cooldownMs', 5000)):
                    requested = shake.get('blueprint', 'dizzy')
                    return
        except Exception: pass
    sound = sensor_config('sound')
    if sound:
        try:
            if codey.sound_sensor.get_loudness() >= sound.get('threshold', 75):
                if sensor_ready('sound', sound.get('cooldownMs', 5000)):
                    requested = sound.get('blueprint', 'screaming')
                    return
        except Exception: pass
    lift = sensor_config('lift')
    if lift:
        total = accel_total()
        delta = abs(total - last_accel_total)
        last_accel_total = total
        low = lift.get('lowAccel', 5)
        high = lift.get('highAccel', 18)
        delta_threshold = lift.get('deltaAccel', 6)
        use_floor = lift.get('useFloorIr', False)
        floor = floor_ir() if use_floor else None
        floor_threshold = lift.get('floorIrThreshold', 8)
        floor_stable_threshold = lift.get('floorIrStableThreshold', 12)
        stable_min = lift.get('stableMin', 7)
        stable_max = lift.get('stableMax', 13)
        try: upright = codey.motion_sensor.is_upright()
        except Exception: upright = True
        now = time.time()
        off_floor = floor is not None and floor <= floor_threshold
        on_floor = floor is not None and floor >= floor_stable_threshold
        if use_floor:
            off_stable = lift.get('offFloorStableSeconds', 0.6)
            on_stable = lift.get('onFloorStableSeconds', 1.0)
            if not lifted:
                if off_floor:
                    if off_floor_since == 0: off_floor_since = now
                    if now - off_floor_since >= off_stable:
                        lifted = True
                        stable_since = 0
                        off_floor_since = 0
                        if sensor_ready('lift', lift.get('cooldownMs', 5000)):
                            requested = lift.get('fearBlueprint', 'fear')
                            return
                else:
                    off_floor_since = 0
            else:
                if on_floor:
                    if stable_since == 0: stable_since = now
                    if now - stable_since >= on_stable:
                        lifted = False
                        stable_since = 0
                        off_floor_since = 0
                        if sensor_ready('putdown', lift.get('putDownCooldownMs', 1000)):
                            requested = lift.get('putDownBlueprint', 'thank_you')
                            return
                else:
                    stable_since = 0
        elif not lifted and (total < low or total > high or delta > delta_threshold or not upright):
            lifted = True
            stable_since = 0
            if sensor_ready('lift', lift.get('cooldownMs', 5000)):
                requested = lift.get('fearBlueprint', 'fear')
                return
        elif lifted:
            if upright and stable_min <= total <= stable_max:
                if stable_since == 0: stable_since = now
                if now - stable_since > 0.8:
                    lifted = False
                    stable_since = 0
                    if sensor_ready('putdown', lift.get('putDownCooldownMs', 1000)):
                        requested = lift.get('putDownBlueprint', 'thank_you')
                        return
            else:
                stable_since = 0

def check_external_triggers():
    global requested, requested_sound, seen_messages
    try:
        val = codey.upload_mode_message.get_info('sound')
        if val and seen_messages.get('sound') != val:
            seen_messages['sound'] = val
            sound = val.split('|', 1)[0]
            if sound in SOUNDS or (sound + '.wav') in SOUNDS:
                requested_sound = sound
                return
        for name in BLUEPRINTS + ['idle']:
            val = codey.upload_mode_message.get_info(name)
            if val and seen_messages.get(name) != val:
                seen_messages[name] = val
                requested = name
                return
    except Exception:
        pass

def play_blueprint(name):
    global current_blueprint
    current_blueprint = name
    if name == 'ack':
        led(0, 180, 255); face(EYES); blueprint_melody(name, 'hi.wav'); nod(16)
    elif name == 'hello':
        led(0, 200, 255); face(HAPPY); blueprint_melody(name, 'hello.wav'); rotate(1, 18, 0.12); rotate(-1, 18, 0.12)
    elif name == 'ready':
        led(0, 255, 120); face(OK); blueprint_melody(name, 'ready.wav'); wiggle(18, 0.10)
    elif name == 'think':
        led(0, 60, 255); face(THINK); blueprint_melody(name, 'curious.wav'); rotate(1, 12, 0.10)
    elif name == 'curious':
        led(80, 0, 255); face(LOOK_L); blueprint_melody(name, 'curious.wav'); face(LOOK_R); rotate(-1, 14, 0.10)
    elif name == 'notify':
        led(255, 255, 255); face(EYES); blueprint_melody(name, 'prompt tone.wav'); shake(20); face(LOOK_R)
    elif name == 'success':
        led(0, 255, 70); face(OK); blueprint_melody(name, 'yummy.wav'); wiggle(25, 0.16); rotate(1, 22, 0.12); face(HAPPY)
    elif name == 'celebrate':
        led(255, 0, 120); face(HAPPY); blueprint_melody(name, 'yeah.wav'); rotate(1, 35, 0.20); rotate(-1, 35, 0.20); wiggle(35, 0.16); led(0, 255, 70); face(OK)
    elif name == 'wow':
        led(255, 80, 0); face(SMALL); blueprint_melody(name, 'wow.wav'); wiggle(22, 0.12); face(EYES)
    elif name == 'laugh':
        led(255, 180, 0); face(HAPPY); blueprint_melody(name, 'laugh.wav'); shake(24); face(HAPPY)
    elif name == 'warn':
        led(255, 120, 0); face(WARN); blueprint_melody(name, 'warning.wav'); wiggle(18, 0.12); face(EYES)
    elif name == 'error':
        led(255, 0, 0); face(WARN); blueprint_melody(name, 'wrong.wav'); rotate(-1, 18, 0.14); wiggle(20, 0.12); face(SAD)
    elif name == 'angry':
        led(255, 0, 0); face(ANGRY); blueprint_melody(name, 'angry.wav'); rotate(1, 28, 0.14); rotate(-1, 28, 0.14)
    elif name == 'sad':
        led(160, 0, 255); face(SAD); blueprint_melody(name, 'sad.wav'); wiggle(12, 0.10)
    elif name == 'sleepy':
        led(0, 20, 80); face(BLINK_THIN); blueprint_melody(name, 'sleepy.wav'); time.sleep(0.4); face(BLINK_TINY)
    elif name == 'bored':
        led(35, 35, 80); face(LOOK_L); blueprint_melody(name, 'sigh.wav'); time.sleep(0.35); face(LOOK_R); time.sleep(0.35); face(BLINK_THIN)
    elif name == 'dizzy':
        led(120, 0, 255); face(SMALL); blueprint_melody(name, 'annoyed.wav'); rotate(1, 18, 0.10); rotate(-1, 18, 0.10); face(THINK)
    elif name == 'screaming':
        led(255, 255, 255); face(WARN); blueprint_melody(name, 'warning.wav'); time.sleep(0.25); led(255, 80, 0); face(SMALL)
    elif name == 'fear':
        led(255, 0, 0); face(WARN); blueprint_melody(name, 'surprised.wav'); time.sleep(0.35); face(SAD)
    elif name == 'thank_you':
        led(0, 255, 120); face(HAPPY); blueprint_melody(name, 'yummy.wav'); time.sleep(0.35); face(OK)
    elif name == 'bye':
        led(0, 120, 255); face(SHY); blueprint_melody(name, 'bye.wav'); rotate(-1, 18, 0.12); face(EYES)
    interruptible_sleep(1.1)
    idle_now()
    current_blueprint = None

idle_now(); wait_release(); prime_seen_messages()
while True:
    check_external_triggers()
    check_sensors()
    if requested_sound:
        sound = requested_sound
        requested_sound = None
        melody(sound)
    elif requested:
        name = requested
        requested = None
        if name == 'idle': idle_now()
        else: play_blueprint(name)
    elif pressed_a():
        index = (index + 1) % len(BLUEPRINTS)
        name = BLUEPRINTS[index]
        preview_name(name); play_blueprint(name); wait_release()
    elif pressed_b():
        index = (index - 1) % len(BLUEPRINTS)
        name = BLUEPRINTS[index]
        preview_name(name); play_blueprint(name); wait_release()
    elif pressed_c():
        idle_now(); wait_release()
    else:
        idle_once()
