"""
VCNL4040 Assistive Switch (JSON-compatible, crash-resistant, beginner-friendly)

Drop this file in CIRCUITPY as code.py.

Features:
- Loads settings from config.json (safe fallback if missing/corrupt).
- Safe proximity reads with automatic re-initialization on I2C errors.
- Persistent OFF (boot button) stored in microcontroller.nvm.
- NeoPixel feedback for ON / OFF and temporary trigger feedback.
- Debug prints that show the proximity and which key(s) were pressed.
- Beginner-friendly comments throughout.
"""

# ------------------------
# Imports
# ------------------------
import time
import json
import board
import busio
import usb_hid
import neopixel
import microcontroller

from digitalio import DigitalInOut, Pull
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
from adafruit_hid.mouse import Mouse
import adafruit_vcnl4040

# ------------------------
# USER SAFETY / BEHAVIOR TOGGLE
# ------------------------
USE_CONFIG = True

# ------------------------
# Default configuration (safe defaults)
# ------------------------
DEFAULT_CONFIG = {
    "proximity_threshold": 10,
    "key_press_duration_seconds": 0.1,
    "loop_delay_seconds": 0.05,
    "only_closer_trigger": True,
    "enable_debug_prints": True,
    "enable_neopixel_feedback": True,
    "neopixel_color_rgb": [0, 255, 0],
    "hold_key_until_proximity_change": False,
    "debounce_time_ms": 200,
    "key_action": {"type": "single_key", "keys": ["SPACE"]},
    "mouse_action": {"type": "none", "scroll_amount": 1},
    "enable_on_off_switch": True,
    "button_debounce_time_ms": 300,
    "on_state_neopixel_color_rgb": [0, 255, 0],
    "off_state_neopixel_color_rgb": [255, 0, 0],
    "delta_trigger": 6,              # NEW: how much above baseline triggers
    "baseline_window": 30            # NEW: how many readings to average
}

# ------------------------
# Load config.json (if present and allowed)
# ------------------------
config = {}
if USE_CONFIG:
    try:
        with open("config.json", "r") as f:
            config = json.load(f)
            merged = DEFAULT_CONFIG.copy()
            merged.update(config)
            config = merged
            print("Loaded config.json.")
    except Exception as e:
        print("Could not load config.json, using defaults. Error:", e)
        config = DEFAULT_CONFIG.copy()
else:
    print("USE_CONFIG is False — using built-in defaults.")
    config = DEFAULT_CONFIG.copy()

# ------------------------
# Map string names to Keycode objects
# ------------------------
keycode_map = {}
for name in dir(Keycode):
    if not name.startswith("_"):
        try:
            keycode_map[name] = getattr(Keycode, name)
        except Exception:
            pass

# ------------------------
# Convert JSON key list to actual Keycode objects
# ------------------------
raw_keys = config.get("key_action", {}).get("keys", ["SPACE"])
ACTION_KEYCODES = []
for k in raw_keys:
    kk = keycode_map.get(k.upper())
    if kk is None:
        print(f"Warning: Unknown key '{k}' in config.json; defaulting to SPACE")
        kk = Keycode.SPACE
    ACTION_KEYCODES.append(kk)

# ------------------------
# Pull other config values into variables
# ------------------------
PROXIMITY_THRESHOLD = int(config.get("proximity_threshold", 10))
KEY_PRESS_DURATION = float(config.get("key_press_duration_seconds", 0.1))
LOOP_DELAY = float(config.get("loop_delay_seconds", 0.05))
ONLY_CLOSER_TRIGGER = bool(config.get("only_closer_trigger", True))
ENABLE_DEBUG_PRINTS = bool(config.get("enable_debug_prints", True))
ENABLE_NEOPIXEL_FEEDBACK = bool(config.get("enable_neopixel_feedback", True))
NEOPIXEL_COLOR = tuple(config.get("neopixel_color_rgb", [0, 255, 0]))
HOLD_KEY_UNTIL_PROXIMITY_CHANGE = bool(config.get("hold_key_until_proximity_change", False))
DEBOUNCE_TIME_MS = int(config.get("debounce_time_ms", 200))
MOUSE_ACTION = config.get("mouse_action", {}).get("type", "none")
MOUSE_SCROLL_AMOUNT = int(config.get("mouse_action", {}).get("scroll_amount", 1))
ENABLE_ON_OFF = bool(config.get("enable_on_off_switch", True))
BUTTON_DEBOUNCE_MS = int(config.get("button_debounce_time_ms", 300))
ON_COLOR = tuple(config.get("on_state_neopixel_color_rgb", [0,255,0]))
OFF_COLOR = tuple(config.get("off_state_neopixel_color_rgb", [255,0,0]))

