/*
 * wee-arraylist.c - array lists management
 *
 * Copyright (C) 2014-2020 Sébastien Helleu <flashcode@flashtux.org>
 *
 * This file is part of WeeChat, the extensible chat client.
 *
 * WeeChat 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.
 *
 * WeeChat 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 WeeChat.  If not, see <https://www.gnu.org/licenses/>.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

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

#include "weechat.h"
#include "wee-arraylist.h"
#include "wee-log.h"
#include "wee-string.h"


/*
 * Compares two arraylist entries (default comparator).
 * It just compares pointers.
 *
 * Returns:
 *   -1: pointer1 < pointer2
 *    0: pointer1 == pointer2
 *    1: pointer1 > pointer2
 */

int
arraylist_cmp_default_cb (void *data, struct t_arraylist *arraylist,
                          void *pointer1, void *pointer2)
{
    /* make C compiler happy */
    (void) data;
    (void) arraylist;

    if (pointer1 < pointer2)
        return -1;
    if (pointer1 > pointer2)
        return 1;
    return 0;
}

/*
 * Creates a new arraylist.
 *
 * Returns pointer to arraylist, NULL if error.
 */

struct t_arraylist *
arraylist_new (int initial_size,
               int sorted,
               int allow_duplicates,
               t_arraylist_cmp *callback_cmp, void *callback_cmp_data,
               t_arraylist_free *callback_free, void *callback_free_data)
{
    struct t_arraylist *new_arraylist;

    /* check arguments */
    if (initial_size < 0)
        return NULL;

    new_arraylist = malloc (sizeof (*new_arraylist));
    if (!new_arraylist)
        return NULL;

    new_arraylist->size = 0;
    if (initial_size > 0)
    {
        new_arraylist->size_alloc = initial_size;
        new_arraylist->size_alloc_min = initial_size;
        new_arraylist->data = calloc (initial_size,
                                      sizeof (*new_arraylist->data));
        if (!new_arraylist->data)
        {
            free (new_arraylist);
            return NULL;
        }
    }
    else
    {
        new_arraylist->size_alloc = 0;
        new_arraylist->size_alloc_min = 0;
        new_arraylist->data = NULL;
    }
    new_arraylist->sorted = sorted;
    new_arraylist->allow_duplicates = allow_duplicates;
    new_arraylist->callback_cmp = (callback_cmp) ?
        callback_cmp : &arraylist_cmp_default_cb;
    new_arraylist->callback_cmp_data = (callback_cmp) ?
        callback_cmp_data : NULL;
    new_arraylist->callback_free = callback_free;
    new_arraylist->callback_free_data = callback_free_data;

    return new_arraylist;
}

/*
 * Returns the size of an arraylist (number of elements).
 */

int
arraylist_size (struct t_arraylist *arraylist)
{
    if (!arraylist)
        return 0;

    return arraylist->size;
}

/*
 * Returns the pointer to an arraylist element, by index.
 */

void *
arraylist_get (struct t_arraylist *arraylist, int index)
{
    if (!arraylist || (index < 0) || (index >= arraylist->size))
        return NULL;

    return arraylist->data[index];
}

/*
 * Adjusts the allocated size of arraylist to add one element (if needed),
 * so that the list has enough allocated data to store (current_size + 1)
 * elements.
 *
 * Returns:
 *   1: OK
 *   0: error
 */

int
arraylist_grow (struct t_arraylist *arraylist)
{
    int new_size_alloc;
    void **data;

    if (!arraylist)
        return 0;

    /* if we have enough space allocated, do nothing */
    if (arraylist->size + 1 <= arraylist->size_alloc)
        return 1;

    new_size_alloc = (arraylist->size_alloc < 2) ?
        2 : arraylist->size_alloc + (arraylist->size_alloc / 2);

    data = realloc (arraylist->data,
                    new_size_alloc * sizeof (*arraylist->data));
    if (!data)
        return 0;
    arraylist->data = data;
    memset (&arraylist->data[arraylist->size_alloc],
            0,
            (new_size_alloc - arraylist->size_alloc) *
            sizeof (*arraylist->data));
    arraylist->size_alloc = new_size_alloc;

    return 1;
}

/*
 * Adjusts the allocated size of arraylist to remove one element (if needed),
 * so that the list has enough allocated data to store (current size - 1)
 * elements.
 *
 * Returns:
 *   1: OK
 *   0: error
 */

