/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */

/*  Monkey HTTP Server
 *  ==================
 *  Copyright 2001-2017 Eduardo Silva <eduardo@monkey.io>
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

#include <ctype.h>
#include <string.h>
#include <time.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifndef _MSC_VER
#include <glob.h>
#endif

#include <mk_core/mk_rconf.h>
#include <mk_core/mk_utils.h>
#include <mk_core/mk_string.h>
#include <mk_core/mk_list.h>

#ifdef _WIN32
#include <Windows.h>
#include <strsafe.h>
#define PATH_MAX MAX_PATH
#endif

/* Raise a configuration schema error */
static void mk_config_error(const char *path, int line, const char *msg)
{
    mk_err("File %s", path);
    mk_err("Error in line %i: %s", line, msg);
}

/* Raise a warning */
static void mk_rconf_warning(const char *path, int line, const char *msg)
{
    mk_warn("Config file warning '%s':\n"
            "\t\t\t\tat line %i: %s",
            path, line, msg);
}


/* Returns a configuration section by [section name] */
struct mk_rconf_section *mk_rconf_section_get(struct mk_rconf *conf,
                                              const char *name)
{
    struct mk_list *head;
    struct mk_rconf_section *section;

    mk_list_foreach(head, &conf->sections) {
        section = mk_list_entry(head, struct mk_rconf_section, _head);
        if (strcasecmp(section->name, name) == 0) {
            return section;
        }
    }

    return NULL;
}

/* Register a key/value entry in the last section available of the struct */
static void mk_rconf_section_entry_add(struct mk_rconf *conf,
                                       const char *key, const char *val)
{
    struct mk_rconf_section *section;
    struct mk_rconf_entry *new;
    struct mk_list *head = &conf->sections;

    if (mk_list_is_empty(&conf->sections) == 0) {
        mk_err("Error: there are not sections available on %s!", conf->file);
        return;
    }

    /* Last section */
    section = mk_list_entry_last(head, struct mk_rconf_section, _head);

    /* Alloc new entry */
    new = mk_mem_alloc(sizeof(struct mk_rconf_entry));
    new->key = mk_string_dup(key);
    new->val = mk_string_dup(val);

    mk_list_add(&new->_head, &section->entries);
}

/* Create a configuration schema */
struct mk_rconf *mk_rconf_create(const char *name)
{
    struct mk_rconf *conf = NULL;

    /* Alloc configuration node */
    conf = mk_mem_alloc_z(sizeof(struct mk_rconf));
    if (!conf) {
        perror("malloc");
        return NULL;
    }
    conf->created = time(NULL);
    conf->file = mk_string_dup(name);
    mk_list_init(&conf->sections);

    return conf;
}

static int is_file_included(struct mk_rconf *conf, const char *path)
{
    struct mk_list *head;
    struct mk_rconf_file *file;

    mk_list_foreach(head, &conf->includes) {
        file = mk_list_entry(head, struct mk_rconf_file, _head);
        if (strcmp(file->path, path) == 0) {
            return MK_TRUE;
        }
    }

    return MK_FALSE;
}

char *mk_rconf_meta_get(struct mk_rconf *conf, char *key)
{
    struct mk_list *head;
    struct mk_rconf_entry *meta;

    mk_list_foreach(head, &conf->metas) {
        meta = mk_list_entry(head, struct mk_rconf_entry, _head);
        if (strcmp(meta->key, key) == 0) {
            return meta->val;
        }
    }

    return NULL;
}

static int mk_rconf_meta_add(struct mk_rconf *conf, char *buf, int len)
{
    int xlen;
    char *p;
    char *tmp;
    struct mk_rconf_entry *meta;

    if (buf[0] != '@') {
        return -1;
    }

    meta = mk_mem_alloc(sizeof(struct mk_rconf_entry));
    if (!meta) {
        return -1;
    }

    p = buf;
    tmp = strchr(p, ' ');
    xlen = (tmp - p);
    meta->key = mk_string_copy_substr(buf, 1, xlen);
    mk_string_trim(&meta->key);

    meta->val = mk_string_copy_substr(buf, xlen + 1, len);
    mk_string_trim(&meta->val);

    mk_list_add(&meta->_head, &conf->metas);
    return 0;
}

