#include "stdio.h"
#include <sys/stat.h>
#include <dirent.h>
#include "string.h"
#include "model_path.h"
#include "esp_wn_models.h"
#ifndef CONFIG_IDF_TARGET_ESP32P4
#include "esp_mn_models.h"
#endif

#ifdef ESP_PLATFORM
#include <sys/dirent.h>
#include "sdkconfig.h"
#include "esp_log.h"
#include "esp_spiffs.h"
#include "esp_idf_version.h"
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
#include "spi_flash_mmap.h"
#endif
#endif

static char *TAG = "MODEL_LOADER";
static char *SRMODE_BASE_PATH = "/srmodel";
static srmodel_list_t *static_srmodels = NULL;

void set_model_base_path(const char *base_path)
{
    SRMODE_BASE_PATH = (char *)base_path;
}

char *get_model_info(char *data, int size)
{
    char *model_info = NULL;
    //Prase
    //if the line starts with '#', the line is a comment
    //else the line is model information
    while(size > 0) {
        if (*data == '#') {
            while (*data != '\n' && size > 1) {
                data ++;
                size --;
            }
            data ++;
            size --;
            continue;
        } else if (data != NULL && size > 0) {
            model_info = (char*)malloc((size + 1) * sizeof(char));
            memcpy(model_info, data, size);
            if (model_info[size - 1] == '\n') {
                model_info[size - 1] = '\0';
            }
            model_info[size] = '\0';
            break;
        }
    }

    return model_info;
}

char *get_wake_words_from_info(char *model_info)
{
    if (model_info == NULL)
        return NULL;

    int info_len = strlen(model_info);
    char *temp = (char *) malloc(info_len + 1);
    memcpy(temp, model_info, info_len);
    temp[info_len] = '\0';
    char *token = strtok(temp, "_");
    char *wake_words = NULL;
    int word_len = 0;
    int i = 1;
    while (token != NULL) {
        if (i % 4 == 3) {
            // find all valid wake word token
            if (wake_words == NULL) {
                word_len = strlen(token) + 1;
                wake_words = (char*) malloc(word_len);
                memcpy(wake_words, token, word_len-1);
                wake_words[word_len-1] = '\0';
            } else {
                word_len += strlen(token) + 1;
                wake_words = (char*) realloc(wake_words, word_len);
                strcat(wake_words, ";");
                strcat(wake_words, token);
                wake_words[word_len-1] = '\0';
            }
        }
        token = strtok(NULL, "_");
        i++;
    }

    free(temp);
    return wake_words;
}

static srmodel_list_t *srmodel_list_alloc(void)
{
    srmodel_list_t *models = (srmodel_list_t *) malloc(sizeof(srmodel_list_t));
    models->model_data = NULL;
    models->model_name = NULL;
    models->model_info = NULL;
    models->num = 0;
#ifdef ESP_PLATFORM
    models->partition = NULL;
#endif
    models->mmap_handle = NULL;

    return models;
}

#ifdef ESP_PLATFORM
srmodel_list_t *read_models_form_spiffs(esp_vfs_spiffs_conf_t *conf)
{
    if (static_srmodels == NULL) {
        static_srmodels = srmodel_list_alloc();
    } else {
        return static_srmodels;
    }

    srmodel_list_t *models = static_srmodels;

    struct dirent *ret;
    DIR *dir = NULL;
    dir = opendir(conf->base_path);
    int model_num = 0;
    int idx = 0;

    if (dir != NULL) {
        // get the number of models
        while ((ret = readdir(dir)) != NULL) {
            // NULL if reach the end of directory

            if (ret->d_type == DT_DIR) { // continue if d_type is not file
                continue;
            }

            int len = strlen(ret->d_name);
            char *suffix = ret->d_name + len - 12;

            if (strcmp(suffix, "_MODEL_INFO_") == 0) {
                model_num ++;
            }
        }

        // allocate model names
        if (model_num == 0) {
            return models;
        } else {
            models->num = model_num;
            models->model_name = malloc(models->num * sizeof(char *));
            for (int i = 0; i < models->num; i++) {
                models->model_name[i] = (char *) calloc(MODEL_NAME_MAX_LENGTH, sizeof(char));
            }
        }

        // read & save model names
        closedir(dir);
        dir = opendir(conf->base_path);
        while ((ret = readdir(dir)) != NULL) {
            // NULL if reach the end of directory

            if (ret->d_type == DT_DIR) { // continue if d_type is not file
                continue;
            }

            int len = strlen(ret->d_name);
            char *suffix = ret->d_name + len - 12;

            if (strcmp(suffix, "_MODEL_INFO_") == 0) {
                memcpy(models->model_name[idx], ret->d_name, (len - 13)*sizeof(char));
                // models->model_name[idx][len-13] = '\0';
                idx ++;
            }
        }
        closedir(dir);
        dir = NULL;

    }
    return models;
}


