#include <WiFi.h>
// #include "ESPAsyncWebSrv.h" //Wokwi
// #include <ESPAsyncWebServer.h> //NodeMCU
#include <ArduinoJson.h>
#include <Wire.h>
#include <LiquidCrystal.h>
#include <Keypad.h>
#include <Arduino.h>
// #include <Tone32.h>
#include <functional>

struct ResultDefusedBomb
{
    bool gameFinished;
    String teamName;
};

// Estrutura para armazenar as mensagens carregadas do arquivo JSON
struct Messages
{
    const char *pageInit = "Pagina Inicial";
    const char *sabotage = "Sabotagem";
    const char *domination = "Dominacao";
    const char *searchDestroy = "Busca";
    const char *startingGame = "Iniciando...";
    const char *canceled = "Cancelado";
};

struct MenuOption
{
    const char *message;
    void (*function)();
    int navigationIcon;
};

// Function scopes
void startSabotageGame();
void startDominationGame();
void startSearchDestroyGame();
void beep(unsigned int frequency, unsigned long duration = 0);
void noBeep();
void showRemainingTime(unsigned long totalGameDuration, unsigned long startTime);
void difusedBomb(int stepTime, ResultDefusedBomb &result);
void bombExploded(ResultDefusedBomb &result);
void updatePasswordDisplay(String &inputPassword, char &key);
bool checkPassword(String &inputPassword);
void handleIncorrectPassword(String &inputPassword, bool &defuseBomb, int &remainingDefuseTime);
ResultDefusedBomb handleDefuseBomb(unsigned long startTime, unsigned long totalGameDuration, int stepTime);
void passwordDisplay();
void splashScreen();
String generateRandomPassword();
void displayIPAndPassword();
void displayMenu(int currentMenu);
void handleKeypadForMenu();
void handleGameFinished(String teamName);
void deleteLastDigit(String &inputPassword);
void startGame(String gameName);
void playAgain(std::function<void()> func);
void disableInput();

// Pins
uint8_t BEEP = 22;
uint8_t RELAY = 2;
uint8_t LED_SUCCESS = 16;
uint8_t LED_FAILURE = 17;

uint8_t LCD_LENGTH = 16;

// Defina as credenciais de WiFi
// const char *ssid = "AirBombAjotark";
// const char *password = "ajotark";

// Crie uma instância do servidor
// AsyncWebServer server(80);

// Crie uma instância do display 16x2 (endereço I2C 0x27)
LiquidCrystal lcd(19, 23, 18, 21, 5, 15);

// Variável global de configurações
// JsonDocument configDoc;

// Variável para armazenar a senha gerada
String generatedPassword = "";

// Variável para armazenar se a senha já foi validada
bool isAuthenticated = true;

// Configuração do teclado 4x4
const byte ROWS = 4; // quatro linhas
const byte COLS = 4; // quatro colunas

char keys[ROWS][COLS] = {
    {'1', '2', '3', 'A'},
    {'4', '5', '6', 'B'},
    {'7', '8', '9', 'C'},
    {'*', '0', '#', 'D'}};

byte rowPins[ROWS] = {13, 12, 14, 27};
byte colPins[COLS] = {26, 25, 33, 32};

Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);

Messages messages;
MenuOption menuOptions[] = {
    {messages.pageInit, nullptr, 0},
    {messages.searchDestroy, startSearchDestroyGame, 1},
    {messages.sabotage, startSabotageGame, 1},
    {messages.domination, startDominationGame, 2},
};

JsonDocument configDoc;

// Configurações do jogo
struct GameConfig
{
    int gameDuration = 30;            // 3600;       // Duração do jogo em segundos
    int timeToArmBomb = 5;            // Tempo para armar a bomba em segundos
    int timeToDefuseBomb = 15;        // Tempo para desarmar a bomba em segundos
    int incorrectPasswordPenalty = 5; // Penalização de senha incorreta
    int timeToActiveRelay = 10;       // Tempo para ativar o rele em segundos
    String password = "12345";        // Senha para desarmar a bomba
    bool needPassword = true;         // Se é necessário senha para desarmar
    bool useSound = true;             // Interação com som
    bool useLED = true;               // Interação com LED
    bool useRelay = true;             // Interação com relé