static int check_indent(const char *line, const char *indent)
{
    while (*line == *indent && *indent) {
        line++;
        indent++;
    }

    if (*indent != '\0') {
        if (isblank(*line)) {
            mk_err("[config] Inconsistent use of tab and space");
        }
        else {
            mk_err("[config] Indentation level is too low");
        }
        return -1;
    }

    if (isblank(*line)) {
        mk_err("[config] Extra indentation level found");
        return -1;
    }

    return 0;
}

/* To call this function from mk_rconf_read */
static int mk_rconf_read_glob(struct mk_rconf *conf, const char * path);

static int mk_rconf_read(struct mk_rconf *conf, const char *path)
{
    int i;
    int len;
    int ret;
    int line = 0;
    int indent_len = -1;
    int n_keys = 0;
    char *buf;
    char tmp[PATH_MAX];
    char *section = NULL;
    char *indent = NULL;
    char *key, *val;
    char *cfg_file = (char *) path;
    struct stat st;
    struct mk_rconf_file *file;
    struct mk_rconf_section *current = NULL;
    FILE *f;

    /* Check if the path exists (relative cases for included files) */
    if (conf->level >= 0) {
        ret = stat(path, &st);
        if (ret == -1 && errno == ENOENT) {
            /* Try to resolve the real path (if exists) */
            if (path[0] == '/') {
                return -1;
            }

            if (conf->root_path) {
                snprintf(tmp, PATH_MAX, "%s/%s", conf->root_path, path);
                cfg_file = tmp;
            }
        }
    }

    /* Check this file have not been included before */
    ret = is_file_included(conf, cfg_file);
    if (ret == MK_TRUE) {
        mk_err("[config] file already included %s", cfg_file);
        return -1;
    }

    conf->level++;

    /* Open configuration file */
    if ((f = fopen(cfg_file, "r")) == NULL) {
        mk_warn("[config] I cannot open %s file", cfg_file);
        return -1;
    }

    /* Allocate temporal buffer to read file content */
    buf = mk_mem_alloc(MK_RCONF_KV_SIZE);
    if (!buf) {
        fclose(f);
        perror("malloc");
        return -1;
    }

    /* looking for configuration directives */
    while (fgets(buf, MK_RCONF_KV_SIZE, f)) {
        len = strlen(buf);
        if (len > 0 && buf[len - 1] == '\n') {
            buf[--len] = 0;
            if (len && buf[len - 1] == '\r') {
                buf[--len] = 0;
            }
        }
        else {
            /*
             * If we don't find a break line, validate if we got an EOF or not. No EOF
             * means that the incoming string is not finished so we must raise an
             * exception.
             */
            if (!feof(f)) {
                mk_config_error(path, line, "Length of content has exceeded limit");
                fclose(f);
                mk_mem_free(buf);
                return -1;
            }
        }

        /* Line number */
        line++;

        if (!buf[0]) {
            continue;
        }

        /* Skip commented lines */
        if (buf[0] == '#') {
            continue;
        }

        if (len > 9 && strncasecmp(buf, "@INCLUDE ", 9) == 0) {
            if (strchr(buf + 9, '*') != NULL) {
                ret = mk_rconf_read_glob(conf, buf + 9);
            }
            else {
                ret = mk_rconf_read(conf, buf + 9);
            }
            if (ret == -1) {
                conf->level--;
                fclose(f);
                if (indent) {
                    mk_mem_free(indent);
                }
                mk_mem_free(buf);
                return -1;
            }
            continue;
        }
        else if (buf[0] == '@' && len > 3) {
            ret = mk_rconf_meta_add(conf, buf, len);
            if (ret == -1) {
                fclose(f);
                if (indent) {
                    mk_mem_free(indent);
                }
                mk_mem_free(buf);
                return -1;
            }
            continue;
        }

        /* Section definition */
        if (buf[0] == '[') {
            int end = -1;
            end = mk_string_char_search(buf, ']', len);
            if (end > 0) {
                /*
                 * Before to add a new section, lets check the previous
                 * one have at least one key set
                 */
                if (current && n_keys == 0) {
                    mk_rconf_warning(path, line, "Previous section did not have keys");
                }

                /* Create new section */
                section = mk_string_copy_substr(buf, 1, end);
                current = mk_rconf_section_add(conf, section);
                if (!current) {
                    fclose(f);
                    if (indent) {
                        mk_mem_free(indent);
                    }
                    mk_mem_free(buf);
                    mk_mem_free(section);
                    return -1;
                }
                mk_mem_free(section);
                n_keys = 0;
                continue;
            }
            else {
                mk_config_error(path, line, "Bad header definition");
                fclose(f);
                mk_mem_free(buf);
                return -1;
            }
        }

        /* No separator defined */
        if (!indent) {
            i = 0;

            do { i++; } while (i < len && isblank(buf[i]));

            indent = mk_string_copy_substr(buf, 0, i);
            indent_len = strlen(indent);

            /* Blank indented line */
            if (i == len) {
                continue;
            }
        }

        /* Validate indentation level */
        if (check_indent(buf, indent) < 0) {
            mk_config_error(path, line, "Invalid indentation level");
            fclose(f);
            return -1;
        }

        if (buf[indent_len] == '#' || indent_len == len) {
            continue;
        }

        if (len - indent_len >= 3 && strncmp(buf + indent_len, "---", 3) == 0) {
            continue;
        }

        /* Get key and val */
        i = mk_string_char_search(buf + indent_len, ' ', len - indent_len);
        key = mk_string_copy_substr(buf + indent_len, 0, i);
        val = mk_string_copy_substr(buf + indent_len + i, 1, len - indent_len - i);

        if (!key || !val || i < 0) {
            mk_config_error(path, line, "Each key must have a value");
            fclose(f);
            mk_mem_free(key);
            mk_mem_free(val);
            return -1;
        }

        /* Trim strings */
        mk_string_trim(&key);
        mk_string_trim(&val);

        if (strlen(val) == 0) {
            mk_config_error(path, line, "Key has an empty value");
            fclose(f);
            mk_mem_free(key);
            mk_mem_free(val);
            return -1;
        }

        /* Register entry: key and val are copied as duplicated */
        mk_rconf_section_entry_add(conf, key, val);

        /* Free temporal key and val */
        mk_mem_free(key);
        mk_mem_free(val);

        n_keys++;
    }

    if (section && n_keys == 0) {
        /* No key, no warning */
    }

    /*
    struct mk_config_section *s;
    struct mk_rconf_entry *e;

    s = conf->section;
    while(s) {
        printf("\n[%s]", s->name);
        e = s->entry;
        while(e) {
            printf("\n   %s = %s", e->key, e->val);
            e = e->next;
        }
        s = s->next;
    }
    fflush(stdout);
    */
    fclose(f);
    if (indent) {
        mk_mem_free(indent);
    }
    mk_mem_free(buf);

    /* Append this file to the list */
    file = mk_mem_alloc(sizeof(struct mk_rconf_file));
    if (!file) {
        conf->level--;
        return -1;
    }

    file->path = mk_string_dup(path);
    mk_list_add(&file->_head, &conf->includes);
    conf->level--;
    return 0;
}

