/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * See the COPYING file for license information.
 *
 * Guillaume Chazarain <guichaz@gmail.com>
 */

/**********************
 * Configuration file *
 **********************/

#include <unistd.h>             /* R_OK */
#include <stdio.h>              /* FILE, f*(), perror(), getdelim() */
#include <string.h>             /* strlen(), memcpy() */
#include <fcntl.h>              /* open(), O_RDONLY, close(), lseek() */

#include "gliv.h"
#include "rcfile.h"
#include "large_files.h"
#include "options.h"
#include "messages.h"
#include "actions.h"

#ifndef HAVE_GETDELIM
#include "../lib/getdelim.h"
#endif

static GHashTable *table = NULL;
static gchar *read_config_file = NULL;
static gchar *write_config_file = NULL;

/* The link between options and the rcfile. */
static options_struct opts = {
/* Default options */
    .fullscreen = FALSE,
    .maximize = FALSE,
    .scaledown = FALSE,
    .menu_bar = TRUE,
    .status_bar = TRUE,
    .scrollbars = TRUE,
    .zoom_pointer = FALSE,
    .alpha_checks = TRUE,
    .dither = FALSE,
    .force = FALSE,
    .build_menus = TRUE,
    .mipmap = FALSE,
    .mnemonics = FALSE,
    .loop = FALSE,
    .one_image = FALSE,
    .delay = 0,
    .history_size = 1000,
    .thumbnails = TRUE,
    .resize_win = TRUE,
    .start_show = FALSE,
    .confirm_quit = TRUE,
    .save_quit = TRUE,
    .recursive = FALSE,
    .transitions = TRUE,
    .keep_transfo = FALSE,
    .opengl_errors = FALSE,
    .filtering = TRUE,
    .duration = 10,
    .fps = 100,
    .thumb_width = 128,
    .thumb_height = 64,
    .notice_time = 500,
    .trans_time = 500,
    .initial_pos = POSITION_CENTER,
    .bg_col = {0, 0, 0, 0},
    .alpha1 = {0, 0x6666, 0x6666, 0x6666},
    .alpha2 = {0, 0x9999, 0x9999, 0x9999}
};

typedef struct {
    const gchar *name;
    gpointer option;
    const gchar *comment;
    const gboolean is_bool;
} option_struct;