    String specialForcesName = "SpecialForces"; // Nome da equipe Special Forces
    String terroristsName = "Terrorists";       // Nome da equipe Terrorists
};

GameConfig gameConfig;

byte ARROW_DOWN[8] = {B00000, B00000, B11111, B11111, B01110, B00100, B00000, B00000};
byte ARROW_UP[8] = {B00000, B00000, B00100, B01110, B11111, B11111, B00000, B00000};
byte LOADING[8] = {B11111, B11111, B11111, B11111, B11111, B11111, B11111, B11111};

void setup()
{
    Serial.begin(115200);

    pinMode(LED_SUCCESS, OUTPUT);
    pinMode(LED_FAILURE, OUTPUT);
    pinMode(RELAY, OUTPUT);

    lcd.begin(LCD_LENGTH, 2);

    lcd.createChar(0, ARROW_DOWN);
    lcd.createChar(1, ARROW_UP);
    lcd.createChar(2, LOADING);

    splashScreen();

    generatedPassword = generateRandomPassword();

    displayIPAndPassword();

    if (isAuthenticated)
    {
        displayMenu(0);
    }
}

void loop()
{
    handleKeypadForMenu();
}

// Função para exibir o menu com a seta e o jogo selecionado
void displayMenu(int currentMenu)
{
    lcd.clear();

    // Exibe o jogo selecionado
    lcd.setCursor(0, 0);
    lcd.print(menuOptions[currentMenu].message);

    // Exibe as setas de navegação
    if (menuOptions[currentMenu].navigationIcon == 0)
    {
        lcd.setCursor(15, 1);
        lcd.write(byte(0));
    }
    else if (menuOptions[currentMenu].navigationIcon == 1)
    {
        lcd.setCursor(15, 0);
        lcd.write(byte(1));
        lcd.setCursor(15, 1);
        lcd.write(byte(0));
    }
    else if (menuOptions[currentMenu].navigationIcon == 2)
    {
        lcd.setCursor(15, 0);
        lcd.write(byte(1));
    }
}

// Função para exibir o IP e a senha gerada no display
void displayIPAndPassword()
{
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("IP:");
    // lcd.print(WiFi.localIP());
    lcd.setCursor(0, 1);
    lcd.print("Senha:");
    lcd.print(generatedPassword);
    delay(2000);
}

// Função para rolar o texto no display (marquee effect) com "loop" contínuo
void scrollText(const char *message, int row, int steps = 1, int delayTime = 200)
{
    int messageLen = strlen(message);

    for (int i = 0; i < steps; i++)
    {
        for (int i = 0; i < messageLen - LCD_LENGTH + 1; i++)
        {
            lcd.setCursor(0, row);
            for (int j = 0; j < LCD_LENGTH; j++)
            {
                lcd.print(message[i + j]);
            }

            delay(delayTime);
        }

        for (int i = messageLen - LCD_LENGTH; i >= 0; i--)
        {
            lcd.setCursor(0, row);
            for (int j = 0; j < LCD_LENGTH; j++)
            {
                if (i + j < messageLen)
                {
                    lcd.print(message[i + j]);
                }
                else
                {
                    lcd.print(message[i + j - messageLen]);
                }
            }

            delay(delayTime);
        }
    }
}

// Função para exibir informações no display
void display(int row, const char *message, bool center = false, bool clear = false)
{
    if (clear)
    {
        lcd.clear();
    }

    if (strlen(message) > LCD_LENGTH)
    {
        scrollText(message, row);
    }
    else
    {
        if (center)
        {
            int len = strlen(message);
            int pad = (LCD_LENGTH - len) / 2;
            lcd.setCursor(pad, row);
        }
        else
        {
            lcd.setCursor(0, row);
        }

        lcd.print(message);
    }
}

void splashScreen()
{
    display(0, "AIRBOMB V1", true, true);
    display(1, "AIRBOMB V1 - @ajotanc");
}

// Função para gerar uma senha aleatória de 4 dígitos
String generateRandomPassword()
{
    String password = "";
    for (int i = 0; i < 4; i++)
    {
        password += String(random(0, 10)); // Gera um número aleatório entre 0 e 9
    }
    return password;
}

// Função para tocar o beep
void playbeep()
{
    tone(BEEP, 1000); // Frequência do som
    delay(100);       // Duração
                      // noTone(BEEP);
}

