/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s:
 * Copyright © 2018 Red Hat, Inc
 *
 * This program 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:
 *       Alexander Larsson <alexl@redhat.com>
 */

#include "config.h"

#include "flatpak-complete.h"
#include "flatpak-installation.h"
#include "flatpak-utils-private.h"

/* Uncomment to get debug traces in /tmp/flatpak-completion-debug.txt (nice
 * to not have it interfere with stdout/stderr)
 */
#if 0
void
flatpak_completion_debug (const gchar *format, ...)
{
  va_list var_args;
  gchar *s;
  static FILE *f = NULL;

  va_start (var_args, format);
  s = g_strdup_vprintf (format, var_args);
  if (f == NULL)
    f = fopen ("/tmp/flatpak-completion-debug.txt", "a+");
  fprintf (f, "%s\n", s);
  fflush (f);
  g_free (s);
}
#else
void
flatpak_completion_debug (const gchar *format, ...)
{
}
#endif

static gboolean
is_word_separator (char c)
{
  return g_ascii_isspace (c);
}

void
flatpak_complete_file (FlatpakCompletion *completion,
                       const char        *file_type)
{
  flatpak_completion_debug ("completing FILE");
  g_print ("%s\n", file_type);
}

void
flatpak_complete_dir (FlatpakCompletion *completion)
{
  flatpak_completion_debug ("completing DIR");
  g_print ("%s\n", "__FLATPAK_DIR");
}

void
flatpak_complete_word (FlatpakCompletion *completion,
                       const char *format,
                       ...)
{
  va_list args;
  const char *rest;
  const char *shell_cur;
  const char *shell_cur_end;
  g_autofree char *string = NULL;

  g_return_if_fail (format != NULL);

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
  va_start (args, format);
  string = g_strdup_vprintf (format, args);
  va_end (args);
#pragma GCC diagnostic pop

  if (!g_str_has_prefix (string, completion->cur))
    return;

  shell_cur = completion->shell_cur ? completion->shell_cur : "";

  rest = string + strlen (completion->cur);

  shell_cur_end = shell_cur + strlen (shell_cur);
  while (shell_cur_end > shell_cur &&
         rest > string &&
         shell_cur_end[-1] == rest[-1] &&
         /* I'm not sure exactly what bash is doing here with =, but this seems to work... */
         shell_cur_end[-1] != '=')
    {
      rest--;
      shell_cur_end--;
    }

  flatpak_completion_debug ("completing word: '%s' (%s)", string, rest);

  g_print ("%s\n", rest);
}

void
flatpak_complete_ref_id (FlatpakCompletion *completion,
                         GPtrArray         *refs)
{
  if (refs == NULL)
    return;

  for (int i = 0; i < refs->len; i++)
    {
      FlatpakDecomposed *ref = g_ptr_array_index (refs, i);
      g_autofree char *id = flatpak_decomposed_dup_id (ref);
      flatpak_complete_word (completion, "%s ", id);
    }
}

void
flatpak_complete_ref_branch (FlatpakCompletion *completion,
                             GPtrArray         *refs)
{
  if (refs == NULL)
    return;

  for (int i = 0; i < refs->len; i++)
    {
      FlatpakDecomposed *ref = g_ptr_array_index (refs, i);
      g_autofree char *branch = flatpak_decomposed_dup_branch (ref);
      flatpak_complete_word (completion, "%s ", branch);
    }
}

void
flatpak_complete_ref (FlatpakCompletion *completion,
                      OstreeRepo        *repo)
{
  g_autoptr(GHashTable) refs = NULL;
  flatpak_completion_debug ("completing REF");

  if (ostree_repo_list_refs (repo,
                             NULL,
                             &refs, NULL, NULL))
    {
      GHashTableIter hashiter;
      gpointer hashkey, hashvalue;

      g_hash_table_iter_init (&hashiter, refs);
      while ((g_hash_table_iter_next (&hashiter, &hashkey, &hashvalue)))
        {
          const char *ref = (const char *) hashkey;
          if (!(g_str_has_prefix (ref, "runtime/") ||
                g_str_has_prefix (ref, "app/")))
            continue;
          flatpak_complete_word (completion, "%s", ref);
        }
    }
}

static int
find_current_element (const char *str)
{
  int count = 0;

  if (g_str_has_prefix (str, "app/"))
    str += strlen ("app/");
  else if (g_str_has_prefix (str, "runtime/"))
    str += strlen ("runtime/");

  while (str != NULL)
    {
      str = strchr (str, '/');
      count++;
      if (str != NULL)
        str = str + 1;
    }

  return count;
}

