
/*
 * Beat Rat -- settings
 *
 * Persisted to ~/.config/beatrat/settings.conf (respecting
 * $XDG_CONFIG_HOME if set), deliberately NOT next to the binary --
 * that would break the moment this gets installed system-wide via the
 * eventual Gentoo ebuild. A plain key=value text file, hand-parsed, in
 * keeping with the project's "no XML/JSON dependency" stance.
 */

#include "settings.h"
#include "log.h"
#include "ui.h"
#include "version.h"

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

/* GCC's -Wformat-truncation is being conservative here about a
 * theoretical extremely-long $HOME/$XDG_CONFIG_HOME -- worst case is a
 * truncated path (still safe, just wrong), not a buffer overflow. Not
 * worth chasing with ever-larger buffers. */
#pragma GCC diagnostic ignored "-Wformat-truncation"

static void label_from_path(const char *path, char *label, size_t label_size);

/* Creates every path component in turn, ignoring EEXIST -- the
 * standard "mkdir -p" pattern. A single-level mkdir() isn't enough
 * here: on a genuinely fresh system, even ~/.local or ~/.config may
 * not exist yet, not just our own beatrat subdirectory under them. */
static void mkdir_p(const char *path) {
    char tmp[768];
    snprintf(tmp, sizeof(tmp), "%s", path);
    size_t len = strlen(tmp);
    if (len > 0 && tmp[len - 1] == '/')
        tmp[len - 1] = '\0';
    for (char *p = tmp + 1; *p; p++) {
        if (*p == '/') {
            *p = '\0';
            if (mkdir(tmp, 0755) != 0 && errno != EEXIST)
                LOG_WARN("settings: could not create '%s' (%s)", tmp, strerror(errno));
            *p = '/';
        }
    }
    if (mkdir(tmp, 0755) != 0 && errno != EEXIST)
        LOG_WARN("settings: could not create '%s' (%s)", tmp, strerror(errno));
}

/* ~/.config/beatrat itself -- the parent of both config/ (settings and
 * keybinds) and plugins/ (empty for now, reserved for a future plugin
 * system). Kept separate from config_dir_path() below so both this
 * file and keybinds.c's own copy resolve to the same root. */
static void config_root_path(char *buf, size_t buf_size) {
    const char *xdg = getenv("XDG_CONFIG_HOME");
    if (xdg && *xdg) {
        snprintf(buf, buf_size, "%s/beatrat", xdg);
        return;
    }

    const char *home = getenv("HOME");
    if (!home || !*home)
        home = "."; /* last-resort fallback; should essentially never happen */

    snprintf(buf, buf_size, "%s/.config/beatrat", home);
}

static void config_dir_path(char *buf, size_t buf_size) {
    char root[768];
    config_root_path(root, sizeof(root));
    snprintf(buf, buf_size, "%s/config", root);
}

/* Creates (if missing) the empty plugins/ folder alongside config/ --
 * nothing reads from it yet, but it exists from first run so a future
 * plugin system has somewhere to look without needing its own
 * first-run setup logic. */
static void ensure_plugins_dir(void) {
    char root[768];
    config_root_path(root, sizeof(root));
    char plugins[900];
    snprintf(plugins, sizeof(plugins), "%s/plugins", root);
    mkdir_p(plugins);
}

static void config_file_path(char *buf, size_t buf_size) {
    char dir[768];
    config_dir_path(dir, sizeof(dir));
    mkdir_p(dir);
    ensure_plugins_dir();
    snprintf(buf, buf_size, "%s/settings.conf", dir);
}

static void data_dir_path(char *buf, size_t buf_size) {
    const char *xdg = getenv("XDG_DATA_HOME");
    if (xdg && *xdg) {
        snprintf(buf, buf_size, "%s/beatrat", xdg);
        return;
    }

    const char *home = getenv("HOME");
    if (!home || !*home)
        home = "."; /* last-resort fallback; should essentially never happen */

    snprintf(buf, buf_size, "%s/.local/share/beatrat", home);
}

