/*
 * parser.c
 * vim: expandtab:ts=4:sts=4:sw=4
 *
 * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
 *
 * SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
 */

#include "config.h"

#include <stdlib.h>
#include <string.h>

#include <glib.h>

#include "common.h"

static gchar**
_parse_args_helper(const char* const inp, int min, int max, gboolean* result, gboolean with_freetext)
{
    if (inp == NULL) {
        *result = FALSE;
        return NULL;
    }

    // copy and strip input of leading/trailing whitespace
    auto_char char* copy = strdup(inp);
    g_strstrip(copy);

    int inp_size = g_utf8_strlen(copy, -1);
    gboolean in_token = FALSE;
    gboolean in_freetext = FALSE;
    gboolean in_quotes = FALSE;
    GString* current_token = g_string_new("");
    int num_tokens = 0;
    GSList* tokens = NULL;

    // add tokens to GSList
    for (int i = 0; i < inp_size; i++) {
        gchar* curr_ch = g_utf8_offset_to_pointer(copy, i);
        gunichar curr_uni = g_utf8_get_char(curr_ch);

        if (curr_uni == '\\' && (i + 1 < inp_size)) {
            if (!in_token) {
                in_token = TRUE;
                if (with_freetext) {
                    num_tokens++;
                }
                if (with_freetext && (num_tokens == max + 1)) {
                    in_freetext = TRUE;
                }
            }
            i++;
            gchar* next_ch = g_utf8_offset_to_pointer(copy, i);
            gunichar next_uni = g_utf8_get_char(next_ch);
            g_string_append_unichar(current_token, next_uni);
            continue;
        }

        if (!in_token) {
            if (curr_uni == ' ') {
                continue;
            } else {
                in_token = TRUE;
                if (with_freetext) {
                    num_tokens++;
                }
                if (with_freetext && (num_tokens == max + 1) && (curr_uni != '"')) {
                    in_freetext = TRUE;
                } else if (curr_uni == '"') {
                    in_quotes = TRUE;
                    i++;
                    if (i < inp_size) {
                        gchar* next_ch = g_utf8_offset_to_pointer(copy, i);
                        gunichar next_uni = g_utf8_get_char(next_ch);

                        if (next_uni == '"') {
                            tokens = g_slist_append(tokens, g_strdup(""));
                            in_token = FALSE;
                            in_quotes = FALSE;
                        } else if (next_uni == '\\' && (i + 1 < inp_size)) {
                            i++;
                            gchar* escaped_ch = g_utf8_offset_to_pointer(copy, i);
                            gunichar escaped_uni = g_utf8_get_char(escaped_ch);
                            g_string_append_unichar(current_token, escaped_uni);
                        } else {
                            g_string_append_unichar(current_token, next_uni);
                        }
                    } else {
                        // trailing quote
                        tokens = g_slist_append(tokens, g_strdup(""));
                        in_token = FALSE;
                        in_quotes = FALSE;
                    }
                }
                if (in_token && curr_uni != '"') {
                    g_string_append_unichar(current_token, curr_uni);
                }
            }
        } else {
            if (in_quotes) {
                if (curr_uni == '"') {
                    tokens = g_slist_append(tokens, g_string_free(current_token, FALSE));
                    current_token = g_string_new("");
                    in_token = FALSE;
                    in_quotes = FALSE;
                } else {
                    g_string_append_unichar(current_token, curr_uni);
                }
            } else {
                if (with_freetext && in_freetext) {
                    g_string_append_unichar(current_token, curr_uni);
                } else if (curr_uni == ' ') {
                    tokens = g_slist_append(tokens, g_string_free(current_token, FALSE));
                    current_token = g_string_new("");
                    in_token = FALSE;
                } else {
                    g_string_append_unichar(current_token, curr_uni);
                }
            }
        }
    }

    if (in_token) {
        tokens = g_slist_append(tokens, g_string_free(current_token, FALSE));
    } else {
        g_string_free(current_token, TRUE);
    }

    int num = g_slist_length(tokens) - 1;

    // if num args not valid return NULL
    if ((num < min) || (num > max)) {
        g_slist_free_full(tokens, free);
        *result = FALSE;
        return NULL;

        // if min allowed is 0 and 0 found, return empty char* array
    } else if (min == 0 && num == 0) {
        g_slist_free_full(tokens, free);
        gchar** args = g_malloc((num + 1) * sizeof(*args));
        args[0] = NULL;
        *result = TRUE;
        return args;

        // otherwise return args array
    } else {
        gchar** args = g_malloc((num + 1) * sizeof(*args));
        GSList* token = tokens;
        token = g_slist_next(token);
        int arg_count = 0;

        while (token) {
            args[arg_count++] = strdup(token->data);
            token = g_slist_next(token);
        }

        args[arg_count] = NULL;
        g_slist_free_full(tokens, free);
        *result = TRUE;
        return args;
    }
}