void
flatpak_complete_partial_ref (FlatpakCompletion *completion,
                              FlatpakKinds       kinds,
                              const char        *only_arch,
                              FlatpakDir        *dir,
                              const char        *remote)
{
  FlatpakKinds matched_kinds;
  const char *pref;
  g_autofree char *id = NULL;
  g_autofree char *arch = NULL;
  g_autofree char *branch = NULL;
  g_autoptr(GPtrArray) refs = NULL;
  int element;
  const char *cur_parts[4] = { NULL };
  g_autoptr(GError) error = NULL;
  int i;

  pref = completion->cur;
  element = find_current_element (pref);
  if (element > 3)
    return;

  flatpak_split_partial_ref_arg_novalidate (pref, kinds,
                                            NULL, NULL,
                                            &matched_kinds, &id, &arch, &branch);

  cur_parts[1] = id;
  cur_parts[2] = arch ? arch : "";
  cur_parts[3] = branch ? branch : "";

  if (remote)
    {
      g_autoptr(FlatpakRemoteState) state = get_remote_state (dir, remote, TRUE, FALSE,
                                                              (element > 2) ? arch : only_arch, NULL,
                                                              NULL, &error);
      if (state != NULL)
        refs = flatpak_dir_find_remote_refs (dir, state,
                                             (element > 1) ? id : NULL,
                                             NULL, /* branch */
                                             NULL, /* default branch */
                                             (element > 2) ? arch : only_arch,
                                             NULL, /* default arch */
                                             matched_kinds,
                                             FIND_MATCHING_REFS_FLAGS_NONE,
                                             NULL, &error);
    }
  else
    {
      refs = flatpak_dir_find_installed_refs (dir,
                                              (element > 1) ? id : NULL,
                                              NULL, /* branch */
                                              (element > 2) ? arch : only_arch,
                                              matched_kinds,
                                              FIND_MATCHING_REFS_FLAGS_NONE,
                                              &error);
    }
  if (refs == NULL)
    flatpak_completion_debug ("find refs error: %s", error->message);

  for (i = 0; refs != NULL && i < refs->len; i++)
    {
      FlatpakDecomposed *ref = g_ptr_array_index (refs, i);
      int j;
      g_autoptr(GString) comp = NULL;
      g_auto(GStrv) parts = g_strsplit (flatpak_decomposed_get_ref (ref), "/", 0);

      if (!g_str_has_prefix (parts[element], cur_parts[element]))
        continue;

      if (flatpak_id_has_subref_suffix (parts[element], -1))
        {
          char *last_dot = strrchr (parts[element], '.');

          if (last_dot == NULL)
            continue; /* Shouldn't really happen */

          /* Only complete to subrefs if fully matching real part.
           * For example, only match org.foo.Bar.Sources for
           * "org.foo.Bar", "org.foo.Bar." or "org.foo.Bar.S", but
           * not for "org.foo" or other shorter prefixes.
           */
          if (strncmp (parts[element], cur_parts[element], last_dot - parts[element] - 1) != 0)
            continue;
        }

      comp = g_string_new (pref);
      g_string_append (comp, parts[element] + strlen (cur_parts[element]));

      /* Only complete on the last part if the user explicitly adds a / */
      if (element >= 2)
        {
          for (j = element + 1; j < 4; j++)
            {
              g_string_append (comp, "/");
              g_string_append (comp, parts[j]);
            }
        }

      flatpak_complete_word (completion, "%s", comp->str);
    }
}

static gboolean
switch_already_in_line (FlatpakCompletion *completion,
                        GOptionEntry      *entry)
{
  guint i = 0;
  guint line_part_len = 0;

  for (; i < completion->original_argc; ++i)
    {
      line_part_len = strlen (completion->original_argv[i]);
      if (line_part_len > 2 &&
          g_strcmp0 (&completion->original_argv[i][2], entry->long_name) == 0)
        return TRUE;

      if (line_part_len == 2 &&
          completion->original_argv[i][1] == entry->short_name)
        return TRUE;
    }

  return FALSE;
}

static gboolean
should_filter_out_option_from_completion (FlatpakCompletion *completion,
                                          GOptionEntry      *entry)
{
  switch (entry->arg)
    {
    case G_OPTION_ARG_NONE:
    case G_OPTION_ARG_STRING:
    case G_OPTION_ARG_INT:
    case G_OPTION_ARG_FILENAME:
    case G_OPTION_ARG_DOUBLE:
    case G_OPTION_ARG_INT64:
      return switch_already_in_line (completion, entry);

    default:
      return FALSE;
    }
}