#ifndef _WIN32
static int mk_rconf_read_glob(struct mk_rconf *conf, const char * path)
{
    int ret = -1;
    glob_t glb;
    char tmp[PATH_MAX];

    const char *glb_path;
    size_t i;
    int ret_glb = -1;

    if (conf->root_path && path[0] != '/') {
        snprintf(tmp, PATH_MAX, "%s/%s", conf->root_path, path);
        glb_path = tmp;
    }
    else {
        glb_path = path;
    }

    ret_glb = glob(glb_path, GLOB_NOSORT, NULL, &glb);
    if (ret_glb != 0) {
        switch(ret_glb){
        case GLOB_NOSPACE:
            mk_warn("[%s] glob: [%s] no space", __FUNCTION__, glb_path);
            break;
        case GLOB_NOMATCH:
            mk_warn("[%s] glob: [%s] no match", __FUNCTION__, glb_path);
            break;
        case GLOB_ABORTED:
            mk_warn("[%s] glob: [%s] aborted", __FUNCTION__, glb_path);
            break;
        default:
            mk_warn("[%s] glob: [%s] other error", __FUNCTION__, glb_path);
        }
        return ret;
    }

    for (i = 0; i < glb.gl_pathc; i++) {
        ret = mk_rconf_read(conf, glb.gl_pathv[i]);
        if (ret < 0) {
            break;
        }
    }

    globfree(&glb);
    return ret;
}
#else
static int mk_rconf_read_glob(struct mk_rconf *conf, const char *path)
{
    char *star, *p0, *p1;
    char pattern[MAX_PATH];
    char buf[MAX_PATH];
    int ret;
    struct stat st;
    HANDLE h;
    WIN32_FIND_DATA data;

    if (strlen(path) > MAX_PATH - 1) {
        return -1;
    }

    star = strchr(path, '*');
    if (star == NULL) {
        return -1;
    }

    /*
     * C:\data\tmp\input_*.conf
     *            0<-----|
     */
    p0 = star;
    while (path <= p0 && *p0 != '\\') {
        p0--;
    }

    /*
     * C:\data\tmp\input_*.conf
     *                   |---->1
     */
    p1 = star;
    while (*p1 && *p1 != '\\') {
        p1++;
    }

    memcpy(pattern, path, (p1 - path));
    pattern[p1 - path] = '\0';

    h = FindFirstFileA(pattern, &data);
    if (h == INVALID_HANDLE_VALUE) {
        return 0;
    }

    do {
        /* Ignore the current and parent dirs */
        if (!strcmp(".", data.cFileName) || !strcmp("..", data.cFileName)) {
            continue;
        }

        /* Avoid an infinite loop */
        if (strchr(data.cFileName, '*')) {
            continue;
        }

        /* Create a path (prefix + filename + suffix) */
        memcpy(buf, path, p0 - path + 1);
        buf[p0 - path + 1] = '\0';

        if (FAILED(StringCchCatA(buf, MAX_PATH, data.cFileName))) {
            continue;
        }
        if (FAILED(StringCchCatA(buf, MAX_PATH, p1))) {
            continue;
        }

        if (strchr(p1, '*')) {
            mk_rconf_read_glob(conf, buf); /* recursive */
            continue;
        }

        ret = stat(buf, &st);
        if (ret == 0 && (st.st_mode & S_IFMT) == S_IFREG) {
            if (mk_rconf_read(conf, buf) < 0) {
                return -1;
            }
        }
    } while (FindNextFileA(h, &data) != 0);

    FindClose(h);
    return 0;
}
#endif

