/* Clapper Playback Library
 * Copyright (C) 2025 Rafał Dzięgiel <rafostar.github@gmail.com>
 *
 * 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
 * <https://www.gnu.org/licenses/>.
 */

#include "clapper-cache-private.h"
#include "clapper-version.h"

#include "clapper-extractable.h"
#include "clapper-playlistable.h"
#include "clapper-reactable.h"

#define CLAPPER_CACHE_HEADER "CLAPPER"

typedef enum
{
  CLAPPER_CACHE_IFACE_EXTRACTABLE = 1,
  CLAPPER_CACHE_IFACE_PLAYLISTABLE,
  CLAPPER_CACHE_IFACE_REACTABLE,
} ClapperCacheIfaces;

static GArray *enum_registry = NULL;
static GArray *flags_registry = NULL;
static gboolean cache_disabled = FALSE;

void
clapper_cache_initialize (void)
{
  const gchar *env = g_getenv ("CLAPPER_DISABLE_CACHE");

  if (G_LIKELY (!env || !g_str_has_prefix (env, "1"))) {
    enum_registry = g_array_new (FALSE, TRUE, sizeof (GEnumValue *));
    flags_registry = g_array_new (FALSE, TRUE, sizeof (GFlagsValue *));
  } else {
    cache_disabled = TRUE;
  }
}

gboolean
clapper_cache_is_disabled (void)
{
  return cache_disabled;
}

GMappedFile *
clapper_cache_open (const gchar *filename, const gchar **data, GError **error)
{
  GMappedFile *file;

  if (G_UNLIKELY (cache_disabled))
    return NULL;

  if (!(file = g_mapped_file_new (filename, FALSE, error)))
    return NULL;

  if (G_UNLIKELY (g_mapped_file_get_length (file) == 0)) {
    g_mapped_file_unref (file);
    g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
        "File is empty");
    return NULL;
  }

  *data = g_mapped_file_get_contents (file);

  /* Header name check */
  if (G_UNLIKELY (g_strcmp0 (*data, CLAPPER_CACHE_HEADER) != 0)) {
    g_mapped_file_unref (file);
    g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
        "Invalid file header");
    return NULL;
  }
  *data += strlen (*data) + 1;

  /* Header version check */
  if (clapper_cache_read_uint (data) != CLAPPER_VERSION_HEX) {
    g_mapped_file_unref (file);
    /* Just different version, so no error set */
    return NULL;
  }

  return file;
}

inline gboolean
clapper_cache_read_boolean (const gchar **data)
{
  gboolean val = *(const gboolean *) *data;
  *data += sizeof (gboolean);

  return val;
}

inline gint
clapper_cache_read_int (const gchar **data)
{
  gint val = *(const gint *) *data;
  *data += sizeof (gint);

  return val;
}

inline guint
clapper_cache_read_uint (const gchar **data)
{
  guint val = *(const guint *) *data;
  *data += sizeof (guint);

  return val;
}

inline gint64
clapper_cache_read_int64 (const gchar **data)
{
  gint64 val = *(const gint64 *) *data;
  *data += sizeof (gint64);

  return val;
}

inline gdouble
clapper_cache_read_double (const gchar **data)
{
  gdouble val = *(const gdouble *) *data;
  *data += sizeof (gdouble);

  return val;
}

inline const gchar *
clapper_cache_read_string (const gchar **data)
{
  const gboolean is_null = clapper_cache_read_boolean (data);
  const gchar *str = NULL;

  if (!is_null) {
    str = *data;
    *data += strlen (str) + 1;
  }

  return str;
}

inline const guint8 *
clapper_cache_read_data (const gchar **data, gsize *size)
{
  const guint8 *val = NULL;

  *size = *(const gsize *) *data;
  *data += sizeof (gsize);

  if (G_LIKELY (*size > 0)) {
    val = (const guint8 *) *data;
    *data += *size;
  }

  return val;
}