// Funções para iniciar os modos de jogo (exemplo)
void startSabotageGame()
{
    // Lógica para iniciar o modo Sabotage
}

void startDominationGame()
{
    // Lógica para iniciar o modo Domination
}

void loading(int stepTime, bool reverse = false)
{
    lcd.setCursor(0, 1);

    if (!reverse)
    {
        // Preenche a tela com os caracteres
        for (int i = 0; i < LCD_LENGTH; i++)
        {
            lcd.write(2); // Adiciona o caractere de loading
            delay(stepTime);
        }
    }
    else
    {
        // Preenche a tela com os caracteres
        for (int i = 0; i < LCD_LENGTH; i++)
        {
            lcd.write(2); // Adiciona o caractere de loading
        }

        // Remove os caracteres do final para o início
        for (int i = LCD_LENGTH; i >= 0; i--)
        {
            lcd.setCursor(i, 1); // Volta para o início da linha
            lcd.write(' ');      // Substitui pelo espaço
            delay(stepTime);     // Espera um pouco antes de remover o próximo
        }
    }
}

void displayTime(unsigned long milliseconds)
{
    // Cálculos
    unsigned long hours = milliseconds / 3600000;
    milliseconds %= 3600000;
    unsigned long minutes = milliseconds / 60000;
    milliseconds %= 60000;
    unsigned long seconds = milliseconds / 1000;
    unsigned long ms = milliseconds % 1000;

    // Formatação e exibição
    char buffer[13];
    sprintf(buffer, "%02lu:%02lu:%02lu:%03lu", hours, minutes, seconds, ms);
    display(1, buffer, true); // Exibe a mensagem na linha 1
}
// Função para iniciar o modo "Busca" (Search & Destroy)
void startSearchDestroyGame()
{
    startGame("Busca e Destruicao");

    display(0, "Jogo Iniciado", true, true);

    unsigned long startTime = millis();
    unsigned long elapsedTime = 0;
    bool plantedBomb = false;

    int totalGameDuration = gameConfig.gameDuration * 1000; // Em milissegundos
    int totalArmTime = gameConfig.timeToArmBomb * 1000;     // Em milissegundos
    int stepTime = totalArmTime / LCD_LENGTH;               // Tempo dividido pelos 16 caracteres do display

    ResultDefusedBomb result;

    while (elapsedTime < totalGameDuration)
    {
        elapsedTime = millis() - startTime;
        int remainingGameTime = totalGameDuration - elapsedTime;

        char key = keypad.getKey();

        if (!plantedBomb)
        {
            displayTime(remainingGameTime); // Exibe o tempo restante do jogo

            if (key == '#')
            {
                display(0, "Armando Bomba...", true, true);
                loading(stepTime);
                display(0, "Bomba Armada!", true, true);
                plantedBomb = true;
            }
        }
        else
        {
            result = handleDefuseBomb(startTime, totalGameDuration, stepTime);
            if (result.gameFinished)
            {
                break;
            }
        }
    }

    if (!result.gameFinished)
    {
        display(0, "Tempo Esgotado!", true, true);
        delay(2000);
    }
    else
    {
        handleGameFinished(result.teamName);
    }
}

void handleGameFinished(String teamName)
{
    display(0, "Fim de Jogo!", true, true);

    char mensagem[50];
    sprintf(mensagem, "O time %s ganhou!", teamName.c_str());

    scrollText(mensagem, 1);
    playAgain(startSearchDestroyGame);
}