static int mk_rconf_path_set(struct mk_rconf *conf, char *file)
{
    char *p;
    char *end;
    char path[PATH_MAX + 1];

#ifdef _MSC_VER
    p = _fullpath(path, file, PATH_MAX + 1);
#else
    p = realpath(file, path);
#endif
    if (!p) {
        return -1;
    }

    /* lookup path ending and truncate */
    end = strrchr(path, '/');
    if (!end) {
        return -1;
    }

    end++;
    *end = '\0';
    conf->root_path = mk_string_dup(path);

    return 0;
}

struct mk_rconf *mk_rconf_open(const char *path)
{
    int ret;
    struct mk_rconf *conf = NULL;

    if (!path) {
        mk_warn("[config] invalid path file %s", path);
        return NULL;
    }

    /* Alloc configuration node */
    conf = mk_mem_alloc_z(sizeof(struct mk_rconf));
    if (!conf) {
        perror("malloc");
        return NULL;
    }
    conf->created = time(NULL);
    conf->file = mk_string_dup(path);
    conf->level = -1;
    mk_list_init(&conf->sections);
    mk_list_init(&conf->includes);
    mk_list_init(&conf->metas);

    /* Set the absolute path for the entrypoint file */
    mk_rconf_path_set(conf, (char *) path);

