#include <Keyboard.h>
#include <Mouse.h>

const long BAUD_RATE = 115200;


String argument(const String &command, int index) {
  int start = 0;
  for (int current = 0; current < index; current++) {
    start = command.indexOf(',', start);
    if (start < 0) return "";
    start++;
  }
  int end = command.indexOf(',', start);
  return end < 0 ? command.substring(start) : command.substring(start, end);
}


// Menschliches Timing direkt auf dem Board: Der PC sieht nur den Befehl,
// die Tipp- und Klick-Rhythmik entsteht hier mit Zufallsjitter.
long humanDelay(long minimum, long maximum) {
  return random(minimum, maximum);
}


void humanClick(uint8_t button) {
  Mouse.press(button);
  delay(humanDelay(25, 95));
  Mouse.release(button);
}


bool writeHex(const String &value) {
  if (value.length() % 2 != 0) return false;
  for (unsigned int index = 0; index < value.length(); index += 2) {
    char high = value.charAt(index);
    char low = value.charAt(index + 1);
    int highValue = isDigit(high) ? high - '0' : tolower(high) - 'a' + 10;
    int lowValue = isDigit(low) ? low - '0' : tolower(low) - 'a' + 10;
    if (highValue < 0 || highValue > 15 || lowValue < 0 || lowValue > 15) {
      return false;
    }
    uint8_t key = (uint8_t) ((highValue << 4) | lowValue);
    Keyboard.press(key);
    delay(humanDelay(10, 40));
    Keyboard.release(key);
    // Normales Tipp-Tempo (~50–80 WPM statt Maschinenschrift).
    delay(humanDelay(70, 230));
    // Nach Wortgrenzen denkt ein Mensch kurz nach.
    if (key == 32) {
      delay(humanDelay(120, 450));
    }
  }
  return true;
}


bool handleCommand(String command) {
  command.trim();
  if (command.length() == 0 || command.length() > 4096) return false;

  String action = argument(command, 0);
  if (action == "health") {
    return true;
  } else if (action == "keyDown") {
    Keyboard.press((uint8_t) argument(command, 1).toInt());
  } else if (action == "keyUp") {
    Keyboard.release((uint8_t) argument(command, 1).toInt());
  } else if (action == "press") {
    uint8_t key = (uint8_t) argument(command, 1).toInt();
    Keyboard.press(key);
    delay(humanDelay(8, 35));
    Keyboard.release(key);
  } else if (action == "writeHex") {
    return writeHex(argument(command, 1));
  } else if (action == "move") {
    int x = constrain(argument(command, 1).toInt(), -127, 127);
    int y = constrain(argument(command, 2).toInt(), -127, 127);
    Mouse.move(x, y, 0);
  } else if (action == "leftClick") {
    humanClick(MOUSE_LEFT);
  } else if (action == "rightClick") {
    humanClick(MOUSE_RIGHT);
  } else if (action == "dragStart") {
    Mouse.press(MOUSE_LEFT);
  } else if (action == "dragEnd") {
    Mouse.release(MOUSE_LEFT);
  } else if (action == "scroll") {
    Mouse.move(0, 0, constrain(argument(command, 1).toInt(), -127, 127));
  } else if (action == "moveTo") {
    // Absolut positionieren: HID kennt nur relative Moves. Erst sicher
    // in die obere linke Ecke laufen, dann in 127er-Schritten zum Ziel.
    int x = argument(command, 1).toInt();
    int y = argument(command, 2).toInt();
    if (x < 0 || y < 0 || x > 16383 || y > 16383) return false;
    for (int i = 0; i < 130; i++) { Mouse.move(-127, -127, 0); }
    while (x > 127) { Mouse.move(127, 0, 0); x -= 127; }
    while (y > 127) { Mouse.move(0, 127, 0); y -= 127; }
    Mouse.move(x, y, 0);
  } else if (action == "releaseAll") {
    Keyboard.releaseAll();
    Mouse.release(MOUSE_LEFT);
    Mouse.release(MOUSE_RIGHT);
    Mouse.release(MOUSE_MIDDLE);
  } else {
    return false;
  }
  return true;
}


void setup() {
  Serial.begin(BAUD_RATE);
  Serial.setTimeout(50);
  // Deutsches Tastaturlayout: Windows deutet die gesendeten Tastencodes
  // nach dem eingestellten Systemlayout. Ohne diese Angabe tippt der Chip
  // US-Codes - aus "y" wird dann "z" und aus "@" ein Anfuehrungszeichen,
  // womit jeder Login mit E-Mail-Adresse fehlschlaegt.
  Keyboard.begin(KeyboardLayout_de_DE);
  Mouse.begin();
  // Zufalls-Seed aus dem Rauschen eines offenen Analog-Pins.
  randomSeed(analogRead(A0) ^ analogRead(A1) ^ (long) micros());
}


void loop() {
  if (!Serial.available()) return;
  bool ok = handleCommand(Serial.readStringUntil('\n'));
  Serial.println(ok ? "OK PILOTNG" : "ERR COMMAND");
}
