/* ide-snippet-context.c
 *
 * Copyright 2013-2019 Christian Hergert <christian@hergert.me>
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define G_LOG_DOMAIN "ide-snippets-context"

#include "config.h"

#include <errno.h>
#include <glib/gi18n.h>
#include <stdlib.h>

#include "ide-snippet-context.h"

/**
 * SECTION:ide-snippet-context
 * @title: IdeSnippetContext
 * @short_description: Context for expanding #IdeSnippetChunk
 *
 * This class is currently used primary as a hashtable. However, the longer
 * term goal is to have it hold onto a GjsContext as well as other languages
 * so that #IdeSnippetChunk can expand themselves by executing
 * script within the context.
 *
 * The #IdeSnippet will build the context and then expand each of the
 * chunks during the insertion/edit phase.
 *
 * Since: 3.32
 */

struct _IdeSnippetContext
{
  GObject     parent_instance;

  GHashTable *shared;
  GHashTable *variables;
  gchar      *line_prefix;
  gint        tab_width;
  guint       use_spaces : 1;
};

struct _IdeSnippetContextClass
{
  GObjectClass parent;
};

G_DEFINE_FINAL_TYPE (IdeSnippetContext, ide_snippet_context, G_TYPE_OBJECT)

enum {
  CHANGED,
  LAST_SIGNAL
};

typedef gchar *(*InputFilter) (const gchar *input);

static GHashTable *filters;
static guint signals[LAST_SIGNAL];

IdeSnippetContext *
ide_snippet_context_new (void)
{
  return g_object_new (IDE_TYPE_SNIPPET_CONTEXT, NULL);
}

void
ide_snippet_context_dump (IdeSnippetContext *context)
{
  GHashTableIter iter;
  gpointer key;
  gpointer value;

  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));

  g_hash_table_iter_init (&iter, context->variables);
  while (g_hash_table_iter_next (&iter, &key, &value))
    g_print (" %s=%s\n", (gchar *) key, (gchar *) value);
}

void
ide_snippet_context_clear_variables (IdeSnippetContext *context)
{
  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));

  g_hash_table_remove_all (context->variables);
}

void
ide_snippet_context_add_variable (IdeSnippetContext *context,
                                  const gchar       *key,
                                  const gchar       *value)
{
  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
  g_return_if_fail (key);

  g_hash_table_replace (context->variables, g_strdup (key), g_strdup (value));
}

void
ide_snippet_context_add_shared_variable (IdeSnippetContext *context,
                                         const gchar       *key,
                                         const gchar       *value)
{
  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
  g_return_if_fail (key);

  g_hash_table_replace (context->shared, g_strdup (key), g_strdup (value));
}

const gchar *
ide_snippet_context_get_variable (IdeSnippetContext *context,
                                  const gchar       *key)
{
  const gchar *ret;

  g_return_val_if_fail (IDE_IS_SNIPPET_CONTEXT (context), NULL);

  if (!(ret = g_hash_table_lookup (context->variables, key)))
    ret = g_hash_table_lookup (context->shared, key);

  return ret;
}

static gchar *
filter_lower (const gchar *input)
{
  return g_utf8_strdown (input, -1);
}

static gchar *
filter_upper (const gchar *input)
{
  return g_utf8_strup (input, -1);
}

static gchar *
filter_capitalize (const gchar *input)
{
  gunichar c;
  GString *str;

  if (!*input)
    return g_strdup ("");

  c = g_utf8_get_char (input);

  if (g_unichar_isupper (c))
    return g_strdup (input);

  str = g_string_new (NULL);
  input = g_utf8_next_char (input);
  g_string_append_unichar (str, g_unichar_toupper (c));
  if (*input)
    g_string_append (str, input);

  return g_string_free (str, FALSE);
}

static gchar *
filter_decapitalize (const gchar *input)
{
  gunichar c;
  GString *str;

  c = g_utf8_get_char (input);

  if (g_unichar_islower (c))
    return g_strdup (input);

  str = g_string_new (NULL);
  input = g_utf8_next_char (input);
  g_string_append_unichar (str, g_unichar_tolower (c));
  g_string_append (str, input);

  return g_string_free (str, FALSE);
}

static gchar *
filter_html (const gchar *input)
{
  gunichar c;
  GString *str;

  str = g_string_new (NULL);

  for (; *input; input = g_utf8_next_char (input))
    {
      c = g_utf8_get_char (input);
      switch (c)
        {
        case '<':
          g_string_append_len (str, "&lt;", 4);
          break;

        case '>':
          g_string_append_len (str, "&gt;", 4);
          break;

        default:
          g_string_append_unichar (str, c);
          break;
        }
    }

  return g_string_free (str, FALSE);
}