void settings_projects_dir(char *buf, size_t buf_size) {
    char dir[768];
    data_dir_path(dir, sizeof(dir));
    snprintf(buf, buf_size, "%s/projects", dir);
    mkdir_p(buf);
}

void settings_samples_dir(char *buf, size_t buf_size) {
    char dir[768];
    data_dir_path(dir, sizeof(dir));
    snprintf(buf, buf_size, "%s/samples", dir);
    mkdir_p(buf);
}

void settings_set_defaults(Settings *s) {
    const char *home = getenv("HOME");
    if (!home || !*home)
        home = "."; /* last-resort fallback; should essentially never happen */
    snprintf(s->default_import_dir, sizeof(s->default_import_dir), "%s/Audio", home);
    snprintf(s->default_export_dir, sizeof(s->default_export_dir), "%s/Audio/Saved Beats", home);
    s->always_on_top = 0;
    s->num_alt_dirs = 0;
    s->output_device[0] = '\0';
    s->input_device[0] = '\0';
    s->volume_percent = 100;
    s->num_hidden_devices = 0;
    s->auto_deselect_region = 1;
}

void settings_load(Settings *s) {
    settings_set_defaults(s);

    char path[768];
    config_file_path(path, sizeof(path));

    FILE *fp = fopen(path, "r");
    if (!fp) {
        LOG_INFO("settings: no existing config at '%s', using defaults", path);
        /* Genuinely fresh config only -- "null" (ALSA's discard/silence
         * PCM) starts hidden from Prev/Next cycling since it's never a
         * meaningful choice for real audio I/O, but it's still enumerated
         * and can be unhidden from the Audio panel for debugging/testing.
         * Written to disk immediately so this seeding happens exactly
         * once: once settings.conf exists, whatever's actually saved
         * there is authoritative, same as keybinds.conf's philosophy. */
        settings_hide_device(s, "null");
        settings_save(s);
        return;
    }

    char line[600];
    while (fgets(line, sizeof(line), fp)) {
        line[strcspn(line, "\r\n")] = '\0';
        char *eq = strchr(line, '=');
        if (!eq)
            continue;
        *eq = '\0';
        char *key = line;
        char *value = eq + 1;

        if (strcmp(key, "default_import_dir") == 0)
            snprintf(s->default_import_dir, sizeof(s->default_import_dir), "%s", value);
        else if (strcmp(key, "default_export_dir") == 0)
            snprintf(s->default_export_dir, sizeof(s->default_export_dir), "%s", value);
        else if (strcmp(key, "always_on_top") == 0)
            s->always_on_top = atoi(value);
        else if (strcmp(key, "output_device") == 0)
            snprintf(s->output_device, sizeof(s->output_device), "%s", value);
        else if (strcmp(key, "input_device") == 0)
            snprintf(s->input_device, sizeof(s->input_device), "%s", value);
        else if (strcmp(key, "volume_percent") == 0)
            s->volume_percent = atoi(value);
        else if (strcmp(key, "auto_deselect_region") == 0)
            s->auto_deselect_region = atoi(value);
        else if (strcmp(key, "hidden_device") == 0 && s->num_hidden_devices < SETTINGS_MAX_HIDDEN_DEVICES) {
            snprintf(s->hidden_devices[s->num_hidden_devices], sizeof(s->hidden_devices[0]), "%s", value);
            s->num_hidden_devices++;
        }
        else if (strcmp(key, "alt_dir") == 0 && s->num_alt_dirs < SETTINGS_MAX_ALT_DIRS) {
            char *sep = strchr(value, '|');
            AltDir *d = &s->alt_dirs[s->num_alt_dirs];
            if (sep) {
                *sep = '\0';
                snprintf(d->label, sizeof(d->label), "%s", value);
                snprintf(d->path, sizeof(d->path), "%s", sep + 1);
            } else {
                /* No label field (shouldn't normally happen) -- derive one. */
                snprintf(d->path, sizeof(d->path), "%s", value);
                label_from_path(value, d->label, sizeof(d->label));
            }
            s->num_alt_dirs++;
        }
    }
    fclose(fp);
    LOG_INFO("settings: loaded from '%s'", path);
}

