/*
 * parser.c
 * vim: expandtab:ts=4:sts=4:sw=4
 *
 * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
 *
 * This file is part of Profanity.
 *
 * Profanity 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 3 of the License, or
 * (at your option) any later version.
 *
 * Profanity 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 Profanity.  If not, see <https://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give permission to
 * link the code of portions of this program with the OpenSSL library under
 * certain conditions as described in each individual source file, and
 * distribute linked combinations including the two.
 *
 * You must obey the GNU General Public License in all respects for all of the
 * code used other than OpenSSL. If you modify file(s) with this exception, you
 * may extend this exception to your version of the file(s), but you are not
 * obligated to do so. If you do not wish to do so, delete this exception
 * statement from your version. If you delete this exception statement from all
 * source files in the program, then also delete it here.
 *
 */

#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;
    char* token_start = &copy[0];
    int token_size = 0;
    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 (!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++;
                    gchar* next_ch = g_utf8_next_char(curr_ch);
                    gunichar next_uni = g_utf8_get_char(next_ch);

                    if (next_uni == '"') {
                        tokens = g_slist_append(tokens, g_strndup(curr_ch,
                                                                  0));
                        token_size = 0;
                        in_token = FALSE;
                        in_quotes = FALSE;
                    } else {
                        token_start = next_ch;
                        token_size += g_unichar_to_utf8(next_uni, NULL);
                    }
                }
                if (curr_uni == '"') {
                    gchar* next_ch = g_utf8_next_char(curr_ch);
                    token_start = next_ch;
                } else {
                    token_start = curr_ch;
                    token_size += g_unichar_to_utf8(curr_uni, NULL);
                }
            }
        } else {
            if (in_quotes) {
                if (curr_uni == '"') {
                    tokens = g_slist_append(tokens, g_strndup(token_start,
                                                              token_size));
                    token_size = 0;
                    in_token = FALSE;
                    in_quotes = FALSE;
                } else {
                    if (curr_uni != '"') {
                        token_size += g_unichar_to_utf8(curr_uni, NULL);
                    }
                }
            } else {
                if (with_freetext && in_freetext) {
                    token_size += g_unichar_to_utf8(curr_uni, NULL);
                } else if (curr_uni == ' ') {
                    tokens = g_slist_append(tokens, g_strndup(token_start,
                                                              token_size));
                    token_size = 0;
                    in_token = FALSE;
                } else {
                    token_size += g_unichar_to_utf8(curr_uni, NULL);
                }
            }
        }
    }

    if (in_token) {
        tokens = g_slist_append(tokens, g_strndup(token_start, token_size));
    }

    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 == ' ') {
            if (!in_quotes) {
                num_tokens++;
            }
        } 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 (num_tokens < tokens) {
            auto_gchar gchar* uni_char = g_malloc(7);
            int len = g_unichar_to_utf8(curr_uni, uni_char);
            uni_char[len] = '\0';
            g_string_append(result, uni_char);
        }
        if (curr_uni == ' ') {
            if (!in_quotes) {
                num_tokens++;
            }
        } 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 (int 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 (int 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 (int 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);
    }
}