void
flatpak_complete_options (FlatpakCompletion *completion,
                          GOptionEntry      *entries)
{
  GOptionEntry *e = entries;
  int i;

  if (completion->cur == NULL || completion->cur[0] != '-')
    return;

  while (e->long_name != NULL)
    {
      if (e->arg_description)
        {
          g_autofree char *prefix = g_strdup_printf ("--%s=", e->long_name);

          if (g_str_has_prefix (completion->cur, prefix))
            {
              if (strcmp (e->arg_description, "ARCH") == 0)
                {
                  const char *arches[] = {"i386", "x86_64", "aarch64", "arm"};
                  for (i = 0; i < G_N_ELEMENTS (arches); i++)
                    flatpak_complete_word (completion, "%s%s ", prefix, arches[i]);
                }
              else if (strcmp (e->arg_description, "SHARE") == 0)
                {
                  for (i = 0; flatpak_context_shares[i] != NULL; i++)
                    flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_shares[i]);
                }
              else if (strcmp (e->arg_description, "DEVICE") == 0)
                {
                  for (i = 0; flatpak_context_devices[i] != NULL; i++)
                    flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_devices[i]);
                }
              else if (strcmp (e->arg_description, "FEATURE") == 0)
                {
                  for (i = 0; flatpak_context_features[i] != NULL; i++)
                    flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_features[i]);
                }
              else if (strcmp (e->arg_description, "SOCKET") == 0)
                {
                  for (i = 0; flatpak_context_sockets[i] != NULL; i++)
                    flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_sockets[i]);
                }
              else if (strcmp (e->arg_description, "FILE") == 0)
                {
                  flatpak_complete_file (completion, "__FLATPAK_FILE");
                }
              else if (strcmp (e->long_name, "installation") == 0)
                {
                  g_autoptr(GPtrArray) installations = NULL;
                  installations = flatpak_get_system_installations (NULL, NULL);
                  for (i = 0; i < installations->len; i++)
                    {
                      FlatpakInstallation *inst = g_ptr_array_index (installations, i);
                      flatpak_complete_word (completion, "%s%s ", prefix, flatpak_installation_get_id (inst));
                    }
                }
              else if (strcmp (e->long_name, "columns") == 0)
                {
                  /* columns are treated separately */
                }
              else
                flatpak_complete_word (completion, "%s", prefix);
            }
          else
            flatpak_complete_word (completion, "%s", prefix);
        }
      else
        {
          /* If this is just a switch, then don't add it multiple
           * times */
          if (!should_filter_out_option_from_completion (completion, e))
            {
              flatpak_complete_word (completion, "--%s ", e->long_name);
            }
          else
            {
              flatpak_completion_debug ("switch --%s is already in line %s", e->long_name, completion->line);
            }
        }

      /* We may end up checking switch_already_in_line twice, but this is
       * for simplicity's sake - the alternative solution would be to
       * continue the loop early and have to increment e. */
      if (e->short_name != 0)
        {
          /* This is a switch, we may not want to add it */
          if (!e->arg_description)
            {
              if (!should_filter_out_option_from_completion (completion, e))
                {
                  flatpak_complete_word (completion, "-%c ", e->short_name);
                }
              else
                {
                  flatpak_completion_debug ("switch -%c is already in line %s", e->short_name, completion->line);
                }
            }
          else
            {
              flatpak_complete_word (completion, "-%c ", e->short_name);
            }
        }
      e++;
    }
}

static void
flatpak_complete_column (FlatpakCompletion *completion,
                         char             **used,
                         const char        *column)
{
  g_autoptr(GString) s = NULL;

  s = g_string_new ("");

  if (used[0] != NULL)
    {
      int i;

      if (g_strv_contains ((const char * const *) used, column))
        return;

      const char *last = NULL;
      last = used[g_strv_length (used) - 1];
      if (!g_str_has_prefix (column, last))
        return;

      for (i = 0; used[i + 1]; i++)
        {
          g_string_append (s, used[i]);
          g_string_append_c (s, ',');
        }
    }

  g_string_append (s, column);
  flatpak_completion_debug ("completing column: %s", s->str);

  g_print ("%s\n", s->str);
}

void
flatpak_complete_columns (FlatpakCompletion *completion,
                          Column            *columns)
{
  int i;
  const char *list = NULL;
  g_auto(GStrv) used = NULL;

  if (!g_str_has_prefix (completion->cur, "--columns="))
    return;

  list = completion->cur + strlen ("--columns=");
  if (strcmp (list, "all") == 0 ||
      strcmp (list, "help") == 0)
    return;

  used = g_strsplit (list, ",", 0);
  flatpak_completion_debug ("complete columns, used: '%s'", list);

  if (g_strv_length (used) <= 1)
    {
      flatpak_complete_column (completion, used, "all");
      flatpak_complete_column (completion, used, "help");
    }

  for (i = 0; columns[i].name; i++)
    flatpak_complete_column (completion, used, columns[i].name);
}

