/*
 * Open Chinese Convert
 *
 * Copyright 2010 BYVoid <byvoid.kcp@gmail.com>
 *
 * 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 "config_reader.h"
#include "dictionary_set.h"

#include <stdio.h>

#define BUFFER_SIZE 8192
#define DICTIONARY_MAX_COUNT 1024
#define CONFIG_DICT_TYPE_OCD "OCD"
#define CONFIG_DICT_TYPE_TEXT "TEXT"

typedef struct {
    opencc_dictionary_type dict_type;
    char *file_name;
    size_t index;
    size_t stamp;
} dictionary_buffer;

struct _config_desc {
    char *title;
    char *description;
    dictionary_set_t dictionary_set;
    char *home_dir;
    dictionary_buffer dicts[DICTIONARY_MAX_COUNT];
    size_t dicts_count;
    size_t stamp;
};
typedef struct _config_desc config_desc;

static config_error errnum = CONFIG_ERROR_VOID;

static int qsort_dictionary_buffer_cmp(const void *a, const void *b) {
    if (((dictionary_buffer *)a)->index < ((dictionary_buffer *)b)->index)
        return -1;
    if (((dictionary_buffer *)a)->index > ((dictionary_buffer *)b)->index)
        return 1;
    return ((dictionary_buffer *)a)->stamp < ((dictionary_buffer *)b)->stamp ? -1 : 1;
}

static int load_dictionary(config_desc *config) {
    if (config->dicts_count == 0)
        return 0;

    qsort(config->dicts, config->dicts_count, sizeof(config->dicts[0]), qsort_dictionary_buffer_cmp);

    size_t i, last_index = 0;
    dictionary_group_t group = dictionary_set_new_group(config->dictionary_set);

    for (i = 0; i < config->dicts_count; i++) {
        if (config->dicts[i].index > last_index) {
            last_index = config->dicts[i].index;
            group = dictionary_set_new_group(config->dictionary_set);
        }
        dictionary_group_load(group, config->dicts[i].file_name, config->home_dir, config->dicts[i].dict_type);
    }

    return 0;
}

static int parse_add_dict(config_desc *config, size_t index, const char *dictstr) {
    const char *pstr = dictstr;

    while (*pstr != '\0' && *pstr != ' ')
        pstr++;

    opencc_dictionary_type dict_type;

    if (strncmp(dictstr, CONFIG_DICT_TYPE_OCD, sizeof(CONFIG_DICT_TYPE_OCD) - 1) == 0)
        dict_type = OPENCC_DICTIONARY_TYPE_DATRIE;
    else if (strncmp(dictstr, CONFIG_DICT_TYPE_TEXT, sizeof(CONFIG_DICT_TYPE_OCD) - 1) == 0)
        dict_type = OPENCC_DICTIONARY_TYPE_TEXT;
    else {
        errnum = CONFIG_ERROR_INVALID_DICT_TYPE;
        return -1;
    }

    while (*pstr != '\0' && (*pstr == ' ' || *pstr == '\t'))
        pstr++;

    size_t i = config->dicts_count++;

    config->dicts[i].dict_type = dict_type;
    config->dicts[i].file_name = mstrcpy(pstr);
    config->dicts[i].index = index;
    config->dicts[i].stamp = config->stamp++;

    return 0;
}

static int parse_property(config_desc *config, const char *key, const char *value) {
    if (strncmp(key, "dict", 4) == 0) {
        int index = 0;
        sscanf(key + 4, "%d", &index);
        return parse_add_dict(config, index, value);
    } else if (strcmp(key, "title") == 0) {
        free(config->title);
        config->title = mstrcpy(value);
        return 0;
    } else if (strcmp(key, "description") == 0) {
        free(config->description);
        config->description = mstrcpy(value);
        return 0;
    }

    errnum = CONFIG_ERROR_NO_PROPERTY;
    return -1;
}

static int parse_line(const char *line, char **key, char **value) {
    const char *line_begin = line;

    while (*line != '\0' && (*line != ' ' && *line != '\t' && *line != '='))
        line++;

    size_t key_len = line - line_begin;

    while (*line != '\0' && *line != '=')
        line++;

    if (*line == '\0')
        return -1;

    assert(*line == '=');

    *key = mstrncpy(line_begin, key_len);

    line++;
    while (*line != '\0' && (*line == ' ' || *line == '\t'))
        line++;

    if (*line == '\0') {
        free(*key);
        return -1;
    }

    *value = mstrcpy(line);

    return 0;
}

static char *parse_trim(char *str) {
    for (; *str != '\0' && (*str == ' ' || *str == '\t'); str++)
        ;
    register char *prs = str;
    for (; *prs != '\0' && *prs != '\n' && *prs != '\r'; prs++)
        ;
    for (prs--; prs > str && (*prs == ' ' || *prs == '\t'); prs--)
        ;
    *(++prs) = '\0';
    return str;
}

static int parse(config_desc *config, const char *filename, const char *home_path) {
    FILE *fp = fopen(filename, "rb");
    if (!fp) {
        size_t pkg_filename_len = strlen(filename) + strlen(home_path) + 2;
        char *pkg_filename = (char *)malloc(sizeof(char) * pkg_filename_len);
        snprintf(pkg_filename, pkg_filename_len, "%s/%s", home_path, filename);
        printf("pkg_filename %s\n", pkg_filename);
        fp = fopen(pkg_filename, "rb");
        if (!fp) {
            free(pkg_filename);
            errnum = CONFIG_ERROR_CANNOT_ACCESS_CONFIG_FILE;
            return -1;
        }
        free(pkg_filename);
    }

    size_t home_dir_len = strlen(home_path) + 1;
    config->home_dir = (char *)malloc(sizeof(char) * home_dir_len);
    snprintf(config->home_dir, home_dir_len, "%s", home_path);

    char buff[BUFFER_SIZE];

    while (fgets(buff, BUFFER_SIZE, fp) != NULL) {
        /* Detect line truncation: if buffer is full and last char is not newline,
         * the line was longer than BUFFER_SIZE-1 bytes. Drain the remainder and
         * treat this as a parse error to avoid processing partial config lines. */
        size_t buff_len = strlen(buff);
        if (buff_len == BUFFER_SIZE - 1 && buff[buff_len - 1] != '\n') {
            int c;
            while ((c = fgetc(fp)) != '\n' && c != EOF)
                ;
            fclose(fp);
            errnum = CONFIG_ERROR_PARSE;
            return -1;
        }

        char *trimed_buff = parse_trim(buff);
        if (*trimed_buff == ';' || *trimed_buff == '#' || *trimed_buff == '\0') {
            /* Comment Line or empty line */
            continue;
        }

        char *key = NULL, *value = NULL;

        if (parse_line(trimed_buff, &key, &value) == -1) {
            free(key);
            free(value);
            fclose(fp);
            errnum = CONFIG_ERROR_PARSE;
            return -1;
        }

        if (parse_property(config, key, value) == -1) {
            free(key);
            free(value);
            fclose(fp);
            return -1;
        }

        free(key);
        free(value);
    }

    fclose(fp);
    return 0;
}