inline GType
clapper_cache_read_enum (const gchar **data)
{
  GType type;
  const gchar *enum_name;
  guint i, n_values;

  enum_name = clapper_cache_read_string (data);
  n_values = clapper_cache_read_uint (data);

  /* If not registered yet */
  if ((type = g_type_from_name (enum_name)) == 0) {
    GEnumValue *values = g_new0 (GEnumValue, n_values + 1);

    for (i = 0; i < n_values; ++i) {
      values[i].value = clapper_cache_read_int (data);
      values[i].value_name = g_intern_string (clapper_cache_read_string (data));
      values[i].value_nick = g_intern_string (clapper_cache_read_string (data));
    }
    g_array_append_val (enum_registry, values); // store statically

    type = g_enum_register_static (g_intern_string (enum_name),
        g_array_index (enum_registry, GEnumValue *, enum_registry->len - 1));
  } else {
    /* Skip over data */
    for (i = 0; i < n_values; ++i) {
      clapper_cache_read_int (data); // value
      clapper_cache_read_string (data); // value_name
      clapper_cache_read_string (data); // value_nick
    }
  }

  return type;
}

inline GType
clapper_cache_read_flags (const gchar **data)
{
  GType type;
  const gchar *flags_name;
  guint i, n_values;

  flags_name = clapper_cache_read_string (data);
  n_values = clapper_cache_read_uint (data);

  /* If not registered yet */
  if ((type = g_type_from_name (flags_name)) == 0) {
    GFlagsValue *values = g_new0 (GFlagsValue, n_values + 1);

    for (i = 0; i < n_values; ++i) {
      values[i].value = clapper_cache_read_int (data);
      values[i].value_name = g_intern_string (clapper_cache_read_string (data));
      values[i].value_nick = g_intern_string (clapper_cache_read_string (data));
    }
    g_array_append_val (flags_registry, values); // store statically

    type = g_flags_register_static (g_intern_string (flags_name),
        g_array_index (flags_registry, GFlagsValue *, flags_registry->len - 1));
  } else {
    /* Skip over data */
    for (i = 0; i < n_values; ++i) {
      clapper_cache_read_int (data); // value
      clapper_cache_read_string (data); // value_name
      clapper_cache_read_string (data); // value_nick
    }
  }

  return type;
}

GType
clapper_cache_read_iface (const gchar **data)
{
  gint iface_id = clapper_cache_read_int (data);

  switch (iface_id) {
    case CLAPPER_CACHE_IFACE_EXTRACTABLE:
      return CLAPPER_TYPE_EXTRACTABLE;
    case CLAPPER_CACHE_IFACE_PLAYLISTABLE:
      return CLAPPER_TYPE_PLAYLISTABLE;
    case CLAPPER_CACHE_IFACE_REACTABLE:
      return CLAPPER_TYPE_REACTABLE;
    default:
      return 0;
  }
}

GParamSpec *
clapper_cache_read_pspec (const gchar **data)
{
  GParamSpec *pspec;
  GType value_type;
  const gchar *name, *nick, *blurb;
  GParamFlags flags;

  value_type = *(const GType *) *data;
  *data += sizeof (GType);

  name = clapper_cache_read_string (data);
  nick = clapper_cache_read_string (data);
  blurb = clapper_cache_read_string (data);

  flags = *(const GParamFlags *) *data;
  *data += sizeof (GParamFlags);

  /* NOTE: C does not guarantee order in which function arguments
   * are evaluated, so read into variables and then create pspec  */

  switch (value_type) {
    case G_TYPE_BOOLEAN:
      pspec = g_param_spec_boolean (name, nick, blurb,
          clapper_cache_read_boolean (data), flags);
      break;
    case G_TYPE_INT:{
      gint minimum = clapper_cache_read_int (data);
      gint maximum = clapper_cache_read_int (data);
      gint default_value = clapper_cache_read_int (data);

      pspec = g_param_spec_int (name, nick, blurb,
          minimum, maximum, default_value, flags);
      break;
    }
    case G_TYPE_UINT:{
      guint minimum = clapper_cache_read_uint (data);
      guint maximum = clapper_cache_read_uint (data);
      guint default_value = clapper_cache_read_uint (data);

      pspec = g_param_spec_uint (name, nick, blurb,
          minimum, maximum, default_value, flags);
      break;
    }
    case G_TYPE_DOUBLE:{
      gdouble minimum = clapper_cache_read_double (data);
      gdouble maximum = clapper_cache_read_double (data);
      gdouble default_value = clapper_cache_read_double (data);

      pspec = g_param_spec_double (name, nick, blurb,
          minimum, maximum, default_value, flags);
      break;
    }
    case G_TYPE_STRING:
      pspec = g_param_spec_string (name, nick, blurb,
          clapper_cache_read_string (data), flags);
      break;
    case G_TYPE_ENUM:{
      GType enum_type = clapper_cache_read_enum (data);
      gint default_value = clapper_cache_read_int (data);

      pspec = g_param_spec_enum (name, nick, blurb,
          enum_type, default_value, flags);
      break;
    }
    case G_TYPE_FLAGS:{
      GType flags_type = clapper_cache_read_flags (data);
      guint default_value = clapper_cache_read_uint (data);

      pspec = g_param_spec_flags (name, nick, blurb,
          flags_type, default_value, flags);
      break;
    }
    default:
      return NULL;
  }

  return g_param_spec_ref_sink (pspec);
}

