#include "arc/std/config.h"

#include "arc/std/errno.h"
#include "arc/std/hashtable.h"
#include "arc/std/io.h"
#include "arc/std/string.h"
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stddef.h>
#include <string.h>

struct ARC_Config {
    ARC_Hashtable *currgroup;

    ARC_Hashtable *groups;
    ARC_Hashtable *keys;
};

typedef struct ARC_ConfigKey {
    ARC_ConfigKeyRead   Read;
    ARC_ConfigKeyDelete Delete;
} ARC_ConfigKey;

typedef struct ARC_ConfigTypeTemplate {
    ARC_ConfigKeyDelete Delete;

    void *data;
} ARC_ConfigTypeTemplate;

typedef struct ARC_ConfigDeleteKeyArgs {
    ARC_Config *config;
    ARC_String *string;
} ARC_ConfigDeleteKeyArgs;

int8_t ARC_Config_KeyComp(void *key1, size_t *key1size, void *key2, size_t *key2size);

void ARC_Config_CreateGroup(ARC_Config *config, ARC_String *name);
void ARC_Config_DestroyGroup(ARC_HashtableNode *group, void *userdata);

void ARC_Config_DestroyGroupNode(ARC_HashtableNode *node , void *userdata);

void ARC_Config_RemoveKey(ARC_HashtableNode *node, void *userdata);

void ARC_Config_AddKey(ARC_Config *config, ARC_String *type, ARC_ConfigKeyRead keyRead, ARC_ConfigKeyDelete keyDelete){
    ARC_ConfigKey *newKey = (ARC_ConfigKey *)malloc(sizeof(ARC_ConfigKey));
    newKey->Read   = keyRead;
    newKey->Delete = keyDelete;

    char *typeval = (char *)malloc(sizeof(char) * type->length);
    strncpy(typeval, type->data, type->length);
    ARC_Hashtable_Add(config->keys, (void *)typeval, type->length, newKey);
}

void ARC_Config_AddKeyCString(ARC_Config *config, const char *type, uint64_t length, ARC_ConfigKeyRead keyRead, ARC_ConfigKeyDelete keyDelete){
    ARC_ConfigKey *newKey = (ARC_ConfigKey *)malloc(sizeof(ARC_ConfigKey));
    newKey->Read   = keyRead;
    newKey->Delete = keyDelete;

    char *typeval = (char *)malloc(sizeof(char) * length);
    strncpy(typeval, type, length);
    ARC_Hashtable_Add(config->keys, (void *)typeval, length, newKey);
}

void ARC_Config_Create(ARC_Config **config){
    *config = (ARC_Config *)malloc(sizeof(ARC_Config));
    (*config)->currgroup = NULL;

    ARC_Hashtable *groups;
    ARC_Hashtable_Create(&groups, ARC_GROUP_BUCKET_SIZE, NULL, ARC_Config_KeyComp);
    (*config)->groups = groups;
    ARC_Config_CreateGroup(*config, NULL); 

    ARC_Hashtable *keys;
    ARC_Hashtable_Create(&keys, ARC_KEY_BUCKET_SIZE, NULL, ARC_Config_KeyComp);
    (*config)->keys = keys;

#ifdef ARC_DEFAULT_CONFIG
    ARC_Defaults_ConfigKey_Create(*config);
#endif
}

void ARC_Config_Destroy(ARC_Config *config){
    ARC_ConfigDeleteKeyArgs keyArgs = {
        .config = config,
        .string = NULL,
    };

    ARC_Hashtable_Destroy(config->groups, ARC_Config_DestroyGroup, (void *)&keyArgs);
    ARC_Hashtable_Destroy(config->keys  , ARC_Config_RemoveKey   , NULL            );
    free(config);
}

//TODO: fix NULL group
void ARC_Config_SetGroup(ARC_Config *config, ARC_String *groupname){
    if(!config){
        return;
    }

    if(groupname == NULL){
        ARC_Hashtable_Get(config->groups, (void *)" ", 1, (void **)&(config->currgroup));
        return;
    }

    ARC_Hashtable_Get(config->groups, (void *)groupname->data, groupname->length, (void **)&(config->currgroup));
    arc_errno = 0;

//    if(arc_errno && arc_errno != ARC_ERRNO_NULL){
//        return;
//    }

    if(config->currgroup){
        return;
    }

    ARC_Config_CreateGroup(config, groupname);
    if(arc_errno){
        return;
    }

    ARC_Hashtable_Get(config->groups, (void *)groupname->data, groupname->length, (void **)&(config->currgroup));
}

