/* dzl-state-machine-buildable.c
 *
 * Copyright (C) 2015 Christian Hergert <christian@hergert.me>
 *
 * This file 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 3 of the
 * License, or (at your option) any later version.
 *
 * This file 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 General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#define G_LOG_DOMAIN "dzl-state-machine"

#include "config.h"

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

#include "dzl-state-machine.h"
#include "dzl-state-machine-buildable.h"

typedef struct
{
  DzlStateMachine *self;
  GtkBuilder      *builder;
  GQueue          *stack;
} StatesParserData;

typedef enum
{
  STACK_ITEM_OBJECT,
  STACK_ITEM_STATE,
  STACK_ITEM_PROPERTY,
} StackItemType;

typedef struct
{
  StackItemType type;
  union {
    struct {
      gchar  *id;
      GSList *classes;
      GSList *properties;
    } object;
    struct {
      gchar  *name;
      GSList *objects;
    } state;
    struct {
      gchar *name;
      gchar *bind_source;
      gchar *bind_property;
      gchar *text;
      GBindingFlags bind_flags;
    } property;
  } u;
} StackItem;

static GtkBuildableIface *dzl_state_machine_parent_buildable;

static void
stack_item_free (StackItem *item)
{
  switch (item->type)
    {
    case STACK_ITEM_OBJECT:
      g_free (item->u.object.id);
      g_slist_free_full (item->u.object.classes, g_free);
      g_slist_free_full (item->u.object.properties, (GDestroyNotify)stack_item_free);
      break;

    case STACK_ITEM_STATE:
      g_free (item->u.state.name);
      g_slist_free_full (item->u.state.objects, (GDestroyNotify)stack_item_free);
      break;

    case STACK_ITEM_PROPERTY:
      g_free (item->u.property.name);
      g_free (item->u.property.bind_source);
      g_free (item->u.property.bind_property);
      g_free (item->u.property.text);
      break;

    default:
      g_assert_not_reached ();
      break;
    }

  g_slice_free (StackItem, item);
}

static StackItem *
stack_item_new (StackItemType type)
{
  StackItem *item;

  item = g_slice_new0 (StackItem);
  item->type = type;

  return item;
}

static void
add_state (StatesParserData  *parser_data,
           StackItem         *item,
           GError           **error)
{
  GSList *iter;

  g_assert (parser_data != NULL);
  g_assert (item != NULL);
  g_assert (item->type == STACK_ITEM_STATE);

  for (iter = item->u.state.objects; iter; iter = iter->next)
    {
      StackItem *stack_obj = iter->data;
      GObject *object;
      GSList *prop_iter;
      GSList *style_iter;

      g_assert (stack_obj->type == STACK_ITEM_OBJECT);
      g_assert (stack_obj->u.object.id != NULL);

      object = gtk_builder_get_object (parser_data->builder, stack_obj->u.object.id);

      if (object == NULL)
        {
          g_set_error (error,
                       GTK_BUILDER_ERROR,
                       GTK_BUILDER_ERROR_INVALID_VALUE,
                       "Unknown object for state '%s': %s",
                       item->u.state.name,
                       stack_obj->u.object.id);
          return;
        }

      if (GTK_IS_WIDGET (object))
        for (style_iter = stack_obj->u.object.classes; style_iter; style_iter = style_iter->next)
          dzl_state_machine_add_style (parser_data->self,
                                       item->u.state.name,
                                       GTK_WIDGET (object),
                                       style_iter->data);

      for (prop_iter = stack_obj->u.object.properties; prop_iter; prop_iter = prop_iter->next)
        {
          StackItem *stack_prop = prop_iter->data;
          GObject *bind_source;

          g_assert (stack_prop->type == STACK_ITEM_PROPERTY);

          if ((stack_prop->u.property.bind_source != NULL) &&
              (stack_prop->u.property.bind_property != NULL) &&
              (bind_source = gtk_builder_get_object (parser_data->builder, stack_prop->u.property.bind_source)))
            {
              dzl_state_machine_add_binding (parser_data->self,
                                             item->u.state.name,
                                             bind_source,
                                             stack_prop->u.property.bind_property,
                                             object,
                                             stack_prop->u.property.name,
                                             stack_prop->u.property.bind_flags);
            }
          else if (stack_prop->u.property.text != NULL)
            {
              GParamSpec *pspec;
              GValue value = G_VALUE_INIT;

              pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (object), stack_prop->u.property.name);

              if (pspec == NULL)
                {
                  g_set_error (error,
                               GTK_BUILDER_ERROR,
                               GTK_BUILDER_ERROR_INVALID_PROPERTY,
                               "No such property: %s",
                               stack_prop->u.property.name);
                  return;
                }

              if (g_type_is_a (pspec->value_type, G_TYPE_OBJECT))
                {
                  GObject *relative;

                  relative = gtk_builder_get_object (parser_data->builder, stack_prop->u.property.text);

                  if (relative == NULL)
                    {
                      g_set_error (error,
                                   GTK_BUILDER_ERROR,
                                   GTK_BUILDER_ERROR_INVALID_VALUE,
                                   "Unknown object for property '%s': %s",
                                   stack_prop->u.property.name,
                                   stack_prop->u.property.text);
                      return;
                    }

                  g_value_init (&value, pspec->value_type);
                  g_value_set_object (&value, relative);
                }
              else if (!gtk_builder_value_from_string (parser_data->builder,
                                                       pspec,
                                                       stack_prop->u.property.text,
                                                       &value,
                                                       error))
                {
                  return;
                }

              dzl_state_machine_add_propertyv (parser_data->self,
                                               item->u.state.name,
                                               object,
                                               stack_prop->u.property.name,
                                               &value);

              g_value_unset (&value);
            }
        }
    }
}

static void
add_object (StatesParserData *parser_data,
            StackItem        *parent,
            StackItem        *item)
{
  g_assert (parser_data != NULL);
  g_assert (parent != NULL);
  g_assert (parent->type == STACK_ITEM_STATE);
  g_assert (item != NULL);
  g_assert (item->type == STACK_ITEM_OBJECT);

  parent->u.state.objects = g_slist_prepend (parent->u.state.objects, item);
}

static void
add_property (StatesParserData *parser_data,
              StackItem        *parent,
              StackItem        *item)
{
  g_assert (parser_data != NULL);
  g_assert (parent != NULL);
  g_assert (parent->type == STACK_ITEM_OBJECT);
  g_assert (item != NULL);
  g_assert (item->type == STACK_ITEM_PROPERTY);

  parent->u.object.properties = g_slist_prepend (parent->u.object.properties, item);
}

static gboolean
check_parent (GMarkupParseContext  *context,
              const gchar          *element_name,
              GError              **error)
{
  const GSList *stack;
  const gchar *parent_name;
  const gchar *our_name;

  stack = g_markup_parse_context_get_element_stack (context);
  our_name = stack->data;
  parent_name = stack->next ? stack->next->data : "";

  if (g_strcmp0 (parent_name, element_name) != 0)
    {
      gint line;
      gint col;

      g_markup_parse_context_get_position (context, &line, &col);
      g_set_error (error,
                   GTK_BUILDER_ERROR,
                   GTK_BUILDER_ERROR_INVALID_TAG,
                   "%d:%d: Element <%s> found in <%s>, expected <%s>.",
                   line, col, our_name, parent_name, element_name);
      return FALSE;
    }

  return TRUE;
}

/*
 * flags_from_string:
 *
 * gtkbuilder.c
 *
 * Copyright (C) 1998-2002 James Henstridge <james@daa.com.au>
 * Copyright (C) 2006-2007 Async Open Source,
 *                         Johan Dahlin <jdahlin@async.com.br>,
 *                         Henrique Romano <henrique@async.com.br>
 *
 * 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.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
 * 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/>.
 */