/* *INDENT-OFF* */
static option_struct option_names[] = {
/* To fill the hash table and the configuration file. */
{ "full-screen",  &opts.fullscreen,   N_("Start in full screen mode"),      1 },
{ "maximize",     &opts.maximize,     N_("Maximize small images"),          1 },
{ "scale-down",   &opts.scaledown,    N_("Scale down large images"),        1 },
{ "menu",         &opts.menu_bar,     N_("Display the menu bar"),           1 },
{ "info",         &opts.status_bar,   N_("Display info about the image"),   1 },
{ "scrollbars",   &opts.scrollbars,   N_("Display scrollbars"),             1 },
{ "zoom-pointer", &opts.zoom_pointer, N_("Zoom centered on pointer"),       1 },
{ "alpha-checks", &opts.alpha_checks, N_("Alpha checks in the background"), 1 },
{ "dither",       &opts.dither,       N_("Dithering"),                      1 },
{ "force-load",   &opts.force,        N_("Try to load every file"),         1 },
{ "build-menus",  &opts.build_menus,  N_("Build images menus at startup"),  1 },
{ "mipmap",       &opts.mipmap,       N_("Build mipmaps"),                  1 },
{ "mnemonics",    &opts.mnemonics,    N_("Make mnemonics for images menus"),1 },
{ "loop",         &opts.loop,         N_("Make the slide show loop"),       1 },
{ "one_image",    &opts.one_image,    N_("Keep only one image in memory"),  1 },
{ "thumbnails",   &opts.thumbnails,   N_("Show thumbnails in images menus"),1 },
{ "start_show",   &opts.start_show,   N_("Start with the slide show"),      1 },
{ "resize_win",   &opts.resize_win,   N_("Automatic window resizing"),      1 },
{ "confirm_quit", &opts.confirm_quit, N_("Confirm before quitting"),        1 },
{ "save_quit",    &opts.save_quit,    N_("Save options when quitting"),     1 },
{ "transitions",  &opts.transitions,  N_("Enable transitions"),             1 },
{ "recursive",    &opts.recursive,    N_("Recursive directory traversal"),  1 },
{ "keep_transfo", &opts.keep_transfo, N_("Keep transfo between images"),    1 },
{ "opengl-errors",&opts.opengl_errors,N_("Check for OpenGL errors"),        1 },
{ "filtering",    &opts.filtering,    N_("Enable OpenGL filtering"),        1 },
{ "fps",          &opts.fps,          N_("Maximum framerate"),              0 },
{ "delay",        &opts.delay,        N_("Delay before hiding the cursor"), 0 },
{ "history",      &opts.history_size, N_("History length"),                 0 },
{ "slide-show",   &opts.duration,     N_("Delay between images"),           0 },
{ "thumb-width",  &opts.thumb_width,  N_("Thumbnail width"),                0 },
{ "thumb-height", &opts.thumb_height, N_("Thumbnail height"),               0 },
{ "notice-time",  &opts.notice_time,  N_("Last/First image notice time"),   0 },
{ "trans-time",   &opts.trans_time,   N_("Transition duration"),            0 },
{ "initial_pos",  &opts.initial_pos,  N_("Initial image position"),         0 },
{ "bg_col_red",   &opts.bg_col.red,   N_("background: red channel"),        0 },
{ "bg_col_green", &opts.bg_col.green, N_("background: green channel"),      0 },
{ "bg_col_blue",  &opts.bg_col.blue,  N_("background: blue channel"),       0 },
{ "alpha1_red",   &opts.alpha1.red,   N_("alpha1 tile: red channel"),       0 },
{ "alpha1_green", &opts.alpha1.green, N_("alpha1 tile: green channel"),     0 },
{ "alpha1_blue",  &opts.alpha1.blue,  N_("alpha1 tile: blue channel"),      0 },
{ "alpha2_red",   &opts.alpha2.red,   N_("alpha2 tile: red channel"),       0 },
{ "alpha2_green", &opts.alpha2.green, N_("alpha2 tile: green channel"),     0 },
{ "alpha2_blue",  &opts.alpha2.blue,  N_("alpha2 tile: blue channel"),      0 },
{ NULL,           NULL,               NULL,                                 0 }
};
/* *INDENT-ON* */

/*
 * Maximum length of the option names, currently
 * it is strlen("opengl-errors") == 13.
 * Used to indent the option file.
 */
#define MAX_OPT_LEN 13

/* Between the options and the keyboard accelerators. */
#define SEPARATOR_ACCELERATORS "==========\n"
#define SEPARATOR_ACTIONS      "========== Actions\n"

static const gchar *user_glivrc(void)
{
    static const gchar *filename = NULL;

    if (filename == NULL)
        /* First time */
        filename = g_build_filename(g_get_home_dir(), ".glivrc", NULL);

    return filename;
}

/*** Loading options. ***/

/* The hash table is the link between an option name and its value. */
static void init_hash_table(void)
{
    gint i;

    table = g_hash_table_new(g_str_hash, g_str_equal);

    for (i = 0; option_names[i].name != NULL; i++)
        g_hash_table_insert(table, (gchar *) option_names[i].name,
                            &option_names[i]);
}

 /* Processes spaces and '#'. */
static gchar *clean_str(const gchar * str)
{
    gchar *new_str;
    gchar *ptr;

    new_str = g_new(gchar, strlen(str) + 1);

    for (ptr = new_str; *str != '\n' && *str != '\0'; str++) {
        if (*str == ' ' || *str == '\t')
            continue;

        if (*str == '#')
            break;

        *ptr = *str;
        ptr++;
    }
    *ptr = '\0';

    return new_str;
}