void ARC_Config_Get(ARC_Config *config, ARC_String *keyname, void **value){
    ARC_ConfigTypeTemplate *temp = NULL;

    uint64_t length = ARC_String_FindCString(keyname, "::", 2);
    if(arc_errno){
        //TODO: Debug info here
        ARC_DEBUG_LOG_ERROR("in ARC_Config_Get(config, keyname, value); length threw error");
        *value = NULL;
        return;
    }

    if(length != ~((uint64_t)0)){
        length--;
        ARC_String *group = NULL;

        if(length != 0){
            ARC_String_CopySubstring(&group, keyname, 0, length);
        }

        ARC_Hashtable *currgroup = config->currgroup;
        ARC_Config_SetGroup(config, group);
        if(arc_errno){
            ARC_DEBUG_LOG_ERROR("in ARC_Config_Get(config, keyname, value); setting group threw error");
            ARC_String_Destroy(group);
            *value = NULL;
            return;
        }

        ARC_String *name;
        ARC_String_CopySubstring(&name, keyname, length + 2, keyname->length - (length + 2));
        ARC_Hashtable_Get(config->currgroup, (void *)name->data, name->length, (void **)&temp);
        ARC_String_Destroy(name);

        config->currgroup = currgroup;
        if(group){
            ARC_String_Destroy(group);
        }

        if(temp == NULL){
            *value = NULL;
            return;
        }

        *value = temp->data;
        return;
    }

    if(!keyname){
        *value = NULL;
        return;
    }

    ARC_Hashtable_Get(config->currgroup, (void *)keyname->data, keyname->length, (void **)&temp);
    if(arc_errno || temp == NULL){
        *value = NULL;
        return;
    }

    *value = temp->data;
}

void ARC_Config_Recurse(ARC_Config *config, ARC_String **data, ARC_String *groupstr, uint8_t *command);

void ARC_Config_SetKeyGroup(ARC_Config *config, ARC_String **data, uint8_t *command){
    uint64_t index     = ARC_String_FindCString(*data, " ", 1);
    uint64_t nextIndex = ARC_String_FindCString(*data, "{", 1);
    if(index == ~(uint64_t)0 || nextIndex == ~(uint64_t)0){
        arc_errno = ARC_ERRNO_DATA;
    }

    if(arc_errno){
        return;
    }

    ARC_String *name, *temp;
    ARC_String_CopySubstring(&temp, *data, index, nextIndex - index - 1);
    ARC_String_StripEndsWhitespace(&name, temp);
    ARC_String_Destroy(temp);

    temp = *data;
    ARC_String_CopySubstring(data, temp, nextIndex + 1, (*data)->length - (nextIndex + 1));
    ARC_String_Destroy(temp);

    ARC_Config_Recurse(config, data, name, command);
    ARC_String_Destroy(name);
}

void ARC_Config_LoadFromKey(ARC_Config *config, ARC_String *keyType, ARC_String *name, ARC_String *value){
    ARC_ConfigKey *key; 
    ARC_Hashtable_Get(config->keys, keyType->data, keyType->length, (void **)&key);
    if(key == NULL){
        arc_errno = ARC_ERRNO_DATA;
        ARC_DEBUG_LOG_ERROR_WITH_VARIABLES("in ARC_Config_LoadFromKey(config, string, value); no matching key: %s", keyType->data);
    }

    if(arc_errno){
        return;
    }

    ARC_ConfigTypeTemplate *templateVal = (ARC_ConfigTypeTemplate *) malloc(sizeof(ARC_ConfigTypeTemplate));
    templateVal->Delete = NULL;
    templateVal->data = NULL;

    uint8_t reference = key->Read(config, value, &(templateVal->data));
    if(!reference){
        templateVal->Delete = key->Delete;
    }

    if(arc_errno){
        return;
    }

    char *nameval = (char *)malloc(sizeof(char) * name->length + 1);
    strncpy(nameval, name->data, name->length);
    nameval[name->length] = '\0';
    ARC_Hashtable_Add(config->currgroup, nameval, name->length, (void *)templateVal);
}