srmodel_list_t *srmodel_spiffs_init(const esp_partition_t *part)
{
    ESP_LOGI(TAG, "\nInitializing models from SPIFFS, partition label: %s\n", part->label);

    esp_vfs_spiffs_conf_t conf = {
        .base_path = SRMODE_BASE_PATH,
        .partition_label = part->label,
        .max_files = 5,
        .format_if_mount_failed = true
    };

    // Use settings defined above to initialize and mount SPIFFS filesystem.
    // Note: esp_vfs_spiffs_register is an all-in-one convenience function.
    esp_err_t ret = esp_vfs_spiffs_register(&conf);

    if (ret != ESP_OK) {
        if (ret == ESP_FAIL) {
            ESP_LOGE(TAG, "Failed to mount or format filesystem\n");
        } else if (ret == ESP_ERR_NOT_FOUND) {
            ESP_LOGE(TAG, "Failed to find SPIFFS partition\n");
        } else {
            ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)\n", esp_err_to_name(ret));
        }
        return NULL;
    }

    size_t total = 0, used = 0;
    ret = esp_spiffs_info(part->label, &total, &used);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)\n", esp_err_to_name(ret));
    } else {
        ESP_LOGI(TAG, "Partition size: total: %d, used: %d\n", total, used);
    }

    // Read all model from path
    srmodel_list_t *models = read_models_form_spiffs(&conf);
    models->partition = (esp_partition_t *)part;
    return models;
}


void srmodel_spiffs_deinit(srmodel_list_t *models)
{
    if (models->partition != NULL) {
        esp_err_t ret = esp_vfs_spiffs_unregister(models->partition->label);
        if (ret != ESP_OK) {
            ESP_LOGW(TAG, "Already unregistered\n");
        } else {
            ESP_LOGI(TAG, "%s has been unregistered\n", models->partition->label);
        }
    }

    if (models != NULL) {
        if (models->num > 0) {
            for (int i = 0; i < models->num; i++) {
                free(models->model_name[i]);
            }
            free(models->model_name);
        }
        free(models);
    }
    models = NULL;

}

#ifdef CONFIG_IDF_TARGET_ESP32
srmodel_list_t *srmodel_config_init()
{
    if (static_srmodels == NULL) {
        static_srmodels = srmodel_list_alloc();
    } else {
        return static_srmodels;
    }

    srmodel_list_t *models = static_srmodels;
    models->num = 2;
    models->model_name = malloc(models->num * sizeof(char *));
    models->model_info = malloc(models->num * sizeof(char *));
    for (int i = 0; i < models->num; i++) {
        models->model_name[i] = (char *) calloc(MODEL_NAME_MAX_LENGTH, sizeof(char));
        models->model_info[i] = NULL;
    }

    // If wakenet is selected in menuconfig, load the wakenet model
    if (strcmp(WAKENET_MODEL_NAME, "NULL") == 0) {
        models->num --;
        free(models->model_name[models->num]);
    } else {
        strcpy(models->model_name[0], WAKENET_MODEL_NAME);
    }

    // If multinet is selected in menuconfig, load the multinet model
    if (strcmp(MULTINET_MODEL_NAME, "NULL") == 0) {
        models->num --;
        free(models->model_name[models->num]);
    } else {
        strcpy(models->model_name[models->num - 1], MULTINET_MODEL_NAME);
    }

    // could not find any avaliable models, return NULL
    if (models->num == 0) {
        free(models->model_name);
        free(models);
        models = NULL;
    }

    return models;
}