static gchar *
filter_camelize (const gchar *input)
{
  gboolean next_is_upper = TRUE;
  gboolean skip = FALSE;
  gunichar c;
  GString *str;

  if (!strchr (input, '_') && !strchr (input, ' ') && !strchr (input, '-'))
    return filter_capitalize (input);

  str = g_string_new (NULL);

  for (; *input; input = g_utf8_next_char (input))
    {
      c = g_utf8_get_char (input);

      switch (c)
        {
        case '_':
        case '-':
        case ' ':
          next_is_upper = TRUE;
          skip = TRUE;
          break;

        default:
          break;
        }

      if (skip)
        {
          skip = FALSE;
          continue;
        }

      if (next_is_upper)
        {
          c = g_unichar_toupper (c);
          next_is_upper = FALSE;
        }
      else
        c = g_unichar_tolower (c);

      g_string_append_unichar (str, c);
    }

  if (g_str_has_suffix (str->str, "Private"))
    g_string_truncate (str, str->len - strlen ("Private"));

  return g_string_free (str, FALSE);
}

static gchar *
filter_functify (const gchar *input)
{
  gunichar last = 0;
  gunichar c;
  gunichar n;
  GString *str;

  str = g_string_new (NULL);

  for (; *input; input = g_utf8_next_char (input))
    {
      c = g_utf8_get_char (input);
      n = g_utf8_get_char (g_utf8_next_char (input));

      if (last)
        {
          if ((g_unichar_islower (last) && g_unichar_isupper (c)) ||
              (g_unichar_isupper (c) && g_unichar_islower (n)))
            g_string_append_c (str, '_');
        }

      if ((c == ' ') || (c == '-'))
        c = '_';

      g_string_append_unichar (str, g_unichar_tolower (c));

      last = c;
    }

  if (g_str_has_suffix (str->str, "_private") ||
      g_str_has_suffix (str->str, "_PRIVATE"))
    g_string_truncate (str, str->len - strlen ("_private"));

  return g_string_free (str, FALSE);
}

static gchar *
filter_namespace (const gchar *input)
{
  gunichar last = 0;
  gunichar c;
  gunichar n;
  GString *str;
  gboolean first_is_lower = FALSE;

  str = g_string_new (NULL);

  for (; *input; input = g_utf8_next_char (input))
    {
      c = g_utf8_get_char (input);
      n = g_utf8_get_char (g_utf8_next_char (input));

      if (c == '_')
        break;

      if (last)
        {
          if ((g_unichar_islower (last) && g_unichar_isupper (c)) ||
              (g_unichar_isupper (c) && g_unichar_islower (n)))
            break;
        }
      else
        first_is_lower = g_unichar_islower (c);

      if ((c == ' ') || (c == '-'))
        break;

      g_string_append_unichar (str, c);

      last = c;
    }

  if (first_is_lower)
    {
      gchar *ret;

      ret = filter_capitalize (str->str);
      g_string_free (str, TRUE);
      return ret;
    }

  return g_string_free (str, FALSE);
}

static gchar *
filter_class (const gchar *input)
{
  gchar *camel;
  gchar *ns;
  gchar *ret = NULL;

  camel = filter_camelize (input);
  ns = filter_namespace (input);

  if (g_str_has_prefix (camel, ns))
    ret = g_strdup (camel + strlen (ns));
  else
    {
      ret = camel;
      camel = NULL;
    }

  g_free (camel);
  g_free (ns);

  return ret;
}

static gchar *
filter_instance (const gchar *input)
{
  const gchar *tmp;
  gchar *funct = NULL;
  gchar *ret;

  if (!strchr (input, '_'))
    {
      funct = filter_functify (input);
      input = funct;
    }

  if ((tmp = strrchr (input, '_')))
    ret = g_strdup (tmp+1);
  else
    ret = g_strdup (input);

  g_free (funct);

  return ret;
}

static gchar *
filter_space (const gchar *input)
{
  GString *str;

  str = g_string_new (NULL);
  for (; *input; input = g_utf8_next_char (input))
    g_string_append_c (str, ' ');

  return g_string_free (str, FALSE);
}

static gchar *
filter_descend_path (const gchar *input)
{
  const gchar *pos;

  if (input == NULL)
    return NULL;

  while (*input == G_DIR_SEPARATOR)
    input++;

  if ((pos = strchr (input, G_DIR_SEPARATOR)))
    return g_strdup (pos + 1);

   return NULL;
}

