/**
 * @file main.cpp
 * @brief Treasure Hunt Orb firmware
 *
 * Features:
 * - SSD1306 OLED Display (I2C)
 * - RC522 RFID Reader (SPI)
 * - White LED indicator
 * - Piezo buzzer
 * - TTP223 capacitive touch sensor
 * - WiFi connectivity
 * - RFID card programming via serial
 */

#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <esp_sleep.h>
// #include <WiFi.h>  // Disabled - not needed for now
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <MFRC522.h>
#include "pins.h"
#include "config.h"
#include "cards.h"

// Display configuration
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

// Create objects
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
MFRC522 rfid(RC522_CS, RC522_RST);

// State tracking
bool displayOK = false;
bool rfidOK = false;
bool wifiOK = false;
unsigned long lastBreathUpdate = 0;
int breathBrightness = 0;
int breathDirection = 1;  // 1 = getting brighter, -1 = getting dimmer

// PWM configuration for LED breathing
#define LED_PWM_CHANNEL  0
#define LED_PWM_FREQ     5000
#define LED_PWM_RESOLUTION 8  // 8-bit = 0-255

// Power management
unsigned long lastActivityTime = 0;
bool displayAsleep = false;

// Programming mode
bool programmingMode = false;
bool waitingForCard = false;
String pendingCardName = "";
String pendingCardClue = "";
String serialBuffer = "";

// Authentication mode (required at startup)
bool authMode = true;           // Start in auth mode
int authCount = 0;              // Number of successful authentications
int lastAuthId = 0;             // Last authenticated ID (for order checking)
String lastAuthName = "";       // Last authenticated name (for display)
#define AUTH_REQUIRED_USERS 5   // Number of users needed to complete auth

// Forward declarations
void handleProgModeCard();
void showStatus();
void showProgModeScreen();
void showAuthScreen();
void resetActivity();
void checkPowerSave();
void enterDeepSleep();
void wakeDisplay();
void checkTouch();

/**
 * @brief Initialize the OLED display
 *
 * Returns:
 *     bool: true if display initialized successfully
 */
bool initDisplay() {
    Wire.begin(I2C_SDA, I2C_SCL);

    if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
        Serial.println("ERROR: SSD1306 display not found!");
        return false;
    }

    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0, 0);
    display.println("Minecraft Orb");
    display.println("-------------");
    display.println("Hello World!");
    display.display();

    Serial.println("OK: Display initialized");
    return true;
}

/**
 * @brief Initialize the RC522 RFID reader
 *
 * Returns:
 *     bool: true if reader initialized successfully
 */
bool initRFID() {
    // Initialize SPI with custom pins for ESP32-C3
    SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI, RC522_CS);

    rfid.PCD_Init();
    delay(50);  // Small delay for RC522 to stabilize

    // Check if RC522 is responding
    byte version = rfid.PCD_ReadRegister(MFRC522::VersionReg);
    if (version == 0x00 || version == 0xFF) {
        Serial.println("ERROR: RC522 not found!");
        return false;
    }

    Serial.print("OK: RC522 initialized (version 0x");
    Serial.print(version, HEX);
    Serial.println(")");
    return true;
}

/**
 * @brief Initialize the LED pin with PWM for breathing effect
 */
void initLED() {
    ledcSetup(LED_PWM_CHANNEL, LED_PWM_FREQ, LED_PWM_RESOLUTION);
    ledcAttachPin(LED_WHITE, LED_PWM_CHANNEL);
    ledcWrite(LED_PWM_CHANNEL, 0);
    Serial.println("OK: LED initialized (PWM)");
}

/**
 * @brief Set LED brightness (0-255)
 */
void setLED(int brightness) {
    ledcWrite(LED_PWM_CHANNEL, brightness);
}

/**
 * @brief Initialize the buzzer pin
 */
void initBuzzer() {
    pinMode(BUZZER_PIN, OUTPUT);
    digitalWrite(BUZZER_PIN, LOW);
    Serial.println("OK: Buzzer initialized");
}

/**
 * @brief Play a tone on the buzzer
 *
 * Args:
 *     frequency: Tone frequency in Hz
 *     duration: Duration in milliseconds
 */
void beep(int frequency, int duration) {
    tone(BUZZER_PIN, frequency, duration);
    delay(duration);
    noTone(BUZZER_PIN);
}

/**
 * @brief Play startup melody
 */
void playStartupSound() {
    beep(523, 100);  // C5
    beep(659, 100);  // E5
    beep(784, 150);  // G5
}

/**
 * @brief Play card detected sound
 */
void playCardSound() {
    beep(880, 100);  // A5
    beep(1109, 150); // C#6
}

/**
 * @brief Play touch feedback sound
 */
void playTouchSound() {
    beep(1318, 50);  // E6 - short chirp
}

// =============================================================================
// Morse Code
// =============================================================================

// Morse code timing (in ms) - 75% slower than standard
#define MORSE_DOT       260
#define MORSE_DASH      (MORSE_DOT * 3)
#define MORSE_SYMBOL    MORSE_DOT           // Gap between dots/dashes
#define MORSE_LETTER    (MORSE_DOT * 3)     // Gap between letters
#define MORSE_WORD      (MORSE_DOT * 7)     // Gap between words
#define MORSE_FREQ      800                 // Tone frequency