ResultDefusedBomb handleDefuseBomb(unsigned long startTime, unsigned long totalGameDuration, int stepTime)
{
    unsigned long defuseStartTime = millis();
    unsigned long defuseElapsedTime = 0;
    int remainingDefuseTime = gameConfig.timeToDefuseBomb * 1000; // Tempo para desarmar
    bool defuseBomb = false;
    String inputPassword = "";

    ResultDefusedBomb result;
    result.gameFinished = false;

    display(0, "Desarme a Bomba:", true, true);

    while (defuseElapsedTime < remainingDefuseTime)
    {
        defuseElapsedTime = millis() - defuseStartTime;
        char key = keypad.getKey();

        if (defuseElapsedTime >= remainingDefuseTime)
        {
            bombExploded(result);
            return result;
        }

        if (!defuseBomb)
        {
            displayTime(remainingDefuseTime - defuseElapsedTime);
        }

        if (defuseBomb && gameConfig.needPassword)
        {
            if (isDigit(key))
            {
                updatePasswordDisplay(inputPassword, key);
            }
            else if (key == 'C')
            {
                deleteLastDigit(inputPassword);
            }
            else if (inputPassword.length() == gameConfig.password.length())
            {
                if (checkPassword(inputPassword))
                {
                    difusedBomb(stepTime, result);
                    return result;
                }
                else
                {
                    handleIncorrectPassword(inputPassword, defuseBomb, remainingDefuseTime);
                }
            }
        }
        else if (!defuseBomb)
        {
            if (key == '*')
            {
                showRemainingTime(totalGameDuration, startTime);
            }
            else if (key == 'D')
            {
                defuseBomb = true;

                if (gameConfig.needPassword)
                {
                    passwordDisplay();
                }
                else
                {
                    difusedBomb(stepTime, result);
                    return result;
                }
            }
        }

        if (remainingDefuseTime - defuseElapsedTime <= gameConfig.timeToActiveRelay * 1000 && gameConfig.useRelay)
        {
            digitalWrite(RELAY, HIGH);
        }

        if (defuseElapsedTime % 1000 < 50)
        {
            if (gameConfig.useLED)
            {
                digitalWrite(LED_FAILURE, HIGH);
            }

            beep(1000);
        }
        else
        {
            if (gameConfig.useLED)
            {
                digitalWrite(LED_FAILURE, LOW);
            }

            noBeep();
        }
    }

    noBeep(); // Para o som se não foi desarmada

    digitalWrite(RELAY, LOW);       // Desliga o relé
    digitalWrite(LED_FAILURE, LOW); // Apaga o LED vermelho
    digitalWrite(LED_SUCCESS, LOW); // Apaga o LED verde

    return result; // Se não foi desarmada, volta para o loop principal
}

void deleteLastDigit(String &inputPassword)
{
    int passwordLength = inputPassword.length();

    if (passwordLength > 0)
    {
        inputPassword.remove(passwordLength - 1);
        passwordLength = inputPassword.length();

        lcd.setCursor(passwordLength - 1, 1);
        lcd.print(inputPassword[passwordLength - 1]);

        lcd.setCursor(passwordLength, 1);
        lcd.print('*');
        lcd.setCursor(passwordLength, 1);
        lcd.blink();
        lcd.cursor();
    }
}

void disableInput()
{
    lcd.noCursor();
    lcd.noBlink();
}

void bombExploded(ResultDefusedBomb &result)
{
    disableInput();

    beep(1500, 500);

    if (gameConfig.useLED)
    {
        for (int i = 0; i < 10; i++)
        {
            digitalWrite(LED_FAILURE, HIGH);
            delay(100);
            digitalWrite(LED_FAILURE, LOW);
            delay(100);
        }
    }

    if (gameConfig.useRelay)
    {
        digitalWrite(RELAY, LOW);
    }

    display(0, "Bomba explodiu!", true, true);
    display(1, "Bomba explodiu!", true);

    result.gameFinished = true;
    result.teamName = gameConfig.terroristsName;

    delay(2000);
}

void beep(unsigned int frequency, unsigned long duration)
{
    if (gameConfig.useSound)
    {
        tone(BEEP, frequency, duration);
    }
}

void noBeep()
{
    if (gameConfig.useSound)
    {
        noTone(BEEP);
    }
}

void showRemainingTime(unsigned long totalGameDuration, unsigned long startTime)
{
    display(0, "Tempo Restante:", true, true);
    unsigned long endTime = millis() + 2000;
    unsigned long lastUpdateTime = 0;

    while (millis() < endTime)
    {
        if (millis() - lastUpdateTime >= 50)
        {
            unsigned long remainingTime = totalGameDuration - (millis() - startTime);
            displayTime(remainingTime);
            lastUpdateTime = millis();
        }
    }

    display(0, "Desarme a Bomba:", true, true);
}

bool checkPassword(String &inputPassword)
{
    disableInput();
    return inputPassword == gameConfig.password;
}

