# BL PWT Version 5.1 / 29.01.2026
# MULTI DATENBANK SELECT mit OLED Display SSD1306 (128x32)
# Displayschoner integriert
VERSION = "PWT Vers 5.1 OLED"

import board
import busio
import digitalio
import pwmio
import rotaryio
import time
import usb_hid
import math
import adafruit_ssd1306  # Benötigt adafruit_ssd1306 und adafruit_framebuf im lib Ordner
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode

# --- KONFIGURATION ---
INACTIVITY_TIMEOUT = 180 
LED_OFF = 0  # Geändert für Common GND (0 = Aus)

# Globale Status-Variablen
current_db_name = "Bootdelay"
last_line1 = ""
last_line2 = ""
last_line3 = ""

# --- HARDWARE SETUP ---
# I2C Initialisierung für OLED (GP6=SDA, GP7=SCL)
try:
    i2c = busio.I2C(scl=board.GP7, sda=board.GP6)
    # 128x32 OLED Display
    display = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c, addr=0x3C)
    display.fill(0)
    display.show()
except Exception as e:
    print("Display Error:", e)

# Peripherie Setup
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)

kbd = Keyboard(usb_hid.devices)
PinPause = 10

# --- SYSTEM FUNKTIONEN ---

def set_led_rgb(r, g, b):
    """Steuert die RGB-LED (Common GND / Normaler Pegel)."""
    # Für Common GND werden die Werte direkt gesetzt (0=aus, 65535=max)
    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):
    """Schaltet das OLED-Display ein oder aus (Energiesparen)."""
    try:
        display.poweron() if state else display.poweroff()
    except:
        pass

def update_ui_db(line2="", line3=""):
    """
    Aktualisiert das OLED Display. 
    Verarbeitet bis zu 3 Zeilen Text.
    """
    global last_line1, last_line2, last_line3
    line1 = current_db_name
    
    # Nur aktualisieren, wenn sich der Inhalt geändert hat
    if line1 != last_line1 or line2 != last_line2 or line3 != last_line3:
        try:
            display.fill(0) # Buffer leeren
            # OLED Text (x, y) - Zeilenabstand ca. 10 Pixel bei 32px Höhe
            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

# --- LOGIK FUNKTIONEN ---

def start_security_timer(seconds):
    """Countdown mit Stealth-Start."""
    start_t = time.monotonic()
    last_sec = -1
    last_pos = encoder.position
    interaction = False
    set_backlight(False)
    set_led_rgb(0, 0, 0)
    
    while time.monotonic() - start_t < seconds:
        now = time.monotonic()
        remaining = int(seconds - (now - start_t))
        curr_pos = encoder.position
        
        if not button.value or curr_pos != last_pos:
            interaction = True
            set_backlight(True)
            
        if interaction:
            set_led_rgb(int((math.sin(now * 4) + 1) * 10000), 0, 0)
            if remaining != last_sec:
                update_ui_db(f"Start in:", f"{remaining} sec")
                last_sec = remaining
            if not button.value:
                time.sleep(0.5)
                return
        
        last_pos = curr_pos
        time.sleep(0.02)