void srmodel_config_deinit(srmodel_list_t *models)
{
    if (models != NULL) {
        if (models->num > 0) {
            for (int i = 0; i < models->num; i++) {
                free(models->model_name[i]);
                if (models->model_info[i] != NULL) {
                    free(models->model_info[i]);
                }
            }
            free(models->model_name);
            free(models->model_info);
        }
        free(models);
    }
    // models is static_srmodels
    static_srmodels = NULL;
}
#endif

model_coeff_getter_t *srmodel_get_model_coeff(char *model_name)
{
    model_coeff_getter_t *gettercb = (model_coeff_getter_t *)&WAKENET_COEFF;
    return gettercb;
}

static uint32_t read_int32(char *data)
{
    uint32_t value = 0;
    value |= data[0] <<  0;
    value |= data[1] <<  8;
    value |= data[2] << 16;
    value |= data[3] << 24;
    return value;
}

srmodel_list_t *srmodel_load(const void *root)
{
    if (static_srmodels == NULL) {
        static_srmodels = srmodel_list_alloc();
        printf("create static models");
    }

    srmodel_list_t *models = static_srmodels;
    char *start = (char *)root;
    char *data = (char *)root;
    int str_len = SRMODEL_STRING_LENGTH;
    int int_len = 4;
    //read model number
    models->num = read_int32(data);
    data += int_len;
    models->model_data = (srmodel_data_t **)malloc(sizeof(srmodel_data_t *) * models->num);
    models->model_name = (char **)malloc(sizeof(char *) * models->num);
    models->model_info = (char **)malloc(sizeof(char *) * models->num);

    for (int i = 0; i < models->num; i++) {
        srmodel_data_t *model_data = (srmodel_data_t *) malloc(sizeof(srmodel_data_t));
        models->model_info[i] = NULL;
        // read model name
        models->model_name[i] = (char *)malloc((strlen(data) + 1) * sizeof(char));
        strcpy(models->model_name[i], data);
        data += str_len;
        //read model number
        int file_num = read_int32(data);
        model_data->num = file_num;
        data += int_len;
        model_data->files = (char **) malloc(sizeof(char *)*file_num);
        model_data->data = (char **) malloc(sizeof(void *)*file_num);
        model_data->sizes = (int *) malloc(sizeof(int) * file_num);

        for (int j = 0; j < file_num; j++) {
            //read file name
            model_data->files[j] = data;
            data += str_len;
            //read file start index
            int index = read_int32(data);
            data += int_len;
            model_data->data[j] = start + index;
            //read file size
            int size = read_int32(data);
            data += int_len;
            model_data->sizes[j] = size;

            // read model info
            if (strcmp(model_data->files[j], "_MODEL_INFO_") == 0) {
                models->model_info[i] = get_model_info(model_data->data[j], model_data->sizes[j]);
            }
        }
        models->model_data[i] = model_data;
    }
    ESP_LOGI(TAG, "Successfully load srmodels");
    set_model_base_path(NULL);
    return models;
}

srmodel_list_t *srmodel_mmap_init(const esp_partition_t *partition)
{
    if (static_srmodels == NULL) {
        static_srmodels = srmodel_list_alloc();
    } else {
        return static_srmodels;
    }

    srmodel_list_t *models = static_srmodels;
    const void *root;

#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
    int free_pages = spi_flash_mmap_get_free_pages(ESP_PARTITION_MMAP_DATA);
    uint32_t storage_size = free_pages * 64 * 1024; // Byte
    ESP_LOGI(TAG, "The storage free size is %ld KB", storage_size / 1024);
    ESP_LOGI(TAG, "The partition size is %ld KB", partition->size / 1024);
    if (storage_size < partition->size) {
        ESP_LOGE(TAG, "The storage free size of this board is less than %s partition required size", partition->label);
    }
    models->mmap_handle = (esp_partition_mmap_handle_t*)malloc(sizeof(esp_partition_mmap_handle_t));
    ESP_ERROR_CHECK(esp_partition_mmap(partition, 0, partition->size, ESP_PARTITION_MMAP_DATA, &root, models->mmap_handle));
#else
    int free_pages = spi_flash_mmap_get_free_pages(SPI_FLASH_MMAP_DATA);
    uint32_t storage_size = free_pages * 64 * 1024; // Byte
    ESP_LOGI(TAG, "The storage free size is %d KB", storage_size / 1024);
    ESP_LOGI(TAG, "The partition size is %d KB", partition->size / 1024);
    if (storage_size < partition->size) {
        ESP_LOGE(TAG, "The storage free size of board is less than %s partition size", partition->label);
    }
    models->mmap_handle = (spi_flash_mmap_handle_t*)malloc(sizeof(spi_flash_mmap_handle_t));
    ESP_ERROR_CHECK(esp_partition_mmap(partition, 0, partition->size, SPI_FLASH_MMAP_DATA, &root, models->mmap_handle));
#endif
    

    models->partition = (esp_partition_t *)partition;
    srmodel_load(root);
    return models;
}