int
arraylist_shrink (struct t_arraylist *arraylist)
{
    int new_size_alloc;
    void **data;

    if (!arraylist)
        return 0;

    /* we don't shrink if we are below the min allocated size */
    if ((arraylist->size_alloc == 0)
        || (arraylist->size_alloc <= arraylist->size_alloc_min))
    {
        return 1;
    }

    /* clear the arraylist if current allocated size is 1 */
    if (arraylist->size_alloc == 1)
    {
        free (arraylist->data);
        arraylist->data = NULL;
        arraylist->size_alloc = 0;
        return 1;
    }

    new_size_alloc = arraylist->size_alloc - (arraylist->size_alloc / 2);

    if (arraylist->size - 1 >= new_size_alloc)
        return 1;

    data = realloc (arraylist->data,
                    new_size_alloc * sizeof (*arraylist->data));
    if (!data)
        return 0;
    arraylist->data = data;
    arraylist->size_alloc = new_size_alloc;

    return 1;
}

/*
 * Performs a binary search in the arraylist to find an element
 * (this function must be called only if the arraylist is sorted).
 *
 * If "index" is not NULL, it is set with the index of element found (or -1 if
 * element was not found).
 *
 * If "index_insert" is not NULL, it is set with the index that must be used to
 * insert the element in the arraylist (to keep arraylist sorted).
 *
 * Returns pointer to element found, NULL if not found.
 */

void *
arraylist_binary_search (struct t_arraylist *arraylist, void *pointer,
                         int *index, int *index_insert)
{
    int ret_index, ret_index_insert, start, end, middle, rc;
    void *ret_pointer;

    ret_index = -1;
    ret_index_insert = -1;
    ret_pointer = NULL;

    if (!arraylist)
        goto end;

    start = 0;
    end = arraylist->size - 1;

    /*
     * statistically we often add at the end, or before first element, so
     * first check these cases (for performance), before doing the binary
     * search
     */
    rc = (arraylist->callback_cmp) (arraylist->callback_cmp_data,
                                    arraylist,
                                    pointer,
                                    arraylist->data[end]);
    if (rc == 0)
    {
        ret_index = end;
        /* by convention, add an element with same value after the last one */
        ret_index_insert = end + 1;
        ret_pointer = arraylist->data[end];
        goto end;
    }
    if (rc > 0)
    {
        ret_index = -1;
        ret_index_insert = -1;
        ret_pointer = NULL;
        goto end;
    }
    if (arraylist->size == 1)
    {
        ret_index = -1;
        ret_index_insert = 0;
        ret_pointer = NULL;
        goto end;
    }

    rc = (arraylist->callback_cmp) (arraylist->callback_cmp_data,
                                    arraylist,
                                    pointer,
                                    arraylist->data[start]);
    if (rc == 0)
    {
        ret_index = start;
        ret_index_insert = start + 1;
        ret_pointer = arraylist->data[start];
        goto end;
    }
    if (rc < 0)
    {
        ret_index = -1;
        ret_index_insert = start;
        ret_pointer = NULL;
        goto end;
    }
    if (arraylist->size == 2)
    {
        ret_index = -1;
        ret_index_insert = end;
        ret_pointer = NULL;
        goto end;
    }

    start++;
    end--;

    /* perform a binary search to find the index */
    while (start <= end)
    {
        middle = (start + end) / 2;

        rc = (arraylist->callback_cmp) (arraylist->callback_cmp_data,
                                        arraylist,
                                        pointer,
                                        arraylist->data[middle]);
        if (rc == 0)
        {
            ret_index = middle;
            ret_index_insert = middle + 1;
            ret_pointer = arraylist->data[middle];
            goto end;
        }

        if (rc < 0)
            end = middle - 1;
        else
            start = middle + 1;

        if (start > end)
        {
            ret_index = -1;
            ret_index_insert = (rc < 0) ? middle : middle + 1;
            ret_pointer = NULL;
        }
    }

end:
    if ((ret_index >= 0) && arraylist->allow_duplicates)
    {
        /*
         * in case of duplicates in table, the index of element found
         * is the first element with the value, and the index for
         * insert is the last element with the value + 1
         */
        start = ret_index - 1;
        while (start >= 0)
        {
            rc = (arraylist->callback_cmp) (
                arraylist->callback_cmp_data,
                arraylist,
                pointer,
                arraylist->data[start]);
            if (rc != 0)
                break;
            start--;
        }
        start++;
        end = ret_index + 1;
        while (end < arraylist->size)
        {
            rc = (arraylist->callback_cmp) (
                arraylist->callback_cmp_data,
                arraylist,
                pointer,
                arraylist->data[end]);
            if (rc != 0)
                break;
            end++;
        }
        end--;
        ret_index = start;
        ret_index_insert = end + 1;
        ret_pointer = arraylist->data[start];
    }

    if (index)
        *index = ret_index;
    if (index_insert)
        *index_insert = ret_index_insert;

    return ret_pointer;
}