// Morse code lookup table (A-Z, 0-9)
// Each string contains dots and dashes for the character
const char* morseTable[] = {
    ".-",     // A
    "-...",   // B
    "-.-.",   // C
    "-..",    // D
    ".",      // E
    "..-.",   // F
    "--.",    // G
    "....",   // H
    "..",     // I
    ".---",   // J
    "-.-",    // K
    ".-..",   // L
    "--",     // M
    "-.",     // N
    "---",    // O
    ".--.",   // P
    "--.-",   // Q
    ".-.",    // R
    "...",    // S
    "-",      // T
    "..-",    // U
    "...-",   // V
    ".--",    // W
    "-..-",   // X
    "-.--",   // Y
    "--..",   // Z
    "-----",  // 0
    ".----",  // 1
    "..---",  // 2
    "...--",  // 3
    "....-",  // 4
    ".....",  // 5
    "-....",  // 6
    "--...",  // 7
    "---..",  // 8
    "----.",  // 9
};

/**
 * @brief Get Morse code pattern for a character
 *
 * Args:
 *     c: Character to look up (A-Z, 0-9)
 *
 * Returns:
 *     const char*: Morse pattern or NULL if not found
 */
const char* getMorsePattern(char c) {
    c = toupper(c);
    if (c >= 'A' && c <= 'Z') {
        return morseTable[c - 'A'];
    } else if (c >= '0' && c <= '9') {
        return morseTable[26 + (c - '0')];
    }
    return NULL;
}

/**
 * @brief Play a single Morse symbol (dot or dash)
 *
 * Args:
 *     isDash: true for dash, false for dot
 *     displayX: X position on display for symbol
 *     displayY: Y position on display for symbol
 */
void playMorseSymbol(bool isDash, int displayX, int displayY) {
    int duration = isDash ? MORSE_DASH : MORSE_DOT;

    // LED on
    setLED(255);

    // Start tone
    tone(BUZZER_PIN, MORSE_FREQ, duration);

    // Update display with symbol
    if (displayOK) {
        display.setTextSize(2);
        display.setCursor(displayX, displayY);
        display.print(isDash ? "-" : ".");
        display.display();
    }

    delay(duration);

    // LED off
    setLED(0);
    noTone(BUZZER_PIN);

    // Gap between symbols
    delay(MORSE_SYMBOL);
}

/**
 * @brief Play a message in Morse code
 *
 * Only shows dots and dashes on the LCD, not the actual message.
 *
 * Args:
 *     message: The message to play
 */
void playMorseMessage(const char* message) {
    Serial.print("Playing Morse: ");
    Serial.println(message);

    // Show header on display (no message text - that's the puzzle!)
    if (displayOK) {
        display.clearDisplay();
        display.setTextSize(1);
        display.setCursor(0, 0);
        display.println("SECRET MESSAGE");
        display.println("INCOMING...");
        display.display();
    }

    int displayX = 0;
    int displayY = 40;  // Bottom area for Morse symbols

    for (int i = 0; message[i] != '\0'; i++) {
        char c = message[i];

        if (c == ' ') {
            // Word gap
            delay(MORSE_WORD - MORSE_LETTER);
            displayX = 0;  // Reset for next word
            if (displayOK) {
                // Clear the Morse symbol area
                display.fillRect(0, 36, 128, 28, SSD1306_BLACK);
                display.display();
            }
            continue;
        }

        const char* pattern = getMorsePattern(c);
        if (pattern == NULL) continue;

        // Play each symbol in the pattern
        for (int j = 0; pattern[j] != '\0'; j++) {
            playMorseSymbol(pattern[j] == '-', displayX, displayY);
            displayX += 14;  // Move cursor for next symbol

            // Wrap if needed
            if (displayX > 114) {
                displayX = 0;
                if (displayOK) {
                    display.fillRect(0, 36, 128, 28, SSD1306_BLACK);
                    display.display();
                }
            }
        }

        // Letter gap - add visual separator
        if (displayOK) {
            display.setCursor(displayX, displayY);
            display.setTextSize(2);
            display.print(" ");
            display.display();
        }
        delay(MORSE_LETTER - MORSE_SYMBOL);
        displayX += 8;  // Extra space between letters
    }

    Serial.println("Morse playback complete");
}

// =============================================================================
// MIFARE Card Read/Write - Store quest data directly on cards
// =============================================================================

// Default MIFARE key (factory default)
MFRC522::MIFARE_Key mifareKey = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};

// Card data layout (MIFARE Classic 1K):
// Sector 1 (blocks 4-7): Name (blocks 4,5 = 32 bytes, block 7 = trailer)
// Sector 2 (blocks 8-11): Clue part 1 (blocks 8,9,10 = 48 bytes)
// Sector 3 (blocks 12-15): Clue part 2 (blocks 12,13,14 = 48 bytes)
// Sector 4 (blocks 16-19): Clue part 3 (blocks 16,17,18 = 48 bytes)
// Total: 32 + 144 = 176 bytes available

#define NAME_BLOCK_START    4   // Sector 1
#define CLUE_BLOCK_START    8   // Sector 2

/**
 * @brief Authenticate a sector for read/write
 *
 * Args:
 *     blockAddr: Any block in the sector to authenticate
 *
 * Returns:
 *     bool: true if authentication successful
 */
bool authenticateSector(byte blockAddr) {
    MFRC522::StatusCode status = rfid.PCD_Authenticate(
        MFRC522::PICC_CMD_MF_AUTH_KEY_A,
        blockAddr,
        &mifareKey,
        &(rfid.uid)
    );
    return (status == MFRC522::STATUS_OK);
}