# NEW: relative trigger settings
DELTA_TRIGGER = int(config.get("delta_trigger", 6))
BASELINE_WINDOW = int(config.get("baseline_window", 30))

# ------------------------
# Helpful startup print
# ------------------------
print("Proximity switch starting with settings:")
print(f" Threshold={PROXIMITY_THRESHOLD}, Key duration={KEY_PRESS_DURATION}s, Loop delay={LOOP_DELAY}s")
print(f" Only closer trigger={ONLY_CLOSER_TRIGGER}, Debug={ENABLE_DEBUG_PRINTS}, NeoPixel={ENABLE_NEOPIXEL_FEEDBACK}")
print(f" Action keys: {raw_keys}, Mouse action: {MOUSE_ACTION}, Hold until change: {HOLD_KEY_UNTIL_PROXIMITY_CHANGE}")
print(f" Delta trigger={DELTA_TRIGGER}, Baseline window={BASELINE_WINDOW}")

# ------------------------
# Hardware Initialization
# ------------------------
keyboard = Keyboard(usb_hid.devices)
mouse = Mouse(usb_hid.devices)

try:
    pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.3, auto_write=False)
except Exception as e:
    pixel = None
    print("NeoPixel not available:", e)

# ------------------------
# IMPORTANT FIX: I2C FOR STEMMA QT
# ------------------------
def init_i2c_and_sensor():
    try:
        i2c = busio.I2C(board.SCL1, board.SDA1)  # <-- CORRECT STEMMA QT PINS
        time.sleep(0.02)
        sensor = adafruit_vcnl4040.VCNL4040(i2c)
        time.sleep(0.02)
        return i2c, sensor
    except Exception as e:
        print("Sensor init failed:", e)
        return None, None

i2c, sensor = init_i2c_and_sensor()
if sensor is None:
    print("Warning: sensor not initialized at startup. Code will keep trying.")

# Boot button setup
if ENABLE_ON_OFF:
    try:
        button = DigitalInOut(board.BUTTON)
        button.pull = Pull.UP
    except Exception as e:
        button = None
        print("Boot button not available:", e)
else:
    button = None

try:
    persistent_off = (microcontroller.nvm[0] == 1)
except Exception:
    persistent_off = False

device_is_on = not persistent_off

if ENABLE_NEOPIXEL_FEEDBACK and pixel is not None:
    pixel.fill(ON_COLOR if device_is_on else OFF_COLOR)
    pixel.show()

if persistent_off:
    print("Device remembered OFF state. Press BOOT button to turn ON.")