GByteArray *
clapper_cache_create (void)
{
  GByteArray *bytes;

  if (G_UNLIKELY (cache_disabled))
    return NULL;

  bytes = g_byte_array_new ();

  /* NOTE: We do not store whether string is NULL here, since it never is */
  g_byte_array_append (bytes, (const guint8 *) CLAPPER_CACHE_HEADER, 8); // 7 + 1
  clapper_cache_store_uint (bytes, CLAPPER_VERSION_HEX);

  return bytes;
}

inline void
clapper_cache_store_boolean (GByteArray *bytes, gboolean val)
{
  g_byte_array_append (bytes, (const guint8 *) &val, sizeof (gboolean));
}

inline void
clapper_cache_store_int (GByteArray *bytes, gint val)
{
  g_byte_array_append (bytes, (const guint8 *) &val, sizeof (gint));
}

inline void
clapper_cache_store_uint (GByteArray *bytes, guint val)
{
  g_byte_array_append (bytes, (const guint8 *) &val, sizeof (guint));
}

inline void
clapper_cache_store_int64 (GByteArray *bytes, gint64 val)
{
  g_byte_array_append (bytes, (const guint8 *) &val, sizeof (gint64));
}

inline void
clapper_cache_store_double (GByteArray *bytes, gdouble val)
{
  g_byte_array_append (bytes, (const guint8 *) &val, sizeof (gdouble));
}

inline void
clapper_cache_store_string (GByteArray *bytes, const gchar *val)
{
  /* Distinguish empty string from NULL */
  const gboolean is_null = (val == NULL);

  clapper_cache_store_boolean (bytes, is_null);
  if (!is_null)
    g_byte_array_append (bytes, (const guint8 *) val, strlen (val) + 1);
}

inline void
clapper_cache_store_data (GByteArray *bytes, const guint8 *val, gsize val_size)
{
  g_byte_array_append (bytes, (const guint8 *) &val_size, sizeof (gsize));
  if (G_LIKELY (val_size > 0))
    g_byte_array_append (bytes, val, val_size);
}

inline void
clapper_cache_store_enum (GByteArray *bytes, GType enum_type)
{
  GEnumClass *enum_class = G_ENUM_CLASS (g_type_class_peek (enum_type));
  guint i;

  clapper_cache_store_string (bytes, g_type_name (enum_type));
  clapper_cache_store_uint (bytes, enum_class->n_values);

  for (i = 0; i < enum_class->n_values; ++i) {
    clapper_cache_store_int (bytes, enum_class->values[i].value);
    clapper_cache_store_string (bytes, enum_class->values[i].value_name);
    clapper_cache_store_string (bytes, enum_class->values[i].value_nick);
  }
}

inline void
clapper_cache_store_flags (GByteArray *bytes, GType flags_type)
{
  GFlagsClass *flags_class = G_FLAGS_CLASS (g_type_class_peek (flags_type));
  guint i;

  clapper_cache_store_string (bytes, g_type_name (flags_type));
  clapper_cache_store_uint (bytes, flags_class->n_values);

  for (i = 0; i < flags_class->n_values; ++i) {
    clapper_cache_store_int (bytes, flags_class->values[i].value);
    clapper_cache_store_string (bytes, flags_class->values[i].value_name);
    clapper_cache_store_string (bytes, flags_class->values[i].value_nick);
  }
}

