/*
 * Copyright (C) 2019 Red Hat Inc.
 *
 * Author:
 *      Matthias Clasen <mclasen@redhat.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 */

#include <string.h>
#include <glib/gstdio.h>
#include <gtk/gtk.h>
#include "testsuite/testutils.h"

#ifdef G_OS_WIN32
# include <io.h>
#endif

struct {
  GtkDirectionType dir;
  const char *ext;
} extensions[] = {
  { GTK_DIR_TAB_FORWARD, "tab" },
  { GTK_DIR_TAB_BACKWARD, "tab-backward" },
  { GTK_DIR_UP, "up" },
  { GTK_DIR_DOWN, "down" },
  { GTK_DIR_LEFT, "left" },
  { GTK_DIR_RIGHT, "right" }
};

static void
check_focus_states (GtkWidget *focus_widget)
{
  GtkStateFlags state;
  GtkWidget *parent;

  if (focus_widget == NULL)
    return;

  /* Check that we set :focus and :focus-within on the focus_widget,
   * and :focus-within on its ancestors
   */

  state = gtk_widget_get_state_flags (focus_widget);
  g_assert_true ((state & (GTK_STATE_FLAG_FOCUSED|GTK_STATE_FLAG_FOCUS_WITHIN)) ==
                  (GTK_STATE_FLAG_FOCUSED|GTK_STATE_FLAG_FOCUS_WITHIN));

  parent = gtk_widget_get_parent (focus_widget);
  while (parent)
    {
      state = gtk_widget_get_state_flags (parent);
      g_assert_true ((state & GTK_STATE_FLAG_FOCUS_WITHIN) == GTK_STATE_FLAG_FOCUS_WITHIN);
      g_assert_true ((state & GTK_STATE_FLAG_FOCUSED) == 0);

      parent = gtk_widget_get_parent (parent);
    }
}

static gboolean
quit_iteration_loop (gpointer user_data)
{
  gboolean *keep_running = user_data;

  *keep_running = FALSE;

  return G_SOURCE_REMOVE;
}

static void
timed_loop (guint millis)
{
  gboolean keep_running = TRUE;

  g_timeout_add (millis, quit_iteration_loop, &keep_running);
  while (keep_running)
    g_main_context_iteration (NULL, TRUE);
}

static char *
generate_focus_chain (GtkWidget        *window,
                      GtkDirectionType  dir)
{
  char *first = NULL;
  char *last = NULL;
  char *name = NULL;
  char *key = NULL;
  GString *output = g_string_new ("");
  GtkWidget *focus;
  int count = 0;

  gtk_window_present (GTK_WINDOW (window));

  /* start without focus */
  gtk_window_set_focus (GTK_WINDOW (window), NULL);

  while (TRUE)
    {
      g_signal_emit_by_name (window, "move-focus", dir);

      focus = gtk_window_get_focus (GTK_WINDOW (window));

      check_focus_states (focus);

      if (focus)
        {
          /* ui files can't put a name on the embedded text,
           * so include the parent entry here
           */
          if (GTK_IS_TEXT (focus))
            name = g_strdup_printf ("%s %s",
                                    gtk_widget_get_name (gtk_widget_get_parent (focus)),
                                    gtk_widget_get_name (focus));
          else
            name = g_strdup (gtk_widget_get_name (focus));

          key = g_strdup_printf ("%s %p", name, focus);
        }
      else
        {
          name = g_strdup ("NONE");
          key = g_strdup (key);
        }

      if (first && g_str_equal (key, first))
        {
          g_string_append (output, "WRAP\n");
          break; /* cycle completed */
        }

      if (last && g_str_equal (key, last))
        {
          g_string_append (output, "STOP\n");
          break; /* dead end */
        }

      g_string_append_printf (output, "%s\n", name);
      timed_loop (100);
      count++;

      if (!first)
        first = g_strdup (key);

      g_free (last);
      last = key;

      if (count == 100)
        {
          g_string_append (output, "ABORT\n");
          break;
        }

      g_free (name);
    }

  g_free (name);
  g_free (key);
  g_free (first);
  g_free (last);

  return g_string_free (output, FALSE);
}