# ------------------------
# Safe read wrapper for VCNL4040
# ------------------------
def safe_read_proximity(timeout_ms=50):
    global i2c, sensor
    start = time.monotonic_ns() // 1_000_000
    while True:
        if sensor is None:
            i2c, sensor = init_i2c_and_sensor()
            if sensor is None:
                time.sleep(0.05)
                return None
        try:
            return sensor.proximity
        except Exception as e:
            print("⚠️ Sensor read error:", e)
            try:
                try:
                    i2c.deinit()
                except Exception:
                    pass
                i2c, sensor = init_i2c_and_sensor()
            except Exception as e2:
                print("Re-init attempt failed:", e2)
        if (time.monotonic_ns() // 1_000_000) - start > timeout_ms:
            print("⏳ safe_read_proximity timeout — skipping this cycle")
            return None

# ------------------------
# Boot button handler
# ------------------------
last_button_press_ms = 0

def handle_boot_button():
    global device_is_on, last_button_press_ms
    if button is None:
        return
    now_ms = time.monotonic_ns() // 1_000_000
    if not button.value and (now_ms - last_button_press_ms) > BUTTON_DEBOUNCE_MS:
        device_is_on = not device_is_on
        last_button_press_ms = now_ms
        try:
            microcontroller.nvm[0] = 0 if device_is_on else 1
        except Exception as e:
            print("Could not write persistent state to nvm:", e)
        if ENABLE_NEOPIXEL_FEEDBACK and pixel is not None:
            pixel.fill(ON_COLOR if device_is_on else OFF_COLOR)
            pixel.show()
        print("Device toggled:", "ON" if device_is_on else "OFF")
        if not device_is_on:
            try:
                keyboard.release_all()
                mouse.release_all()
            except Exception:
                pass

# ------------------------
# MAIN LOOP VARIABLES
# ------------------------
last_proximity = None
last_trigger_time_ms = 0
DEBOUNCE_MS = int(DEBOUNCE_TIME_MS)

# NEW: baseline tracking
baseline_buffer = []
baseline = None

# NEW: trigger state
trigger_armed = True

def now_ms():
    return time.monotonic_ns() // 1_000_000

print("Ready. Entering main loop...")

# ------------------------
# MAIN LOOP
# ------------------------
while True:
    handle_boot_button()
    if not device_is_on:
        time.sleep(LOOP_DELAY)
        continue

    proximity = safe_read_proximity(timeout_ms=60)
    if proximity is None:
        time.sleep(LOOP_DELAY)
        continue

    # Update baseline buffer
    baseline_buffer.append(proximity)
    if len(baseline_buffer) > BASELINE_WINDOW:
        baseline_buffer.pop(0)

    # Compute baseline
    baseline = sum(baseline_buffer) / len(baseline_buffer)

    if last_proximity is None:
        last_proximity = proximity

    delta = proximity - baseline

    if ENABLE_DEBUG_PRINTS:
        print(f"Proximity: {proximity} (last: {last_proximity}) ARMED={trigger_armed} DELTA={delta:.1f} BASE={baseline:.1f}")

    # Arm trigger when we return to near baseline
    if delta < 1:  # within 1 point of baseline
        trigger_armed = True

    # Trigger when delta exceeds threshold
    if trigger_armed and delta >= DELTA_TRIGGER:
        current_ms = now_ms()
        if (current_ms - last_trigger_time_ms) > DEBOUNCE_MS:

            if MOUSE_ACTION != "none":
                try:
                    if MOUSE_ACTION == "scroll_up":
                        mouse.move(wheel=MOUSE_SCROLL_AMOUNT)
                    elif MOUSE_ACTION == "scroll_down":
                        mouse.move(wheel=-MOUSE_SCROLL_AMOUNT)
                    elif MOUSE_ACTION == "left_click":
                        mouse.click(1)
                    elif MOUSE_ACTION == "right_click":
                        mouse.click(2)
                except Exception as e:
                    print("Mouse action failed:", e)

            else:
                try:
                    for kc, name in zip(ACTION_KEYCODES, raw_keys):
                        keyboard.press(kc)

                    if ENABLE_NEOPIXEL_FEEDBACK and pixel is not None:
                        pixel.fill(NEOPIXEL_COLOR)
                        pixel.show()

                    time.sleep(KEY_PRESS_DURATION)
                    keyboard.release_all()

                    if ENABLE_NEOPIXEL_FEEDBACK and pixel is not None:
                        pixel.fill(ON_COLOR)
                        pixel.show()

                    if ENABLE_DEBUG_PRINTS:
                        print("Pressed keys:", ", ".join(raw_keys))
                except Exception as e:
                    print("Keyboard action failed:", e)

            last_trigger_time_ms = current_ms
            trigger_armed = False  # disarm until it returns to baseline

    last_proximity = proximity
    time.sleep(LOOP_DELAY)
