#include "starcode.h"
#include <vector>
#include <algorithm>
#include <string.h>
#include <stdint.h>
#include <esp_log.h>
#include "drivers/bottom_half.h"
#include "char_lcd.h"
#include "esp_timer.h"

static const char* TAG = "star_code";

volatile bool handling_new_starcodes = false;
static volatile bool system_initialized = false;

// TODO: use the semaphore, convert to RWLock?
static volatile SemaphoreHandle_t star_codes_sem;
static std::vector<StarCodeEntry> star_codes;

static const char EMPTY_STAR_CODE_HEADER[] = "          ";

esp_timer_handle_t starcode_delay_timer;
/// @brief `true` if we are delaying for a starcode
static volatile bool delaying_for_starcode;
static volatile StarCodeEntry* current_starcode = nullptr;
/// @brief `true` when we are handling user input for a starcode
static volatile bool doing_starcode = false;
static uint16_t starcode_waiting_on_release;
static char current[STARCODE_MAX_LEN + 1];
static size_t current_idx;

static void starcode_trigger_cb(void* arg) {
    (void) arg;

    delaying_for_starcode = false;

    if (current_starcode != nullptr) {
        if (current_starcode->triggered_sem != nullptr)
            xSemaphoreGive(current_starcode->triggered_sem);
        if (current_starcode->callback != nullptr) 
            (current_starcode->callback)();
        
        current_starcode = nullptr;
    }

    // TODO: rename star code everywhere to starcode
    lcd_print_header();
}


void star_code_handle_keypad(uint16_t* just_pressed, uint16_t* just_released) {
    if ((!delaying_for_starcode) && handling_new_starcodes &&  (*just_pressed & (1 << KeypadKey::star))) {
        current_idx = 0;
        current[current_idx] = '\0';
        doing_starcode = true;
    }
    if (doing_starcode) {
        // If we get a press while handling a starcode, we also want to capture the release of that key.
        starcode_waiting_on_release |= *just_pressed;

        KeypadKey key;
        while (take_key(&key, just_pressed)) {
            if (key == KeypadKey::star) {
                current_idx = 0;
                current[current_idx] = '\0';
            } else if (key == KeypadKey::pound) {
                doing_starcode = false;
                if (current_idx != 0) {
                    trigger_star_code(current);
                }
            } else {
                // shift the digits left if neccesary
                if (current_idx >= STARCODE_MAX_LEN) {
                    for (int i = 1; i < current_idx; i++) {
                        current[i-1] = current[i];
                    }
                    current_idx--;
                }
                // append the character
                current[current_idx++] = char_of_keypad_key(key);
                current[current_idx] = '\0';
            }
            lcd_print_header_star_code();
        }
    }

    // capture any releases from starcodes
    uint16_t new_just_released = (*just_released) & (~starcode_waiting_on_release);
    starcode_waiting_on_release = starcode_waiting_on_release & (~*just_released);
    *just_released = new_just_released;
}

void init_star_code_system() {
    star_codes_sem = xSemaphoreCreateBinary();
    xSemaphoreGive(star_codes_sem);

    const esp_timer_create_args_t timer_args = {
        .callback = &starcode_trigger_cb,
        .arg = nullptr,
        .dispatch_method = ESP_TIMER_TASK,
        .name = "starcode_trigger",
        .skip_unhandled_events = false,
    };
    
    ESP_ERROR_CHECK(esp_timer_create(&timer_args, &starcode_delay_timer));

    handling_new_starcodes = true;
    system_initialized = true;
}

/// Checks if a triggered code matches an expected code.
/// @return true iff the codes match, where '*'s in the expected code can match any character in the triggered code
static bool check_code_match(const char* triggered, const char* expected) {
    size_t triggered_len = strlen(triggered);
    size_t match_len = strlen(triggered);

    if (triggered_len != match_len) 
        return false;

    for (int i = 0; i < triggered_len; i++) {
        if (!(expected[i] == '*' || expected[i] == triggered[i])) {
            return false;
        }
    }

    return true;
}