/*
 * Performs a standard search in the arraylist to find an element
 * (this function must be called only if the arraylist is NOT sorted).
 *
 * If "index" is not NULL, it is set with the index of element found (or -1 if
 * element was not found).
 *
 * If "index_insert" is not NULL, it is set to -1 (elements are always added
 * at the end of list when it is not sorted).
 *
 * Returns pointer to element found, NULL if not found.
 */

void *
arraylist_standard_search (struct t_arraylist *arraylist, void *pointer,
                           int *index, int *index_insert)
{
    int i;

    if (!arraylist)
        goto end;

    for (i = 0; i < arraylist->size; i++)
    {
        if ((arraylist->callback_cmp) (arraylist->callback_cmp_data,
                                       arraylist, arraylist->data[i],
                                       pointer) == 0)
        {
            if (index)
                *index = i;
            if (index_insert)
                *index_insert = -1;
            return arraylist->data[i];
        }
    }

end:
    if (index)
        *index = -1;
    if (index_insert)
        *index_insert = -1;
    return NULL;
}

/*
 * Searches an element in the arraylist.
 *
 * If "index" is not NULL, it is set with the index of element found (or -1 if
 * element was not found).
 *
 * If "index_insert" is not NULL, it is set with the index that must be used to
 * insert the element in the arraylist (to keep arraylist sorted).
 *
 * Returns pointer to element found, NULL if not found.
 */

void *
arraylist_search (struct t_arraylist *arraylist, void *pointer,
                  int *index, int *index_insert)
{
    if (index)
        *index = -1;
    if (index_insert)
        *index_insert = -1;

    if (!arraylist || (arraylist->size == 0))
        return NULL;

    if (arraylist->sorted)
    {
        return arraylist_binary_search (arraylist, pointer,
                                        index, index_insert);
    }
    else
    {
        return arraylist_standard_search (arraylist, pointer,
                                          index, index_insert);
    }
}

/*
 * Inserts an element at a given index (and shifts next elements by one
 * position), or at automatic index if the arraylist is sorted.
 *
 * If the index is negative and that the arraylist is not sorted, the element
 * is added at the end of arraylist.
 *
 * If the arraylist is sorted, the argument "index" is ignored (the element
 * will be inserted at appropriate position, to keep arraylist sorted).
 *
 * Returns the index of the new element (>= 0) or -1 if error.
 */

int
arraylist_insert (struct t_arraylist *arraylist, int index, void *pointer)
{
    int index_insert, i;

    if (!arraylist)
        return -1;

    if (arraylist->sorted)
    {
        (void) arraylist_search (arraylist, pointer, &index, &index_insert);
        if ((index >= 0) && !arraylist->allow_duplicates)
        {
            while ((index < arraylist->size)
                   && (((arraylist->callback_cmp) (
                            arraylist->callback_cmp_data,
                            arraylist, arraylist->data[index],
                            pointer)) == 0))
            {
                arraylist_remove (arraylist, index);
            }
        }
        else
            index = index_insert;
    }
    else if (!arraylist->allow_duplicates)
    {
        /*
         * arraylist is not sorted and does not allow duplicates, then we
         * remove any element with the same value
         */
        i = 0;
        while (i < arraylist->size)
        {
            if ((arraylist->callback_cmp) (arraylist->callback_cmp_data,
                                           arraylist, arraylist->data[i],
                                           pointer) == 0)
            {
                arraylist_remove (arraylist, i);
            }
            else
                i++;
        }
    }

    /* if index is negative or too big, add at the end */
    if ((index < 0) || (index > arraylist->size))
        index = arraylist->size;

    if (!arraylist_grow (arraylist))
        return -1;

    /* shift next elements by one position */
    if (index < arraylist->size)
    {
        memmove (&arraylist->data[index + 1],
                 &arraylist->data[index],
                 (arraylist->size - index) * sizeof (*arraylist->data));
    }

    /* set element */
    arraylist->data[index] = pointer;

    (arraylist->size)++;

    return index;
}