gboolean
flags_from_string (GType         type,
                   const gchar  *string,
                   guint        *flags_value,
                   GError      **error)
{
  GFlagsClass *fclass;
  gchar *endptr, *prevptr;
  guint i, j, value;
  gchar *flagstr;
  GFlagsValue *fv;
  const gchar *flag;
  gunichar ch;
  gboolean eos, ret;

  g_return_val_if_fail (G_TYPE_IS_FLAGS (type), FALSE);
  g_return_val_if_fail (string != 0, FALSE);

  ret = TRUE;

  endptr = NULL;
  errno = 0;
  value = g_ascii_strtoull (string, &endptr, 0);
  if (errno == 0 && endptr != string) /* parsed a number */
    *flags_value = value;
  else
    {
      fclass = g_type_class_ref (type);

      flagstr = g_strdup (string);
      for (value = i = j = 0; ; i++)
        {

          eos = flagstr[i] == '\0';

          if (!eos && flagstr[i] != '|')
            continue;

          flag = &flagstr[j];
          endptr = &flagstr[i];

          if (!eos)
            {
              flagstr[i++] = '\0';
              j = i;
            }

          /* trim spaces */
          for (;;)
            {
              ch = g_utf8_get_char (flag);
              if (!g_unichar_isspace (ch))
                break;
              flag = g_utf8_next_char (flag);
            }

          while (endptr > flag)
            {
              prevptr = g_utf8_prev_char (endptr);
              ch = g_utf8_get_char (prevptr);
              if (!g_unichar_isspace (ch))
                break;
              endptr = prevptr;
            }

          if (endptr > flag)
            {
              *endptr = '\0';
              fv = g_flags_get_value_by_name (fclass, flag);

              if (!fv)
                fv = g_flags_get_value_by_nick (fclass, flag);

              if (fv)
                value |= fv->value;
              else
                {
                  g_set_error (error,
                               GTK_BUILDER_ERROR,
                               GTK_BUILDER_ERROR_INVALID_VALUE,
                               "Unknown flag: `%s'",
                               flag);
                  ret = FALSE;
                  break;
                }
            }

          if (eos)
            {
              *flags_value = value;
              break;
            }
        }

      g_free (flagstr);

      g_type_class_unref (fclass);
    }

  return ret;
}