bool add_star_code(StarCodeEntry code) {
    ESP_LOGI(TAG, "Adding starcode: %s", code.code);
    if (code.code == nullptr || strlen(code.code) > STARCODE_MAX_LEN) {
        ESP_LOGW(TAG, "invalid code");
        return false;
    }
    if (code.display_text != nullptr && strlen(code.display_text) > STARCODE_DISPLAY_TEXT_MAX_LEN) {
        ESP_LOGW(TAG, "invalid display_text");
        return false;
    }

    // check for a existing entry
    auto it = std::find_if(star_codes.begin(), star_codes.end(), [&](const StarCodeEntry& other) {
        return check_code_match(code.code, other.code);
    });

    if (it != star_codes.end()) {
        // existing star code found!
        ESP_LOGW(TAG, "Duplicate starcode %s", code.code);
        return false;
    }

    star_codes.push_back(code);
    return true;
}

bool add_star_codes(const StarCodeEntry* codes, size_t len) {
    bool success = true;
    for (int i = 0; i < len; i++) {
        if (!add_star_code(codes[i])) {
            success = false;
        }
    }
    return success;
}

bool rm_star_code(const char* code) {
    ESP_LOGI(TAG, "Removing starcode: %s", code);

    auto it = std::find_if(star_codes.begin(), star_codes.end(), [&](const StarCodeEntry& star_code) {
        return strcmp(code, star_code.code) == 0;
    });
    
    if (it == star_codes.end()) {
        ESP_LOGW(TAG, "Failed to remove star code %s", code);
        return false;
    }

    star_codes.erase(it);
    return true;
}

bool rm_star_codes(const StarCodeEntry* codes, size_t len) {
    bool success = true;
    for (int i = 0; i < len; i++) {
        if (!rm_star_code(codes[i].code)) {
            success = false;
        }
    }
    return success;
}

bool rm_star_codes_str(const char** codes, size_t len) {
    bool success = true;
    for (int i = 0; i < len; i++) {
        if (!rm_star_code(codes[i])) {
            success = false;
        }
    }
    return success;
}

void clear_star_codes() {
    star_codes.clear();
}

bool trigger_star_code(const char* code) {
    auto it = std::find_if(star_codes.begin(), star_codes.end(), [&](const StarCodeEntry& other) {
        return check_code_match(code, other.code);
    });

    uint64_t delay_us = 2'000'000;
    delaying_for_starcode = true;
    if (it != star_codes.end()) {
        current_starcode = &*it;
        delay_us = current_starcode->delay_us;
    }

    ESP_ERROR_CHECK(esp_timer_start_once(starcode_delay_timer, delay_us));

    return current_starcode != nullptr;
}

void pause_star_code_system() {
    doing_starcode = false;
    handling_new_starcodes = false;
}

void resume_star_code_system() {
    handling_new_starcodes = system_initialized;
}

void lcd_print_header_star_code() {
    if (!lcd_header_enabled()) return;

    // TODO: consider upping the display text size to be able to overwrite the game_state area.
    if (delaying_for_starcode) {
        if (current_starcode == nullptr) {
            lcd_print(0, 0, "Invalid starcode    ");
        } else if (current_starcode->display_text != nullptr) {
            char buf[21];
            snprintf(buf, sizeof(buf), "%-20s", current_starcode->display_text);
            lcd_print(0, 0, buf);
        } else {
            lcd_print(0, 0, EMPTY_STAR_CODE_HEADER);
        }
    } else if (doing_starcode) {
        char buf[STARCODE_MAX_LEN + 2];
        snprintf(buf, sizeof(buf), "*%-9s", current);
        lcd_print(0, 0, buf);
    } else {
        lcd_print(0, 0, EMPTY_STAR_CODE_HEADER);
    }
}

bool lcd_starcode_displaying_result() {
    return delaying_for_starcode;
}