static gchar *
filter_stripsuffix (const gchar *input)
{
  const gchar *endpos;

  g_return_val_if_fail (input, NULL);

  endpos = strrchr (input, '.');
  if (endpos)
    return g_strndup (input, (endpos - input));

  return g_strdup (input);
}

static gchar *
filter_slash_to_dots (const gchar *input)
{
  GString *str;
  gunichar ch;

  if (input == NULL)
    return NULL;

  str = g_string_new (NULL);

  for (; *input; input = g_utf8_next_char (input))
    {
      ch = g_utf8_get_char (input);

      if (ch == G_DIR_SEPARATOR)
        g_string_append_c (str, '.');
      else
        g_string_append_unichar (str, ch);
    }

  return g_string_free (str, FALSE);
}

static gchar *
apply_filter (gchar       *input,
              const gchar *filter)
{
  InputFilter filter_func;
  gchar *tmp;

  if ((filter_func = g_hash_table_lookup (filters, filter)))
    {
      tmp = input;
      input = filter_func (input);
      g_free (tmp);
    }

  return input;
}

static gchar *
apply_filters (GString     *str,
               const gchar *filters_list)
{
  gchar **filter_names;
  gchar *input = g_string_free (str, FALSE);
  gint i;

  filter_names = g_strsplit (filters_list, "|", 0);

  for (i = 0; filter_names[i]; i++)
    input = apply_filter (input, filter_names[i]);

  g_strfreev (filter_names);

  return input;
}

static gchar *
scan_forward (const gchar  *input,
              const gchar **endpos,
              gunichar      needle)
{
  const gchar *begin = input;

  for (; *input; input = g_utf8_next_char (input))
    {
      gunichar c = g_utf8_get_char (input);

      if (c == needle)
        {
          *endpos = input;
          return g_strndup (begin, (input - begin));
        }
    }

  *endpos = NULL;

  return NULL;
}

gchar *
ide_snippet_context_expand (IdeSnippetContext *context,
                            const gchar       *input)
{
  const gchar *expand;
  gunichar c;
  gboolean is_dynamic;
  GString *str;
  gchar key[12];
  glong n;
  gint i;

  g_return_val_if_fail (IDE_IS_SNIPPET_CONTEXT (context), NULL);
  g_return_val_if_fail (input, NULL);

  is_dynamic = (*input == '$');

  str = g_string_new (NULL);

  for (; *input; input = g_utf8_next_char (input))
    {
      c = g_utf8_get_char (input);
      if (c == '\\')
        {
          input = g_utf8_next_char (input);
          if (!*input)
            break;
          c = g_utf8_get_char (input);
        }
      else if (is_dynamic && c == '$')
        {
          input = g_utf8_next_char (input);
          if (!*input)
            break;
          c = g_utf8_get_char (input);
          if (g_unichar_isdigit (c))
            {
              errno = 0;
              n = strtol (input, (gchar * *) &input, 10);
              if (((n == LONG_MIN) || (n == LONG_MAX)) && errno == ERANGE)
                break;
              input--;
              g_snprintf (key, sizeof key, "%ld", n);
              key[sizeof key - 1] = '\0';
              expand = ide_snippet_context_get_variable (context, key);
              if (expand)
                g_string_append (str, expand);
              continue;
            }
          else
            {
              if (strchr (input, '|'))
                {
                  g_autofree gchar *lkey = NULL;

                  lkey = g_strndup (input, strchr (input, '|') - input);
                  expand = ide_snippet_context_get_variable (context, lkey);
                  if (expand)
                    {
                      g_string_append (str, expand);
                      input = strchr (input, '|') - 1;
                    }
                  else
                    input += strlen (input) - 1;
                }
              else
                {
                  expand = ide_snippet_context_get_variable (context, input);
                  if (expand)
                    g_string_append (str, expand);
                  else
                    {
                      g_string_append_c (str, '$');
                      g_string_append (str, input);
                    }
                  input += strlen (input) - 1;
                }
              continue;
            }
        }
      else if (is_dynamic && c == '|')
        return apply_filters (str, input + 1);
      else if (c == '`')
        {
          const gchar *endpos = NULL;
          gchar *slice;

          slice = scan_forward (input + 1, &endpos, '`');

          if (slice)
            {
              gchar *expanded;

              input = endpos;

              expanded = ide_snippet_context_expand (context, slice);

              g_string_append (str, expanded);

              g_free (expanded);
              g_free (slice);

              continue;
            }
        }
      else if (c == '\t')
        {
          if (context->use_spaces)
            for (i = 0; i < context->tab_width; i++)
              g_string_append_c (str, ' ');

          else
            g_string_append_c (str, '\t');
          continue;
        }
      else if (c == '\n')
        {
          g_string_append_c (str, '\n');
          if (context->line_prefix)
            g_string_append (str, context->line_prefix);
          continue;
        }
      g_string_append_unichar (str, c);
    }

  return g_string_free (str, FALSE);
}

