# BL PWT Version 5.1 / 30.01.2026
# MULTI DATABASE SELECT with OLED Display SSD1306 (128x32)
# DIRECT HID OUTPUT VERSION - NO MANUAL MAPPING TABLE
VERSION = "PWT Vers 5.1 HID DIR"

import board
import busio
import digitalio
import pwmio
import rotaryio
import time
import usb_hid
import adafruit_ssd1306
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS

# --- CONFIGURATION ---
INACTIVITY_TIMEOUT = 180 
LED_OFF = 0 

# Global status variables
current_db_name = "System Ready"
last_line1, last_line2, last_line3 = "", "", ""

# --- HARDWARE SETUP ---
try:
    i2c = busio.I2C(scl=board.GP7, sda=board.GP6)
    display = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c, addr=0x3C)
    display.fill(0)
    display.show()
except Exception as e:
    print("Display Error:", e)

encoder = rotaryio.IncrementalEncoder(board.GP11, board.GP12)
button = digitalio.DigitalInOut(board.GP9)
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP

led_r = pwmio.PWMOut(board.GP3, frequency=5000, duty_cycle=LED_OFF)
led_g = pwmio.PWMOut(board.GP5, frequency=5000, duty_cycle=LED_OFF)
led_b = pwmio.PWMOut(board.GP4, frequency=5000, duty_cycle=LED_OFF)

# HID Setup
kbd = Keyboard(usb_hid.devices)
layout = KeyboardLayoutUS(kbd) # Nutzt die Standard-Bibliothek statt manuellem Mapping

# --- SYSTEM FUNCTIONS ---

def set_led_rgb(r, g, b):
    led_r.duty_cycle = int(min(max(r, 0), 65535))
    led_g.duty_cycle = int(min(max(g, 0), 65535))
    led_b.duty_cycle = int(min(max(b, 0), 65535))

def set_backlight(state):
    try:
        display.poweron() if state else display.poweroff()
    except:
        pass

def update_ui_db(line2="", line3=""):
    global last_line1, last_line2, last_line3
    line1 = current_db_name
    if line1 != last_line1 or line2 != last_line2 or line3 != last_line3:
        try:
            display.fill(0)
            display.text(line1[:20], 0, 0, 1)
            if line2:
                display.text(line2[:20], 0, 11, 1)
            if line3:
                display.text(line3[:20], 0, 22, 1)
            display.show()
            last_line1, last_line2, last_line3 = line1, line2, line3
        except:
            pass

# --- LOGIC FUNCTIONS ---

def ask_for_pin(correct_pin):
    global PinPause, last_line1, last_line2, last_line3
    while True:
        input_pin, last_pos, last_action, is_asleep = "", 0, time.monotonic(), False
        encoder.position = 0
        last_line1, last_line2, last_line3 = "", "", ""
        set_backlight(True)
        while len(input_pin) < len(correct_pin):
            now, pos = time.monotonic(), encoder.position
            digit = pos % 10
            if pos != last_pos:
                last_action, is_asleep, last_pos = now, False, pos
                set_backlight(True)
            if not button.value:
                last_action = now
                if is_asleep:
                    is_asleep = False; set_backlight(True); time.sleep(0.3)
                else:
                    input_pin += str(digit); time.sleep(0.3)
                    while not button.value: pass
                last_pos = encoder.position
            if not is_asleep and (now - last_action > 60):
                is_asleep = True; display.fill(0); display.show(); last_line1 = ""; set_backlight(False); set_led_rgb(0,0,0)
            if not is_asleep:
                update_ui_db("Enter PIN:", "*" * len(input_pin) + str(digit))
                set_led_rgb(0, 0, 15000)
            time.sleep(0.05)
        if input_pin == correct_pin:
            update_ui_db("PIN OK"); set_led_rgb(0, 20000, 0); time.sleep(0.5); return
        else:
            set_led_rgb(20000, 0, 0); update_ui_db("ERROR!"); time.sleep(1)
            error_start = time.monotonic()
            while time.monotonic() - error_start < PinPause:
                set_led_rgb(25000 if int(time.monotonic()*5)%2==0 else 0, 0, 0)
                time.sleep(0.05)
            PinPause += 10; last_line1 = ""

def load_db_index():
    db_list = []
    try:
        with open("DB_Index.txt", "r", encoding="utf-8") as f:
            lines = [l.strip() for l in f if l.strip()]
            for i in range(0, len(lines), 2):
                if i+1 < len(lines): db_list.append({"name": lines[i], "file": lines[i+1]})
    except: return []
    return db_list

def load_vault_titles(filename):
    titles = []
    try:
        with open(filename, "r", encoding="utf-8") as f:
            current_title, in_block = None, False
            for line in f:
                line = line.strip()
                if line == "{": in_block, current_title = True, None
                elif line == "}":
                    if in_block and current_title: titles.append(current_title)
                    in_block = False
                elif in_block and current_title is None: current_title = line
    except: pass
    return titles