/*
 * Take a full line of input and return an array of strings representing
 * the arguments of a command.
 * If the number of arguments found is less than min, or more than max
 * NULL is returned.
 *
 * inp - The line of input
 * min - The minimum allowed number of arguments
 * max - The maximum allowed number of arguments
 *
 * Returns - An NULL terminated array of strings representing the arguments
 * of the command, or NULL if the validation fails.
 *
 * E.g. the following input line:
 *
 * /cmd arg1 arg2
 *
 * Will return a pointer to the following array:
 *
 * { "arg1", "arg2", NULL }
 *
 */
gchar**
parse_args(const char* const inp, int min, int max, gboolean* result)
{
    return _parse_args_helper(inp, min, max, result, FALSE);
}

/*
 * Take a full line of input and return an array of strings representing
 * the arguments of a command.  This function handles when the last parameter
 * to the command is free text e.g.
 *
 * /msg user@host here is a message
 *
 * If the number of arguments found is less than min, or more than max
 * NULL is returned.
 *
 * inp - The line of input
 * min - The minimum allowed number of arguments
 * max - The maximum allowed number of arguments
 *
 * Returns - An NULL terminated array of strings representing the arguments
 * of the command, or NULL if the validation fails.
 *
 * E.g. the following input line:
 *
 * /cmd arg1 arg2 some free text
 *
 * Will return a pointer to the following array:
 *
 * { "arg1", "arg2", "some free text", NULL }
 *
 */
gchar**
parse_args_with_freetext(const char* const inp, int min, int max, gboolean* result)
{
    return _parse_args_helper(inp, min, max, result, TRUE);
}

/*
 * Will just take everything after the first space as the argument.
 * Used for `/correct` so that we also include quotation marks.
 */
gchar**
parse_args_as_one(const char* const inp, int min, int max, gboolean* result)
{
    gchar** args = g_malloc0(2 * sizeof(*args));
    int length = g_utf8_strlen(inp, -1);
    gchar* space = g_utf8_strchr(inp, length, ' ');
    if (space) {
        int sub_length = g_utf8_strlen(space, -1);
        if (sub_length > 1) {
            args[0] = g_strdup(space + 1);
            *result = TRUE;
            return args;
        } else {
            g_free(args);
        }
    } else {
        g_free(args);
    }

    *result = FALSE;
    return NULL;
}

int
count_tokens(const char* const string)
{
    int length = g_utf8_strlen(string, -1);
    gboolean in_quotes = FALSE;
    int num_tokens = 0;

    // include first token
    num_tokens++;

    for (int i = 0; i < length; i++) {
        gchar* curr_ch = g_utf8_offset_to_pointer(string, i);
        gunichar curr_uni = g_utf8_get_char(curr_ch);

        if (curr_uni == '\\' && (i + 1 < length)) {
            i++;
            continue;
        }

        if (curr_uni == ' ') {
            if (!in_quotes) {
                num_tokens++;
                while (i + 1 < length) {
                    gchar* next_ch = g_utf8_offset_to_pointer(string, i + 1);
                    gunichar next_uni = g_utf8_get_char(next_ch);
                    if (next_uni == ' ') {
                        i++;
                    } else {
                        break;
                    }
                }
            }
        } else if (curr_uni == '"') {
            if (in_quotes) {
                in_quotes = FALSE;
            } else {
                in_quotes = TRUE;
            }
        }
    }

    return num_tokens;
}