void srmodel_mmap_deinit(srmodel_list_t *models)
{
    if (models != NULL) {
        if (models->mmap_handle) {
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
            esp_partition_munmap(*(esp_partition_mmap_handle_t *)models->mmap_handle); // support esp-idf v5
#else
            spi_flash_munmap(*(spi_flash_mmap_handle_t *)models->mmap_handle);  // support esp-idf v4
#endif 
        }


        if (models->num > 0) {
            for (int i = 0; i < models->num; i++) {
                free(models->model_data[i]->files);
                free(models->model_data[i]->data);
                free(models->model_data[i]->sizes);
                free(models->model_data[i]);
                free(models->model_name[i]);
                if (models->model_info[i] != NULL)
                    free(models->model_info[i]);
            }
        }
        free(models->model_data);
        free(models->model_name);
        free(models->mmap_handle);
        free(models->model_info);
        free(models);
    }
    models = NULL;
    static_srmodels = NULL;
}

#endif

char *get_model_base_path(void)
{
    return SRMODE_BASE_PATH;
}

srmodel_list_t *get_static_srmodels(void)
{
    return static_srmodels;
}

static char *join_path(const char *dirname, const char *filename)
{
    if (dirname == NULL || filename == NULL) {
        return NULL;
    }
    int dirname_len = strlen(dirname);
    int filename_len = strlen(filename);
    int len = filename_len + dirname_len + 2;
    char *path = calloc(len, sizeof(char));
    memcpy(path, dirname, dirname_len);
    if (dirname[dirname_len - 1] == '/') {
        memcpy(path + dirname_len, filename, filename_len);
    } else {
        path[dirname_len] = '/';
        memcpy(path + dirname_len + 1, filename, filename_len);
    }
    return path;
}

// read srmodel from sdcard
srmodel_list_t *srmodel_sdcard_init(const char *base_path)
{
    printf("Initializing models from path: %s\n", base_path);
    if (static_srmodels == NULL) {
        static_srmodels = srmodel_list_alloc();
    } else {
        return static_srmodels;
    }

    srmodel_list_t *models = static_srmodels;
    set_model_base_path(base_path);

    struct dirent *ret;
    DIR *dir = NULL;
    dir = opendir(base_path);
    int model_num = 0;
    int idx = 0;
    FILE *fp;

    if (dir != NULL) {
        // get the number of models
        while ((ret = readdir(dir)) != NULL) {
            // NULL if reach the end of directory

            if (ret->d_type == DT_DIR) { // if d_type is directory
                char *sub_path = join_path(base_path, ret->d_name);
                char *info_file = join_path(sub_path, "_MODEL_INFO_");
                fp = fopen(info_file, "r");
                if (fp != NULL) {
                    model_num ++;  // If _MODLE_INFO_ file exists, model_num ++
                }
                printf("%s -> %s\n", sub_path, info_file);
                fclose(fp);
                free(sub_path);
                free(info_file);
            }
        }
        closedir(dir);

        // allocate srmodel_list_t struct
        if (model_num == 0) {
            return models;
        } else {
            models->num = model_num;
#ifdef ESP_PLATFORM
            models->partition = NULL;
#endif
            models->model_name = malloc(models->num * sizeof(char *));
            models->model_info = malloc(models->num * sizeof(char *));
            for (int i = 0; i < models->num; i++) {
                models->model_name[i] = (char *) calloc(MODEL_NAME_MAX_LENGTH, sizeof(char));
            }
        }


        // read & save model names
        dir = opendir(base_path);
        while ((ret = readdir(dir)) != NULL) {
            // NULL if reach the end of directory

            if (ret->d_type == DT_DIR) { // if d_type is directory
                char *sub_path = join_path(base_path, ret->d_name);
                char *info_file = join_path(sub_path, "_MODEL_INFO_");
                fp = fopen(info_file, "r");
                if (fp != NULL) {
                    memcpy(models->model_name[idx], ret->d_name, strlen(ret->d_name));
                    fseek(fp, 0L, SEEK_END);
                    int file_size = ftell(fp);
                    fseek(fp, 0, SEEK_SET);
                    char *data = (char *)malloc(file_size);
                    fread(data, file_size, 1, fp);
                    models->model_info[idx] = get_model_info(data, file_size);
                    free(data);
                    idx++;
                }
                fclose(fp);
                free(sub_path);
                free(info_file);
            }
        }
        closedir(dir);
        dir = NULL;
    }
    return models;
}