/**
 * @brief Write quest data to MIFARE card
 *
 * Args:
 *     name: Quest name (max 32 chars)
 *     clue: Quest clue (max 128 chars)
 *
 * Returns:
 *     bool: true if write successful
 */
bool writeQuestToCard(const char* name, const char* clue) {
    byte buffer[18];  // 16 data + 2 CRC
    MFRC522::StatusCode status;

    Serial.println("Writing quest data to card...");

    // Write name (2 blocks = 32 bytes) in sector 1
    if (!authenticateSector(NAME_BLOCK_START)) {
        Serial.println("ERROR: Auth failed for name sector");
        return false;
    }

    // Block 4: first 16 chars of name
    memset(buffer, 0, 16);
    strncpy((char*)buffer, name, 16);
    status = rfid.MIFARE_Write(4, buffer, 16);
    if (status != MFRC522::STATUS_OK) {
        Serial.print("ERROR: Write block 4 failed: ");
        Serial.println(rfid.GetStatusCodeName(status));
        return false;
    }

    // Block 5: next 16 chars of name
    memset(buffer, 0, 16);
    if (strlen(name) > 16) {
        strncpy((char*)buffer, name + 16, 16);
    }
    status = rfid.MIFARE_Write(5, buffer, 16);
    if (status != MFRC522::STATUS_OK) {
        Serial.print("ERROR: Write block 5 failed: ");
        Serial.println(rfid.GetStatusCodeName(status));
        return false;
    }

    // Write clue (up to 9 blocks = 144 bytes) across sectors 2, 3, 4
    int clueLen = strlen(clue);
    int clueOffset = 0;
    byte blocks[] = {8, 9, 10, 12, 13, 14, 16, 17, 18};  // Skip sector trailers

    for (int i = 0; i < 9 && clueOffset < clueLen; i++) {
        byte blockAddr = blocks[i];

        // Authenticate sector if needed (new sector every 4 blocks)
        if (blockAddr == 8 || blockAddr == 12 || blockAddr == 16) {
            if (!authenticateSector(blockAddr)) {
                Serial.print("ERROR: Auth failed for block ");
                Serial.println(blockAddr);
                return false;
            }
        }

        memset(buffer, 0, 16);
        int copyLen = min(16, clueLen - clueOffset);
        strncpy((char*)buffer, clue + clueOffset, copyLen);
        clueOffset += 16;

        status = rfid.MIFARE_Write(blockAddr, buffer, 16);
        if (status != MFRC522::STATUS_OK) {
            Serial.print("ERROR: Write block ");
            Serial.print(blockAddr);
            Serial.print(" failed: ");
            Serial.println(rfid.GetStatusCodeName(status));
            return false;
        }
    }

    Serial.println("OK: Quest data written to card");
    return true;
}

/**
 * @brief Read quest data from MIFARE card
 *
 * Args:
 *     name: Buffer for quest name (min 33 bytes)
 *     clue: Buffer for quest clue (min 145 bytes)
 *
 * Returns:
 *     bool: true if read successful and data found
 */
bool readQuestFromCard(char* name, char* clue) {
    byte buffer[18];
    byte size = 18;
    MFRC522::StatusCode status;

    // Read name from sector 1
    if (!authenticateSector(NAME_BLOCK_START)) {
        Serial.println("ERROR: Auth failed for name sector");
        return false;
    }

    // Read block 4
    size = 18;
    status = rfid.MIFARE_Read(4, buffer, &size);
    if (status != MFRC522::STATUS_OK) {
        Serial.println("ERROR: Read block 4 failed");
        return false;
    }
    memcpy(name, buffer, 16);

    // Read block 5
    size = 18;
    status = rfid.MIFARE_Read(5, buffer, &size);
    if (status != MFRC522::STATUS_OK) {
        Serial.println("ERROR: Read block 5 failed");
        return false;
    }
    memcpy(name + 16, buffer, 16);
    name[32] = '\0';

    // Check if card has data (name not empty/all zeros)
    bool hasData = false;
    for (int i = 0; i < 32 && !hasData; i++) {
        if (name[i] != 0 && name[i] != 0xFF) hasData = true;
    }
    if (!hasData) {
        Serial.println("Card appears blank");
        return false;
    }

    // Read clue from sectors 2, 3, 4
    byte blocks[] = {8, 9, 10, 12, 13, 14, 16, 17, 18};
    int clueOffset = 0;

    for (int i = 0; i < 9; i++) {
        byte blockAddr = blocks[i];

        // Authenticate sector if needed
        if (blockAddr == 8 || blockAddr == 12 || blockAddr == 16) {
            if (!authenticateSector(blockAddr)) {
                Serial.print("ERROR: Auth failed for block ");
                Serial.println(blockAddr);
                return false;
            }
        }

        size = 18;
        status = rfid.MIFARE_Read(blockAddr, buffer, &size);
        if (status != MFRC522::STATUS_OK) {
            Serial.print("ERROR: Read block ");
            Serial.print(blockAddr);
            Serial.println(" failed");
            return false;
        }

        memcpy(clue + clueOffset, buffer, 16);
        clueOffset += 16;
    }
    clue[144] = '\0';

    // Trim trailing nulls/spaces from name and clue
    for (int i = 31; i >= 0 && (name[i] == 0 || name[i] == ' '); i--) {
        name[i] = '\0';
    }
    for (int i = 143; i >= 0 && (clue[i] == 0 || clue[i] == ' '); i--) {
        clue[i] = '\0';
    }

    return true;
}

// =============================================================================
// INPUT Command - Touch-based number input
// =============================================================================