dictionary_set_t config_get_dictionary_set(config_t t_config) {
    config_desc *config = (config_desc *)t_config;

    if (config->dictionary_set != NULL) {
        dictionary_set_close(config->dictionary_set);
    }

    config->dictionary_set = dictionary_set_open();
    load_dictionary(config);

    return config->dictionary_set;
}

config_error config_errno(void) { return errnum; }

void config_perror(const char *spec) {
    perr(spec);
    perr("\n");
    switch (errnum) {
        case CONFIG_ERROR_VOID:
            break;
        case CONFIG_ERROR_CANNOT_ACCESS_CONFIG_FILE:
            perror(_("Can not access configuration file"));
            break;
        case CONFIG_ERROR_PARSE:
            perr(_("Configuration file parse error"));
            break;
        case CONFIG_ERROR_NO_PROPERTY:
            perr(_("Invalid property"));
            break;
        case CONFIG_ERROR_INVALID_DICT_TYPE:
            perr(_("Invalid dictionary type"));
            break;
        default:
            perr(_("Unknown"));
    }
}

config_t config_open(const char *filename, const char *home_path) {
    config_desc *config = (config_desc *)malloc(sizeof(config_desc));

    config->title = NULL;
    config->description = NULL;
    config->home_dir = NULL;
    config->dicts_count = 0;
    config->stamp = 0;
    config->dictionary_set = NULL;

    if (parse(config, filename, home_path) == -1) {
        config_close((config_t)config);
        return (config_t)-1;
    }

    return (config_t)config;
}

void config_close(config_t t_config) {
    config_desc *config = (config_desc *)t_config;

    size_t i;
    for (i = 0; i < config->dicts_count; i++)
        free(config->dicts[i].file_name);

    free(config->title);
    free(config->description);
    free(config->home_dir);
    free(config);
}