void ARC_Config_UnloadFromKey(ARC_Config *config, ARC_String *keyType, ARC_String *name, ARC_String *value){
    ARC_ConfigDeleteKeyArgs keyArgs = {
        .config = config,
        .string = value,
    };

    ARC_Hashtable_Remove(config->currgroup, name->data, name->length, ARC_Config_DestroyGroupNode, &keyArgs);
}

void ARC_Config_GetNameAndValue(ARC_String *data, ARC_String **name, ARC_String **value){
        uint64_t index = ARC_String_FindCString(data, "=", 1);
        if(arc_errno || index == ~(uint64_t)0){
            *name  = NULL;
            *value = NULL;
            return;
        }

        ARC_String_CopySubstring(name, data, 0, index - 1);
        index++;

        ARC_String *dataTemp = *name;
        ARC_String_StripEndsWhitespace(name, dataTemp);
        ARC_String_Destroy(dataTemp);

        ARC_String_CopySubstring(&dataTemp, data, index, data->length - index);
        ARC_String_StripEndsWhitespace(value, dataTemp);
        ARC_String_Destroy(dataTemp);
}

void ARC_Config_Recurse(ARC_Config *config, ARC_String **data, ARC_String *groupstr, uint8_t *command){
    ARC_Config_SetGroup(config, groupstr);
    if(arc_errno){
        return;
    }

    ARC_Hashtable *group = config->currgroup;

    while(*data && (*data)->length){
        ARC_String *dataTemp = *data;
        ARC_String_StripEndsWhitespace(data, dataTemp);
        ARC_String_Destroy(dataTemp);

        // break out of current group
        if((*data)->data[0] == '}'){
            config->currgroup = NULL;

            dataTemp = *data;
            ARC_String_CopySubstring(data, dataTemp, 1, dataTemp->length - 1);
            ARC_String_Destroy(dataTemp);
            return;
        }

        // set group
        if(!(config->currgroup)){
            config->currgroup = group;
        }

        // get keys type
        uint64_t index = ARC_String_FindCString(*data, " ", 1);
        if(arc_errno || index == ~(uint64_t)0){
            return;
        }

        ARC_String *keyType, *keyTypeTemp;
        ARC_String_CopySubstring(&keyTypeTemp, *data, 0, index);
        ARC_String_StripEndsWhitespace(&keyType, keyTypeTemp);
        ARC_String_Destroy(keyTypeTemp);

        if(ARC_String_EqualsCString(keyType, "group", 5)){
            ARC_Config_SetKeyGroup(config, data, command);
            ARC_String_Destroy(keyType);
            config->currgroup = group;
            if(arc_errno){
                return;
            }

            continue;
        }

        // get and copy up to the ;
        ARC_String *nameAndValue;
        uint64_t nextIndex = ARC_String_FindCString(*data, ";", 1);
        if(nextIndex == ~(uint64_t)0){
            arc_errno = ARC_ERRNO_DATA;
        }

        if(arc_errno){
            ARC_String_Destroy(keyType);
            return;
        }

        ARC_String_CopySubstring(&nameAndValue, *data, index, nextIndex - (index + 1));
        if(arc_errno){
            ARC_String_Destroy(keyType);
            return;
        }

        // remove up to the ; from data string
        dataTemp = *data;
        ARC_String_CopySubstring(data, dataTemp, nextIndex, (*data)->length - nextIndex);
        ARC_String_Destroy(dataTemp);
        if(arc_errno){
            ARC_String_Destroy(keyType);
            ARC_String_Destroy(nameAndValue);
            return;
        }

        // get name and value of string
        ARC_String *name, *value;
        ARC_Config_GetNameAndValue(nameAndValue, &name, &value);
        ARC_String_Destroy(nameAndValue);
        if(arc_errno){
            ARC_String_Destroy(keyType);
            return;
        }

        // load from key
        if(*command == ARC_CONFIG_FILE_IO_LOAD){
            ARC_Config_LoadFromKey(config, keyType, name, value);

            ARC_String_Destroy(keyType);
            ARC_String_Destroy(name   );
            ARC_String_Destroy(value  );

            if(arc_errno){
                return;
            }

            continue;
        }

        // unload from key
        if(*command == ARC_CONFIG_FILE_IO_UNLOAD){
            ARC_Config_UnloadFromKey(config, keyType, name, value);

            ARC_String_Destroy(keyType);
            ARC_String_Destroy(name   );
            ARC_String_Destroy(value  );

            if(arc_errno){
                return;
            }

            continue;
        }

        // config file wasn't loaded correctly
        ARC_String_Destroy(keyType);
        ARC_String_Destroy(name   );
        ARC_String_Destroy(value  );
        arc_errno = ARC_ERRNO_DATA;
        return;
    }

    config->currgroup = group;
}

