#include "fzftab.mdh"
#include "fzftab.pro"
#include <stdarg.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

const char* get_color(char* file, const struct stat* sb);
const char* colorize_from_mode(char* file, const struct stat* sb);
const char* colorize_from_name(char* file);
int compile_patterns(char* nam, char** list_colors);
char* ftb_strcat(char* dst, int n, ...);

/* Indixes into the terminal string arrays. */
#define COL_NO 0
#define COL_FI 1
#define COL_DI 2
#define COL_LN 3
#define COL_PI 4
#define COL_SO 5
#define COL_BD 6
#define COL_CD 7
#define COL_OR 8
#define COL_MI 9
#define COL_SU 10
#define COL_SG 11
#define COL_TW 12
#define COL_OW 13
#define COL_ST 14
#define COL_EX 15
#define COL_LC 16
#define COL_RC 17
#define COL_EC 18
#define COL_TC 19
#define COL_SP 20
#define COL_MA 21
#define COL_HI 22
#define COL_DU 23
#define COL_SA 24

#define NUM_COLS 25

/* Names of the terminal strings. */
static char* colnames[] = { "no", "fi", "di", "ln", "pi", "so", "bd", "cd", "or", "mi", "su", "sg",
    "tw", "ow", "st", "ex", "lc", "rc", "ec", "tc", "sp", "ma", "hi", "du", "sa", NULL };

/* Default values. */
static char* defcols[]
    = { "0", "0", "1;31", "1;36", "33", "1;35", "1;33", "1;33", NULL, NULL, "37;41", "30;43",
          "30;42", "34;42", "37;44", "1;32", "\033[", "m", NULL, "0", "0", "7", NULL, NULL, "0" };

static char* fzf_tab_module_version;

struct pattern {
    Patprog pat;
    char color[50];
};

static int opt_list_type = OPT_INVALID;
static int pat_cnt = 0;
static struct pattern* name_color = NULL;
static char mode_color[NUM_COLS][20];

// TODO: use ZLS_COLORS ?
int compile_patterns(char* nam, char** list_colors)
{
    int i, j;
    // clean old name_color and set pat_cnt = 0
    if (pat_cnt != 0) {
        while (--pat_cnt) {
            freepatprog(name_color[pat_cnt].pat);
        }
        free(name_color);
    }
    // initialize mode_color with default value
    for (i = 0; i < NUM_COLS; i++) {
        if (defcols[i]) {
            strcpy(mode_color[i], defcols[i]);
        }
    }
    // empty array, just return
    if (list_colors == NULL) {
        return 0;
    }

    // how many pattens?
    while (list_colors[pat_cnt] != NULL) {
        pat_cnt++;
    }
    name_color = zshcalloc(pat_cnt * sizeof(struct pattern));

    for (i = 0; i < pat_cnt; i++) {
        char* pat = ztrdup(list_colors[i]);
        char* color = strrchr(pat, '=');
        if (color == NULL)
            continue;
        *color = '\0';
        tokenize(pat);
        remnulargs(pat);

        // mode=color
        bool skip = false;
        for (j = 0; j < NUM_COLS; j++) {
            if (strpfx(colnames[j], list_colors[i])) {
                strcpy(mode_color[j], color + 1);
                name_color[i].pat = NULL;
                skip = true;
            }
        }
        if (skip) {
            continue;
        }

        // name=color
        if ((name_color[i].pat = patcompile(pat, PAT_ZDUP, NULL))) {
            strcpy(name_color[i].color, color + 1);
        }

        free(pat);
    }
    return 0;
}

static char get_suffix(char* file, const struct stat* sb)
{
    struct stat sb2;
    mode_t filemode = sb->st_mode;

    if (S_ISLNK(filemode))
        if (strpfx(mode_color[COL_LN], "target")) {
            if (stat(file, &sb2) == -1) {
                return '@';
            }
            return get_suffix(file, &sb2);
        } else {
            return '@';
        }
    else
        return file_type(filemode);
}

const char* get_color(char* file, const struct stat* sb)
{
    // no list-colors, return NULL
    if (pat_cnt == 0) {
        return NULL;
    }
    const char* ret;
    if ((ret = colorize_from_mode(file, sb)) || (ret = colorize_from_name(file))) {
        return ret;
    }
    return mode_color[COL_FI];
}