static void
states_parser_start_element (GMarkupParseContext  *context,
                             const gchar          *element_name,
                             const gchar         **attribute_names,
                             const gchar         **attribute_values,
                             gpointer              user_data,
                             GError              **error)
{
  StatesParserData *parser_data = user_data;
  StackItem *item;

  g_assert (context != NULL);
  g_assert (element_name != NULL);
  g_assert (parser_data != NULL);

  if (g_strcmp0 (element_name, "state") == 0)
    {
      const gchar *name;

      if (!check_parent (context, "states", error))
        return;

      if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error,
                                        G_MARKUP_COLLECT_STRING, "name", &name,
                                        G_MARKUP_COLLECT_INVALID))
        return;

      item = stack_item_new (STACK_ITEM_STATE);
      item->u.state.name = g_strdup (name);
      g_queue_push_head (parser_data->stack, item);
    }
  else if (g_strcmp0 (element_name, "states") == 0)
    {
      if (!check_parent (context, "object", error))
        return;
    }
  else if (g_strcmp0 (element_name, "object") == 0)
    {
      const gchar *id;

      if (!check_parent (context, "state", error))
        return;

      if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error,
                                        G_MARKUP_COLLECT_STRING, "id", &id,
                                        G_MARKUP_COLLECT_INVALID))
        return;

      item = stack_item_new (STACK_ITEM_OBJECT);
      item->u.object.id = g_strdup (id);
      g_queue_push_head (parser_data->stack, item);
    }
  else if (g_strcmp0 (element_name, "property") == 0)
    {
      const gchar *name = NULL;
      const gchar *translatable = NULL;
      const gchar *bind_source = NULL;
      const gchar *bind_property = NULL;
      const gchar *bind_flags_str = NULL;
      GBindingFlags bind_flags = 0;

      if (!check_parent (context, "object", error))
        return;

      if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error,
                                        G_MARKUP_COLLECT_STRING, "name", &name,
                                        G_MARKUP_COLLECT_STRING|G_MARKUP_COLLECT_OPTIONAL, "translatable", &translatable,
                                        G_MARKUP_COLLECT_STRING|G_MARKUP_COLLECT_OPTIONAL, "bind-source", &bind_source,
                                        G_MARKUP_COLLECT_STRING|G_MARKUP_COLLECT_OPTIONAL, "bind-property", &bind_property,
                                        G_MARKUP_COLLECT_STRING|G_MARKUP_COLLECT_OPTIONAL, "bind-flags", &bind_flags_str,
                                        G_MARKUP_COLLECT_INVALID))
        return;

      if (name != NULL)
        {
          if (g_strcmp0 (translatable, "yes") == 0)
            {
              const gchar *domain;

              domain = gtk_builder_get_translation_domain (parser_data->builder);
              name = dgettext (domain, name);
            }
        }

      if ((bind_flags_str != NULL) && !flags_from_string (G_TYPE_BINDING_FLAGS, bind_flags_str, &bind_flags, error))
        return;

      item = stack_item_new (STACK_ITEM_PROPERTY);
      item->u.property.name = g_strdup (name);
      item->u.property.bind_source = g_strdup (bind_source);
      item->u.property.bind_property = g_strdup (bind_property);
      item->u.property.bind_flags = bind_flags;
      g_queue_push_head (parser_data->stack, item);
    }
  else if (g_strcmp0 (element_name, "style") == 0)
    {
      if (!check_parent (context, "object", error))
        return;
    }
  else if (g_strcmp0 (element_name, "class") == 0)
    {
      const gchar *name = NULL;

      if (!check_parent (context, "style", error))
        return;

      if (!g_markup_collect_attributes (element_name, attribute_names, attribute_values, error,
                                        G_MARKUP_COLLECT_STRING, "name", &name,
                                        G_MARKUP_COLLECT_INVALID))
        return;

      item = g_queue_peek_head (parser_data->stack);
      g_assert (item->type == STACK_ITEM_OBJECT);

      item->u.object.classes = g_slist_prepend (item->u.object.classes, g_strdup (name));
    }
  else
    {
      const GSList *stack;
      const gchar *parent_name;
      const gchar *our_name;
      gint line;
      gint col;

      stack = g_markup_parse_context_get_element_stack (context);
      our_name = stack->data;
      parent_name = stack->next ? stack->next->data : "";

      g_markup_parse_context_get_position (context, &line, &col);
      g_set_error (error,
                   GTK_BUILDER_ERROR,
                   GTK_BUILDER_ERROR_INVALID_TAG,
                   "%d:%d: Unknown element <%s> found in <%s>.",
                   line, col, our_name, parent_name);
    }

  return;
}