void ARC_Config_StripComment(ARC_String *original, ARC_String **stripped, ARC_String *lineStart, ARC_String *lineEnd){
    ARC_String *current = NULL;
    ARC_String_Copy(&current, original);

    uint64_t index = ARC_String_Find(original, lineStart);
    while(index != ~(uint64_t)0){
        ARC_String *commentString;
        ARC_String_CopySubstring(&commentString, current, index + lineStart->length, current->length - (index + lineStart->length));

        uint64_t endIndex = ARC_String_Find(commentString, lineEnd);
        ARC_String_Destroy(commentString);
        if(endIndex == ~(uint64_t)0){
            ARC_DEBUG_LOG_ERROR("ARC_Config_RemoveComments(original, commentRemoved); No newline found when stripping single line comment");
            arc_errno = ARC_ERRNO_DATA;
            ARC_String_Destroy(current);
            *stripped = NULL;
            break;
        }

        ARC_String *currentTemp = current;
        ARC_String_RemoveSection(&current, currentTemp, index, endIndex + lineStart->length + lineEnd->length);
        ARC_String_Destroy(currentTemp);

        index = ARC_String_Find(current, lineStart);
    }

    *stripped = current;
}

void ARC_Config_RemoveComments(ARC_String *original, ARC_String **commentRemoved){
    ARC_String *lineStart, *lineEnd;

    //Single Line Comment
    ARC_String_Create(&lineStart, "//", 2);
    ARC_String_Create(&lineEnd  , "\n", 1);

    ARC_String *singleLineStripped;
    ARC_Config_StripComment(original, &singleLineStripped, lineStart, lineEnd);

    ARC_String_Destroy(lineStart);
    ARC_String_Destroy(lineEnd  );

    if(arc_errno){
        commentRemoved = NULL;
        return;
    }

    //Multi Line Comment
    ARC_String_Create(&lineStart, "/*", 2);
    ARC_String_Create(&lineEnd  , "*/", 2);

    ARC_Config_StripComment(singleLineStripped, commentRemoved, lineStart, lineEnd);
    ARC_String_Destroy(singleLineStripped);

    ARC_String_Destroy(lineStart);
    ARC_String_Destroy(lineEnd  );
}

void ARC_Config_RunCommand(ARC_Config *config, ARC_String *command){
    ARC_String *space;
    ARC_String_Create(&space, " " , 1);

    uint64_t index = ARC_String_Find(command, space);
    if(index == ~(uint64_t)0){
        arc_errno = ARC_ERRNO_DATA;
        ARC_String_Destroy(space);
        return;
    }

    ARC_String *commandOpt;
    ARC_String_CopySubstring(&commandOpt, command, 0, index);

    ARC_String *commandArgTemp, *commandArg;
    ARC_String_CopySubstring(&commandArgTemp, command, index + space->length, command->length - (index + space->length));
    ARC_String_StripWhitespace(&commandArg, commandArgTemp);
    ARC_String_Destroy(commandArgTemp);

    if(ARC_String_EqualsCString(command, "load", 4)){
        ARC_Config_FileIO(config, commandArg, ARC_CONFIG_FILE_IO_LOAD);
    }
    else if(ARC_String_EqualsCString(command, "unload", 6)){
        ARC_Config_FileIO(config, commandArg, ARC_CONFIG_FILE_IO_UNLOAD);
    }
    else {
        arc_errno = ARC_ERRNO_DATA;
    }

    ARC_String_Destroy(commandOpt);
    ARC_String_Destroy(commandArg);
    ARC_String_Destroy(space     );
}

