/*
 * Copyright © 2020 Benjamin Otte
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: Benjamin Otte <otte@gnome.org>
 */

#include <glib.h>

G_BEGIN_DECLS

#ifndef GDK_ARRAY_TYPE_NAME
#define GDK_ARRAY_TYPE_NAME GdkArray
#endif

#ifndef GDK_ARRAY_NAME
#define GDK_ARRAY_NAME gdk_array
#endif

#ifndef GDK_ARRAY_ELEMENT_TYPE
#define GDK_ARRAY_ELEMENT_TYPE gpointer
#endif

#ifdef GDK_ARRAY_PREALLOC
#if GDK_ARRAY_PREALLOC == 0
#undef GDK_ARRAY_PREALLOC
#endif
#endif

#ifdef GDK_ARRAY_NULL_TERMINATED
#define GDK_ARRAY_REAL_SIZE(_size) ((_size) + 1)
#define GDK_ARRAY_MAX_SIZE (G_MAXSIZE / sizeof (_T_) - 1)
#else
#define GDK_ARRAY_REAL_SIZE(_size) (_size)
#define GDK_ARRAY_MAX_SIZE (G_MAXSIZE / sizeof (_T_))
#endif

/* make this readable */
#define _T_ GDK_ARRAY_ELEMENT_TYPE
#define GdkArray GDK_ARRAY_TYPE_NAME
#define gdk_array_paste_more(GDK_ARRAY_NAME, func_name) GDK_ARRAY_NAME ## _ ## func_name
#define gdk_array_paste(GDK_ARRAY_NAME, func_name) gdk_array_paste_more (GDK_ARRAY_NAME, func_name)
#define gdk_array(func_name) gdk_array_paste (GDK_ARRAY_NAME, func_name)

typedef struct GdkArray GdkArray;

struct GdkArray
{
  _T_ *start;
  _T_ *end;
  _T_ *end_allocation;
#ifdef GDK_ARRAY_PREALLOC
  _T_ preallocated[GDK_ARRAY_REAL_SIZE(GDK_ARRAY_PREALLOC)];
#endif
};

/* no G_GNUC_UNUSED here, if you don't use an array type, remove it. */
static inline void
gdk_array(init) (GdkArray *self)
{
#ifdef GDK_ARRAY_PREALLOC
  self->start = self->preallocated;
  self->end = self->start;
  self->end_allocation = self->start + GDK_ARRAY_PREALLOC;
#ifdef GDK_ARRAY_NULL_TERMINATED
  *self->start = *(_T_[1]) { 0 };
#endif
#else
  self->start = NULL;
  self->end = NULL;
  self->end_allocation = NULL;
#endif
}

G_GNUC_UNUSED static inline gsize
gdk_array(get_capacity) (const GdkArray *self)
{
  return self->end_allocation - self->start;
}

G_GNUC_UNUSED static inline gsize
gdk_array(get_size) (const GdkArray *self)
{
  return self->end - self->start;
}

static inline void
gdk_array(free_elements) (_T_ *start,
                          _T_ *end)
{
#ifdef GDK_ARRAY_FREE_FUNC
  _T_ *e;
  for (e = start; e < end; e++)
#ifdef GDK_ARRAY_BY_VALUE
    GDK_ARRAY_FREE_FUNC (e);
#else
    GDK_ARRAY_FREE_FUNC (*e);
#endif
#endif
}

/* no G_GNUC_UNUSED here */
static inline void
gdk_array(clear) (GdkArray *self)
{
  gdk_array(free_elements) (self->start, self->end);

#ifdef GDK_ARRAY_PREALLOC
  if (self->start != self->preallocated)
#endif
    g_free (self->start);
  gdk_array(init) (self);
}

/*
 * gdk_array_steal:
 * @self: the array
 *
 * Steals all data in the array and clears the array.
 *
 * If you need to know the size of the data, you should query it
 * beforehand.
 *
 * Returns: The array's data
 **/
G_GNUC_UNUSED static inline _T_ *
gdk_array(steal) (GdkArray *self)
{
  _T_ *result;

#ifdef GDK_ARRAY_PREALLOC
  if (self->start == self->preallocated)
    {
      gsize size = GDK_ARRAY_REAL_SIZE (gdk_array(get_size) (self));
      result = g_new (_T_, size);
      memcpy (result, self->preallocated, sizeof (_T_) * size);
    }
  else
#endif
    result = self->start;

  gdk_array(init) (self);

  return result;
}

G_GNUC_UNUSED static inline _T_ *
gdk_array(get_data) (const GdkArray *self)
{
  return self->start;
}

G_GNUC_UNUSED static inline _T_ *
gdk_array(index) (const GdkArray *self,
                  gsize           pos)
{
  return self->start + pos;
}

G_GNUC_UNUSED static inline gboolean
gdk_array(is_empty) (const GdkArray *self)
{
  return self->end == self->start;
}