void handleIncorrectPassword(String &inputPassword, bool &defuseBomb, int &remainingDefuseTime)
{
    disableInput();

    display(0, "Senha Incorreta.", true, true);
    display(1, "Tente Novamente!", true);
    beep(2000, 500);

    delay(200);

    remainingDefuseTime -= gameConfig.incorrectPasswordPenalty * 1000; // Penalidade de 5 segundos
    inputPassword = "";                                                // Limpa o campo da senha

    display(0, "Desarme a Bomba:", true, true);
    defuseBomb = false;
}

void passwordDisplay()
{
    display(0, "Senha:", true, true);

    lcd.setCursor(0, 1);
    for (int len = 0; len < gameConfig.password.length(); len++)
    {
        lcd.print('*');
    }

    lcd.setCursor(0, 1);
    lcd.blink(); // Ativa o piscar do cursor
    lcd.cursor();
}

void updatePasswordDisplay(String &inputPassword, char &key)
{
    if (inputPassword.length() < gameConfig.password.length())
    {
        inputPassword += key;

        lcd.setCursor(inputPassword.length() - 1, 1);
        lcd.print(key);

        for (int i = 0; i < inputPassword.length() - 1; i++)
        {
            lcd.setCursor(i, 1);
            lcd.print('*');
        }

        lcd.setCursor(inputPassword.length(), 1);
        lcd.blink();
        lcd.cursor();
    }
}

void difusedBomb(int stepTime, ResultDefusedBomb &result)
{
    display(0, "Bomba Desarmada!", true, true);
    loading(stepTime, true);

    if (gameConfig.useLED)
    {
        digitalWrite(LED_FAILURE, LOW);  // Apaga o LED vermelho
        digitalWrite(LED_SUCCESS, HIGH); // Liga o LED verde
    }

    beep(2000, 500); // Som de desarme

    if (gameConfig.useRelay)
    {
        digitalWrite(RELAY, LOW);
    }

    result.gameFinished = true;
    result.teamName = gameConfig.specialForcesName;
}

/*
  playAgain(minhaFuncao);
  playAgain([]() { Serial.println("Olá, mundo!"); });
  playAgain(std::bind(minhaOutraFuncao, 5, 10));

  void minhaFuncao() {
    Serial.println("Olá, mundo!");
  }

  void minhaOutraFuncao(int x, int y) {
    Serial.println("Olá, mundo! x = " + String(x) + ", y = " + String(y));
  }
*/
void playAgain(std::function<void()> func)
{
    display(0, "Jogar novamente?", true, true);
    display(1, "D: Sim / C: Nao", true);

    char key = keypad.getKey();

    if (key == 'D')
    {
        func();
    }
    else if (key == 'C')
    {
        displayMenu(0);
    }
}

void startGame(String gameName)
{
    if (gameConfig.useLED)
    {
        digitalWrite(LED_FAILURE, LOW);
        digitalWrite(LED_SUCCESS, LOW);
    }

    if (gameConfig.useRelay)
    {
        digitalWrite(RELAY, LOW);
    }

    if (gameConfig.useSound)
    {
        noBeep();
    }

    display(0, gameName.c_str(), true, true);
    scrollText("Pressione qualquer tecla para comecar", 1, 1, 100);

    keypad.waitForKey();
}

void handleKeypadForMenu()
{
    char key = keypad.getKey();
    static int currentMenu = 0;

    if (key)
    {
        tone(BEEP, 2400, 30); // Toca o beep

        switch (key)
        {
        case 'A': // Mover para cima
            if (currentMenu > 0)
            {
                currentMenu--;
                displayMenu(currentMenu);
            }
            break;
        case 'B': // Mover para baixo
            if (currentMenu < (sizeof(menuOptions) / sizeof(menuOptions[0])) - 1)
            {
                currentMenu++;
                displayMenu(currentMenu);
            }
            break;
        case 'C': // Cancelar
            displayMenu(0);
            break;
        case 'D': // Confirmar seleção
            if (menuOptions[currentMenu].function != nullptr)
            {
                lcd.clear();
                delay(100);

                menuOptions[currentMenu].function();
            }
            break;
        }

        Serial.println(currentMenu);
    }
}
NOCOMNCVCCGNDINLED1PWRRelay Module