static void process_line(const gchar * line)
{
    gchar **res;
    option_struct *opt;
    gchar *clean;

    if (*line == '\n' || *line == '\0' || *line == '#')
        /* Skip this line. */
        return;

    clean = clean_str(line);

    /* res[0]: option name ; res[1]: value */
    res = g_strsplit(clean, "=", 2);

    if (res[0] != NULL && res[1] != NULL && res[2] == NULL) {
        /* No error during split. */
        opt = g_hash_table_lookup(table, res[0]);

        if (opt != NULL) {
            /* Option found. */

            if (opt->is_bool) {
                gboolean *bool;

                bool = (gboolean *) opt->option;
                *bool = g_strcasecmp(res[1], "true") ? FALSE : TRUE;
            } else {
                /* opt->is_bool == FALSE */
                gint *value;

                value = (gint *) opt->option;
                *value = (gint) g_strtod(res[1], NULL);
            }
        } else
            g_printerr(_("Unknown option in configuration file: %s\n"), res[0]);
    } else
        g_printerr(_("Parse error in configuration file: %s\n"), clean);

    g_free(clean);
    g_strfreev(res);
}

#ifndef SYSCONFDIR
#define SYSCONFDIR "/etc"
#endif

typedef gboolean(*rcfile_func) (const gchar * filename);

static const gchar *foreach_rcfile(rcfile_func func, const gchar * filename)
{
    gchar *system_wide[] = { SYSCONFDIR "/glivrc", "/etc/glivrc" };
    gint i;

    if (func(filename))
        return filename;

    filename = user_glivrc();
    if (func(filename))
        return filename;

    for (i = 0; i < G_N_ELEMENTS(system_wide); i++) {
        if (func(system_wide[i]))
            return system_wide[i];
    }

    return NULL;
}

static gboolean load_rc_file(const gchar * filename)
{
    FILE *file;
    gchar *line = NULL;
    size_t nb = 0;

    if (filename == NULL || !g_file_test(filename, G_FILE_TEST_IS_REGULAR))
        return FALSE;

    file = fopen(filename, "r");
    if (file == NULL) {
        perror(filename);
        return FALSE;
    }

    while (getdelim(&line, &nb, '\n', file) >= 0) {
        if (g_str_equal(line, SEPARATOR_ACCELERATORS) ||
            g_str_equal(line, SEPARATOR_ACTIONS))
            break;

        process_line(line);
    }

    g_free(line);
    fclose(file);
    return TRUE;
}

options_struct *load_rc(gboolean default_file, const gchar * filename)
{
    if (default_file == FALSE && filename == NULL) {
        opts.save_quit = FALSE;
        return &opts;
    }

    init_hash_table();

    read_config_file = g_strdup(foreach_rcfile(load_rc_file, filename));
    write_config_file = g_strdup(filename);

    g_hash_table_destroy(table);

    return &opts;
}

static void process_action_line(const gchar * line)
{
    static gchar *action_name = NULL;
    const gchar *str = line;
    gint length;
    gboolean is_action_name;

    if (*line == '\n' || *line == '\0' || *line == '#')
        /* Skip this line. */
        return;

    if (g_str_has_prefix(line, "action_name") == FALSE &&
        g_str_has_prefix(line, "action_command") == FALSE)
        return;

    is_action_name = g_str_has_prefix(line, "action_name");
    str += is_action_name ? strlen("action_name") : strlen("action_command");
    while (*str == ' ' || *str == '\t')
        str++;

    if (*str != '=')
        /* Invalid line. */
        return;

    do {
        str++;
    } while (*str == ' ' || *str == '\t');

    length = strlen(str);
    while (length > 0 && str[length - 1] == '\n')
        length--;

    if (is_action_name) {
        g_free(action_name);
        action_name = g_strndup(str, length);

    } else if (action_name != NULL) {
        gchar *action_command = g_strndup(str, length);

        add_action(action_name, action_command);
        g_free(action_name);
        g_free(action_command);
        action_name = NULL;
    }
}