void
flatpak_complete_context (FlatpakCompletion *completion)
{
  flatpak_complete_options (completion, flatpak_context_get_option_entries ());
}

static gchar *
pick_word_at (const char *s,
              int         cursor,
              int        *out_word_begins_at)
{
  int begin, end;

  if (s[0] == '\0')
    {
      if (out_word_begins_at != NULL)
        *out_word_begins_at = -1;
      return NULL;
    }

  if (is_word_separator (s[cursor]) && ((cursor > 0 && is_word_separator (s[cursor - 1])) || cursor == 0))
    {
      if (out_word_begins_at != NULL)
        *out_word_begins_at = cursor;
      return g_strdup ("");
    }

  while (cursor > 0 && !is_word_separator (s[cursor - 1]))
    cursor--;
  begin = cursor;

  end = begin;
  while (!is_word_separator (s[end]) && s[end] != '\0')
    end++;

  if (out_word_begins_at != NULL)
    *out_word_begins_at = begin;

  return g_strndup (s + begin, end - begin);
}

static gboolean
parse_completion_line_to_argv (const char        *initial_completion_line,
                               FlatpakCompletion *completion)
{
  gboolean parse_result = g_shell_parse_argv (initial_completion_line,
                                              &completion->original_argc,
                                              &completion->original_argv,
                                              NULL);

  /* Make a shallow copy of argv, which will be our "working set" */
  completion->argc = completion->original_argc;
  completion->argv = g_memdup2 (completion->original_argv,
                                sizeof (gchar *) * (completion->original_argc + 1));

  return parse_result;
}

FlatpakCompletion *
flatpak_completion_new (const char *arg_line,
                        const char *arg_point,
                        const char *arg_cur)
{
  FlatpakCompletion *completion;
  g_autofree char *initial_completion_line = NULL;
  int _point;
  char *endp;
  int cur_begin;
  int i;

  _point = strtol (arg_point, &endp, 10);
  if (endp == arg_point || *endp != '\0')
    return NULL;

  /* Ensure we're not going oob if we got weird arguments. */
  _point = MIN (_point, strlen (arg_line));

  completion = g_new0 (FlatpakCompletion, 1);
  completion->line = g_strdup (arg_line);
  completion->shell_cur = g_strdup (arg_cur);
  completion->point = _point;

  flatpak_completion_debug ("========================================");
  flatpak_completion_debug ("completion_point=%d", completion->point);
  flatpak_completion_debug ("completion_shell_cur='%s'", completion->shell_cur);
  flatpak_completion_debug ("----");
  flatpak_completion_debug (" 0123456789012345678901234567890123456789012345678901234567890123456789");
  flatpak_completion_debug ("'%s'", completion->line);
  flatpak_completion_debug (" %*s^", completion->point, "");

  /* compute cur and prev */
  completion->prev = NULL;
  completion->cur = pick_word_at (completion->line, completion->point, &cur_begin);
  if (cur_begin > 0)
    {
      gint prev_end;
      for (prev_end = cur_begin - 1; prev_end >= 0; prev_end--)
        {
          if (!is_word_separator (completion->line[prev_end]))
            {
              completion->prev = pick_word_at (completion->line, prev_end, NULL);
              break;
            }
        }

      initial_completion_line = g_strndup (completion->line, cur_begin);
    }
  else
    initial_completion_line = g_strdup ("");

  flatpak_completion_debug ("'%s'", initial_completion_line);
  flatpak_completion_debug ("----");

  flatpak_completion_debug (" cur='%s'", completion->cur);
  flatpak_completion_debug ("prev='%s'", completion->prev);

  if (!parse_completion_line_to_argv (initial_completion_line,
                                      completion))
    {
      /* it's very possible the command line can't be parsed (for
       * example, missing quotes etc) - in that case, we just
       * don't autocomplete at all
       */
      flatpak_completion_free (completion);
      return NULL;
    }

  flatpak_completion_debug ("completion_argv %i:", completion->original_argc);
  for (i = 0; i < completion->original_argc; i++)
    flatpak_completion_debug ("%s", completion->original_argv[i]);

  flatpak_completion_debug ("----");

  return completion;
}

void
flatpak_completion_free (FlatpakCompletion *completion)
{
  g_free (completion->cur);
  g_free (completion->prev);
  g_free (completion->line);
  g_free (completion->argv);
  g_free (completion->shell_cur);
  g_strfreev (completion->original_argv);
  g_free (completion);
}
