#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include <config.h>
#include <constants.h>
#include <logger.h>

config_t *global_config;

static int
config_load_log_level(config_t *config, char *value)
{
    if(strcmp(value, "debug") == 0)
    {
        setlogmask(LOG_UPTO(LOG_DEBUG));
        config->logging.level = LOG_DEBUG;
        return 0;
    }
    if(strcmp(value, "info") == 0)
    {
        setlogmask(LOG_UPTO(LOG_INFO));
        config->logging.level = LOG_INFO;
        return 0;
    }
    if(strcmp(value, "notice") == 0)
    {
        setlogmask(LOG_UPTO(LOG_NOTICE));
        config->logging.level = LOG_NOTICE;
        return 0;
    }
    if(strcmp(value, "warning") == 0)
    {
        setlogmask(LOG_UPTO(LOG_WARNING));
        config->logging.level = LOG_WARNING;
        return 0;
    }
    if(strcmp(value, "err") == 0)
    {
        setlogmask(LOG_UPTO(LOG_ERR));
        config->logging.level = LOG_ERR;
        return 0;
    }
    if(strcmp(value, "crit") == 0)
    {
        setlogmask(LOG_UPTO(LOG_CRIT));
        config->logging.level = LOG_CRIT;
        return 0;
    }
    if(strcmp(value, "emerg") == 0)
    {
        setlogmask(LOG_UPTO(LOG_EMERG));
        config->logging.level = LOG_EMERG;
        return 0;
    }
    LOGGER_WARNING("invalid log-level '%s'\n", value);

    return 0;
}

static int
config_load_log_file(config_t *config, char *value)
{
    if(strcmp(value, "stdout") == 0)
    {
        config->logging.file = stdout;
        return 0;
    }
    if(strcmp(value, "stderr") == 0)
    {
        config->logging.file = stderr;
        return 0;
    }
    config->logging.file = fopen(value, "a+");
    return 0;
}

static void
config_init_relay_configs(config_t *config)
{
    config->relay_configs = malloc(sizeof(config_relay_t) * config->relay_count);
    for(uint8_t i = 0; i < config->relay_count; ++i)
    {
        config->relay_configs[i].driver = RELAY_DRIVER_NONE;
        config->relay_configs[i].inverted = 0;
        config->relay_configs[i].init = -1;
        config->relay_configs[i].pin = 0;
        config->relay_configs[i].pulse_duration = 0;
    }
}

static void
config_load_section_controller(config_t *config, toml_table_t* controller)
{
    toml_datum_t config_entry;

    config_entry = toml_string_in(controller, "name");
    if(config_entry.ok)
    {
        config_load_string(&config->name, config_entry.u.s);
        free(config_entry.u.s);
    }

    config_entry = toml_string_in(controller, "database");
    if(config_entry.ok)
    {
        config_load_string(&config->database, config_entry.u.s);
        free(config_entry.u.s);
    }

    config_entry = toml_string_in(controller, "user");
    if(config_entry.ok)
    {
        config_load_string(&config->user, config_entry.u.s);
        free(config_entry.u.s);
    }

    config_entry = toml_string_in(controller, "group");
    if(config_entry.ok)
    {
        config_load_string(&config->group, config_entry.u.s);
        free(config_entry.u.s);
    }

    config_entry = toml_string_in(controller, "mqtt-host");
    if(config_entry.ok)
    {
        config_load_string(&config->mqtt_host, config_entry.u.s);
        free(config_entry.u.s);
    }

    config_entry = toml_int_in(controller, "relays-init");
    if(config_entry.ok)
    {
        config->relays_init = config_entry.u.i;
    }


    config_entry = toml_string_in(controller, "include");
    if(config_entry.ok)
    {
        config_load_string(&config->include, config_entry.u.s);
        free(config_entry.u.s);
    }
}

static void
config_load_section_logging(config_t *config, toml_table_t* logging)
{
    toml_datum_t config_entry;

    config_entry = toml_string_in(logging, "level");
    if(config_entry.ok)
    {
        config_load_log_level(config, config_entry.u.s);
        free(config_entry.u.s);
    }

    config_entry = toml_string_in(logging, "file");
    if(config_entry.ok)
    {
        config_load_log_file(config, config_entry.u.s);
        free(config_entry.u.s);
    }
} 

static void
config_load_section_ports(config_t *config, toml_table_t* ports)
{
    toml_datum_t config_entry;

    config_entry = toml_int_in(ports, "discovery");
    if(config_entry.ok)
    {
        config->ports.discovery = config_entry.u.i;
    }

    config_entry = toml_int_in(ports, "mqtt");
    if(config_entry.ok)
    {
        config->ports.mqtt = config_entry.u.i;
    }
}

static void
config_load_section_relay(config_t *config, toml_table_t* relay, int relay_num)
{
    toml_datum_t config_entry;

    config_entry = toml_int_in(relay, "pin");
    if(config_entry.ok)
    {
        config->relay_configs[relay_num].pin = config_entry.u.i;
    }

    config_entry = toml_int_in(relay, "inverted");
    if(config_entry.ok)
    {
        config->relay_configs[relay_num].inverted = config_entry.u.i;
    }

    config_entry = toml_int_in(relay, "init");
    if(config_entry.ok)
    {
        config->relay_configs[relay_num].init = config_entry.u.i;
    }

    config_entry = toml_int_in(relay, "pulse-duration");
    if(config_entry.ok)
    {
        config->relay_configs[relay_num].pulse_duration = config_entry.u.i;
    }

    config_entry = toml_string_in(relay, "driver");
    if(config_entry.ok)
    {
        for(int i = 0; config_entry.u.s[i] != '\0'; ++i)
        {
            config_entry.u.s[i] = tolower(config_entry.u.s[i]);
        }

        if(strcmp(config_entry.u.s, "gpio") == 0)
        {
            config->relay_configs[relay_num].driver = RELAY_DRIVER_GPIO;
        }
        else if(strcmp(config_entry.u.s, "piface") == 0)
        {
            config->relay_configs[relay_num].driver = RELAY_DRIVER_PIFACE;
        }
        else
        {
            LOGGER_WARNING("invalid driver '%s' in section for relay %d\n", config_entry.u.s, relay_num);
        }
        free(config_entry.u.s);
    }
}