const char* colorize_from_name(char* file)
{
    int i;
    for (i = 0; i < pat_cnt; i++) {
        if (name_color && name_color[i].pat && pattry(name_color[i].pat, file)) {
            return name_color[i].color;
        }
    }
    return NULL;
}

const char* colorize_from_mode(char* file, const struct stat* sb)
{
    struct stat sb2;

    switch (sb->st_mode & S_IFMT) {
    case S_IFDIR:
        return mode_color[COL_DI];
    case S_IFLNK: {
        if (strpfx(mode_color[COL_LN], "target")) {
            if (stat(file, &sb2) == -1) {
                return mode_color[COL_OR];
            }
            return get_color(file, &sb2);
        } else {
          return mode_color[COL_LN];
        }
    }
    case S_IFIFO:
        return mode_color[COL_PI];
    case S_IFSOCK:
        return mode_color[COL_SO];
    case S_IFBLK:
        return mode_color[COL_BD];
    case S_IFCHR:
        return mode_color[COL_CD];
    default:
        break;
    }

    if (sb->st_mode & S_ISUID) {
        return mode_color[COL_SU];
    } else if (sb->st_mode & S_ISGID) {
        return mode_color[COL_SG];
        // tw ow st ??
    } else if (sb->st_mode & S_IXUSR) {
        return mode_color[COL_EX];
    }

    /* Check for suffix alias */
    size_t len = strlen(file);
    /* shortest valid suffix format is a.b */
    if (len > 2) {
        const char* suf = file + len - 1;
        while (suf > file + 1) {
            if (suf[-1] == '.') {
                if (sufaliastab->getnode(sufaliastab, suf)) {
                    return mode_color[COL_SA];
                }
                break;
            }
            suf--;
        }
    }

    return NULL;
}

struct {
    char** array;
    size_t len;
    size_t size;
} ftb_compcap;

// Usage:
// initialize               fzf-tab-generate-compcap -i
// output to _ftb_compcap   fzf-tab-generate-compcap -o
// add a entry              fzf-tab-generate-compcap word desc opts
static int bin_fzf_tab_compcap_generate(char* nam, char** args, Options ops, UNUSED(int func))
{
    int i;

    if (OPT_ISSET(ops, 'o')) {
        // write final result to _ftb_compcap
        setaparam("_ftb_compcap", ftb_compcap.array);
        ftb_compcap.array = NULL;
        return 0;
    } else if (OPT_ISSET(ops, 'i')) {
        // init
        if (ftb_compcap.array)
            freearray(ftb_compcap.array);
        ftb_compcap.array = zshcalloc(1024 * sizeof(char*));
        ftb_compcap.len = 0, ftb_compcap.size = 1024;
        return 0;
    }
    if (ftb_compcap.array == NULL) {
        zwarnnam(nam, "please initialize it first");
        return 1;
    }

    // get paramaters
    char **words = getaparam(args[0]), **dscrs = getaparam(args[1]), *opts = getsparam(args[2]);
    if (!words || !dscrs || !opts) {
        zwarnnam(nam, "wrong argument");
        return 1;
    }

    char *bs = metafy("\2", 1, META_DUP), *nul = metafy("\0word\0", 6, META_DUP);
    size_t dscrs_cnt = arrlen(dscrs);

    for (i = 0; words[i]; i++) {
        // TODO: replace '\n'
        char* dscr = i < dscrs_cnt ? dscrs[i] : words[i];
        char *buffer = ftb_strcat(NULL, 5, dscr, bs, opts, nul, words[i]);
        ftb_compcap.array[ftb_compcap.len++] = buffer;

        if (ftb_compcap.len == ftb_compcap.size) {
            ftb_compcap.array
                = zrealloc(ftb_compcap.array, (1024 + ftb_compcap.size) * sizeof(char*));
            ftb_compcap.size += 1024;
            memset(ftb_compcap.array + ftb_compcap.size - 1024, 0, 1024 * sizeof(char*));
        }
    }

    zsfree(bs);
    zsfree(nul);

    return 0;
}

