/*
  esp32s3_ble_to_usb_keyboard.ino
  - NimBLE peripheral that accepts writes to a writable characteristic
  - Forwards contents to USB HID keyboard
  - 1:1 Wiedergabe aller Zeichen wie in der App eingegeben
  - Unterstützt deutsche Umlaute korrekt über UTF-8
*/

// WICHTIG: Diese Includes müssen VOR anderen Bibliotheken stehen
#include "USB.h"
#include "USBHIDKeyboard.h"
#include <NimBLEDevice.h>

// USB HID Keyboard
USBHIDKeyboard Keyboard;

// WICHTIG: Diese UUIDs müssen mit der iOS-App übereinstimmen!
#define SERVICE_UUID        "12345678-1234-1234-1234-1234567890ab"
#define CHAR_UUID           "abcd1234-5678-90ab-cdef-1234567890ab"

// Puffer für mehrere BLE-Pakete
String inputBuffer = "";
unsigned long lastPacketTime = 0;
const unsigned long PACKET_TIMEOUT = 250;
bool processingPending = false;

// Umlaut-Keycodes für deutsches Layout
#define KEY_O_UMLAUT  0xBB
#define KEY_A_UMLAUT  0xBC
#define KEY_U_UMLAUT  0xB7
#define KEY_ESZETT    0xB5

// Funktion zum Senden eines ASCII-Zeichens
// Da Keyboard.begin(KeyboardLayout_de_DE) verwendet wird, übernimmt die Bibliothek
// automatisch das Layout-Mapping für alle ASCII-Zeichen
// Erhöhte Delays für Windows-Kompatibilität
void sendASCIIChar(uint8_t c) {
  // Die USBHIDKeyboard-Bibliothek mit deutschem Layout mappt automatisch:
  // - y/z Tausch
  // - Sonderzeichen (@, ?, :, ;, etc.)
  // - Alle anderen ASCII-Zeichen
  Keyboard.write(c);
  delay(80);  // Erhöht für Windows-Kompatibilität (war 20ms)
}

// Funktion zum Senden eines Unicode-Zeichens (1:1 Wiedergabe)
// Alle Zeichen werden genau so gesendet, wie sie eingegeben wurden
void sendUnicodeChar(uint16_t codepoint) {
  // Deutsche Umlaute: Sende die richtigen HID-Keycodes
  switch(codepoint) {
    case 0x00E4: // ä (UTF-8: 0xC3 0xA4)
      Keyboard.press(KEY_A_UMLAUT);
      delay(60);  // Erhöht für Windows (war 30ms)
      Keyboard.release(KEY_A_UMLAUT);
      delay(50);  // Erhöht für Windows (war 20ms)
      return;
      
    case 0x00F6: // ö (UTF-8: 0xC3 0xB6)
      Keyboard.press(KEY_O_UMLAUT);
      delay(60);  // Erhöht für Windows (war 30ms)
      Keyboard.release(KEY_O_UMLAUT);
      delay(50);  // Erhöht für Windows (war 20ms)
      return;
      
    case 0x00FC: // ü (UTF-8: 0xC3 0xBC)
      Keyboard.press(KEY_U_UMLAUT);
      delay(60);  // Erhöht für Windows (war 30ms)
      Keyboard.release(KEY_U_UMLAUT);
      delay(50);  // Erhöht für Windows (war 20ms)
      return;
      
    case 0x00DF: // ß (UTF-8: 0xC3 0x9F)
      Keyboard.press(KEY_ESZETT);
      delay(60);  // Erhöht für Windows (war 30ms)
      Keyboard.release(KEY_ESZETT);
      delay(50);  // Erhöht für Windows (war 20ms)
      return;
      
    case 0x00C4: // Ä (UTF-8: 0xC3 0x84)
      Keyboard.press(KEY_LEFT_SHIFT);
      delay(40);  // Erhöht für Windows (war 15ms)
      Keyboard.press(KEY_A_UMLAUT);
      delay(60);  // Erhöht für Windows (war 30ms)
      Keyboard.release(KEY_A_UMLAUT);
      delay(40);  // Erhöht für Windows (war 15ms)
      Keyboard.release(KEY_LEFT_SHIFT);
      delay(50);  // Erhöht für Windows (war 20ms)
      return;
      
    case 0x00D6: // Ö (UTF-8: 0xC3 0x96)
      Keyboard.press(KEY_LEFT_SHIFT);
      delay(40);  // Erhöht für Windows (war 15ms)
      Keyboard.press(KEY_O_UMLAUT);
      delay(60);  // Erhöht für Windows (war 30ms)
      Keyboard.release(KEY_O_UMLAUT);
      delay(40);  // Erhöht für Windows (war 15ms)
      Keyboard.release(KEY_LEFT_SHIFT);
      delay(50);  // Erhöht für Windows (war 20ms)
      return;
      
    case 0x00DC: // Ü (UTF-8: 0xC3 0x9C)
      Keyboard.press(KEY_LEFT_SHIFT);
      delay(40);  // Erhöht für Windows (war 15ms)
      Keyboard.press(KEY_U_UMLAUT);
      delay(60);  // Erhöht für Windows (war 30ms)
      Keyboard.release(KEY_U_UMLAUT);
      delay(40);  // Erhöht für Windows (war 15ms)
      Keyboard.release(KEY_LEFT_SHIFT);
      delay(50);  // Erhöht für Windows (war 20ms)
      return;
      
    default:
      // ASCII-Zeichen (0x00-0x7F): Die Bibliothek mit deutschem Layout mappt automatisch
      if (codepoint < 0x80) {
        sendASCIIChar((uint8_t)codepoint);
      } else if (codepoint < 0x100) {
        // Extended ASCII: direkt senden
        Keyboard.write((uint8_t)codepoint);
        delay(80);  // Erhöht für Windows (war 20ms)
      } else {
        // Andere Unicode-Zeichen: Diese sollten bereits als UTF-8 behandelt werden
        // und kommen hier normalerweise nicht an, da sie in sendUTF8String dekodiert werden
        Keyboard.write((uint8_t)(codepoint & 0xFF));
        delay(80);  // Erhöht für Windows (war 20ms)
      }
      return;
  }
}