void
config_init()
{
    global_config = calloc(1, sizeof(config_t));

    config_load_string(&global_config->name, DEFAULT_CONTROLLER_NAME);

    config_load_string(&global_config->database, DEFAULT_DATABASE_PATH);

    config_load_string(&global_config->mqtt_host, DEFAULT_MQTT_HOST);


    global_config->user = NULL;
    global_config->group = NULL;

    global_config->include = NULL;

    global_config->relays_init = 0;
    global_config->relay_count = 0;

    config_load_string(&global_config->mqtt_host, "127.0.0.1");

    global_config->ports.mqtt = DEFAULT_MQTT_PORT;
    global_config->ports.discovery = DEFAULT_DISCOVERY_PORT;

    global_config->logging.level = LOG_DEBUG;
    global_config->logging.file = stdout;
}

void
config_free()
{
    free(global_config->name);
    free(global_config->database);
    free(global_config->user);
    free(global_config->group);
    free(global_config->mqtt_host);
    free(global_config->include);

    free(global_config);
}

void
config_load_string(char **holder, const char *value)
{
    if(*holder)
    {
        free(*holder);
    }
    size_t value_len = strlen(value);

    char *new_holder = malloc(sizeof(char) * (value_len + 1));
    strcpy(new_holder, value);
    new_holder[value_len] = '\0';

    *holder = new_holder;
}

static int
config_try_file(const char *path)
{
    if(access(path, F_OK) != 0)
    {
        return 1; 
    }
    if(access(path, R_OK) != 0)
    {
        return 1; 
    }
    return 0;
}

void
config_load(config_t *config, const char *cli_config_file)
{
    if(cli_config_file)
    {
        if(config_try_file(cli_config_file) == 0)
        {
            config_load_file(config, cli_config_file);
            return;
        }
        LOGGER_CRIT("unable to open the passed config file '%s'\n", cli_config_file);
        exit(1);
    }
    if(config_try_file(DEFAULT_CONFIG_PATH) == 0)
    {
        config_load_file(config, DEFAULT_CONFIG_PATH);
        return;
    }
    if(config_try_file(DEFAULT_GLOBAL_CONFIG_PATH) == 0)
    {
        config_load_file(config, DEFAULT_GLOBAL_CONFIG_PATH);
        return;
    }
}

void
config_load_file(config_t *config, const char *file_name)
{
    FILE *fp;
    toml_table_t* config_toml;
    char errbuf[256];

    /* Open the file and parse content */
    fp = fopen(file_name, "r");
    if(fp == NULL) {
        LOGGER_CRIT("unable to open config file '%s'\n", file_name);
        exit(1);
    }
    config_toml = toml_parse_file(fp, errbuf, sizeof(errbuf));
    fclose(fp);
    if(config_toml == NULL) {
        LOGGER_CRIT("unable to parse config file '%s': %s\n", file_name, errbuf);
        exit(1);
    }

    toml_table_t* controller = toml_table_in(config_toml, "controller");
    if(controller)
    {
        config_load_section_controller(config, controller);
    }

    toml_table_t* logging = toml_table_in(config_toml, "logging");
    if(logging)
    {
        config_load_section_logging(config, logging);
    }

    toml_table_t* ports = toml_table_in(config_toml, "ports");
    if(ports)
    {
        config_load_section_ports(config, ports);
    }

    toml_array_t* relays = toml_array_in(config_toml, "relays");
    if(relays)
    {
        config->relay_count = toml_array_nelem(relays);
        config_init_relay_configs(config);

        for(int i = 0; i < config->relay_count; ++i)
        {
            config_load_section_relay(config, toml_table_at(relays, i), i);
        }
    }

    toml_free(config_toml);

    LOGGER_DEBUG("Loaded config from %s\n", file_name);
}

void
config_load_directory(config_t *config, const char *directory_name)
{
    struct dirent *directory_entry;
    DIR *directory;

    (void)config;

    directory = opendir(directory_name);
    if(directory == NULL)
    {
        LOGGER_CRIT("cannot open directory '%s': %s\n", directory_name, strerror(errno));
        exit(1);
    }

    while((directory_entry = readdir(directory)) != NULL)
    {
        struct stat sb;
        const char *entry_name = directory_entry->d_name;

        size_t copied = 0;

        // Add 2 for '/' and '\0'.
        size_t path_len = strlen(directory_name) + strlen(entry_name) + 1;
        char *path = malloc(sizeof(char) * (path_len + 1));
        path[0] = '\0';

        strncat(path + copied, directory_name, path_len - copied);
        copied = strlen(path);
        if(path[copied - 1] != '/')
        {
            strncat(path + copied, "/", path_len - copied);
            copied = strlen(path);
        }
        strncat(path + copied, entry_name, path_len - copied);

        if(stat(path, &sb))
        {
            LOGGER_WARNING("failed to get info for '%s': %s\n", path, strerror(errno));
        }
        if(S_ISREG(sb.st_mode))
        {
            config_load_file(config, path);
        }
        free(path);
    }

    closedir(directory);
}