#define INPUT_TIMEOUT_MS    3000    // 3 seconds of no touch = submit

/**
 * @brief Handle INPUT command - wait for touch-based number input
 *
 * Args:
 *     answer: The correct answer (1-10)
 *     message: Message to display if correct
 *
 * Returns:
 *     bool: true if correct answer entered
 */
bool handleInputCommand(int answer, const char* message) {
    int count = 0;
    unsigned long lastTouchTime = millis();
    bool lastState = false;
    bool submitted = false;

    Serial.print("INPUT command: expecting ");
    Serial.println(answer);

    // Show initial prompt
    if (displayOK) {
        display.clearDisplay();
        display.setTextSize(1);
        display.setCursor(0, 0);
        display.println("ENTER CODE");
        display.println("----------------");
        display.println();
        display.println("Find and touch the");
        display.println("secret button to");
        display.println("enter your answer.");
        display.println();
        display.println("Wait 3s to submit.");
        display.display();
    }

    // Wait for first touch to start
    Serial.println("Waiting for input...");

    while (!submitted) {
        bool currentTouch = digitalRead(TOUCH_PIN);

        // Detect rising edge (new touch)
        if (currentTouch && !lastState) {
            resetActivity();
            count++;
            lastTouchTime = millis();

            Serial.print("Touch! Count: ");
            Serial.println(count);

            // Audio feedback
            beep(1000, 50);

            // Visual feedback
            setLED(255);
            delay(50);
            setLED(0);

            // Update display
            if (displayOK) {
                display.clearDisplay();
                display.setTextSize(1);
                display.setCursor(0, 0);
                display.println("ENTER CODE");
                display.println("----------------");
                display.setTextSize(4);
                display.setCursor(50, 28);
                display.println(count);
                display.display();
            }

            // Cap at 10
            if (count >= 10) {
                delay(500);
                submitted = true;
            }
        }

        lastState = currentTouch;

        // Check for timeout (3 seconds of no touch after at least one input)
        if (count > 0 && (millis() - lastTouchTime > INPUT_TIMEOUT_MS)) {
            submitted = true;
        }

        delay(50);  // Debounce
    }

    Serial.print("Submitted: ");
    Serial.println(count);

    // Check answer
    if (count == answer) {
        Serial.println("CORRECT!");

        // Success sound
        beep(523, 100);
        beep(659, 100);
        beep(784, 100);
        beep(1047, 200);

        // Flash LED
        for (int i = 0; i < 5; i++) {
            setLED(255);
            delay(100);
            setLED(0);
            delay(100);
        }

        // Show success message
        if (displayOK) {
            display.clearDisplay();
            display.setTextSize(1);
            display.setCursor(0, 0);
            display.println("CORRECT!");
            display.println("----------------");
            display.println();
            display.println(message);
            display.display();
        }

        delay(5000);  // Show message for 5 seconds
        return true;
    } else {
        Serial.println("WRONG!");

        // Failure sound
        beep(200, 300);
        beep(150, 400);

        // Show failure
        if (displayOK) {
            display.clearDisplay();
            display.setTextSize(2);
            display.setCursor(0, 20);
            display.println("  WRONG!");
            display.display();
        }

        delay(2000);
        return false;
    }
}

/**
 * @brief Initialize the touch sensor pin
 */
void initTouch() {
    pinMode(TOUCH_PIN, INPUT);
    Serial.println("OK: Touch sensor initialized");
}

// =============================================================================
// Power Management (Battery Operation)
// =============================================================================

/**
 * @brief Reset the activity timer (call on any user interaction)
 */
void resetActivity() {
    lastActivityTime = millis();
    if (displayAsleep) {
        wakeDisplay();
    }
}

/**
 * @brief Turn off the OLED display to save power
 */
void sleepDisplay() {
    if (!displayAsleep && displayOK) {
        display.ssd1306_command(SSD1306_DISPLAYOFF);
        displayAsleep = true;
        Serial.println("Display off (power save)");
    }
}

/**
 * @brief Turn the OLED display back on and refresh content
 */
void wakeDisplay() {
    if (displayAsleep && displayOK) {
        display.ssd1306_command(SSD1306_DISPLAYON);
        displayAsleep = false;
        Serial.println("Display on");
        // Refresh current screen
        if (authMode) {
            showAuthScreen();
        } else if (programmingMode) {
            showProgModeScreen();
        } else {
            showStatus();
        }
    }
}

/**
 * @brief Enter deep sleep to conserve battery
 *
 * Wakes on touch sensor press (GPIO5 HIGH).
 * After wake, the ESP32 resets and runs setup() again.
 * Requires TOUCH_PIN on GPIO0-5 for deep sleep wake support.
 */
void enterDeepSleep() {
    Serial.println("Entering deep sleep...");
    Serial.flush();

    // Turn off peripherals
    if (displayOK) {
        display.ssd1306_command(SSD1306_DISPLAYOFF);
    }
    setLED(0);
    noTone(BUZZER_PIN);
    rfid.PCD_SoftPowerDown();

    // Configure wake on touch button (HIGH = touched)
    esp_deep_sleep_enable_gpio_wakeup(BIT(TOUCH_PIN), ESP_GPIO_WAKEUP_GPIO_HIGH);
    esp_deep_sleep_start();
    // Device resets after wake — execution never reaches here
}

/**
 * @brief Check inactivity timers and manage power saving
 *
 * Called every loop iteration. Turns off display after DISPLAY_TIMEOUT_MS,
 * enters deep sleep after SLEEP_TIMEOUT_MS.
 */