    /* Read entrypoint */
    ret = mk_rconf_read(conf, path);
    if (ret == -1) {
        mk_rconf_free(conf);
        return NULL;
    }

    return conf;
}

void mk_rconf_free(struct mk_rconf *conf)
{
    struct mk_list *head, *tmp;
    struct mk_rconf_section *section;
    struct mk_rconf_entry *entry;
    struct mk_rconf_file *file;

    /* Remove included files */
    mk_list_foreach_safe(head, tmp, &conf->includes) {
        file = mk_list_entry(head, struct mk_rconf_file, _head);
        mk_list_del(&file->_head);
        mk_mem_free(file->path);
        mk_mem_free(file);
    }

    /* Remove metas */
    mk_list_foreach_safe(head, tmp, &conf->metas) {
        entry = mk_list_entry(head, struct mk_rconf_entry, _head);
        mk_list_del(&entry->_head);
        mk_mem_free(entry->key);
        mk_mem_free(entry->val);
        mk_mem_free(entry);
    }

    /* Free sections */
    mk_list_foreach_safe(head, tmp, &conf->sections) {
        section = mk_list_entry(head, struct mk_rconf_section, _head);
        mk_list_del(&section->_head);

        /* Free section entries */
        mk_rconf_free_entries(section);

        /* Free section node */
        mk_mem_free(section->name);
        mk_mem_free(section);
    }
    if (conf->file) {
        mk_mem_free(conf->file);
    }

    mk_mem_free(conf->root_path);
    mk_mem_free(conf);
}

void mk_rconf_free_entries(struct mk_rconf_section *section)
{
    struct mk_rconf_entry *entry;
    struct mk_list *head, *tmp;

    mk_list_foreach_safe(head, tmp, &section->entries) {
        entry = mk_list_entry(head, struct mk_rconf_entry, _head);
        mk_list_del(&entry->_head);

        /* Free memory assigned */
        mk_mem_free(entry->key);
        mk_mem_free(entry->val);
        mk_mem_free(entry);
    }
}

/* Register a new section into the configuration struct */
struct mk_rconf_section *mk_rconf_section_add(struct mk_rconf *conf,
                                              char *name)
{
    struct mk_rconf_section *new;

    /* Alloc section node */
    new = mk_mem_alloc(sizeof(struct mk_rconf_section));
    if (!new) {
        return NULL;
    }
    new->name = mk_string_dup(name);
    mk_list_init(&new->entries);
    mk_list_add(&new->_head, &conf->sections);

    return new;
}

/* Return the value of a key of a specific section */
void *mk_rconf_section_get_key(struct mk_rconf_section *section,
                               char *key, int mode)
{
    int on, off;
    struct mk_rconf_entry *entry;
    struct mk_list *head;

    mk_list_foreach(head, &section->entries) {
        entry = mk_list_entry(head, struct mk_rconf_entry, _head);

        if (strcasecmp(entry->key, key) == 0) {
            switch (mode) {
            case MK_RCONF_STR:
                return (void *) mk_string_dup(entry->val);
            case MK_RCONF_NUM:
                return (void *) strtol(entry->val, (char **) NULL, 10);
            case MK_RCONF_BOOL:
                on = strcasecmp(entry->val, MK_RCONF_ON);
                off = strcasecmp(entry->val, MK_RCONF_OFF);

                if (on != 0 && off != 0) {
                    return (void *) -1;
                }
                else if (on >= 0) {
                    return (void *) MK_TRUE;
                }
                else {
                    return (void *) MK_FALSE;
                }
            case MK_RCONF_LIST:
                return (void *)mk_string_split_line(entry->val);
            }
        }
    }
    return NULL;
}
