import board, busio, time, usb_hid, json, neopixel, microcontroller
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
from adafruit_hid.mouse import Mouse
import adafruit_vl53l0x
from digitalio import DigitalInOut, Pull

# --- Load Configuration from config.json ---
config = {}
try:
    with open("config.json", "r") as config_file:
        config = json.load(config_file)
except (OSError, ValueError) as e:
    print(f"Config error: {e}. Using defaults.")
    config = {
        "proximity_threshold": 10,
        "key_press_duration_seconds": 0.1,
        "loop_delay_seconds": 0.25,
        "only_closer_trigger": True,
        "enable_debug_prints": False,
        "enable_neopixel_feedback": False,
        "neopixel_color_rgb": [0, 10, 0],
        "hold_key_until_proximity_change": False,
        "debounce_time_ms": 200,
        "key_action": {"type": "single_key", "keys": ["TAB"]},
        "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]
    }

# --- Map string keycodes ---
keycode_map = {k: getattr(Keycode, k) for k in dir(Keycode) if not k.startswith("_")}

PROXIMITY_THRESHOLD = config["proximity_threshold"]
KEY_PRESS_DURATION = config["key_press_duration_seconds"]
LOOP_DELAY = config["loop_delay_seconds"]
ONLY_CLOSER_TRIGGER = config["only_closer_trigger"]
ENABLE_DEBUG_PRINTS = config["enable_debug_prints"]
ENABLE_NEOPIXEL_FEEDBACK = config["enable_neopixel_feedback"]
NEOPIXEL_COLOR = tuple(config["neopixel_color_rgb"])
HOLD_KEY_UNTIL_PROXIMITY_CHANGE = config["hold_key_until_proximity_change"]
DEBOUNCE_TIME_MS = config["debounce_time_ms"]

KEY_ACTION_TYPE = config["key_action"].get("type", "single_key")
RAW_KEYS_LIST = config["key_action"].get("keys", ["TAB"])

ACTION_KEYCODES = []
for key_str in RAW_KEYS_LIST:
    keycode = keycode_map.get(key_str.upper())
    if keycode: ACTION_KEYCODES.append(keycode)
    else:
        print(f"Warning: Unknown key '{key_str}'. Using TAB.")
        ACTION_KEYCODES.append(Keycode.TAB)

MOUSE_ACTION_TYPE = config["mouse_action"].get("type", "none")
MOUSE_SCROLL_AMOUNT = config["mouse_action"].get("scroll_amount", 1)

ENABLE_ON_OFF_SWITCH = config["enable_on_off_switch"]
BUTTON_DEBOUNCE_TIME_MS = config["button_debounce_time_ms"]
ON_STATE_NEOPIXEL_COLOR = tuple(config["on_state_neopixel_color_rgb"])
OFF_STATE_NEOPIXEL_COLOR = tuple(config["off_state_neopixel_color_rgb"])

print(f"Config loaded: Threshold={PROXIMITY_THRESHOLD}, Key Action={KEY_ACTION_TYPE}, Mouse Action={MOUSE_ACTION_TYPE}, Hold={HOLD_KEY_UNTIL_PROXIMITY_CHANGE}, Debounce={DEBOUNCE_TIME_MS}ms, Sensor: VL53L0X, NeoPixel Enabled: {ENABLE_NEOPIXEL_FEEDBACK}, NeoPixel Color: {NEOPIXEL_COLOR}, On/Off Switch Enabled: {ENABLE_ON_OFF_SWITCH}")

# --- I2C and device setup ---
try:
    i2c = busio.I2C(board.SCL1, board.SDA1)
except ValueError as e:
    print(f"I2C error: {e}. Check STEMMA QT.")
    while True: time.sleep(1)

vl53 = adafruit_vl53l0x.VL53L0X(i2c)
keyboard = Keyboard(usb_hid.devices)
mouse = Mouse(usb_hid.devices)
pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.5, auto_write=False)

# --- On/Off button setup with persistent OFF ---
if ENABLE_ON_OFF_SWITCH:
    button = DigitalInOut(board.BUTTON)
    button.pull = Pull.UP
    last_button_press_time = 0
    if microcontroller.nvm[0] == 1:
        device_is_on = False
        print("Device is persistently OFF. Press boot button to turn ON.")
    else:
        device_is_on = True
        print("Device ON.")
