/*  Copyright 2015 Red Hat, Inc.
 *
 * GTK+ 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 of the
 * License, or (at your option) any later version.
 *
 * GLib 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 GTK+; see the file COPYING.  If not,
 * see <http://www.gnu.org/licenses/>.
 *
 * Author: Matthias Clasen
 */

#include <stdlib.h>
#include <string.h>

#include <glib/gi18n.h>
#include <gtk/gtk.h>


typedef struct {
  GtkBuilder *builder;
  GList *classes;
  gboolean packing;
  gboolean packing_started;
  gchar **attribute_names;
  gchar **attribute_values;
  GString *value;
  gboolean unclosed_starttag;
  gint indent;
} ParserData;

static gboolean
value_is_default (ParserData *data,
                  gint        i)
{
  GType type;
  GObjectClass *class;
  GParamSpec *pspec;
  GValue value = { 0, };
  gboolean ret;
  GError *error = NULL;
  const gchar *class_name;
  const gchar *value_string;
  const gchar *property_name;
  gchar *canonical_name;

  class_name = (const gchar *)data->classes->data;
  value_string =(const gchar *)data->value->str;
  property_name = (const gchar *)data->attribute_values[i];

  type = g_type_from_name (class_name);
  if (type == G_TYPE_INVALID)
    return FALSE;

  class = g_type_class_ref (type);
  canonical_name = g_strdup (property_name);
  g_strdelimit (canonical_name, "_", '-');
  if (data->packing)
    pspec = gtk_container_class_find_child_property (class, canonical_name);
  else
    pspec = g_object_class_find_property (class, canonical_name);
  g_free (canonical_name);
  g_type_class_unref (class);

  if (pspec == NULL)
    {
      if (data->packing)
        g_printerr (_("Packing property %s::%s not found\n"), class_name, property_name);
      else
        g_printerr (_("Property %s::%s not found\n"), class_name, property_name);
      return FALSE;
    }
  else if (g_type_is_a (G_PARAM_SPEC_VALUE_TYPE (pspec), G_TYPE_OBJECT))
    return FALSE;

  if (!gtk_builder_value_from_string (data->builder, pspec, value_string, &value, &error))
    {
      g_printerr (_("Couldn't parse value: %s\n"), error->message);
      g_error_free (error);
      ret = FALSE;
    }
  else
    ret = g_param_value_defaults (pspec, &value);

  g_value_reset (&value);

  return ret;
}

static void
maybe_emit_property (ParserData *data)
{
  gchar *escaped;
  gint i;
  gboolean bound;

  for (i = 0; data->attribute_names[i]; i++)
    {
      if (strcmp (data->attribute_names[i], "name") == 0)
        {
          if (data->classes == NULL)
            break;

          if (value_is_default (data, i))
            return;
        }
    }

  if (data->packing)
    {
      if (!data->packing_started)
        {
          g_print ("%*s<packing>\n", data->indent, "");
          data->indent += 2;
          data->packing_started = TRUE;
        }
    }

  bound = FALSE;

  g_print ("%*s<property", data->indent, "");
  for (i = 0; data->attribute_names[i]; i++)
    {
      if (strcmp (data->attribute_names[i], "bind-source") == 0 ||
          strcmp (data->attribute_names[i], "bind_source") == 0)
        bound = TRUE;

      escaped = g_markup_escape_text (data->attribute_values[i], -1);
      g_print (" %s=\"%s\"", data->attribute_names[i], escaped);
      g_free (escaped);
    }

  if (bound)
    {
      g_print ("/>\n");
    }
  else
    {
      escaped = g_markup_escape_text (data->value->str, -1);
      g_print (">%s</property>\n", escaped);
      g_free (escaped);
    }
}

static void
maybe_close_starttag (ParserData *data)
{
  if (data->unclosed_starttag)
    {
      g_print (">\n");
      data->unclosed_starttag = FALSE;
    }
}

static gboolean
stack_is (GMarkupParseContext *context,
          ...)
{
  va_list args;
  gchar *s, *p;
  const GSList *stack;

  stack = g_markup_parse_context_get_element_stack (context);

  va_start (args, context);
  s = va_arg (args, gchar *);
  while (s)
    {
      if (stack == NULL)
        {
          va_end (args);
          return FALSE;
        }

      p = (gchar *)stack->data;
      if (strcmp (s, p) != 0)
        {
          va_end (args);
          return FALSE;
        }

      s = va_arg (args, gchar *);
      stack = stack->next;
    }

  va_end (args);
  return TRUE;
}