void checkPowerSave() {
    unsigned long elapsed = millis() - lastActivityTime;

    if (elapsed >= SLEEP_TIMEOUT_MS) {
        enterDeepSleep();
    } else if (elapsed >= DISPLAY_TIMEOUT_MS) {
        sleepDisplay();
    }
}

/**
 * @brief Poll touch sensor to wake display / reset activity timer
 */
void checkTouch() {
    if (digitalRead(TOUCH_PIN)) {
        resetActivity();
    }
}

// WiFi disabled - not needed for now
#if 0
bool initWiFi() {
    Serial.print("Connecting to WiFi: ");
    Serial.println(WIFI_SSID);

    if (displayOK) {
        display.clearDisplay();
        display.setCursor(0, 0);
        display.setTextSize(1);
        display.println("Minecraft Orb");
        display.println("-------------");
        display.println();
        display.println("Connecting WiFi...");
        display.println(WIFI_SSID);
        display.display();
    }

    WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

    unsigned long startTime = millis();
    while (WiFi.status() != WL_CONNECTED) {
        if (millis() - startTime > WIFI_TIMEOUT_MS) {
            Serial.println("WARN: WiFi connection timeout");
            return false;
        }
        delay(500);
        Serial.print(".");
    }

    Serial.println();
    Serial.print("OK: WiFi connected, IP: ");
    Serial.println(WiFi.localIP());
    return true;
}
#endif

/**
 * @brief Display status on OLED
 */
void showStatus() {
    display.clearDisplay();
    display.setCursor(0, 0);
    display.setTextSize(1);
    display.println("Minecraft Orb");
    display.println("-------------");
    display.println();
    display.print("RFID: ");
    display.println(rfidOK ? "OK" : "FAIL");
    display.println();
    display.println("Scan RFID card...");
    display.display();
}

/**
 * @brief Show authentication mode screen
 */
void showAuthScreen() {
    display.clearDisplay();
    display.setCursor(0, 0);
    display.setTextSize(1);
    display.println("=== AUTH REQUIRED ===");
    display.println();

    if (authCount == 0) {
        display.println("Scan AUTH cards");
        display.println("in the correct order");
        display.println();
        display.print("Need ");
        display.print(AUTH_REQUIRED_USERS);
        display.println(" players");
    } else {
        display.print("Authenticated: ");
        display.print(authCount);
        display.print("/");
        display.println(AUTH_REQUIRED_USERS);
        display.print("Last: ");
        display.println(lastAuthName);
        display.println();
        display.print("Need ");
        display.print(AUTH_REQUIRED_USERS - authCount);
        display.println(" more");
    }
    display.display();
}

/**
 * @brief Check for RFID card and handle accordingly
 *
 * In programming mode: delegates to handleProgModeCard()
 * In normal mode: reads quest data directly from the card
 */