void ARC_Config_RemoveAndRunCommands(ARC_Config *config, ARC_String *original, ARC_String **commandRemoved){
    ARC_String *current;
    ARC_String_Copy(&current, original);

    ARC_String *lineStart, *lineEnd;
    ARC_String_Create(&lineStart, "#" , 1);
    ARC_String_Create(&lineEnd  , "\n", 1);

    uint64_t index = ARC_String_Find(current, lineStart);

    while(index != ~(uint64_t)0){
        uint64_t endIndex = ARC_String_Find(current, lineEnd);
        if(endIndex == ~(uint64_t)0){
            arc_errno = ARC_ERRNO_DATA;
            ARC_String_Destroy(current  );
            ARC_String_Destroy(lineStart);
            ARC_String_Destroy(lineEnd  );
            *commandRemoved = NULL;
            return;
        }

        ARC_String *command;
        ARC_String_CopySubstring(&command, current, index + lineStart->length, endIndex - (index + lineStart->length));
        ARC_Config_RunCommand(config, command);

        ARC_String *currentTemp = current;
        ARC_String_RemoveSubstring(&current, currentTemp, command);
        ARC_String_Destroy(command);
        ARC_String_Destroy(currentTemp);
    }

    ARC_String_Destroy(lineStart);
    ARC_String_Destroy(lineEnd  );

    *commandRemoved = current;
}

void ARC_Config_FileIO(ARC_Config *config, ARC_String *path, uint8_t command){
    arc_errno = 0; //TODO: Remove this, just testing
    ARC_String *data;
    ARC_IO_FileToStr(path, &data);
    if(arc_errno){
        ARC_DEBUG_LOG_ERROR_WITH_VARIABLES("ARC_IO_FileToStr(%s, &data, &size);\n", path->data);
        return;
    }

    ARC_String_AppendCStringWithStrlen(&data, "\n");

    ARC_String *temp = data;
    ARC_Config_RemoveComments(temp, &data);
    ARC_String_Destroy(temp);

    temp = data;
    ARC_Config_RemoveAndRunCommands(config, temp, &data);
    ARC_String_Destroy(temp);

    temp = data;
    ARC_String_StripEndsWhitespace(&data, temp);
    ARC_String_Destroy(temp);

    ARC_Config_Recurse(config, &data, NULL, &command);
    if(data){
        ARC_String_Destroy(data);
    }
}

int8_t ARC_Config_KeyComp(void *key1, size_t *key1size, void *key2, size_t *key2size){
    if(*key1size - *key2size){
        return -1;
    }

    return strncmp((const char *)key1, (const char *)key2, *key1size);
}

void ARC_Config_CreateGroup(ARC_Config *config, ARC_String *name){
    ARC_Hashtable *data;
    ARC_Hashtable_Create(&data, ARC_GROUP_DATA_BUCKET_SIZE, NULL, ARC_Config_KeyComp);

    if(name){
        char *nameval = (char *)malloc(sizeof(char) * name->length);
        strncpy(nameval, name->data, name->length);
        ARC_Hashtable_Add(config->groups, nameval, name->length, (void *)data);
        return;
    }

    char *emptyGroup = (char *)malloc(sizeof(char));
    emptyGroup[0] = ' ';
    ARC_Hashtable_Add(config->groups, emptyGroup, 1, (void *)data);
}

void ARC_Config_DestroyGroup(ARC_HashtableNode *group, void *userdata){
    free((char *)group->key);
    ARC_Hashtable_Destroy((ARC_Hashtable *)group->data, ARC_Config_DestroyGroupNode, userdata);
}

void ARC_Config_DestroyGroupNode(ARC_HashtableNode *node, void *userdata){
    free((char *)node->key);

    ARC_ConfigTypeTemplate *temp = (ARC_ConfigTypeTemplate *)node->data;
    if(temp->Delete && temp->data && userdata){
        ARC_ConfigDeleteKeyArgs *args = (ARC_ConfigDeleteKeyArgs *)userdata;

        temp->Delete(args->config, args->string, temp->data);
    }

    free(temp);
    node->data = NULL;
}

void ARC_Config_RemoveKey(ARC_HashtableNode *node, void *userdata){
    free((char *)node->key);

    if(!node->data){
        arc_errno = ARC_ERRNO_NULL;
        return;
    }

    free((ARC_ConfigKey *)node->data);
}