G_GNUC_UNUSED static inline void
gdk_array(reserve) (GdkArray *self,
                    gsize      n)
{
  gsize new_capacity, size, capacity;

  if (G_UNLIKELY (n > GDK_ARRAY_MAX_SIZE))
    g_error ("requesting array size of %zu, but maximum size is %zu", n, GDK_ARRAY_MAX_SIZE);

  capacity = gdk_array(get_capacity) (self);
  if (n <= capacity)
     return;

  size = gdk_array(get_size) (self);
  /* capacity * 2 can overflow, that's why we MAX() */
  new_capacity = MAX (GDK_ARRAY_REAL_SIZE (n), capacity * 2);

#ifdef GDK_ARRAY_PREALLOC
  if (self->start == self->preallocated)
    {
      self->start = g_new (_T_, new_capacity);
      memcpy (self->start, self->preallocated, sizeof (_T_) * GDK_ARRAY_REAL_SIZE (size));
    }
  else
#endif
#ifdef GDK_ARRAY_NULL_TERMINATED
  if (self->start == NULL)
    {
      self->start = g_new (_T_, new_capacity);
      *self->start = *(_T_[1]) { 0 };
    }
  else
#endif
    self->start = g_renew (_T_, self->start, new_capacity);

  self->end = self->start + size;
  self->end_allocation = self->start + new_capacity;
#ifdef GDK_ARRAY_NULL_TERMINATED
  self->end_allocation--;
#endif
}

G_GNUC_UNUSED static inline void
gdk_array(splice) (GdkArray *self,
                   gsize      pos,
                   gsize      removed,
                   gboolean   stolen,
#ifdef GDK_ARRAY_BY_VALUE
                   const _T_ *additions,
#else
                   _T_       *additions,
#endif
                   gsize      added)
{
  gsize size;
  gsize remaining;

  size = gdk_array(get_size) (self);
  g_assert (pos + removed <= size);
  remaining = size - pos - removed;

  if (!stolen)
    gdk_array(free_elements) (gdk_array(index) (self, pos),
                              gdk_array(index) (self, pos + removed));

  gdk_array(reserve) (self, size - removed + added);

  if (GDK_ARRAY_REAL_SIZE (remaining) && removed != added)
    memmove (gdk_array(index) (self, pos + added),
             gdk_array(index) (self, pos + removed),
             GDK_ARRAY_REAL_SIZE (remaining) * sizeof (_T_));

  if (added)
    {
      if (additions)
        memcpy (gdk_array(index) (self, pos),
                additions,
                added * sizeof (_T_));
#ifndef GDK_ARRAY_NO_MEMSET
      else
        memset (gdk_array(index) (self, pos), 0, added * sizeof (_T_));
#endif
    }


  /* might overflow, but does the right thing */
  self->end += added - removed;
}

G_GNUC_UNUSED static void
gdk_array(set_size) (GdkArray *self,
                     gsize     new_size)
{
  gsize old_size = gdk_array(get_size) (self);
  if (new_size > old_size)
    gdk_array(splice) (self, old_size, 0, FALSE, NULL, new_size - old_size);
  else
    gdk_array(splice) (self, new_size, old_size - new_size, FALSE, NULL, 0);
}

G_GNUC_UNUSED static void
gdk_array(append) (GdkArray *self,
#ifdef GDK_ARRAY_BY_VALUE
                   _T_       *value)
#else
                   _T_        value)
#endif
{
  gdk_array(splice) (self, 
                     gdk_array(get_size) (self),
                     0,
                     FALSE,
#ifdef GDK_ARRAY_BY_VALUE
                     value,
#else
                     &value,
#endif
                     1);
}

#ifdef GDK_ARRAY_BY_VALUE
G_GNUC_UNUSED static _T_ *
gdk_array(get) (const GdkArray *self,
                gsize           pos)
{
  return gdk_array(index) (self, pos);
}
#else
G_GNUC_UNUSED static _T_
gdk_array(get) (const GdkArray *self,
                gsize           pos)
 {
   return *gdk_array(index) (self, pos);
 }
#endif

#ifndef GDK_ARRAY_NO_UNDEF

#undef _T_
#undef GdkArray
#undef gdk_array_paste_more
#undef gdk_array_paste
#undef gdk_array
#undef GDK_ARRAY_REAL_SIZE
#undef GDK_ARRAY_MAX_SIZE

#undef GDK_ARRAY_BY_VALUE
#undef GDK_ARRAY_ELEMENT_TYPE
#undef GDK_ARRAY_FREE_FUNC
#undef GDK_ARRAY_NAME
#undef GDK_ARRAY_NULL_TERMINATED
#undef GDK_ARRAY_PREALLOC
#undef GDK_ARRAY_TYPE_NAME
#undef GDK_ARRAY_NO_MEMSET
#endif

G_END_DECLS