/*
 * Adds an element at the end of arraylist (or in the middle if the arraylist
 * is sorted).
 *
 * Returns the index of the new element (>= 0) or -1 if error.
 */

int
arraylist_add (struct t_arraylist *arraylist, void *pointer)
{
    if (!arraylist)
        return -1;

    return arraylist_insert (arraylist, -1, pointer);
}

/*
 * Removes one element from the arraylist.
 *
 * Returns the index removed or -1 if error.
 */

int
arraylist_remove (struct t_arraylist *arraylist, int index)
{
    if (!arraylist || (index < 0) || (index >= arraylist->size))
        return -1;

    if (arraylist->callback_free)
    {
        (arraylist->callback_free) (arraylist->callback_free_data,
                                    arraylist,
                                    arraylist->data[index]);
    }

    if (index < arraylist->size - 1)
    {
        memmove (&arraylist->data[index],
                 &arraylist->data[index + 1],
                 (arraylist->size - index - 1) * sizeof (*arraylist->data));
        memset (&arraylist->data[arraylist->size - 1], 0,
                sizeof (*arraylist->data));
    }
    else
    {
        memset (&arraylist->data[index], 0, sizeof (*arraylist->data));
    }

    arraylist_shrink (arraylist);

    (arraylist->size)--;

    return index;
}

/*
 * Removes all elements in the arraylist.
 *
 * Returns:
 *   1: OK
 *   0: error
 */

int
arraylist_clear (struct t_arraylist *arraylist)
{
    int i;

    if (!arraylist)
        return 0;

    if (arraylist->callback_free)
    {
        for (i = 0; i < arraylist->size; i++)
        {
            (arraylist->callback_free) (arraylist->callback_free_data,
                                        arraylist,
                                        arraylist->data[i]);
        }
    }

    if (arraylist->data)
    {
        if (arraylist->size_alloc != arraylist->size_alloc_min)
        {
            free (arraylist->data);
            arraylist->data = NULL;
            arraylist->size_alloc = 0;
            if (arraylist->size_alloc_min > 0)
            {
                arraylist->data = calloc (arraylist->size_alloc_min,
                                          sizeof (*arraylist->data));
                if (!arraylist->data)
                    return 0;
                arraylist->size_alloc = arraylist->size_alloc_min;
            }
        }
        else if (arraylist->size_alloc > 0)
        {
            memset (arraylist->data,
                    0,
                    arraylist->size_alloc * sizeof (*arraylist->data));
        }
    }

    arraylist->size = 0;

    return 1;
}

/*
 * Frees an arraylist.
 */

void
arraylist_free (struct t_arraylist *arraylist)
{
    int i;

    if (!arraylist)
        return;

    if (arraylist->callback_free)
    {
        for (i = 0; i < arraylist->size; i++)
        {
            (arraylist->callback_free) (arraylist->callback_free_data,
                                        arraylist,
                                        arraylist->data[i]);
        }
    }

    if (arraylist->data)
        free (arraylist->data);

    free (arraylist);
}

/*
 * Prints an arraylist in WeeChat log file (usually for crash dump).
 */

void
arraylist_print_log (struct t_arraylist *arraylist, const char *name)
{
    int i;

    log_printf ("[arraylist %s (addr:0x%lx)]", name, arraylist);
    log_printf ("  size . . . . . . . . . : %d", arraylist->size);
    log_printf ("  size_alloc . . . . . . : %d", arraylist->size_alloc);
    log_printf ("  size_alloc_min . . . . : %d", arraylist->size_alloc_min);
    log_printf ("  sorted . . . . . . . . : %d", arraylist->sorted);
    log_printf ("  allow_duplicates . . . : %d", arraylist->allow_duplicates);
    log_printf ("  data . . . . . . . . . : 0x%lx", arraylist->data);
    if (arraylist->data)
    {
        for (i = 0; i < arraylist->size_alloc; i++)
        {
            log_printf ("    data[%08d] . . . : 0x%lx",
                        i, arraylist->data[i]);
        }
    }
    log_printf ("  callback_cmp . . . . . : 0x%lx", arraylist->callback_cmp);
    log_printf ("  callback_cmp_data. . . : 0x%lx",
                arraylist->callback_cmp_data);
    log_printf ("  callback_free. . . . . : 0x%lx", arraylist->callback_free);
    log_printf ("  callback_free_data . . : 0x%lx",
                arraylist->callback_free_data);
}