void srmodel_sdcard_deinit(srmodel_list_t *models)
{
    if (models != NULL) {
        if (models->num > 0) {
            for (int i = 0; i < models->num; i++) {
                free(models->model_name[i]);
                free(models->model_info[i]);
            }
        }
        free(models);
    }
    models = NULL;
}



srmodel_list_t *esp_srmodel_init(const char *partition_label)
{
#ifdef ESP_PLATFORM

#ifdef CONFIG_IDF_TARGET_ESP32
    return srmodel_config_init();
#else

#ifdef CONFIG_MODEL_IN_SDCARD
    // Read model data from SD card 
    return srmodel_sdcard_init(partition_label);
#else
    // Read model data from flash partition
    const esp_partition_t *partition = NULL;
    // find spiffs partition
    partition = esp_partition_find_first(
                    ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, partition_label
                );

    if (partition) {
        return srmodel_mmap_init(partition);
    } else {
        ESP_LOGE(TAG, "Can not find %s in partition table", partition_label);
    }

    return NULL;
#endif

#endif
#else
    return srmodel_sdcard_init(partition_label);
#endif
}

void esp_srmodel_deinit(srmodel_list_t *models)
{
#ifdef ESP_PLATFORM

#ifdef CONFIG_IDF_TARGET_ESP32
    return srmodel_config_deinit(models);
#else
    return srmodel_mmap_deinit(models);
#endif

#else
    return srmodel_sdcard_deinit(models);
#endif
}

// repackage strstr function to support needle==NULL
static char *esp_strstr(const char *haystack, const char *needle)
{
    if (needle == NULL) {
        return (char *)haystack;
    } else {
        return (char *)strstr(haystack, needle);
    }
}

char *esp_srmodel_filter(srmodel_list_t *models, const char *keyword1, const char *keyword2)
{
    if (models == NULL) {
        return NULL;
    }

    // return the first model name including specific keyword
    for (int i = 0; i < models->num; i++) {

        if (esp_strstr(models->model_name[i], keyword1) != NULL) {
            if (esp_strstr(models->model_name[i], keyword2) != NULL) {
                return models->model_name[i];
            }
        }
    }

    return NULL;
}

int esp_srmodel_exists(srmodel_list_t *models, char *model_name)
{
    if (models == NULL) {
        return -1;
    }

    for (int i = 0; i < models->num; i++) {
        if (strcmp(models->model_name[i], model_name) == 0) {
            return i;
        }
    }

    return -1;
}

char *esp_srmodel_get_wake_words(srmodel_list_t *models, char *model_name)
{
    if (models == NULL) {
        return NULL;
    }

    for (int i = 0; i < models->num; i++) {
        if (strcmp(models->model_name[i], model_name) == 0) {
            return get_wake_words_from_info(models->model_info[i]);
        }
    }
    return NULL;
}