void checkRFID() {
    if (!rfidOK) return;

    // Check for new card
    if (!rfid.PICC_IsNewCardPresent()) return;
    if (!rfid.PICC_ReadCardSerial()) return;

    resetActivity();

    // Handle programming mode separately
    if (programmingMode && waitingForCard) {
        handleProgModeCard();
        rfid.PICC_HaltA();
        rfid.PCD_StopCrypto1();
        return;
    }

    // Read card data
    char uidStr[24];
    formatUID(rfid.uid.uidByte, rfid.uid.size, uidStr);

    Serial.print("Card detected! UID: ");
    Serial.println(uidStr);

    char cardName[33];
    char cardClue[145];

    if (!readQuestFromCard(cardName, cardClue)) {
        // Blank or unreadable card
        Serial.println("Blank/unreadable card");
        beep(330, 200);

        if (displayOK) {
            display.clearDisplay();
            display.setCursor(0, 0);
            display.setTextSize(1);
            display.println("BLANK CARD");
            display.println();
            display.println("UID:");
            display.println(uidStr);
            display.display();
        }

        delay(1500);
        if (displayOK) {
            if (authMode) showAuthScreen();
            else if (!programmingMode) showStatus();
        }
        rfid.PICC_HaltA();
        rfid.PCD_StopCrypto1();
        return;
    }

    // Card read successfully
    Serial.print("Card: ");
    Serial.println(cardName);

    String nameStr = String(cardName);
    nameStr.toUpperCase();

    // Handle authentication mode
    if (authMode) {
        // In auth mode, only AUTH cards are accepted
        if (!nameStr.startsWith("AUTH")) {
            Serial.println("Auth mode: Non-AUTH card rejected");
            beep(220, 300);

            if (displayOK) {
                display.clearDisplay();
                display.setCursor(0, 0);
                display.setTextSize(1);
                display.println("AUTH REQUIRED!");
                display.println();
                display.println("Please scan your");
                display.println("AUTH card first.");
                display.display();
            }

            delay(1500);
            if (displayOK) showAuthScreen();
            rfid.PICC_HaltA();
            rfid.PCD_StopCrypto1();
            return;
        }

        // Parse AUTH card: FULLNAME|ID|TXT
        String clueStr = String(cardClue);
        int sep1 = clueStr.indexOf('|');
        int sep2 = clueStr.indexOf('|', sep1 + 1);

        if (sep1 > 0 && sep2 > sep1) {
            String fullName = clueStr.substring(0, sep1);
            int cardId = clueStr.substring(sep1 + 1, sep2).toInt();
            String authTxt = clueStr.substring(sep2 + 1);

            Serial.print("  Name: ");
            Serial.println(fullName);
            Serial.print("  ID: ");
            Serial.println(cardId);

            // Check order: ID must be greater than last
            if (cardId > lastAuthId) {
                // Correct order - accept authentication
                authCount++;
                lastAuthId = cardId;
                lastAuthName = fullName;

                Serial.println("  -> Auth sequence OK");

                beep(523, 100);
                beep(659, 100);
                beep(784, 150);

                if (displayOK) {
                    display.clearDisplay();
                    display.setCursor(0, 0);
                    display.setTextSize(1);
                    display.println("AUTH OK!");
                    display.println("----------------");
                    display.println();
                    display.println(fullName);
                    display.println();
                    display.print("Position: ");
                    display.println(authCount);
                    display.println();
                    display.println(authTxt);
                    display.display();
                }

                for (int i = 0; i < 3; i++) {
                    setLED(255);
                    delay(100);
                    setLED(0);
                    delay(100);
                }

                delay(2000);

                // Check if all users authenticated
                if (authCount >= AUTH_REQUIRED_USERS) {
                    authMode = false;
                    Serial.println();
                    Serial.print("All ");
                    Serial.print(AUTH_REQUIRED_USERS);
                    Serial.println(" users authenticated! Quest starting...");

                    // Victory sound
                    beep(523, 150);
                    beep(659, 150);
                    beep(784, 150);
                    beep(1047, 300);

                    // Celebration LED
                    for (int i = 0; i < 10; i++) {
                        setLED(255);
                        delay(100);
                        setLED(0);
                        delay(100);
                    }

                    // Show first clue (stays until next card scan)
                    if (displayOK) {
                        display.clearDisplay();
                        display.setCursor(0, 0);
                        display.setTextSize(1);
                        display.println("QUEST BEGINS!");
                        display.println("================");
                        display.println();
                        display.println("Where frogs sing");
                        display.println("and lilies float,");
                        display.println();
                        display.println("In the garden,");
                        display.println("find the moat.");
                        display.display();
                    }

                    Serial.println("First clue: Where frogs sing and lilies float, In the garden, find the moat.");

                    rfid.PICC_HaltA();
                    rfid.PCD_StopCrypto1();
                    return;
                }
            } else {
                // Wrong order - reset sequence
                Serial.println("  -> WRONG ORDER! Resetting...");
                Serial.print("     Expected ID > ");
                Serial.print(lastAuthId);
                Serial.print(", got ");
                Serial.println(cardId);

                authCount = 0;
                lastAuthId = 0;
                lastAuthName = "";

                beep(200, 200);
                beep(150, 200);
                beep(100, 400);

                if (displayOK) {
                    display.clearDisplay();
                    display.setCursor(0, 0);
                    display.setTextSize(2);
                    display.println("WRONG");
                    display.println("ORDER!");
                    display.setTextSize(1);
                    display.println();
                    display.println("Sequence reset.");
                    display.println("Start over!");
                    display.display();
                }

                for (int i = 0; i < 10; i++) {
                    setLED(255);
                    delay(50);
                    setLED(0);
                    delay(50);
                }

                delay(2000);
            }
        } else {
            Serial.println("ERROR: Invalid AUTH format");
            beep(220, 300);
        }

        if (displayOK) showAuthScreen();
        rfid.PICC_HaltA();
        rfid.PCD_StopCrypto1();
        return;
    }

    // Normal mode: check for command cards
    if (nameStr.startsWith("MORSE")) {
        // MORSE command: play clue as Morse code
        Serial.println("Command: MORSE");
        playMorseMessage(cardClue);
        delay(1000);
    } else if (nameStr.startsWith("INPUT")) {
        // INPUT command: parse ANSWER|MESSAGE from clue
        Serial.println("Command: INPUT");
        String clueStr = String(cardClue);
        int sep = clueStr.indexOf('|');
        if (sep > 0) {
            int answer = clueStr.substring(0, sep).toInt();
            String message = clueStr.substring(sep + 1);
            handleInputCommand(answer, message.c_str());
        } else {
            Serial.println("ERROR: Invalid INPUT format - expected ANSWER|MESSAGE");
        }
        delay(1000);
    } else if (nameStr.startsWith("AUTH")) {
        // AUTH command: parse FULLNAME|ID|TXT from clue
        Serial.println("Command: AUTH");
        String clueStr = String(cardClue);

        // Parse three fields: FULLNAME|ID|TXT
        int sep1 = clueStr.indexOf('|');
        int sep2 = clueStr.indexOf('|', sep1 + 1);

        if (sep1 > 0 && sep2 > sep1) {
            String fullName = clueStr.substring(0, sep1);
            String odooId = clueStr.substring(sep1 + 1, sep2);
            String authTxt = clueStr.substring(sep2 + 1);

            Serial.print("  Name: ");
            Serial.println(fullName);
            Serial.print("  ID: ");
            Serial.println(odooId);
            Serial.print("  Text: ");
            Serial.println(authTxt);

            // Success sound
            beep(523, 100);
            beep(659, 100);
            beep(784, 150);

            // Display auth info
            if (displayOK) {
                display.clearDisplay();
                display.setCursor(0, 0);
                display.setTextSize(1);
                display.println("AUTHENTICATED");
                display.println("----------------");
                display.println();
                display.println(fullName);
                display.println();
                display.print("ID: ");
                display.println(odooId);
                display.println();
                display.println(authTxt);
                display.display();
            }

            // Flash LED green-style (rapid flashes)
            for (int i = 0; i < 5; i++) {
                setLED(255);
                delay(80);
                setLED(0);
                delay(80);
            }

            delay(3000);
        } else {
            Serial.println("ERROR: Invalid AUTH format - expected FULLNAME|ID|TXT");
            beep(220, 300);
        }
    } else {
        // Regular quest card - display clue
        Serial.print("Clue: ");
        Serial.println(cardClue);

        playCardSound();

        if (displayOK) {
            display.clearDisplay();
            display.setCursor(0, 0);
            display.setTextSize(1);
            display.println(cardName);
            display.println("----------------");
            display.println();
            display.println(cardClue);
            display.display();
        }

        // Flash LED to indicate success
        for (int i = 0; i < 3; i++) {
            setLED(255);
            delay(100);
            setLED(0);
            delay(100);
        }

        delay(3000);  // Show clue for 3 seconds
    }

    if (displayOK && !programmingMode && !authMode) {
        showStatus();
    }

    rfid.PICC_HaltA();
    rfid.PCD_StopCrypto1();
}