static void
start_element (GMarkupParseContext  *context,
               const gchar          *element_name,
               const gchar         **attribute_names,
               const gchar         **attribute_values,
               gpointer              user_data,
               GError              **error)
{
  gint i;
  gchar *escaped;
  ParserData *data = user_data;

  maybe_close_starttag (data);

  if (strcmp (element_name, "property") == 0)
    {
      g_assert (data->attribute_names == NULL);
      g_assert (data->attribute_values == NULL);
      g_assert (data->value == NULL);

      data->attribute_names = g_strdupv ((gchar **)attribute_names);
      data->attribute_values = g_strdupv ((gchar **)attribute_values);
      data->value = g_string_new ("");

      return;
    }
  else if (strcmp (element_name, "packing") == 0)
    {
      data->packing = TRUE;
      data->packing_started = FALSE;

      return;
    }
  else if (strcmp (element_name, "attribute") == 0)
    {
      /* attribute in label has no content */
      if (data->classes == NULL ||
          strcmp ((gchar *)data->classes->data, "GtkLabel") != 0)
        data->value = g_string_new ("");
    }
  else if (stack_is (context, "item", "items", NULL) ||
           stack_is (context, "action-widget", "action-widgets", NULL) ||
           stack_is (context, "mime-type", "mime-types", NULL) ||
           stack_is (context, "pattern", "patterns", NULL) ||
           stack_is (context, "application", "applications", NULL) ||
           stack_is (context, "col", "row", "data", NULL) ||
           stack_is (context, "mark", "marks", NULL) ||
           stack_is (context, "action", "accessibility", NULL))
    {
      data->value = g_string_new ("");
    }
  else if (strcmp (element_name, "object") == 0 ||
           strcmp (element_name, "template") == 0)
    {
      for (i = 0; attribute_names[i]; i++)
        {
          if (strcmp (attribute_names[i], "class") == 0)
            {
              data->classes = g_list_prepend (data->classes,
                                              g_strdup (attribute_values[i]));
              break;
            }
        }
    }

  g_print ("%*s<%s", data->indent, "", element_name);
  for (i = 0; attribute_names[i]; i++)
    {
      escaped = g_markup_escape_text (attribute_values[i], -1);
      g_print (" %s=\"%s\"", attribute_names[i], escaped);
      g_free (escaped);
    }
  data->unclosed_starttag = TRUE;
  data->indent += 2;
}

static void
end_element (GMarkupParseContext  *context,
             const gchar          *element_name,
             gpointer              user_data,
             GError              **error)
{
  ParserData *data = user_data;

  if (strcmp (element_name, "property") == 0)
    {
      maybe_emit_property (data);

      g_clear_pointer (&data->attribute_names, g_strfreev);
      g_clear_pointer (&data->attribute_values, g_strfreev);
      g_string_free (data->value, TRUE);
      data->value = NULL;
      return;
    }
  else if (strcmp (element_name, "packing") == 0)
    {
      data->packing = FALSE;
      if (!data->packing_started)
        return;
    }
  else if (data->value != 0)
    {
      gchar *escaped;

      escaped = g_markup_escape_text (data->value->str, -1);
      g_print ("%s%s</%s>\n", data->unclosed_starttag ? ">" : "", escaped, element_name);
      g_free (escaped);
      g_string_free (data->value, TRUE);
      data->value = NULL;
      data->unclosed_starttag = FALSE;
      data->indent -= 2;
      return;
    }
  else if (strcmp (element_name, "object") == 0 ||
           strcmp (element_name, "template") == 0)
    {
      g_free (data->classes->data);
      data->classes = g_list_delete_link (data->classes, data->classes);
    }

  data->indent -= 2;

  if (data->unclosed_starttag)
    g_print ("/>\n");
  else
    g_print ("%*s</%s>\n", data->indent, "", element_name);

  data->unclosed_starttag = FALSE;
}

static void
text (GMarkupParseContext  *context,
      const gchar          *text,
      gsize                 text_len,
      gpointer              user_data,
      GError              **error)
{
  ParserData *data = user_data;

  if (data->value)
    {
      g_string_append_len (data->value, text, text_len);
      return;
    }
}

static void
passthrough (GMarkupParseContext  *context,
             const gchar          *text,
             gsize                 text_len,
             gpointer              user_data,
             GError              **error)
{
  ParserData *data = user_data;

  maybe_close_starttag (data);

  g_print ("%*s%s\n", data->indent, "", text);
}

GMarkupParser parser = {
  start_element,
  end_element,
  text,
  passthrough,
  NULL
};

static void
do_simplify (const gchar *filename)
{
  GMarkupParseContext *context;
  GError *error = NULL;
  gchar *buffer;
  ParserData data;

  if (!g_file_get_contents (filename, &buffer, NULL, &error))
    {
      g_printerr (_("Can't load file: %s\n"), error->message);
      exit (1);
    }

  data.builder = gtk_builder_new ();
  data.classes = NULL;
  data.attribute_names = NULL;
  data.attribute_values = NULL;
  data.value = NULL;
  data.packing = FALSE;
  data.packing_started = FALSE;
  data.unclosed_starttag = FALSE;
  data.indent = 0;

  context = g_markup_parse_context_new (&parser, G_MARKUP_TREAT_CDATA_AS_TEXT, &data, NULL);
  if (!g_markup_parse_context_parse (context, buffer, -1, &error))
    {
      g_printerr (_("Can't parse file: %s\n"), error->message);
      exit (1);
    }
}

static void
do_validate (const gchar *filename)
{
  GtkBuilder *builder;
  GError *error = NULL;
  gint ret;

  builder = gtk_builder_new ();
  ret = gtk_builder_add_from_file (builder, filename, &error);
  g_object_unref (builder);

  if (ret == 0)
    {
      g_printerr ("%s\n", error->message);
      exit (1);
    }
}

static void
usage (void)
{
  g_print (_("Usage:\n"
             "  gtk-builder-tool [COMMAND] FILE\n"
             "\n"
             "Commands:\n"
             "  validate    Validate the file\n"
             "  simplify    Simplify the file\n"
             "\n"
             "Validate and simplify GtkBuilder .ui files.\n"));
  exit (1);
}

int
main (int argc, char *argv[])
{
  g_set_prgname ("gtk-builder-tool");

  gtk_init (NULL, NULL);

  gtk_test_register_all_types ();

  if (argc < 3)
    usage ();

  if (strcmp (argv[1], "validate") == 0)
    do_validate (argv[2]);
  else if (strcmp (argv[1], "simplify") == 0)
    do_simplify (argv[2]);
  else
    usage ();

  return 0;
}