// cat n string, return the pointer to the new string
// dst will be reallocated if is not big enough
// if dst is NULL, it will be allocated
char* ftb_strcat(char* dst, int n, ...)
{
    int i, idx;

    va_list valist;
    va_start(valist, n);

    char* final = dst ? zrealloc(dst, 128) : zalloc(128);
    size_t size = 128, max_len = 128 - 1;
    dst = final;

    for (i = 0, idx = 0; i < n; i++) {
        char* src = va_arg(valist, char*);
        if (src == NULL)
            continue;
        for (; *src != '\0'; dst++, src++, idx++) {
            if (idx == max_len) {
                size += size / 2;
                final = zrealloc(final, size);
                max_len = size - 1;
                dst = &final[idx];
            }
            *dst = *src;
        }
    }
    *dst = '\0';
    va_end(valist);

    return final;
}

struct file_color {
    char *fc_begin;
    char *fc_end;
    char *sc;
    char *suffix;
};

// accept an
static struct file_color* fzf_tab_colorize(char* file)
{
    struct file_color *fc = zalloc(sizeof(struct file_color));

    file = unmeta(file);

    struct stat sb;
    if (lstat(file, &sb) == -1) {
        return NULL;
    }

    char suffix[2] = {0};
    if (isset(opt_list_type)) {
        suffix[0] = get_suffix(file, &sb);
    }
    const char* color = get_color(file, &sb);

    char* symlink = NULL;
    const char* symcolor = "";
    if ((sb.st_mode & S_IFMT) == S_IFLNK) {
        symlink = zshcalloc(PATH_MAX);
        size_t end = readlink(file, symlink, PATH_MAX);
        symlink[end] = '\0';
        if (stat(file, &sb) == -1) {
            symcolor = mode_color[COL_OR];
        } else {
            char tmp[PATH_MAX];
            realpath(file, tmp);
            symcolor = get_color(file, &sb);
        }
    }

    if (color != NULL) {
        fc->fc_begin = ftb_strcat(NULL, 3, mode_color[COL_LC], color, mode_color[COL_RC]);
        fc->fc_end = ftb_strcat(NULL, 3, mode_color[COL_LC], "0", mode_color[COL_RC]);
    } else {
        fc->fc_begin = ztrdup("");
        fc->fc_end = ztrdup("");
    }

    fc->suffix = ztrdup(suffix);

    if (symlink != NULL) {
        fc->sc = ftb_strcat(NULL, 7, mode_color[COL_LC], symcolor, mode_color[COL_RC],
                              symlink, mode_color[COL_LC], "0", mode_color[COL_RC]);
        free(symlink);
    } else {
        fc->sc = ztrdup("");
    }

    fc->fc_begin = metafy(fc->fc_begin, -1, META_REALLOC);
    fc->fc_end = metafy(fc->fc_end, -1, META_REALLOC);
    fc->sc = metafy(fc->sc, -1, META_REALLOC);
    fc->suffix = metafy(fc->suffix, -1, META_REALLOC);

    return fc;
}