// Funktion zum Dekodieren und Senden von UTF-8 Zeichen
// Verarbeitet den Input-String Byte für Byte und dekodiert UTF-8 korrekt
void sendUTF8String(const String& text) {
  size_t i = 0;
  
  while (i < text.length()) {
    uint8_t byte1 = (uint8_t)text[i];
    
    // Prüfe auf UTF-8 Multi-Byte Zeichen
    if ((byte1 & 0xE0) == 0xC0 && i + 1 < text.length()) {
      // 2-Byte UTF-8 Zeichen (für deutsche Umlaute)
      uint8_t byte2 = (uint8_t)text[i + 1];
      
      // Validiere UTF-8: Zweites Byte muss mit 10xxxxxx beginnen
      if ((byte2 & 0xC0) == 0x80) {
        // Dekodiere UTF-8 zu Unicode Codepoint
        uint16_t codepoint = ((byte1 & 0x1F) << 6) | (byte2 & 0x3F);
        sendUnicodeChar(codepoint);
        i += 2;  // Überspringe beide UTF-8 Bytes
        continue;
      }
    } else if ((byte1 & 0xF0) == 0xE0 && i + 2 < text.length()) {
      // 3-Byte UTF-8 Zeichen
      uint8_t byte2 = (uint8_t)text[i + 1];
      uint8_t byte3 = (uint8_t)text[i + 2];
      
      if ((byte2 & 0xC0) == 0x80 && (byte3 & 0xC0) == 0x80) {
        uint16_t codepoint = ((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F);
        sendUnicodeChar(codepoint);
        i += 3;
        continue;
      }
    }
    
    // ASCII-Zeichen oder ungültiges UTF-8: direkt senden
    sendUnicodeChar((uint16_t)byte1);
    i++;
  }
}

// Server Callbacks
class ServerCallbacks: public NimBLEServerCallbacks {
  void onConnect(NimBLEServer* pServer, BLEConnInfo& connInfo) {
    Serial.println("[BLE] Client verbunden");
    inputBuffer = "";
    processingPending = false;
  }
  
  void onDisconnect(NimBLEServer* pServer, BLEConnInfo& connInfo) {
    Serial.println("[BLE] Client getrennt");
    if (inputBuffer.length() > 0) {
      processInputBuffer();
    }
  }
};

// Funktion zur Verarbeitung des gepufferten Textes
void processInputBuffer() {
  if (inputBuffer.length() == 0) {
    return;
  }

  if (USB.VID() == 0) {
    Serial.println("[ERROR] USB HID nicht initialisiert!");
    inputBuffer = "";
    return;
  }

  Serial.println("\n>>> VERARBEITE TEXT <<<");
  Serial.print("[BUFFER] ");
  Serial.print(inputBuffer.length());
  Serial.println(" Zeichen");

  String input = inputBuffer;
  inputBuffer = "";
  processingPending = false;

  int charsSent = 0;
  int i = 0;
  
  while (i < input.length()) {
    // Prüfe auf Token {ENTER}, {TAB}, etc.
    if (input[i] == '{') {
      int j = input.indexOf('}', i);
      if (j > i) {
        // Sende bisherigen Text (1:1 Wiedergabe)
        if (i > 0) {
          String textChunk = input.substring(0, i);
          sendUTF8String(textChunk);
          charsSent += textChunk.length();
          delay(100);  // Erhöht für Windows-Kompatibilität (war 50ms)
        }
        
        // Verarbeite Token
        String token = input.substring(i+1, j);
        
        if (token.equalsIgnoreCase("ENTER")) {
          Keyboard.write(KEY_RETURN);
          delay(150);  // Erhöht für Windows (war 100ms)
          charsSent++;
        } else if (token.equalsIgnoreCase("TAB")) {
          Keyboard.write(KEY_TAB);
          delay(150);  // Erhöht für Windows (war 100ms)
          charsSent++;
        } else if (token.equalsIgnoreCase("ESC")) {
          Keyboard.write(KEY_ESC);
          delay(150);  // Erhöht für Windows (war 100ms)
          charsSent++;
        } else if (token.equalsIgnoreCase("F5")) {
          Keyboard.press(KEY_F5);
          delay(80);   // Erhöht für Windows (war 50ms)
          Keyboard.releaseAll();
          delay(80);   // Erhöht für Windows (war 50ms)
          charsSent++;
        } else if (token.startsWith("CTRL+")) {
          char c = token.charAt(5);
          Keyboard.press(KEY_LEFT_CTRL);
          delay(80);   // Erhöht für Windows (war 50ms)
          Keyboard.write(c);
          delay(80);   // Erhöht für Windows (war 50ms)
          Keyboard.releaseAll();
          delay(80);   // Erhöht für Windows (war 50ms)
          charsSent++;
        }
        
        input = input.substring(j + 1);
        i = 0;
        continue;
      }
    }
    i++;
  }
  
  // Sende verbleibenden Text (1:1 Wiedergabe)
  if (input.length() > 0) {
    sendUTF8String(input);
    charsSent += input.length();
    delay(100);  // Erhöht für Windows-Kompatibilität (war 50ms)
  }
  
  delay(150);  // Erhöht für Windows-Kompatibilität (war 100ms)
  
  Serial.print("[OK] ");
  Serial.print(charsSent);
  Serial.println(" Zeichen gesendet\n");
}

class MyCallbacks : public NimBLECharacteristicCallbacks {
  void onWrite(NimBLECharacteristic* pCharacteristic, BLEConnInfo& connInfo) {
    std::string value = pCharacteristic->getValue();
    
    if (value.length() == 0) {
      return;
    }

    if (USB.VID() == 0) {
      return;
    }

    String packet = String(value.c_str());
    inputBuffer += packet;
    lastPacketTime = millis();
    processingPending = true;
  }
};

void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.println("\n=== ESP32-S3 BLE to USB Keyboard ===");
  
  USB.begin();
  delay(3000);
  
  // Verwende deutsches Tastaturlayout (QWERTZ)
  Keyboard.begin(KeyboardLayout_de_DE);
  delay(1000);
  Serial.println("[OK] USB HID Keyboard initialisiert (DE-Layout)");
  
  if (USB.VID() != 0) {
    Serial.print("[OK] USB device erkannt (VID: 0x");
    Serial.print(USB.VID(), HEX);
    Serial.print(", PID: 0x");
    Serial.print(USB.PID(), HEX);
    Serial.println(")");
  }
  
  NimBLEDevice::init("Anamnesomat Bridge");
  NimBLEServer* pServer = NimBLEDevice::createServer();
  pServer->setCallbacks(new ServerCallbacks());
  
  NimBLEService* pService = pServer->createService(SERVICE_UUID);
  NimBLECharacteristic* pChar = pService->createCharacteristic(
      CHAR_UUID, 
      NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR
  );
  pChar->setCallbacks(new MyCallbacks());
  
  pService->start();
  
  NimBLEAdvertising* pAdv = NimBLEDevice::getAdvertising();
  pAdv->addServiceUUID(SERVICE_UUID);
  pAdv->start();
  
  Serial.println("[OK] BLE Service gestartet");
  Serial.println("[OK] Bereit - warte auf Daten...\n");
}

void loop() {
  if (processingPending && inputBuffer.length() > 0) {
    unsigned long timeSinceLastPacket = millis() - lastPacketTime;
    if (timeSinceLastPacket >= PACKET_TIMEOUT) {
      processInputBuffer();
    }
  }
  
  static unsigned long lastBLECheck = 0;
  if (millis() - lastBLECheck > 5000) {
    lastBLECheck = millis();
    NimBLEAdvertising* pAdv = NimBLEDevice::getAdvertising();
    if (pAdv != nullptr && !pAdv->isAdvertising()) {
      pAdv->start();
    }
  }
  
  delay(100);
}