def send_char_ch(char):
    """Tastatur-Mapping für Schweizer Layout (CH)."""
    if not usb_hid.devices: return
    try:
        if char == '!':
            kbd.press(Keycode.LEFT_ALT)
            for _ in range(2):
                kbd.press(Keycode.KEYPAD_THREE); kbd.release(Keycode.KEYPAD_THREE)
            kbd.release(Keycode.LEFT_ALT)
            time.sleep(0.03); return
        if 'a' <= char <= 'z' or 'A' <= char <= 'Z':
            shift = 'A' <= char <= 'Z'
            c = char.upper()
            if c == 'Y': c = 'Z'
            elif c == 'Z': c = 'Y'
            code = getattr(Keycode, c)
            if shift: kbd.press(Keycode.LEFT_SHIFT)
            kbd.press(code); kbd.release_all()
            time.sleep(0.03); return
        mapping = {
            '1': (0x1E, 0, 0), '2': (0x1F, 0, 0), '3': (0x20, 0, 0), '4': (0x21, 0, 0),
            '5': (0x22, 0, 0), '6': (0x23, 0, 0), '7': (0x24, 0, 0), '8': (0x25, 0, 0),
            '9': (0x26, 0, 0), '0': (0x27, 0, 0), '+': (0x1E, 1, 0), '-': (0x38, 0, 0),
            '.': (0x37, 0, 0), ':': (0x37, 1, 0), ',': (0x36, 0, 0), ';': (0x36, 1, 0),
            ' ': (0x2C, 0, 0), '_': (0x38, 1, 0), '?': (0x2D, 1, 0), '=': (0x27, 1, 0),
            '/': (0x24, 1, 0), '*': (0x20, 1, 0), '#': (0x20, 0, 1), '@': (0x1F, 0, 1),
            '(': (0x25, 1, 0), ')': (0x26, 1, 0), '[': (0x2F, 0, 1), ']': (0x30, 0, 1),
            '{': (0x34, 0, 1), '}': (0x32, 0, 1), '$': (0x32, 0, 0), '%': (0x22, 1, 0),
            '&': (0x23, 1, 0), '€': (0x08, 0, 1), '£': (0x32, 1, 0), '|': (0x31, 1, 1),
            'ü': (0x2F, 0, 0), 'ö': (0x33, 0, 0), 'ä': (0x34, 0, 0), 'é': (0x33, 1, 0),
            'Ü': (0x2F, 1, 0), 'Ö': (0x33, 1, 0), 'Ä': (0x34, 1, 0), '<': (0x64, 0, 0),
            '>': (0x64, 1, 0), '"': (0x1F, 1, 0)
        }
        if char in mapping:
            code, s, a = mapping[char]
            if a: kbd.press(Keycode.RIGHT_ALT)
            if s: kbd.press(Keycode.LEFT_SHIFT)
            kbd.press(code); kbd.release_all()
            time.sleep(0.04)
    except: pass

# --- PIN & DATABASE FUNKTIONEN ---

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("Eingabe:", "*" * 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, last_line2
    encoder.position = 0; set_backlight(True); current_db_name = "DB AUSWAHL"; 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():
    """Initialer Wartezustand."""
    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 EINGABE"
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(["DB AUSWAHL", "INFOS"])

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 == "DB AUSWAHL":
                vault_file = select_database(db_list)
                vault_titles = load_vault_titles(vault_file); vault_titles.extend(["DB AUSWAHL", "INFOS"])
                idx, encoder.position = 0, 0; set_led_rgb(0, 20000, 0); update_ui_db(vault_titles[0])
            elif sel == "INFOS":
                set_led_rgb(20000, 0, 20000); update_ui_db("Sende...", "Service Info")
                if usb_hid.devices:
                    for line in ["INFOS: " + VERSION, "Symbole:", '+-,.:;?!"*$%&/()=?_@#€£{}[]<>', "Gebrauchsanleitung:", "https://smf.pcaarburg.ch/Smurfy_CH_Hardware_projects/SMFY_PWT_2026/Dokumente/PasswortTresor_Anleitung.pdf"]:
                        for char in line: send_char_ch(char)
                        kbd.send(Keycode.ENTER)
                else:
                    update_ui_db("KEIN USB!"); time.sleep(1.5)
                update_ui_db("Fertig"); 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("KEIN USB!"); time.sleep(1.5)
                    else:
                        for i, line in enumerate(entry["lines"]):
                            set_led_rgb(20000, 0, 0); update_ui_db("Sende...", f"Zeile {i+1}")
                            for char in line: send_char_ch(char)
                            while not button.value: pass
                            time.sleep(0.1)
                            if i < len(entry["lines"]) - 1:
                                set_led_rgb(0, 20000, 0); update_ui_db("Bereit für", f"Zeile {i+2}...")
                                while button.value: pass
                                time.sleep(0.2)
                    set_led_rgb(0, 20000, 0); update_ui_db("Fertig"); time.sleep(1.5); set_led_rgb(0, 0, 20000); update_ui_db(sel)
    time.sleep(0.01)