int settings_save(const Settings *s) {
    char path[768];
    config_file_path(path, sizeof(path));

    FILE *fp = fopen(path, "w");
    if (!fp) {
        LOG_ERROR("settings: could not write '%s'", path);
        return -1;
    }
    fprintf(fp, "default_import_dir=%s\n", s->default_import_dir);
    fprintf(fp, "default_export_dir=%s\n", s->default_export_dir);
    fprintf(fp, "always_on_top=%d\n", s->always_on_top);
    fprintf(fp, "output_device=%s\n", s->output_device);
    fprintf(fp, "input_device=%s\n", s->input_device);
    fprintf(fp, "volume_percent=%d\n", s->volume_percent);
    fprintf(fp, "auto_deselect_region=%d\n", s->auto_deselect_region);
    for (int i = 0; i < s->num_hidden_devices; i++)
        fprintf(fp, "hidden_device=%s\n", s->hidden_devices[i]);
    for (int i = 0; i < s->num_alt_dirs; i++)
        fprintf(fp, "alt_dir=%s|%s\n", s->alt_dirs[i].label, s->alt_dirs[i].path);
    fclose(fp);
    return 0;
}

void settings_append_char_to(char *field, size_t field_size, char c) {
    size_t len = strlen(field);
    if (len + 1 < field_size) {
        field[len] = c;
        field[len + 1] = '\0';
    }
}

void settings_backspace_from(char *field) {
    size_t len = strlen(field);
    if (len > 0)
        field[len - 1] = '\0';
}

static void label_from_path(const char *path, char *label, size_t label_size) {
    const char *slash = strrchr(path, '/');
    const char *base = slash ? slash + 1 : path;
    if (*base == '\0')
        base = path; /* path ended in '/' (or was just "/") -- fall back to the whole thing */
    snprintf(label, label_size, "%s", base);
}

int settings_add_alt_dir(Settings *s, const char *path) {
    if (s->num_alt_dirs >= SETTINGS_MAX_ALT_DIRS) {
        LOG_WARN("settings: alternate directory list full (max %d)", SETTINGS_MAX_ALT_DIRS);
        return -1;
    }
    for (int i = 0; i < s->num_alt_dirs; i++) {
        if (strcmp(s->alt_dirs[i].path, path) == 0) {
            LOG_INFO("settings: '%s' is already in the alternate directory list", path);
            return -1;
        }
    }
    AltDir *d = &s->alt_dirs[s->num_alt_dirs];
    snprintf(d->path, sizeof(d->path), "%s", path);
    label_from_path(path, d->label, sizeof(d->label));
    s->num_alt_dirs++;
    return s->num_alt_dirs - 1;
}

void settings_remove_alt_dir(Settings *s, int idx) {
    if (idx < 0 || idx >= s->num_alt_dirs)
        return;
    for (int i = idx; i < s->num_alt_dirs - 1; i++)
        s->alt_dirs[i] = s->alt_dirs[i + 1];
    s->num_alt_dirs--;
}

int settings_is_device_hidden(const Settings *s, const char *name) {
    for (int i = 0; i < s->num_hidden_devices; i++)
        if (strcmp(s->hidden_devices[i], name) == 0)
            return 1;
    return 0;
}

int settings_hide_device(Settings *s, const char *name) {
    if (settings_is_device_hidden(s, name))
        return -1;
    if (s->num_hidden_devices >= SETTINGS_MAX_HIDDEN_DEVICES) {
        LOG_WARN("settings: hidden-device list full (max %d)", SETTINGS_MAX_HIDDEN_DEVICES);
        return -1;
    }
    snprintf(s->hidden_devices[s->num_hidden_devices], sizeof(s->hidden_devices[0]), "%s", name);
    s->num_hidden_devices++;
    return 0;
}