gboolean
clapper_cache_store_iface (GByteArray *bytes, GType iface)
{
  gint iface_id = 0;

  if (iface == CLAPPER_TYPE_EXTRACTABLE)
    iface_id = CLAPPER_CACHE_IFACE_EXTRACTABLE;
  else if (iface == CLAPPER_TYPE_PLAYLISTABLE)
    iface_id = CLAPPER_CACHE_IFACE_PLAYLISTABLE;
  else if (iface == CLAPPER_TYPE_REACTABLE)
    iface_id = CLAPPER_CACHE_IFACE_REACTABLE;
  else
    return FALSE;

  clapper_cache_store_int (bytes, iface_id);
  return TRUE;
}

gboolean
clapper_cache_store_pspec (GByteArray *bytes, GParamSpec *pspec)
{
  GParamFlags flags;
  const gboolean is_enum = G_IS_PARAM_SPEC_ENUM (pspec);
  const gboolean is_flags = (!is_enum && G_IS_PARAM_SPEC_FLAGS (pspec));

  if (is_enum) {
    GType enum_type = G_TYPE_ENUM;
    g_byte_array_append (bytes, (const guint8 *) &enum_type, sizeof (GType));
  } else if (is_flags) {
    GType flags_type = G_TYPE_FLAGS;
    g_byte_array_append (bytes, (const guint8 *) &flags_type, sizeof (GType));
  } else {
    g_byte_array_append (bytes, (const guint8 *) &pspec->value_type, sizeof (GType));
  }

  clapper_cache_store_string (bytes, g_param_spec_get_name (pspec));
  clapper_cache_store_string (bytes, g_param_spec_get_nick (pspec));
  clapper_cache_store_string (bytes, g_param_spec_get_blurb (pspec));

  flags = pspec->flags;
  flags &= ~G_PARAM_STATIC_STRINGS; // Data read from cache is never static
  g_byte_array_append (bytes, (const guint8 *) &flags, sizeof (GParamFlags));

  switch (pspec->value_type) {
    case G_TYPE_BOOLEAN:{
      GParamSpecBoolean *p = (GParamSpecBoolean *) pspec;
      clapper_cache_store_boolean (bytes, p->default_value);
      break;
    }
    case G_TYPE_INT:{
      GParamSpecInt *p = (GParamSpecInt *) pspec;
      clapper_cache_store_int (bytes, p->minimum);
      clapper_cache_store_int (bytes, p->maximum);
      clapper_cache_store_int (bytes, p->default_value);
      break;
    }
    case G_TYPE_UINT:{
      GParamSpecUInt *p = (GParamSpecUInt *) pspec;
      clapper_cache_store_uint (bytes, p->minimum);
      clapper_cache_store_uint (bytes, p->maximum);
      clapper_cache_store_uint (bytes, p->default_value);
      break;
    }
    case G_TYPE_DOUBLE:{
      GParamSpecDouble *p = (GParamSpecDouble *) pspec;
      clapper_cache_store_double (bytes, p->minimum);
      clapper_cache_store_double (bytes, p->maximum);
      clapper_cache_store_double (bytes, p->default_value);
      break;
    }
    case G_TYPE_STRING:{
      GParamSpecString *p = (GParamSpecString *) pspec;
      clapper_cache_store_string (bytes, p->default_value);
      break;
    }
    default:{
      if (is_enum) {
        GParamSpecEnum *p = (GParamSpecEnum *) pspec;
        clapper_cache_store_enum (bytes, pspec->value_type);
        clapper_cache_store_int (bytes, p->default_value);
        break;
      } else if (is_flags) {
        GParamSpecFlags *p = (GParamSpecFlags *) pspec;
        clapper_cache_store_flags (bytes, pspec->value_type);
        clapper_cache_store_uint (bytes, p->default_value);
        break;
      }
      return FALSE;
    }
  }

  return TRUE;
}

gboolean
clapper_cache_write (const gchar *filename, GByteArray *bytes, GError **error)
{
  gchar *dirname = g_path_get_dirname (filename);
  gboolean has_dir;

  has_dir = (g_mkdir_with_parents (dirname, 0755) == 0);
  g_free (dirname);

  if (!has_dir) {
    g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
        "Could not create directory to store cache content");
    return FALSE;
  }

  /* Using "g_file_set_contents" to replace file atomically */
  return g_file_set_contents (filename, (const gchar *) bytes->data, bytes->len, error);
}
