#include <rlang.h>
#include <ctype.h>
#include <stdlib.h>

#include "decl/names-decl.h"
#include "utils.h"

// 3 leading '.' + 1 trailing '\0' + 24 characters
#define MAX_IOTA_SIZE 28

r_obj* ffi_names_as_unique(r_obj* names, r_obj* quiet) {
    return names_as_unique(names, r_lgl_get(quiet, 0));
}

// [[ export() ]]
r_obj* names_as_unique(r_obj* names, bool quiet) {
    if (is_unique_names(names) && !any_has_suffix(names)) {
        return names;
    }

    r_ssize n = r_length(names);

    r_obj* new_names = r_obj_encode_utf8(names);
    if (new_names == names) {
        new_names = r_clone(names);
    }
    KEEP(new_names);
    r_obj* const* v_new_names = r_chr_cbegin(new_names);

    for (r_ssize i = 0; i < n; ++i) {
        r_obj* elt = v_new_names[i];

        // Set `NA` and dots values to "" so they get replaced by `...n`
        // later on
        if (needs_suffix(elt)) {
            r_chr_poke(new_names, i, r_strs.empty);
            continue;
        }

        // Strip `...n` suffixes
        const char* nm = r_str_c_string(elt);
        int pos = suffix_pos(nm);
        if (pos >= 0) {
            elt = Rf_mkCharLenCE(nm, pos, Rf_getCharCE(elt));
            r_chr_poke(new_names, i, elt);
            continue;
        }
    }

    // Append all duplicates with a suffix

    r_obj* dups = KEEP(chr_detect_dups(new_names));
    const int* dups_ptr = r_lgl_cbegin(dups);

    for (r_ssize i = 0; i < n; ++i) {
        r_obj* elt = v_new_names[i];

        if (elt != r_strs.empty && !dups_ptr[i]) {
            continue;
        }

        const char* name = r_str_c_string(elt);

        int size = strlen(name);
        int buf_size = size + MAX_IOTA_SIZE;

        R_CheckStack2(buf_size);
        char buf[buf_size];
        buf[0] = '\0';

        r_memcpy(buf, name, size);
        int remaining = buf_size - size;

        int needed = snprintf(buf + size, remaining, "...%" R_PRI_SSIZE, i + 1);
        if (needed >= remaining) {
            stop_large_name();
        }

        r_chr_poke(
            new_names,
            i,
            Rf_mkCharLenCE(buf, size + needed, Rf_getCharCE(elt))
        );
    }

    if (!quiet) {
        names_inform_repair(names, new_names);
    }

    FREE(2);
    return new_names;
}

static bool is_unique_names(r_obj* names) {
    if (r_typeof(names) != R_TYPE_character) {
        r_abort("`names` must be a character vector.");
    }

    r_ssize n = r_length(names);
    r_obj* const* v_names = r_chr_cbegin(names);

    if (Rf_any_duplicated(names, FALSE)) {
        return false;
    }

    for (r_ssize i = 0; i < n; ++i) {
        r_obj* elt = v_names[i];

        if (needs_suffix(elt)) {
            return false;
        }
    }

    return true;
}

static bool any_has_suffix(r_obj* names) {
    r_ssize n = r_length(names);
    r_obj* const* v_names = r_chr_cbegin(names);

    for (r_ssize i = 0; i < n; ++i) {
        const char* elt = r_str_c_string(v_names[i]);

        if (suffix_pos(elt) >= 0) {
            return true;
        }
    }

    return false;
}

static ptrdiff_t suffix_pos(const char* name) {
    int n = strlen(name);

    const char* suffix_end = NULL;
    int in_dots = 0;
    bool in_digits = false;

    for (const char* ptr = name + n - 1; ptr >= name; --ptr) {
        char c = *ptr;

        if (in_digits) {
            if (c == '.') {
                in_digits = false;
                in_dots = 1;
                continue;
            }

            if (isdigit(c)) {
                continue;
            }

            goto done;
        }

        switch (in_dots) {
        case 0:
            if (isdigit(c)) {
                in_digits = true;
                continue;
            }
            goto done;
        case 1:
        case 2:
            if (c == '.') {
                ++in_dots;
                continue;
            }
            goto done;
        case 3:
            suffix_end = ptr + 1;
            if (isdigit(c)) {
                in_dots = 0;
                in_digits = true;
                continue;
            }
            goto done;

        default:
            r_stop_internal("Unexpected state.");
        }
    }

done:
    if (suffix_end) {
        return suffix_end - name;
    } else {
        return -1;
    }
}

static bool needs_suffix(r_obj* str) {
    return str == r_strs.na || str == r_strs.dots || str == r_strs.empty ||
        is_dotdotint(r_str_c_string(str));
}

static bool is_dotdotint(const char* name) {
    int n = strlen(name);

    if (n < 3) {
        return false;
    }
    if (name[0] != '.' || name[1] != '.') {
        return false;
    }

    if (name[2] == '.') {
        name += 3;
    } else {
        name += 2;
    }

    return (bool) strtol(name, NULL, 10);
}

static void names_inform_repair(r_obj* old_names, r_obj* new_names) {
    r_obj* call =
        KEEP(r_call3(r_sym("names_inform_repair"), old_names, new_names));
    r_eval(call, rlang_ns_env);
    FREE(1);
}

static void stop_large_name(void) {
    r_abort("Can't tidy up name because it is too large.");
}