else:
    device_is_on = True

# --- NeoPixel initial state ---
if ENABLE_NEOPIXEL_FEEDBACK:
    pixel.fill(ON_STATE_NEOPIXEL_COLOR if device_is_on else OFF_STATE_NEOPIXEL_COLOR)
    pixel.show()
else:
    pixel.fill((0,0,0))
    pixel.show()

key_is_held = False
proximity_at_press = 0
last_range = vl53.range
last_trigger_time = 0

# --- Helper: check boot/on-off button ---
def check_boot_button():
    global device_is_on, last_button_press_time, key_is_held, proximity_at_press
    current_time_ms = time.monotonic_ns() // 1_000_000
    if ENABLE_ON_OFF_SWITCH:
        if not button.value and (current_time_ms - last_button_press_time) > BUTTON_DEBOUNCE_TIME_MS:
            device_is_on = not device_is_on
            last_button_press_time = current_time_ms
            microcontroller.nvm[0] = 0 if device_is_on else 1
            print(f"Device Toggled! New state: {'ON' if device_is_on else 'OFF'}")
            if ENABLE_NEOPIXEL_FEEDBACK:
                pixel.fill(ON_STATE_NEOPIXEL_COLOR if device_is_on else OFF_STATE_NEOPIXEL_COLOR)
                pixel.show()
            if not device_is_on:
                keyboard.release_all()
                mouse.release_all()
                key_is_held = False
                proximity_at_press = 0

# --- Main Loop ---
while True:
    check_boot_button()

    if device_is_on:
        try:
            current_range = vl53.range
            if current_range > 8000:
                if ENABLE_DEBUG_PRINTS: print("Out of range.")
                time.sleep(LOOP_DELAY)
                continue

            if ENABLE_DEBUG_PRINTS: print(f"Current range: {current_range}mm")

            change_detected = False
            if ONLY_CLOSER_TRIGGER:
                if (last_range - current_range) >= PROXIMITY_THRESHOLD:
                    change_detected = True
            else:
                if abs(current_range - last_range) >= PROXIMITY_THRESHOLD:
                    change_detected = True

            if change_detected:
                if MOUSE_ACTION_TYPE != "none":
                    if MOUSE_ACTION_TYPE == "scroll_up":
                        mouse.move(wheel=MOUSE_SCROLL_AMOUNT)
                        if ENABLE_DEBUG_PRINTS: print(f"Scrolled up by {MOUSE_SCROLL_AMOUNT} units!")
                    elif MOUSE_ACTION_TYPE == "scroll_down":
                        mouse.move(wheel=-MOUSE_SCROLL_AMOUNT)
                        if ENABLE_DEBUG_PRINTS: print(f"Scrolled down by {MOUSE_SCROLL_AMOUNT} units!")
                    elif MOUSE_ACTION_TYPE == "left_click":
                        mouse.click(1)
                        if ENABLE_DEBUG_PRINTS: print("Left mouse click!")
                    elif MOUSE_ACTION_TYPE == "right_click":
                        mouse.click(2)
                        if ENABLE_DEBUG_PRINTS: print("Right mouse click!")
                else:
                    pressed_keys = []
                    for keycode, keyname in zip(ACTION_KEYCODES, RAW_KEYS_LIST):
                        keyboard.press(keycode)
                        pressed_keys.append(keyname)
                    time.sleep(KEY_PRESS_DURATION)
                    keyboard.release_all()
                    if ENABLE_DEBUG_PRINTS:
                        print(f"Pressed keys: {', '.join(pressed_keys)}")

            last_range = current_range
            time.sleep(LOOP_DELAY)

        except OSError as e:
            print(f"Sensor error: {e}")
            try:
                i2c = busio.I2C(board.SCL1, board.SDA1)
                vl53 = adafruit_vl53l0x.VL53L0X(i2c)
                print("Sensor re-initialized.")
            except ValueError:
                print("Failed to re-initialize sensor. Check connections.")
                time.sleep(1)
    else:
        time.sleep(LOOP_DELAY)