/**
 * @brief Slow breathing LED effect to indicate system is running
 *
 * Uses exponential curve so LED spends more time dim than bright.
 * Full breath cycle takes ~4 seconds (2s up, 2s down)
 */
void breatheLED() {
    unsigned long now = millis();
    // Update every 20ms for smooth animation
    if (now - lastBreathUpdate >= 20) {
        lastBreathUpdate = now;

        // Update linear position (0-255)
        breathBrightness += breathDirection * 3;

        // Reverse direction at limits
        if (breathBrightness >= 255) {
            breathBrightness = 255;
            breathDirection = -1;
        } else if (breathBrightness <= 0) {
            breathBrightness = 0;
            breathDirection = 1;
        }

        // Apply exponential curve: more time dim, quick peak
        // Formula: (x^3) / 65025 gives range 0-255 with cubic falloff
        int actualBrightness = ((long)breathBrightness * breathBrightness * breathBrightness) / 65025;
        setLED(actualBrightness);
    }
}

/**
 * @brief Show programming mode screen on OLED
 */
void showProgModeScreen() {
    display.clearDisplay();
    display.setCursor(0, 0);
    display.setTextSize(1);
    display.println("=== PROG MODE ===");
    display.println();
    if (waitingForCard) {
        display.println("Scan card to write:");
        display.println(pendingCardName);
    } else {
        display.println("Commands:");
        display.println("SCAN ADD EXIT");
        display.println();
        display.println("Writes to MIFARE");
        display.println("cards directly.");
    }
    display.display();
}

/**
 * @brief Print help message for serial commands
 */
void printHelp() {
    Serial.println();
    Serial.println("=== Minecraft Orb - Help ===");
    Serial.println();
    Serial.println("Commands:");
    Serial.println("  PROG              Enter programming mode");
    Serial.println("  EXIT              Exit programming mode");
    Serial.println("  SCAN              Read card contents");
    Serial.println("  ADD <name>|<clue> Write quest to card");
    Serial.println("  HELP              Show this help");
    Serial.println();
    Serial.println("Examples:");
    Serial.println("  ADD Quest 1|Look under the old oak tree");
    Serial.println("  ADD MORSE|HELLO WORLD");
    Serial.println("  ADD INPUT|5|The treasure is in the garden");
    Serial.println("  ADD AUTH|John Smith|12345|Welcome!");
    Serial.println();
    Serial.println("Command cards (use as name):");
    Serial.println("  MORSE - Plays clue as Morse code (LED + buzzer)");
    Serial.println("  INPUT - Touch puzzle: tap button N times");
    Serial.println("          Format: <answer 1-10>|<success message>");
    Serial.println("  AUTH  - Authentication card");
    Serial.println("          Format: <full name>|<id>|<text>");
    Serial.println();
    Serial.println("Data is written directly to MIFARE Classic 1K cards.");
    Serial.println();
}

/**
 * @brief Process serial command
 *
 * Args:
 *     cmd: Command string to process
 */
void processCommand(String cmd) {
    cmd.trim();
    if (cmd.length() == 0) return;

    String cmdUpper = cmd;
    cmdUpper.toUpperCase();

    // Commands available anytime
    if (cmdUpper == "HELP") {
        printHelp();
        return;
    }

    if (cmdUpper == "PROG") {
        programmingMode = true;
        waitingForCard = false;
        setLED(255);  // Solid LED in prog mode
        Serial.println();
        Serial.println(">>> Entered programming mode");
        printHelp();
        if (displayOK) showProgModeScreen();
        return;
    }

    // Commands only in programming mode
    if (!programmingMode) {
        Serial.println("Not in programming mode. Send PROG first.");
        return;
    }

    if (cmdUpper == "EXIT") {
        programmingMode = false;
        waitingForCard = false;
        pendingCardName = "";
        pendingCardClue = "";
        setLED(0);
        Serial.println(">>> Exited programming mode");
        if (displayOK) showStatus();
        return;
    }

    if (cmdUpper == "SCAN") {
        waitingForCard = true;
        pendingCardName = "";
        pendingCardClue = "";
        Serial.println("Scan a card to see its UID...");
        if (displayOK) {
            display.clearDisplay();
            display.setCursor(0, 0);
            display.println("=== SCAN MODE ===");
            display.println();
            display.println("Present card to");
            display.println("read UID...");
            display.display();
        }
        return;
    }

    if (cmdUpper.startsWith("ADD ")) {
        String params = cmd.substring(4);
        int sep = params.indexOf('|');
        if (sep < 0) {
            Serial.println("ERROR: Format: ADD <name>|<clue>");
            return;
        }
        pendingCardName = params.substring(0, sep);
        pendingCardClue = params.substring(sep + 1);
        pendingCardName.trim();
        pendingCardClue.trim();

        if (pendingCardName.length() == 0 || pendingCardClue.length() == 0) {
            Serial.println("ERROR: Name and clue cannot be empty");
            pendingCardName = "";
            pendingCardClue = "";
            return;
        }

        waitingForCard = true;
        Serial.print("Scan card to add as '");
        Serial.print(pendingCardName);
        Serial.println("'...");
        if (displayOK) showProgModeScreen();
        return;
    }

    Serial.print("Unknown command: ");
    Serial.println(cmd);
    Serial.println("Type HELP for available commands");
}