static int bin_fzf_tab_candidates_generate(char* nam, char** args, Options ops, UNUSED(int func))
{
    int i, j;

    if (OPT_ISSET(ops, 'i')) {
        // compile list_colors pattern
        if (*args == NULL) {
            zwarnnam(nam, "please specify an array");
            return 1;
        } else {
            char** array = getaparam(*args);
            return compile_patterns(nam, array);
        }
    }

    char **ftb_compcap = getaparam("_ftb_compcap"), **group_colors = getaparam("group_colors"),
         *default_color = getsparam("default_color"), *prefix = getsparam("prefix");

    size_t group_colors_len = arrlen(group_colors);
    size_t ftb_compcap_len = arrlen(ftb_compcap);

    char *bs = metafy("\b", 1, META_DUP), *nul = metafy("\0", 1, META_DUP),
         *soh = metafy("\2", 1, META_DUP);

    char **candidates = zshcalloc(sizeof(char*) * (ftb_compcap_len + 1)),
         *filepath = zshcalloc(PATH_MAX * sizeof(char)), *dpre = zshcalloc(128),
         *dsuf = zshcalloc(128);

    char* first_word = NULL;
    int same_word = 1;

    for (i = 0; i < ftb_compcap_len; i++) {
        char *word = "", *group = NULL, *realdir = NULL;
        strcpy(dpre, "");
        strcpy(dsuf, "");

        // extract all the variables what we need
        char** compcap = sepsplit(ftb_compcap[i], soh, 1, 0);
        char* desc = compcap[0];
        char** info = sepsplit(compcap[1], nul, 1, 0);

        for (j = 0; info[j]; j += 2) {
            if (!strcmp(info[j], "word")) {
                word = info[j + 1];
                // unquote word
                parse_subst_string(word);
                remnulargs(word);
                untokenize(word);
            } else if (!strcmp(info[j], "group")) {
                group = info[j + 1];
            } else if (!strcmp(info[j], "realdir")) {
                realdir = info[j + 1];
            }
        }

        // check if all the words are the same
        if (first_word == NULL) {
            first_word = ztrdup(word);
        } else if (same_word && strcmp(word, first_word) != 0) {
            same_word = 0;
        }

        // add character and color to describe the type of the files
        if (realdir) {
            filepath = ftb_strcat(filepath, 2, realdir, word);
            struct file_color *fc = fzf_tab_colorize(filepath);
            if (fc != NULL) {
                dpre = ftb_strcat(dpre, 2, fc->fc_end, fc->fc_begin);
                if (fc->sc[0] != '\0') {
                    dsuf = ftb_strcat(dsuf, 4, fc->fc_end, fc->suffix, " -> ", fc->sc);
                } else {
                    dsuf = ftb_strcat(dsuf, 2, fc->fc_end, fc->suffix);
                }
                if (dpre[0] != '\0') {
                    setiparam("colorful", 1);
                }
                free(fc);
            }
        }

        char* result = zshcalloc(256 * sizeof(char));

        // add color to description if they have group index
        if (group) {
            // use strtol ?
            int group_index = atoi(group);
            char* color = group_index >= group_colors_len ? "" : group_colors[group_index - 1];
            // add prefix
            result = ftb_strcat(
                result, 11, group, bs, color, prefix, dpre, nul, group, bs, desc, nul, dsuf);
        } else {
            result = ftb_strcat(result, 6, default_color, dpre, nul, desc, nul, dsuf);
        }
        // quotedzputs(result, stdout);
        // putchar('\n');
        candidates[i] = result;

        freearray(info);
        freearray(compcap);
    }

    setaparam("tcandidates", candidates);
    setiparam("same_word", same_word);
    zsfree(dpre);
    zsfree(dsuf);
    zsfree(filepath);
    zsfree(first_word);

    return 0;
}

static struct builtin bintab[] = {
    BUILTIN("fzf-tab-compcap-generate", 0, bin_fzf_tab_compcap_generate, 0, -1, 0, "io", NULL),
    BUILTIN("fzf-tab-candidates-generate", 0, bin_fzf_tab_candidates_generate, 0, -1, 0, "i", NULL),
};

static struct paramdef patab[] = {
    STRPARAMDEF("FZF_TAB_MODULE_VERSION", &fzf_tab_module_version),
};

// clang-format off
static struct features module_features = {
    bintab, sizeof(bintab) / sizeof(*bintab),
    NULL, 0,
    NULL, 0,
    patab, sizeof(patab) / sizeof(*patab),
    0,
};
// clang-format on

int setup_(UNUSED(Module m)) { return 0; }

int features_(Module m, char*** features)
{
    *features = featuresarray(m, &module_features);
    return 0;
}

int enables_(Module m, int** enables) { return handlefeatures(m, &module_features, enables); }

int boot_(UNUSED(Module m))
{
    fzf_tab_module_version = ztrdup("0.2.2");
    // different zsh version may have different value of list_type's index
    // so query it dynamically
    opt_list_type = optlookup("list_types");
    return 0;
}

int cleanup_(UNUSED(Module m)) { return setfeatureenables(m, &module_features, NULL); }

int finish_(UNUSED(Module m))
{
    printf("fzf-tab module unloaded.\n");
    fflush(stdout);
    return 0;
}