char*
get_start(const char* const string, int tokens)
{
    GString* result = g_string_new("");
    int length = g_utf8_strlen(string, -1);
    gboolean in_quotes = FALSE;
    int num_tokens = 0;

    // include first token
    num_tokens++;

    for (int i = 0; i < length; i++) {
        gchar* curr_ch = g_utf8_offset_to_pointer(string, i);
        gunichar curr_uni = g_utf8_get_char(curr_ch);

        if (curr_uni == '\\' && (i + 1 < length)) {
            if (num_tokens < tokens) {
                g_string_append_unichar(result, curr_uni);
            }
            i++;
            curr_ch = g_utf8_offset_to_pointer(string, i);
            curr_uni = g_utf8_get_char(curr_ch);
            if (num_tokens < tokens) {
                g_string_append_unichar(result, curr_uni);
            }
            continue;
        }

        if (num_tokens < tokens) {
            g_string_append_unichar(result, curr_uni);
        }
        if (curr_uni == ' ') {
            if (!in_quotes) {
                num_tokens++;
                while (i + 1 < length) {
                    gchar* next_ch = g_utf8_offset_to_pointer(string, i + 1);
                    gunichar next_uni = g_utf8_get_char(next_ch);
                    if (next_uni == ' ') {
                        if (num_tokens <= tokens) {
                            g_string_append_unichar(result, next_uni);
                        }
                        i++;
                    } else {
                        break;
                    }
                }
            }
        } else if (curr_uni == '"') {
            if (in_quotes) {
                in_quotes = FALSE;
            } else {
                in_quotes = TRUE;
            }
        }
    }

    return g_string_free(result, FALSE);
}

GHashTable*
parse_options(gchar** args, gchar** opt_keys, gboolean* res)
{
    GList* keys = NULL;
    for (guint i = 0; i < g_strv_length(opt_keys); i++) {
        keys = g_list_append(keys, opt_keys[i]);
    }

    GHashTable* options = NULL;

    // no options found, success
    if (args[0] == NULL) {
        options = g_hash_table_new(g_str_hash, g_str_equal);
        *res = TRUE;
        g_list_free(keys);
        return options;
    }

    // validate options
    GList* found_keys = NULL;
    for (guint curr = 0; curr < g_strv_length(args); curr += 2) {
        // check if option valid
        if (g_list_find_custom(keys, args[curr], (GCompareFunc)g_strcmp0) == NULL) {
            *res = FALSE;
            g_list_free(found_keys);
            g_list_free(keys);
            return options;
        }

        // check if duplicate
        if (g_list_find_custom(found_keys, args[curr], (GCompareFunc)g_strcmp0)) {
            *res = FALSE;
            g_list_free(found_keys);
            g_list_free(keys);
            return options;
        }

        // check value given
        if (args[curr + 1] == NULL) {
            *res = FALSE;
            g_list_free(found_keys);
            g_list_free(keys);
            return options;
        }

        found_keys = g_list_append(found_keys, args[curr]);
    }
    g_list_free(found_keys);
    g_list_free(keys);

    // create map
    options = g_hash_table_new(g_str_hash, g_str_equal);
    *res = TRUE;
    for (guint curr = 0; curr < g_strv_length(args); curr += 2) {
        g_hash_table_insert(options, args[curr], args[curr + 1]);
    }

    return options;
}

void
options_destroy(GHashTable* options)
{
    if (options) {
        g_hash_table_destroy(options);
    }
}