static void
states_parser_end_element (GMarkupParseContext  *context,
                           const gchar          *element_name,
                           gpointer              user_data,
                           GError              **error)
{
  StatesParserData *parser_data = user_data;
  StackItem *item;

  g_assert (context != NULL);
  g_assert (element_name != NULL);
  g_assert (parser_data != NULL);

  if (g_strcmp0 (element_name, "state") == 0)
    {
      item = g_queue_pop_head (parser_data->stack);
      g_assert (item->type == STACK_ITEM_STATE);
      add_state (parser_data, item, error);
      stack_item_free (item);
    }
  else if (g_strcmp0 (element_name, "object") == 0)
    {
      StackItem *parent;

      item = g_queue_pop_head (parser_data->stack);
      g_assert (item->type == STACK_ITEM_OBJECT);

      parent = g_queue_peek_head (parser_data->stack);
      g_assert (parent->type == STACK_ITEM_STATE);

      add_object (parser_data, parent, item);
    }
  else if (g_strcmp0 (element_name, "property") == 0)
    {
      StackItem *parent;

      item = g_queue_pop_head (parser_data->stack);
      g_assert (item->type == STACK_ITEM_PROPERTY);

      parent = g_queue_peek_head (parser_data->stack);
      g_assert (parent->type == STACK_ITEM_OBJECT);

      add_property (parser_data, parent, item);
    }
}

static void
states_parser_text (GMarkupParseContext  *context,
                    const gchar          *text,
                    gsize                 text_len,
                    gpointer              user_data,
                    GError              **error)
{
  StatesParserData *parser_data = user_data;
  StackItem *item;

  g_assert (parser_data != NULL);

  item = g_queue_peek_head (parser_data->stack);
  if ((item != NULL) && (item->type == STACK_ITEM_PROPERTY))
    item->u.property.text = g_strndup (text, text_len);
}

static GMarkupParser StatesParser = {
  states_parser_start_element,
  states_parser_end_element,
  states_parser_text,
};

static gboolean
dzl_state_machine_buildable_custom_tag_start (GtkBuildable  *buildable,
                                              GtkBuilder    *builder,
                                              GObject       *child,
                                              const gchar   *tagname,
                                              GMarkupParser *parser,
                                              gpointer      *data)
{
  DzlStateMachine *self = (DzlStateMachine *)buildable;

  g_assert (DZL_IS_STATE_MACHINE (self));
  g_assert (GTK_IS_BUILDER (builder));
  g_assert (tagname != NULL);
  g_assert (parser != NULL);
  g_assert (data != NULL);

  if (g_strcmp0 (tagname, "states") == 0)
    {
      StatesParserData *parser_data;

      parser_data = g_slice_new0 (StatesParserData);
      parser_data->self = g_object_ref (DZL_STATE_MACHINE (buildable));
      parser_data->builder = g_object_ref (builder);
      parser_data->stack = g_queue_new ();

      *parser = StatesParser;
      *data = parser_data;

      return TRUE;
    }

  return FALSE;
}

static void
dzl_state_machine_buildable_custom_finished (GtkBuildable *buildable,
                                             GtkBuilder   *builder,
                                             GObject      *child,
                                             const gchar  *tagname,
                                             gpointer      user_data)
{
  DzlStateMachine *self = (DzlStateMachine *)buildable;

  g_assert (DZL_IS_STATE_MACHINE (self));
  g_assert (GTK_IS_BUILDER (builder));
  g_assert (tagname != NULL);

  if (g_strcmp0 (tagname, "states") == 0)
    {
      StatesParserData *parser_data = user_data;

      g_object_unref (parser_data->self);
      g_object_unref (parser_data->builder);
      g_queue_free_full (parser_data->stack, (GDestroyNotify)stack_item_free);
      g_slice_free (StatesParserData, parser_data);
    }
}

/**
 * dzl_state_machine_buildable_iface_init: (skip)
 */
void
dzl_state_machine_buildable_iface_init (GtkBuildableIface *iface)
{
  g_assert (iface != NULL);

  dzl_state_machine_parent_buildable = g_type_interface_peek_parent (iface);

  iface->custom_tag_start = dzl_state_machine_buildable_custom_tag_start;
  iface->custom_finished = dzl_state_machine_buildable_custom_finished;
}