void
ide_snippet_context_set_tab_width (IdeSnippetContext *context,
                                   gint               tab_width)
{
  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
  context->tab_width = tab_width;
}

void
ide_snippet_context_set_use_spaces (IdeSnippetContext *context,
                                    gboolean           use_spaces)
{
  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
  context->use_spaces = !!use_spaces;
}

void
ide_snippet_context_set_line_prefix (IdeSnippetContext *context,
                                     const gchar       *line_prefix)
{
  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
  g_free (context->line_prefix);
  context->line_prefix = g_strdup (line_prefix);
}

void
ide_snippet_context_emit_changed (IdeSnippetContext *context)
{
  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
  g_signal_emit (context, signals[CHANGED], 0);
}

static void
ide_snippet_context_finalize (GObject *object)
{
  IdeSnippetContext *context = (IdeSnippetContext *)object;

  g_clear_pointer (&context->shared, g_hash_table_unref);
  g_clear_pointer (&context->variables, g_hash_table_unref);
  g_clear_pointer (&context->line_prefix, g_free);

  G_OBJECT_CLASS (ide_snippet_context_parent_class)->finalize (object);
}

static void
ide_snippet_context_class_init (IdeSnippetContextClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = ide_snippet_context_finalize;

  signals[CHANGED] = g_signal_new ("changed",
                                    IDE_TYPE_SNIPPET_CONTEXT,
                                    G_SIGNAL_RUN_FIRST,
                                    0,
                                    NULL, NULL, NULL,
                                    G_TYPE_NONE,
                                    0);

  filters = g_hash_table_new (g_str_hash, g_str_equal);
  g_hash_table_insert (filters, (gpointer) "lower", filter_lower);
  g_hash_table_insert (filters, (gpointer) "upper", filter_upper);
  g_hash_table_insert (filters, (gpointer) "capitalize", filter_capitalize);
  g_hash_table_insert (filters, (gpointer) "decapitalize", filter_decapitalize);
  g_hash_table_insert (filters, (gpointer) "html", filter_html);
  g_hash_table_insert (filters, (gpointer) "camelize", filter_camelize);
  g_hash_table_insert (filters, (gpointer) "functify", filter_functify);
  g_hash_table_insert (filters, (gpointer) "namespace", filter_namespace);
  g_hash_table_insert (filters, (gpointer) "class", filter_class);
  g_hash_table_insert (filters, (gpointer) "space", filter_space);
  g_hash_table_insert (filters, (gpointer) "stripsuffix", filter_stripsuffix);
  g_hash_table_insert (filters, (gpointer) "instance", filter_instance);
  g_hash_table_insert (filters, (gpointer) "slash_to_dots", filter_slash_to_dots);
  g_hash_table_insert (filters, (gpointer) "descend_path", filter_descend_path);
}

static void
ide_snippet_context_init (IdeSnippetContext *context)
{
  GDateTime *dt;
  gchar *str;

  context->variables = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

  context->shared = g_hash_table_new_full (g_str_hash,
                                                 g_str_equal,
                                                 g_free,
                                                 g_free);

#define ADD_VARIABLE(k, v) \
  g_hash_table_insert (context->shared, g_strdup (k), g_strdup (v))

  ADD_VARIABLE ("username", g_get_user_name ());
  ADD_VARIABLE ("fullname", g_get_real_name ());
  ADD_VARIABLE ("author", g_get_real_name ());

  dt = g_date_time_new_now_local ();
  str = g_date_time_format (dt, "%Y");
  ADD_VARIABLE ("year", str);
  g_free (str);
  str = g_date_time_format (dt, "%b");
  ADD_VARIABLE ("shortmonth", str);
  g_free (str);
  str = g_date_time_format (dt, "%d");
  ADD_VARIABLE ("day", str);
  g_free (str);
  str = g_date_time_format (dt, "%a");
  ADD_VARIABLE ("shortweekday", str);
  g_free (str);
  g_date_time_unref (dt);

  ADD_VARIABLE ("email", "unknown@domain.org");

#undef ADD_VARIABLE
}