static void load_actions_file(const gchar * filename)
{
    FILE *file;
    gchar *line = NULL;
    size_t nb = 0;

    if (filename == NULL)
        return;

    file = fopen(filename, "r");
    if (file == NULL) {
        perror(filename);
        return;
    }

    /* Search the actions separator. */
    while (getdelim(&line, &nb, '\n', file) != -1 &&
           g_str_equal(line, SEPARATOR_ACTIONS) == FALSE);

    if (g_str_equal(line, SEPARATOR_ACTIONS) == FALSE) {
        g_free(line);
        fclose(file);
        return;
    }

    while (getdelim(&line, &nb, '\n', file) >= 0) {
        if (g_str_equal(line, SEPARATOR_ACCELERATORS) ||
            g_str_equal(line, SEPARATOR_ACTIONS))
            break;

        process_action_line(line);
    }

    g_free(line);
    fclose(file);
}

void load_actions(void)
{
    load_actions_file(read_config_file);
}

static void load_accelerators_file(const gchar * filename)
{
    FILE *file;
    gchar *line = NULL;
    size_t nb = 0;
    off_t position;
    gint fd;

    if (filename == NULL)
        return;

    file = fopen(filename, "r");
    if (file == NULL) {
        perror(filename);
        return;
    }

    /* Search the accelerators separator. */
    while (getdelim(&line, &nb, '\n', file) != -1 &&
           g_str_equal(line, SEPARATOR_ACCELERATORS) == FALSE);

    if (g_str_equal(line, SEPARATOR_ACCELERATORS) == FALSE) {
        g_free(line);
        fclose(file);
        return;
    }

    g_free(line);

    position = ftello(file);
    fd = fileno(file);
    if (lseek(fd, position, SEEK_SET) < 0) {
        perror("lseek");
        fclose(file);
        return;
    }

    gtk_accel_map_load_fd(fd);

    fclose(file);
}

void load_accelerators(void)
{
    load_accelerators_file(read_config_file);
}


/*** Saving options. ***/

static void write_option_line(FILE * f, gint index)
{
    gint i, value;

    fputs(option_names[index].name, f);

    for (i = strlen(option_names[index].name); i < MAX_OPT_LEN; i++)
        fputc(' ', f);

    value = *((gint *) option_names[index].option);

    if (option_names[index].is_bool)
        fprintf(f, " = %s\n\n", value ? "True" : "False");
    else
        fprintf(f, " = %d\n\n", value);
}

static gboolean write_rc(const gchar * filename)
{
    FILE *file;
    gint i;

    if (filename == NULL)
        return FALSE;

    file = fopen(filename, "w");
    if (file == NULL) {
        perror(filename);
        return FALSE;
    }

    fprintf(file, _("# Configuration file for GLiv %s\n\n"), VERSION);
    fputs(_("# Option names are case sensitive.\n"), file);
    fputs(_("# Option values are case insensitive.\n\n"), file);

    for (i = 0; option_names[i].name != NULL; i++) {
        /* The comment line. */
        fprintf(file, "# %s\n", _(option_names[i].comment));

        /* The option line. */
        write_option_line(file, i);
    }

    /* The separation between options and actions. */
    fprintf(file, "%s\n", SEPARATOR_ACTIONS);
    write_actions(file);

    /* The separation between actions and keyboard accelerators. */
    fprintf(file, "%s\n", SEPARATOR_ACCELERATORS);
    fflush(file);

    gtk_accel_map_save_fd(fileno(file));

    fclose(file);
    return TRUE;
}

void save_rc(options_struct * opt)
{
    memcpy(&opts, opt, sizeof(options_struct));

    write_rc(get_write_config_file());
}

const gchar *get_read_config_file(void)
{
    return read_config_file;
}

/* Yes it's racy, but there's nothing wrong in this case */
static gboolean can_write_config_file(const gchar * file)
{
    gchar *dirname;
    gboolean res;

    if (file == NULL)
        return FALSE;

    if (g_file_test(file, G_FILE_TEST_IS_DIR))
        return FALSE;

    if (g_file_test(file, G_FILE_TEST_EXISTS))
        return !access(file, W_OK);

    dirname = g_path_get_dirname(file);
    res = g_file_test(dirname, G_FILE_TEST_IS_DIR) && !access(dirname, W_OK);
    g_free(dirname);

    return res;
}

const gchar *get_write_config_file(void)
{
    if (can_write_config_file(write_config_file))
        return write_config_file;

    if (can_write_config_file(user_glivrc()))
        return user_glivrc();

    return NULL;
}