void settings_unhide_device(Settings *s, const char *name) {
    for (int i = 0; i < s->num_hidden_devices; i++) {
        if (strcmp(s->hidden_devices[i], name) == 0) {
            for (int j = i; j < s->num_hidden_devices - 1; j++)
                snprintf(s->hidden_devices[j], sizeof(s->hidden_devices[0]), "%s", s->hidden_devices[j + 1]);
            s->num_hidden_devices--;
            return;
        }
    }
}

static void draw_field(Display *dpy, Window win, GC gc, int x, int y, int w, int h,
                        const char *text, int focused) {
    XSetForeground(dpy, gc, 0xFFFFFF);
    XFillRectangle(dpy, win, gc, x, y, w, h);
    XSetForeground(dpy, gc, focused ? 0x000000 : 0x808080);
    if (focused) {
        XSetLineAttributes(dpy, gc, 2, LineSolid, CapButt, JoinMiter);
        XDrawRectangle(dpy, win, gc, x, y, w, h);
        XSetLineAttributes(dpy, gc, 1, LineSolid, CapButt, JoinMiter);
    } else {
        XDrawRectangle(dpy, win, gc, x, y, w, h);
    }
    XSetForeground(dpy, gc, 0x000000);
    XDrawString(dpy, win, gc, x + 4, y + h - 6, text, (int)strlen(text));
    if (focused) {
        int cursor_x = x + 4 + (int)strlen(text) * 6 + 1;
        XDrawLine(dpy, win, gc, cursor_x, y + 3, cursor_x, y + h - 3);
    }
}