static GtkDirectionType
get_dir_for_file (const char *path)
{
  char *p;
  int i;

  p = strrchr (path, '.');
  p++;

  for (i = 0; i < G_N_ELEMENTS (extensions); i++)
    {
      if (strcmp (p, extensions[i].ext) == 0)
        return extensions[i].dir;
    }

  g_error ("Could not find direction for %s", path);

  return 0;
}

static gboolean
load_ui_file (GFile *ui_file,
              GFile *ref_file,
              const char *ext)
{
  GtkBuilder *builder;
  GtkWidget *window;
  char *output;
  char *diff;
  char *ui_path, *ref_path;
  GError *error = NULL;
  GtkDirectionType dir;
  gboolean success = FALSE;
  gboolean keep_running = TRUE;
  guint timeout_handle_id;

  ui_path = g_file_get_path (ui_file);

  builder = gtk_builder_new_from_file (ui_path);
  window = GTK_WIDGET (gtk_builder_get_object (builder, "window"));

  g_assert_nonnull (window);

  gtk_window_present (GTK_WINDOW (window));

  timeout_handle_id = g_timeout_add (2000,
                                     quit_iteration_loop,
                                     &keep_running);
  while (keep_running && !gtk_window_is_active (GTK_WINDOW (window)))
    g_main_context_iteration (NULL, TRUE);

  if (keep_running)
    g_source_remove (timeout_handle_id);

  if (!gtk_window_is_active (GTK_WINDOW (window)))
    {
      g_print ("Skipping focus tests because window did not get focus. Headless display?");
      exit (77);
    }

  if (ext)
    {
      int i;

      for (i = 0; i < G_N_ELEMENTS (extensions); i++)
        {
          if (g_str_equal (ext, extensions[i].ext))
            {
              output = generate_focus_chain (window, extensions[i].dir);
              g_print ("%s", output);
              success = TRUE;
              goto out;
            }
        }

      g_error ("Not a supported direction: %s", ext);
      goto out;
    }

  g_assert_nonnull (ref_file);

  ref_path = g_file_get_path (ref_file);

  dir = get_dir_for_file (ref_path);
  output = generate_focus_chain (window, dir);
  diff = diff_string_with_file (ref_path, output, -1, &error);
  g_assert_no_error (error);

  if (diff && diff[0])
    g_print ("Resulting output doesn't match reference:\n%s", diff);
  else
     success = TRUE;
  g_free (ref_path);
  g_free (diff);

out:
  g_free (output);
  g_free (ui_path);

  return success;
}

static const char *arg_generate;

static const GOptionEntry options[] = {
  { "generate", 0, 0, G_OPTION_ARG_STRING, &arg_generate, "DIRECTION" },
  { NULL }
};

int
main (int argc, char **argv)
{
  GOptionContext *context;
  GFile *ui_file;
  GFile *ref_file;
  GError *error = NULL;
  gboolean success;

  context = g_option_context_new ("- run focus chain tests");
  g_option_context_add_main_entries (context, options, NULL);
  g_option_context_set_ignore_unknown_options (context, TRUE);

  if (!g_option_context_parse (context, &argc, &argv, &error))
    {
      g_error ("Option parsing failed: %s\n", error->message);
      return 1;
    }
  g_option_context_free (context);

  gtk_init ();

  if (arg_generate)
    {
      g_assert_cmpint (argc, ==, 2);

      ui_file = g_file_new_for_commandline_arg (argv[1]);

      success = load_ui_file (ui_file, NULL, arg_generate);

      g_object_unref (ui_file);
    }
  else
    {
      g_assert_cmpint (argc, ==, 3);

      ui_file = g_file_new_for_commandline_arg (argv[1]);
      ref_file = g_file_new_for_commandline_arg (argv[2]);

      success = load_ui_file (ui_file, ref_file, NULL);

      g_object_unref (ui_file);
      g_object_unref (ref_file);
    }

  return success ? 0 : 1;
}