def load_vault_entry(filename, entry_index):
    curr_idx, in_block = -1, False
    try:
        with open(filename, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if line == "{": in_block, curr_lines, curr_title = True, [], None
                elif line == "}":
                    if in_block:
                        curr_idx += 1
                        if curr_idx == entry_index: return {"title": curr_title, "lines": curr_lines}
                    in_block = False
                elif in_block:
                    if curr_title is None: curr_title = line
                    else: curr_lines.append(line)
    except: pass
    return None

def select_database(db_list):
    if not db_list: return "PWT_DB.txt"
    idx, last_pos, last_action, is_asleep = 0, 0, time.monotonic(), False
    global current_db_name, last_line1
    encoder.position = 0; set_backlight(True); current_db_name = "SELECT DATABASE"; last_line1 = ""
    update_ui_db(db_list[0]["name"])
    while True:
        now, pos = time.monotonic(), encoder.position
        if pos != last_pos:
            last_action, is_asleep, last_pos = now, False, pos
            set_backlight(True); idx = pos % len(db_list); update_ui_db(db_list[idx]["name"]); set_led_rgb(0, 0, 20000)
        if not button.value:
            time.sleep(0.3); current_db_name = db_list[idx]["name"]; last_line1 = ""; return db_list[idx]["file"]
        if not is_asleep and (now - last_action > INACTIVITY_TIMEOUT):
            is_asleep = True; display.fill(0); display.show(); last_line1 = ""; set_backlight(False); set_led_rgb(0, 0, 0)
        time.sleep(0.01)
        
def powerfail():
    global last_line1
    display.fill(0); display.show()
    set_backlight(False)
    set_led_rgb(0, 2000, 0) 
    last_line1 = ""
    last_pos = encoder.position
    while True:
        curr_pos = encoder.position
        if not button.value or curr_pos != last_pos:
            break
        time.sleep(0.05)

# --- MAIN LOOP ---
powerfail()
current_db_name = "PIN ENTRY"
try:
    with open("PIN.txt", "r") as f: ask_for_pin(f.read().strip())
except: pass

db_list = load_db_index()
vault_file = select_database(db_list)
vault_titles = load_vault_titles(vault_file)
vault_titles.extend(["SELECT DATABASE", "SERVICE INFO"])

encoder.position, idx, last_pos, last_action, is_asleep = 0, 0, 0, time.monotonic(), False
set_backlight(True); set_led_rgb(0, 20000, 0); update_ui_db(vault_titles[0] if vault_titles else "")

while True:
    now = time.monotonic()
    if not is_asleep and (now - last_action > INACTIVITY_TIMEOUT):
        is_asleep = True; display.fill(0); display.show(); last_line1 = ""; set_backlight(False); set_led_rgb(0, 0, 0)

    pos = encoder.position
    if pos != last_pos:
        last_action, is_asleep, last_pos = now, False, pos
        set_backlight(True)
        if vault_titles:
            idx = pos % len(vault_titles); update_ui_db(vault_titles[idx])
        set_led_rgb(0, 0, 20000)

    if not button.value:
        last_action = now
        if is_asleep:
            is_asleep = False; set_backlight(True); time.sleep(0.3)
        else:
            time.sleep(0.2)
            sel = vault_titles[idx]
            if sel == "SELECT DATABASE":
                vault_file = select_database(db_list)
                vault_titles = load_vault_titles(vault_file); vault_titles.extend(["SELECT DATABASE", "SERVICE INFO"])
                idx, encoder.position = 0, 0; set_led_rgb(0, 20000, 0); update_ui_db(vault_titles[0])
            elif sel == "SERVICE INFO":
                set_led_rgb(20000, 0, 20000); update_ui_db("Sending...", "Info")
                if usb_hid.devices:
                    layout.write(f"VERSION: {VERSION}\n")
                time.sleep(1.5); set_led_rgb(0, 0, 20000); update_ui_db(sel)
            else:
                entry = load_vault_entry(vault_file, idx)
                if entry:
                    if not usb_hid.devices:
                        update_ui_db("NO USB!"); time.sleep(1.5)
                    else:
                        for i, line in enumerate(entry["lines"]):
                            set_led_rgb(20000, 0, 0); update_ui_db("Sending...", f"Line {i+1}")
                            layout.write(line) # Direktes Senden des Strings ohne Mapping-Funktion
                            
                            while not button.value: pass
                            time.sleep(0.1)
                            
                            if i < len(entry["lines"]) - 1:
                                set_led_rgb(0, 20000, 0); update_ui_db("Ready for", "next Line...")
                                while button.value: pass
                                time.sleep(0.3)
                        
                        set_led_rgb(0, 20000, 0); update_ui_db("Done"); time.sleep(1.5)
                        set_led_rgb(0, 0, 20000); update_ui_db(sel)
    time.sleep(0.01)