/**
 * @brief Handle card scan in programming mode
 *
 * Writes quest data directly to the MIFARE card.
 */
void handleProgModeCard() {
    char uidStr[24];
    formatUID(rfid.uid.uidByte, rfid.uid.size, uidStr);

    if (pendingCardName.length() > 0) {
        // Writing quest data to card
        Serial.print("Writing to card ");
        Serial.print(uidStr);
        Serial.println("...");

        if (writeQuestToCard(pendingCardName.c_str(), pendingCardClue.c_str())) {
            Serial.print("OK: Card programmed as '");
            Serial.print(pendingCardName);
            Serial.println("'");
            beep(880, 100);
            beep(1109, 150);

            if (displayOK) {
                display.clearDisplay();
                display.setCursor(0, 0);
                display.println("CARD PROGRAMMED!");
                display.println();
                display.println(pendingCardName);
                display.display();
                delay(1500);
            }
        } else {
            Serial.println("ERROR: Failed to write to card");
            beep(220, 300);  // Low tone for error

            if (displayOK) {
                display.clearDisplay();
                display.setCursor(0, 0);
                display.println("WRITE FAILED!");
                display.println();
                display.println("Check card type");
                display.display();
                delay(1500);
            }
        }

        pendingCardName = "";
        pendingCardClue = "";
        waitingForCard = false;
    } else {
        // Just scanning - read and display card contents
        Serial.print("Card UID: ");
        Serial.println(uidStr);

        char name[33];
        char clue[145];
        if (readQuestFromCard(name, clue)) {
            Serial.print("  Name: ");
            Serial.println(name);
            Serial.print("  Clue: ");
            Serial.println(clue);

            if (displayOK) {
                display.clearDisplay();
                display.setCursor(0, 0);
                display.println("CARD CONTENTS:");
                display.println(name);
                display.println("----------------");
                display.println(clue);
                display.display();
                delay(2000);
            }
        } else {
            Serial.println("  -> Blank or unreadable card");

            if (displayOK) {
                display.clearDisplay();
                display.setCursor(0, 0);
                display.println("BLANK CARD");
                display.println();
                display.println("UID:");
                display.println(uidStr);
                display.display();
                delay(1500);
            }
        }

        beep(1000, 100);
        waitingForCard = false;
    }

    if (displayOK) showProgModeScreen();
}

/**
 * @brief Check serial port for incoming commands
 *
 * Echoes characters back so user can see what they're typing.
 */
void checkSerial() {
    while (Serial.available()) {
        resetActivity();
        char c = Serial.read();
        if (c == '\n' || c == '\r') {
            Serial.println();  // Echo newline
            if (serialBuffer.length() > 0) {
                processCommand(serialBuffer);
                serialBuffer = "";
            }
            Serial.print("> ");  // Prompt for next command
        } else if (c == 8 || c == 127) {
            // Handle backspace
            if (serialBuffer.length() > 0) {
                serialBuffer.remove(serialBuffer.length() - 1);
                Serial.print("\b \b");  // Erase character on terminal
            }
        } else {
            serialBuffer += c;
            Serial.print(c);  // Echo character
        }
    }
}

void setup() {
    // Initialize serial
    Serial.begin(115200);
    delay(1000);  // Give time for USB CDC to initialize

    Serial.println();
    Serial.println("================================");
    Serial.println("  Minecraft Orb - Hello World");
    Serial.println("================================");
    Serial.println();

    // Initialize components
    initLED();
    initBuzzer();
    initTouch();
    displayOK = initDisplay();
    rfidOK = initRFID();
    initCardDatabase();
    // wifiOK = initWiFi();  // Disabled - not needed for now

    // Play startup sound
    playStartupSound();

    Serial.println();
    Serial.println("Initialization complete!");
    Serial.println("Type HELP for commands, PROG to program cards");
    Serial.println();
    Serial.print("> ");

    // Show auth screen on display (auth required at startup)
    if (displayOK) {
        delay(1000);
        showAuthScreen();
    }

    Serial.println("Authentication required - scan AUTH cards in order");

    // Start activity timer for power management
    lastActivityTime = millis();
}

void loop() {
    // Check serial for programming commands
    checkSerial();

    // Slow breathing LED as heartbeat (only when not in programming mode or auth mode)
    if (!programmingMode && !authMode) {
        breatheLED();
    }

    // Check for RFID cards
    checkRFID();

    // Check touch sensor (wake display on touch)
    checkTouch();

    // Power management (display timeout + light sleep)
    checkPowerSave();

    // Small delay to prevent overwhelming the CPU
    delay(50);
}