void settings_draw(const Settings *s, Display *dpy, Window win, GC gc,
                    int win_w, int win_h, int active_field, const char *altdir_input) {
    int box_w = SETTINGS_BOX_W, box_h = SETTINGS_BOX_H;
    int x = (win_w - box_w) / 2;
    int y = (win_h - box_h) / 2;

    ui_draw_bevel(dpy, win, gc, x, y, box_w, box_h, 0);

    XSetForeground(dpy, gc, 0x000000);
    XDrawString(dpy, win, gc, x + 8, y + 18, "Settings", 8);
    {
        char verbuf[32];
        snprintf(verbuf, sizeof(verbuf), "Beat Rat v%s", BEATRAT_VERSION);
        int vw = (int)strlen(verbuf) * 6;
        XSetForeground(dpy, gc, 0x606060);
        XDrawString(dpy, win, gc, x + box_w - vw - 8, y + 18, verbuf, (int)strlen(verbuf));
        XSetForeground(dpy, gc, 0x000000);
    }

    const char *import_label = "Default import directory:";
    XDrawString(dpy, win, gc, x + 8, y + 38, import_label, (int)strlen(import_label));
    draw_field(dpy, win, gc, x + 8, y + SETTINGS_IMPORT_FIELD_Y, box_w - 16, SETTINGS_FIELD_H,
               s->default_import_dir, active_field == 0);

    const char *export_label = "Default export directory:";
    XDrawString(dpy, win, gc, x + 8, y + 82, export_label, (int)strlen(export_label));
    draw_field(dpy, win, gc, x + 8, y + SETTINGS_EXPORT_FIELD_Y, box_w - 16, SETTINGS_FIELD_H,
               s->default_export_dir, active_field == 1);

    int cb_x = x + SETTINGS_CHECKBOX_X;
    int cb_y = y + SETTINGS_CHECKBOX_Y;
    XSetForeground(dpy, gc, 0xFFFFFF);
    XFillRectangle(dpy, win, gc, cb_x, cb_y, SETTINGS_CHECKBOX_SIZE, SETTINGS_CHECKBOX_SIZE);
    XSetForeground(dpy, gc, 0x000000);
    XDrawRectangle(dpy, win, gc, cb_x, cb_y, SETTINGS_CHECKBOX_SIZE, SETTINGS_CHECKBOX_SIZE);
    if (s->always_on_top) {
        XDrawLine(dpy, win, gc, cb_x + 2, cb_y + 2,
                  cb_x + SETTINGS_CHECKBOX_SIZE - 2, cb_y + SETTINGS_CHECKBOX_SIZE - 2);
        XDrawLine(dpy, win, gc, cb_x + SETTINGS_CHECKBOX_SIZE - 2, cb_y + 2,
                  cb_x + 2, cb_y + SETTINGS_CHECKBOX_SIZE - 2);
    }
    const char *cb_label = "Always on top";
    XDrawString(dpy, win, gc, cb_x + SETTINGS_CHECKBOX_SIZE + 8, cb_y + 13,
                cb_label, (int)strlen(cb_label));

    int cb2_x = x + SETTINGS_CHECKBOX_X;
    int cb2_y = y + SETTINGS_CHECKBOX2_Y;
    XSetForeground(dpy, gc, 0xFFFFFF);
    XFillRectangle(dpy, win, gc, cb2_x, cb2_y, SETTINGS_CHECKBOX_SIZE, SETTINGS_CHECKBOX_SIZE);
    XSetForeground(dpy, gc, 0x000000);
    XDrawRectangle(dpy, win, gc, cb2_x, cb2_y, SETTINGS_CHECKBOX_SIZE, SETTINGS_CHECKBOX_SIZE);
    if (s->auto_deselect_region) {
        XDrawLine(dpy, win, gc, cb2_x + 2, cb2_y + 2,
                  cb2_x + SETTINGS_CHECKBOX_SIZE - 2, cb2_y + SETTINGS_CHECKBOX_SIZE - 2);
        XDrawLine(dpy, win, gc, cb2_x + SETTINGS_CHECKBOX_SIZE - 2, cb2_y + 2,
                  cb2_x + 2, cb2_y + SETTINGS_CHECKBOX_SIZE - 2);
    }
    const char *cb2_label = "Auto-deselect region after copy/cut/void";
    XDrawString(dpy, win, gc, cb2_x + SETTINGS_CHECKBOX_SIZE + 8, cb2_y + 13,
                cb2_label, (int)strlen(cb2_label));

    ui_draw_button(dpy, win, gc, x + SETTINGS_CHECKBOX_X, y + SETTINGS_KEYBINDS_BTN_Y,
                   SETTINGS_KEYBINDS_BTN_W, SETTINGS_KEYBINDS_BTN_H, "Keybinds...", 0);

    const char *alt_label = "Add alternate import directory (Enter to add):";
    XDrawString(dpy, win, gc, x + 8, y + SETTINGS_ALTDIR_FIELD_Y - 8, alt_label, (int)strlen(alt_label));
    draw_field(dpy, win, gc, x + 8, y + SETTINGS_ALTDIR_FIELD_Y, box_w - 16, SETTINGS_FIELD_H,
               altdir_input, active_field == 2);

    int list_y = y + SETTINGS_ALTDIR_LIST_Y;
    if (s->num_alt_dirs == 0) {
        XSetForeground(dpy, gc, 0x606060);
        const char *none = "(none added yet)";
        XDrawString(dpy, win, gc, x + 8, list_y + 14, none, (int)strlen(none));
    } else {
        for (int i = 0; i < s->num_alt_dirs; i++) {
            int ry = list_y + i * SETTINGS_ALTDIR_ROW_H;
            char line[600];
            snprintf(line, sizeof(line), "%s  --  %s", s->alt_dirs[i].label, s->alt_dirs[i].path);
            XSetForeground(dpy, gc, 0x000000);
            XDrawString(dpy, win, gc, x + 8, ry + 14, line, (int)strlen(line));
            ui_draw_button(dpy, win, gc, x + box_w - 16 - SETTINGS_ALTDIR_REMOVE_W, ry,
                           SETTINGS_ALTDIR_REMOVE_W, SETTINGS_ALTDIR_ROW_H - 2, "x", 0);
        }
    }

    XSetForeground(dpy, gc, 0x606060);
    const char *hint = "Click a field or Tab to switch -- type to edit -- Enter/Esc to close";
    XDrawString(dpy, win, gc, x + 8, y + box_h - 10, hint, (int)strlen(hint));
}

