/* bge-wdgt-spec.c
 *
 * Copyright 2026 Eva M
 *
 * 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 <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#include <gtk/gtk.h>

#include "../bge-animation-private.h"
#include "bge-marshalers.h"
#include "bge-wdgt-spec-private.h"
#include "bge.h"
#include "fmt/parser.h"
#include "graphene-gobject.h"
#include "util.h"

#define ARGBUF_SIZE 128

static void
_marshal_DIRECT__ARGS_DIRECT (GClosure                *closure,
                              GValue                  *return_value,
                              guint                    n_param_values,
                              const GValue            *param_values,
                              gpointer invocation_hint G_GNUC_UNUSED,
                              gpointer                 marshal_data);

static void
_marshal_BOXED__ARGS_DIRECT (GClosure                *closure,
                             GValue                  *return_value,
                             guint                    n_param_values,
                             const GValue            *param_values,
                             gpointer invocation_hint G_GNUC_UNUSED,
                             gpointer                 marshal_data);

typedef enum
{
  VALUE_OBJECT = 0,
  VALUE_CONSTANT,
  VALUE_COMPONENT,
  VALUE_TRANSFORM,
  VALUE_CLOSURE,
  VALUE_COERCION,
  VALUE_TRACK_TRANSITION,
  VALUE_SPECIAL,
  VALUE_VARIABLE,
  VALUE_REFERENCE_OBJECT,
  VALUE_PROPERTY,
  VALUE_CHILD,
  VALUE_ALLOCATION_WIDTH,
  VALUE_ALLOCATION_HEIGHT,
  VALUE_ALLOCATION_TRANSFORM,
  VALUE_MEASURE_MINIMUM_WIDTH,
  VALUE_MEASURE_NATURAL_WIDTH,
  VALUE_MEASURE_MINIMUM_HEIGHT,
  VALUE_MEASURE_NATURAL_HEIGHT,
  VALUE_MEASURE_FOR_SIZE,
  VALUE_WIDGET_WIDTH,
  VALUE_WIDGET_HEIGHT,
} ValueKind;

typedef enum
{
  TRANSITION_EASE = 0,
  TRANSITION_SPRING,
} TransitionKind;

typedef GskTransform *(*TransformCallFunc) (GskTransform *next,
                                            const GValue  args[]);

typedef struct
{
  const char       *name;
  guint             n_args;
  GType             args[16];
  gpointer          func;
  TransformCallFunc call;
} TransformInstr;

typedef void (*SnapshotCallFunc) (GtkSnapshot *snapshot,
                                  const GValue args[],
                                  const GValue rest[],
                                  guint        n_rest);

typedef struct
{
  const char      *name;
  guint            n_args;
  guint            n_rest;
  GType            args[16];
  gpointer         func;
  SnapshotCallFunc call;
} SnapshotInstr;

static void
deinit_value (gpointer ptr);
BGE_DEFINE_DATA (
    value,
    Value,
    {
      char     *name;
      GType     type;
      ValueKind kind;
      union
      {
        GValue     constant;
        GPtrArray *component;
        struct
        {
          TransformCallFunc func;
          ValueData        *next;
          GPtrArray        *args;
        } transform;
        struct
        {
          GPtrArray      *args;
          GClosureMarshal marshal;
          GCallback       func;
          gpointer        user_data;
          GDestroyNotify  destroy_user_data;
        } closure;
        struct
        {
          ValueData *src;
          struct
          {
            ValueData *damping_ratio;
            ValueData *mass;
            ValueData *stiffness;
          } spring;
        } track_transition;
        BgeWdgtSpecialValue special;
        struct
        {
          ValueData *value;
        } coercion;
        struct
        {
          ValueData  *object;
          char       *prop_name;
          GParamFlags pspec_flags;
        } property;
        struct
        {
          ValueData *parent_widget;
          char      *builder_type;
          GPtrArray *css_classes;
        } child;
        struct
        {
          ValueData *widget;
        } allocation;
      };
    },
    deinit_value (self);)

static void
deinit_value (gpointer ptr)
{
  ValueData *value = ptr;

  g_clear_pointer (&value->name, g_free);
  switch (value->kind)
    {
    case VALUE_OBJECT:
      break;
    case VALUE_CONSTANT:
      g_value_unset (&value->constant);
      break;
    case VALUE_COMPONENT:
      g_clear_pointer (&value->component, g_ptr_array_unref);
      break;
    case VALUE_TRANSFORM:
      g_clear_pointer (&value->transform.next, value_data_unref);
      g_clear_pointer (&value->transform.args, g_ptr_array_unref);
      break;
    case VALUE_CLOSURE:
      g_clear_pointer (&value->closure.args, g_ptr_array_unref);
      if (value->closure.destroy_user_data != NULL)
        g_clear_pointer (&value->closure.user_data, value->closure.destroy_user_data);
      break;
    case VALUE_COERCION:
      g_clear_pointer (&value->coercion.value, value_data_unref);
      break;
    case VALUE_TRACK_TRANSITION:
      g_clear_pointer (&value->track_transition.src, value_data_unref);
      g_clear_pointer (&value->track_transition.spring.damping_ratio, value_data_unref);
      g_clear_pointer (&value->track_transition.spring.mass, value_data_unref);
      g_clear_pointer (&value->track_transition.spring.stiffness, value_data_unref);
      break;
    case VALUE_SPECIAL:
      break;
    case VALUE_VARIABLE:
      break;
    case VALUE_REFERENCE_OBJECT:
      break;
    case VALUE_PROPERTY:
      g_clear_pointer (&value->property.object, value_data_unref);
      g_clear_pointer (&value->property.prop_name, g_free);
      break;
    case VALUE_CHILD:
      g_clear_pointer (&value->child.parent_widget, value_data_unref);
      g_clear_pointer (&value->child.builder_type, g_free);
      g_clear_pointer (&value->child.css_classes, g_ptr_array_unref);
      break;
    case VALUE_ALLOCATION_WIDTH:
    case VALUE_ALLOCATION_HEIGHT:
    case VALUE_ALLOCATION_TRANSFORM:
      g_clear_pointer (&value->allocation.widget, value_data_unref);
      break;
    case VALUE_MEASURE_MINIMUM_WIDTH:
    case VALUE_MEASURE_NATURAL_WIDTH:
    case VALUE_MEASURE_MINIMUM_HEIGHT:
    case VALUE_MEASURE_NATURAL_HEIGHT:
    case VALUE_MEASURE_FOR_SIZE:
    case VALUE_WIDGET_WIDTH:
    case VALUE_WIDGET_HEIGHT:
      break;
    default:
      g_assert_not_reached ();
    }
}

static void
deinit_transition (gpointer ptr);
BGE_DEFINE_DATA (
    transition,
    Transition,
    {
      TransitionKind kind;
      union
      {
        struct
        {
          double    seconds;
          BgeEasing easing;
        } ease;
        struct
        {
          ValueData *damping_ratio;
          ValueData *mass;
          ValueData *stiffness;
        } spring;
      };
    },
    deinit_transition (self);)

static void
deinit_transition (gpointer ptr)
{
  TransitionData *transition = ptr;

  switch (transition->kind)
    {
    case TRANSITION_EASE:
      break;
    case TRANSITION_SPRING:
      g_clear_pointer (&transition->spring.damping_ratio, value_data_unref);
      g_clear_pointer (&transition->spring.mass, value_data_unref);
      g_clear_pointer (&transition->spring.stiffness, value_data_unref);
      break;
    default:
      g_assert_not_reached ();
    }
}

BGE_DEFINE_DATA (
    snapshot_call,
    SnapshotCall,
    {
      BgeWdgtSnapshotInstrKind kind;
      SnapshotCallFunc         func;
      GPtrArray               *args;
      GPtrArray               *rest;
      ValueData               *child;
    },
    BGE_RELEASE_DATA (args, g_ptr_array_unref);
    BGE_RELEASE_DATA (rest, g_ptr_array_unref);
    BGE_RELEASE_DATA (child, value_data_unref))

BGE_DEFINE_DATA (
    snapshot,
    Snapshot,
    {
      GPtrArray *calls;
    },
    BGE_RELEASE_DATA (calls, g_ptr_array_unref))

BGE_DEFINE_DATA (
    state,
    State,
    {
      char         *name;
      GHashTable   *setters;
      GHashTable   *transitions;
      SnapshotData *snapshot;
    },
    BGE_RELEASE_DATA (name, g_free);
    BGE_RELEASE_DATA (setters, g_hash_table_unref);
    BGE_RELEASE_DATA (transitions, g_hash_table_unref);
    BGE_RELEASE_DATA (snapshot, snapshot_data_unref))

/* --------------------------- */
/* Spec Builder Implementation */
/* --------------------------- */

struct _BgeWdgtSpec
{
  GObject parent_instance;

  char *name;

  GHashTable *values;
  GPtrArray  *anon_values;
  GHashTable *states;
  GPtrArray  *children;
  GPtrArray  *nonchildren;

  StateData *init_state;
  StateData *default_state;

  ValueData *reference;

  struct
  {
    ValueData *motion_x;
    ValueData *motion_y;
  } special_values;
};

G_DEFINE_FINAL_TYPE (BgeWdgtSpec, bge_wdgt_spec, G_TYPE_OBJECT);

enum
{
  PROP_0,

  PROP_NAME,

  LAST_PROP
};
static GParamSpec *props[LAST_PROP] = { 0 };

static gboolean
check_can_coerce_type (GType dest,
                       GType src);

static void
coerce_value (const GValue *in,
              GType         dest_type,
              GValue       *out);

static ValueData *
wrap_coerce_value (BgeWdgtSpec *self,
                   ValueData   *value,
                   GType        dest_type);

static gboolean
lookup_transform_instr (const char     *lookup_name,
                        TransformInstr *out);

static gboolean
lookup_snapshot_push_instr (const char    *lookup_name,
                            SnapshotInstr *out);
static gboolean
lookup_snapshot_transform_instr (const char    *lookup_name,
                                 SnapshotInstr *out);
static gboolean
lookup_snapshot_append_instr (const char    *lookup_name,
                              SnapshotInstr *out);

static void
ensure_state_snapshot (StateData *state);

static void
bge_wdgt_spec_dispose (GObject *object)
{
  BgeWdgtSpec *self = BGE_WDGT_SPEC (object);

  g_clear_pointer (&self->name, g_free);

  g_clear_pointer (&self->values, g_hash_table_unref);
  g_clear_pointer (&self->anon_values, g_ptr_array_unref);
  g_clear_pointer (&self->states, g_hash_table_unref);
  g_clear_pointer (&self->children, g_ptr_array_unref);
  g_clear_pointer (&self->nonchildren, g_ptr_array_unref);

  g_clear_pointer (&self->init_state, state_data_unref);
  g_clear_pointer (&self->default_state, state_data_unref);

  g_clear_pointer (&self->reference, value_data_unref);

  G_OBJECT_CLASS (bge_wdgt_spec_parent_class)->dispose (object);
}

static void
bge_wdgt_spec_get_property (GObject    *object,
                            guint       prop_id,
                            GValue     *value,
                            GParamSpec *pspec)
{
  BgeWdgtSpec *self = BGE_WDGT_SPEC (object);

  switch (prop_id)
    {
    case PROP_NAME:
      g_value_set_string (value, bge_wdgt_spec_get_name (self));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
bge_wdgt_spec_set_property (GObject      *object,
                            guint         prop_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
  BgeWdgtSpec *self = BGE_WDGT_SPEC (object);

  switch (prop_id)
    {
    case PROP_NAME:
      bge_wdgt_spec_set_name (self, g_value_get_string (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
bge_wdgt_spec_class_init (BgeWdgtSpecClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->set_property = bge_wdgt_spec_set_property;
  object_class->get_property = bge_wdgt_spec_get_property;
  object_class->dispose      = bge_wdgt_spec_dispose;

  props[PROP_NAME] =
      g_param_spec_string (
          "name",
          NULL, NULL, NULL,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, LAST_PROP, props);

  g_type_ensure (BGE_TYPE_EASING);
  g_type_ensure (BGE_TYPE_WDGT_TIME);

  /* Make sure these names are available for the parser */
  g_type_ensure (GRAPHENE_TYPE_POINT);
  g_type_ensure (GRAPHENE_TYPE_POINT3D);
  g_type_ensure (GRAPHENE_TYPE_SIZE);
  g_type_ensure (GRAPHENE_TYPE_RECT);
  g_type_ensure (GRAPHENE_TYPE_VEC2);
  g_type_ensure (GRAPHENE_TYPE_VEC3);
  g_type_ensure (GRAPHENE_TYPE_VEC4);
  g_type_ensure (GRAPHENE_TYPE_QUAD);
  g_type_ensure (GRAPHENE_TYPE_QUATERNION);
  g_type_ensure (GRAPHENE_TYPE_MATRIX);
  g_type_ensure (GRAPHENE_TYPE_PLANE);
  g_type_ensure (GRAPHENE_TYPE_FRUSTUM);
  g_type_ensure (GRAPHENE_TYPE_SPHERE);
  g_type_ensure (GRAPHENE_TYPE_BOX);
  g_type_ensure (GRAPHENE_TYPE_TRIANGLE);
  g_type_ensure (GRAPHENE_TYPE_EULER);
  g_type_ensure (GRAPHENE_TYPE_RAY);

  /* Ensure GTK names are available for the parser */
  g_type_ensure (gtk_at_context_get_type ());
  g_type_ensure (gtk_about_dialog_get_type ());
  g_type_ensure (gtk_accessible_get_type ());
  g_type_ensure (gtk_accessible_announcement_priority_get_type ());
  g_type_ensure (gtk_accessible_autocomplete_get_type ());
  g_type_ensure (gtk_accessible_invalid_state_get_type ());
  g_type_ensure (gtk_accessible_list_get_type ());
  g_type_ensure (gtk_accessible_platform_state_get_type ());
  g_type_ensure (gtk_accessible_property_get_type ());
  g_type_ensure (gtk_accessible_range_get_type ());
  g_type_ensure (gtk_accessible_relation_get_type ());
  g_type_ensure (gtk_accessible_role_get_type ());
  g_type_ensure (gtk_accessible_sort_get_type ());
  g_type_ensure (gtk_accessible_state_get_type ());
  g_type_ensure (gtk_accessible_text_get_type ());
  g_type_ensure (gtk_accessible_text_content_change_get_type ());
  g_type_ensure (gtk_accessible_text_granularity_get_type ());
  g_type_ensure (gtk_accessible_tristate_get_type ());
  g_type_ensure (gtk_action_bar_get_type ());
  g_type_ensure (gtk_actionable_get_type ());
  g_type_ensure (gtk_activate_action_get_type ());
  g_type_ensure (gtk_adjustment_get_type ());
  g_type_ensure (gtk_alert_dialog_get_type ());
  g_type_ensure (gtk_align_get_type ());
  g_type_ensure (gtk_alternative_trigger_get_type ());
  g_type_ensure (gtk_any_filter_get_type ());
  g_type_ensure (gtk_app_chooser_get_type ());
  g_type_ensure (gtk_app_chooser_button_get_type ());
  g_type_ensure (gtk_app_chooser_dialog_get_type ());
  g_type_ensure (gtk_app_chooser_widget_get_type ());
  g_type_ensure (gtk_application_get_type ());
  g_type_ensure (gtk_application_inhibit_flags_get_type ());
  g_type_ensure (gtk_application_window_get_type ());
  g_type_ensure (gtk_arrow_type_get_type ());
  g_type_ensure (gtk_aspect_frame_get_type ());
  g_type_ensure (gtk_assistant_get_type ());
  g_type_ensure (gtk_assistant_page_get_type ());
  g_type_ensure (gtk_assistant_page_type_get_type ());
  g_type_ensure (gtk_baseline_position_get_type ());
  g_type_ensure (gtk_bin_layout_get_type ());
  g_type_ensure (gtk_bitset_get_type ());
  g_type_ensure (gtk_bitset_iter_get_type ());
  g_type_ensure (gtk_bookmark_list_get_type ());
  g_type_ensure (gtk_bool_filter_get_type ());
  g_type_ensure (gtk_border_get_type ());
  g_type_ensure (gtk_border_style_get_type ());
  g_type_ensure (gtk_box_get_type ());
  g_type_ensure (gtk_box_layout_get_type ());
  g_type_ensure (gtk_buildable_get_type ());
  g_type_ensure (gtk_builder_get_type ());
  g_type_ensure (gtk_builder_cscope_get_type ());
  g_type_ensure (gtk_builder_closure_flags_get_type ());
  g_type_ensure (gtk_builder_error_get_type ());
  g_type_ensure (gtk_builder_list_item_factory_get_type ());
  g_type_ensure (gtk_builder_scope_get_type ());
  g_type_ensure (gtk_button_get_type ());
  g_type_ensure (gtk_buttons_type_get_type ());
  g_type_ensure (gtk_cclosure_expression_get_type ());
  g_type_ensure (gtk_calendar_get_type ());
  g_type_ensure (gtk_callback_action_get_type ());
  g_type_ensure (gtk_cell_area_get_type ());
  g_type_ensure (gtk_cell_area_box_get_type ());
  g_type_ensure (gtk_cell_area_context_get_type ());
  g_type_ensure (gtk_cell_editable_get_type ());
  g_type_ensure (gtk_cell_layout_get_type ());
  g_type_ensure (gtk_cell_renderer_get_type ());
  g_type_ensure (gtk_cell_renderer_accel_get_type ());
  g_type_ensure (gtk_cell_renderer_accel_mode_get_type ());
  g_type_ensure (gtk_cell_renderer_combo_get_type ());
  g_type_ensure (gtk_cell_renderer_mode_get_type ());
  g_type_ensure (gtk_cell_renderer_pixbuf_get_type ());
  g_type_ensure (gtk_cell_renderer_progress_get_type ());
  g_type_ensure (gtk_cell_renderer_spin_get_type ());
  g_type_ensure (gtk_cell_renderer_spinner_get_type ());
  g_type_ensure (gtk_cell_renderer_state_get_type ());
  g_type_ensure (gtk_cell_renderer_text_get_type ());
  g_type_ensure (gtk_cell_renderer_toggle_get_type ());
  g_type_ensure (gtk_cell_view_get_type ());
  g_type_ensure (gtk_center_box_get_type ());
  g_type_ensure (gtk_center_layout_get_type ());
  g_type_ensure (gtk_check_button_get_type ());
  g_type_ensure (gtk_closure_expression_get_type ());
  g_type_ensure (gtk_collation_get_type ());
  g_type_ensure (gtk_color_button_get_type ());
  g_type_ensure (gtk_color_chooser_get_type ());
  g_type_ensure (gtk_color_chooser_dialog_get_type ());
  g_type_ensure (gtk_color_chooser_widget_get_type ());
  g_type_ensure (gtk_color_dialog_get_type ());
  g_type_ensure (gtk_color_dialog_button_get_type ());
  g_type_ensure (gtk_column_view_get_type ());
  g_type_ensure (gtk_column_view_cell_get_type ());
  g_type_ensure (gtk_column_view_column_get_type ());
  g_type_ensure (gtk_column_view_row_get_type ());
  g_type_ensure (gtk_column_view_sorter_get_type ());
  g_type_ensure (gtk_combo_box_get_type ());
  g_type_ensure (gtk_combo_box_text_get_type ());
  g_type_ensure (gtk_constant_expression_get_type ());
  g_type_ensure (gtk_constraint_get_type ());
  g_type_ensure (gtk_constraint_attribute_get_type ());
  g_type_ensure (gtk_constraint_guide_get_type ());
  g_type_ensure (gtk_constraint_layout_get_type ());
  g_type_ensure (gtk_constraint_layout_child_get_type ());
  g_type_ensure (gtk_constraint_relation_get_type ());
  g_type_ensure (gtk_constraint_strength_get_type ());
  g_type_ensure (gtk_constraint_target_get_type ());
  g_type_ensure (gtk_constraint_vfl_parser_error_get_type ());
  g_type_ensure (gtk_content_fit_get_type ());
  g_type_ensure (gtk_corner_type_get_type ());
  g_type_ensure (gtk_css_provider_get_type ());
  g_type_ensure (gtk_css_section_get_type ());
  g_type_ensure (gtk_custom_filter_get_type ());
  g_type_ensure (gtk_custom_layout_get_type ());
  g_type_ensure (gtk_custom_sorter_get_type ());
  g_type_ensure (gtk_debug_flags_get_type ());
  g_type_ensure (gtk_delete_type_get_type ());
  g_type_ensure (gtk_dialog_get_type ());
  g_type_ensure (gtk_dialog_error_get_type ());
  g_type_ensure (gtk_dialog_flags_get_type ());
  g_type_ensure (gtk_direction_type_get_type ());
  g_type_ensure (gtk_directory_list_get_type ());
  g_type_ensure (gtk_drag_icon_get_type ());
  g_type_ensure (gtk_drag_source_get_type ());
  g_type_ensure (gtk_drawing_area_get_type ());
  g_type_ensure (gtk_drop_controller_motion_get_type ());
  g_type_ensure (gtk_drop_down_get_type ());
  g_type_ensure (gtk_drop_target_get_type ());
  g_type_ensure (gtk_drop_target_async_get_type ());
  g_type_ensure (gtk_editable_get_type ());
  g_type_ensure (gtk_editable_label_get_type ());
  g_type_ensure (gtk_editable_properties_get_type ());
  g_type_ensure (gtk_emoji_chooser_get_type ());
  g_type_ensure (gtk_entry_get_type ());
  g_type_ensure (gtk_entry_buffer_get_type ());
  g_type_ensure (gtk_entry_completion_get_type ());
  g_type_ensure (gtk_entry_icon_position_get_type ());
  g_type_ensure (gtk_event_controller_get_type ());
  g_type_ensure (gtk_event_controller_focus_get_type ());
  g_type_ensure (gtk_event_controller_key_get_type ());
  g_type_ensure (gtk_event_controller_legacy_get_type ());
  g_type_ensure (gtk_event_controller_motion_get_type ());
  g_type_ensure (gtk_event_controller_scroll_get_type ());
  g_type_ensure (gtk_event_controller_scroll_flags_get_type ());
  g_type_ensure (gtk_event_sequence_state_get_type ());
  g_type_ensure (gtk_every_filter_get_type ());
  g_type_ensure (gtk_expander_get_type ());
  g_type_ensure (gtk_expression_get_type ());
  g_type_ensure (gtk_expression_watch_get_type ());
  g_type_ensure (gtk_file_chooser_get_type ());
  g_type_ensure (gtk_file_chooser_action_get_type ());
  g_type_ensure (gtk_file_chooser_dialog_get_type ());
  g_type_ensure (gtk_file_chooser_error_get_type ());
  g_type_ensure (gtk_file_chooser_native_get_type ());
  g_type_ensure (gtk_file_chooser_widget_get_type ());
  g_type_ensure (gtk_file_dialog_get_type ());
  g_type_ensure (gtk_file_filter_get_type ());
  g_type_ensure (gtk_file_launcher_get_type ());
  g_type_ensure (gtk_filter_get_type ());
  g_type_ensure (gtk_filter_change_get_type ());
  g_type_ensure (gtk_filter_list_model_get_type ());
  g_type_ensure (gtk_filter_match_get_type ());
  g_type_ensure (gtk_fixed_get_type ());
  g_type_ensure (gtk_fixed_layout_get_type ());
  g_type_ensure (gtk_fixed_layout_child_get_type ());
  g_type_ensure (gtk_flatten_list_model_get_type ());
  g_type_ensure (gtk_flow_box_get_type ());
  g_type_ensure (gtk_flow_box_child_get_type ());
  g_type_ensure (gtk_font_button_get_type ());
  g_type_ensure (gtk_font_chooser_get_type ());
  g_type_ensure (gtk_font_chooser_dialog_get_type ());
  g_type_ensure (gtk_font_chooser_level_get_type ());
  g_type_ensure (gtk_font_chooser_widget_get_type ());
  g_type_ensure (gtk_font_dialog_get_type ());
  g_type_ensure (gtk_font_dialog_button_get_type ());
  g_type_ensure (gtk_font_level_get_type ());
  g_type_ensure (gtk_font_rendering_get_type ());
  g_type_ensure (gtk_frame_get_type ());
  g_type_ensure (gtk_gl_area_get_type ());
  g_type_ensure (gtk_gesture_get_type ());
  g_type_ensure (gtk_gesture_click_get_type ());
  g_type_ensure (gtk_gesture_drag_get_type ());
  g_type_ensure (gtk_gesture_long_press_get_type ());
  g_type_ensure (gtk_gesture_pan_get_type ());
  g_type_ensure (gtk_gesture_rotate_get_type ());
  g_type_ensure (gtk_gesture_single_get_type ());
  g_type_ensure (gtk_gesture_stylus_get_type ());
  g_type_ensure (gtk_gesture_swipe_get_type ());
  g_type_ensure (gtk_gesture_zoom_get_type ());
  g_type_ensure (gtk_graphics_offload_get_type ());
  g_type_ensure (gtk_graphics_offload_enabled_get_type ());
  g_type_ensure (gtk_grid_get_type ());
  g_type_ensure (gtk_grid_layout_get_type ());
  g_type_ensure (gtk_grid_layout_child_get_type ());
  g_type_ensure (gtk_grid_view_get_type ());
  g_type_ensure (gtk_header_bar_get_type ());
  g_type_ensure (gtk_im_context_get_type ());
  g_type_ensure (gtk_im_context_simple_get_type ());
  g_type_ensure (gtk_im_multicontext_get_type ());
  g_type_ensure (gtk_icon_lookup_flags_get_type ());
  g_type_ensure (gtk_icon_paintable_get_type ());
  g_type_ensure (gtk_icon_size_get_type ());
  g_type_ensure (gtk_icon_theme_get_type ());
  g_type_ensure (gtk_icon_theme_error_get_type ());
  g_type_ensure (gtk_icon_view_get_type ());
  g_type_ensure (gtk_icon_view_drop_position_get_type ());
  g_type_ensure (gtk_image_get_type ());
  g_type_ensure (gtk_image_type_get_type ());
  g_type_ensure (gtk_info_bar_get_type ());
  g_type_ensure (gtk_input_hints_get_type ());
  g_type_ensure (gtk_input_purpose_get_type ());
  g_type_ensure (gtk_inscription_get_type ());
  g_type_ensure (gtk_inscription_overflow_get_type ());
  g_type_ensure (gtk_interface_color_scheme_get_type ());
  g_type_ensure (gtk_interface_contrast_get_type ());
  g_type_ensure (gtk_justification_get_type ());
  g_type_ensure (gtk_keyval_trigger_get_type ());
  g_type_ensure (gtk_label_get_type ());
  g_type_ensure (gtk_layout_child_get_type ());
  g_type_ensure (gtk_layout_manager_get_type ());
  g_type_ensure (gtk_level_bar_get_type ());
  g_type_ensure (gtk_level_bar_mode_get_type ());
  g_type_ensure (gtk_license_get_type ());
  g_type_ensure (gtk_link_button_get_type ());
  g_type_ensure (gtk_list_base_get_type ());
  g_type_ensure (gtk_list_box_get_type ());
  g_type_ensure (gtk_list_box_row_get_type ());
  g_type_ensure (gtk_list_header_get_type ());
  g_type_ensure (gtk_list_item_get_type ());
  g_type_ensure (gtk_list_item_factory_get_type ());
  g_type_ensure (gtk_list_scroll_flags_get_type ());
  g_type_ensure (gtk_list_store_get_type ());
  g_type_ensure (gtk_list_tab_behavior_get_type ());
  g_type_ensure (gtk_list_view_get_type ());
  g_type_ensure (gtk_lock_button_get_type ());
  g_type_ensure (gtk_map_list_model_get_type ());
  g_type_ensure (gtk_media_controls_get_type ());
  g_type_ensure (gtk_media_file_get_type ());
  g_type_ensure (gtk_media_stream_get_type ());
  g_type_ensure (gtk_menu_button_get_type ());
  g_type_ensure (gtk_message_dialog_get_type ());
  g_type_ensure (gtk_message_type_get_type ());
  g_type_ensure (gtk_mnemonic_action_get_type ());
  g_type_ensure (gtk_mnemonic_trigger_get_type ());
  g_type_ensure (gtk_mount_operation_get_type ());
  g_type_ensure (gtk_movement_step_get_type ());
  g_type_ensure (gtk_multi_filter_get_type ());
  g_type_ensure (gtk_multi_selection_get_type ());
  g_type_ensure (gtk_multi_sorter_get_type ());
  g_type_ensure (gtk_named_action_get_type ());
  g_type_ensure (gtk_native_get_type ());
  g_type_ensure (gtk_native_dialog_get_type ());
  g_type_ensure (gtk_natural_wrap_mode_get_type ());
  g_type_ensure (gtk_never_trigger_get_type ());
  g_type_ensure (gtk_no_selection_get_type ());
  g_type_ensure (gtk_notebook_get_type ());
  g_type_ensure (gtk_notebook_page_get_type ());
  g_type_ensure (gtk_notebook_tab_get_type ());
  g_type_ensure (gtk_nothing_action_get_type ());
  g_type_ensure (gtk_number_up_layout_get_type ());
  g_type_ensure (gtk_numeric_sorter_get_type ());
  g_type_ensure (gtk_object_expression_get_type ());
  g_type_ensure (gtk_ordering_get_type ());
  g_type_ensure (gtk_orientable_get_type ());
  g_type_ensure (gtk_orientation_get_type ());
  g_type_ensure (gtk_overflow_get_type ());
  g_type_ensure (gtk_overlay_get_type ());
  g_type_ensure (gtk_overlay_layout_get_type ());
  g_type_ensure (gtk_overlay_layout_child_get_type ());
  g_type_ensure (gtk_pack_type_get_type ());
  g_type_ensure (gtk_pad_action_type_get_type ());
  g_type_ensure (gtk_pad_controller_get_type ());
  g_type_ensure (gtk_page_orientation_get_type ());
  g_type_ensure (gtk_page_set_get_type ());
  g_type_ensure (gtk_page_setup_get_type ());
  g_type_ensure (gtk_pan_direction_get_type ());
  g_type_ensure (gtk_paned_get_type ());
  g_type_ensure (gtk_paper_size_get_type ());
  g_type_ensure (gtk_param_expression_get_type ());
  g_type_ensure (gtk_password_entry_get_type ());
  g_type_ensure (gtk_password_entry_buffer_get_type ());
  g_type_ensure (gtk_pick_flags_get_type ());
  g_type_ensure (gtk_picture_get_type ());
  g_type_ensure (gtk_policy_type_get_type ());
  g_type_ensure (gtk_popover_get_type ());
  g_type_ensure (gtk_popover_menu_get_type ());
  g_type_ensure (gtk_popover_menu_bar_get_type ());
  g_type_ensure (gtk_popover_menu_flags_get_type ());
  g_type_ensure (gtk_position_type_get_type ());
  g_type_ensure (gtk_print_context_get_type ());
  g_type_ensure (gtk_print_dialog_get_type ());
  g_type_ensure (gtk_print_duplex_get_type ());
  g_type_ensure (gtk_print_error_get_type ());
  g_type_ensure (gtk_print_operation_get_type ());
  g_type_ensure (gtk_print_operation_action_get_type ());
  g_type_ensure (gtk_print_operation_preview_get_type ());
  g_type_ensure (gtk_print_operation_result_get_type ());
  g_type_ensure (gtk_print_pages_get_type ());
  g_type_ensure (gtk_print_quality_get_type ());
  g_type_ensure (gtk_print_settings_get_type ());
  g_type_ensure (gtk_print_setup_get_type ());
  g_type_ensure (gtk_print_status_get_type ());
  g_type_ensure (gtk_progress_bar_get_type ());
  g_type_ensure (gtk_propagation_limit_get_type ());
  g_type_ensure (gtk_propagation_phase_get_type ());
  g_type_ensure (gtk_property_expression_get_type ());
  g_type_ensure (gtk_range_get_type ());
  g_type_ensure (gtk_recent_info_get_type ());
  g_type_ensure (gtk_recent_manager_get_type ());
  g_type_ensure (gtk_recent_manager_error_get_type ());
  g_type_ensure (gtk_requisition_get_type ());
  g_type_ensure (gtk_response_type_get_type ());
  g_type_ensure (gtk_revealer_get_type ());
  g_type_ensure (gtk_revealer_transition_type_get_type ());
  g_type_ensure (gtk_root_get_type ());
  g_type_ensure (gtk_scale_get_type ());
  g_type_ensure (gtk_scale_button_get_type ());
  g_type_ensure (gtk_scroll_info_get_type ());
  g_type_ensure (gtk_scroll_step_get_type ());
  g_type_ensure (gtk_scroll_type_get_type ());
  g_type_ensure (gtk_scrollable_get_type ());
  g_type_ensure (gtk_scrollable_policy_get_type ());
  g_type_ensure (gtk_scrollbar_get_type ());
  g_type_ensure (gtk_scrolled_window_get_type ());
  g_type_ensure (gtk_search_bar_get_type ());
  g_type_ensure (gtk_search_entry_get_type ());
  g_type_ensure (gtk_section_model_get_type ());
  g_type_ensure (gtk_selection_filter_model_get_type ());
  g_type_ensure (gtk_selection_mode_get_type ());
  g_type_ensure (gtk_selection_model_get_type ());
  g_type_ensure (gtk_sensitivity_type_get_type ());
  g_type_ensure (gtk_separator_get_type ());
  g_type_ensure (gtk_settings_get_type ());
  g_type_ensure (gtk_shortcut_get_type ());
  g_type_ensure (gtk_shortcut_action_get_type ());
  g_type_ensure (gtk_shortcut_action_flags_get_type ());
  g_type_ensure (gtk_shortcut_controller_get_type ());
  g_type_ensure (gtk_shortcut_label_get_type ());
  g_type_ensure (gtk_shortcut_manager_get_type ());
  g_type_ensure (gtk_shortcut_scope_get_type ());
  g_type_ensure (gtk_shortcut_trigger_get_type ());
  g_type_ensure (gtk_shortcut_type_get_type ());
  g_type_ensure (gtk_shortcuts_group_get_type ());
  g_type_ensure (gtk_shortcuts_section_get_type ());
  g_type_ensure (gtk_shortcuts_shortcut_get_type ());
  g_type_ensure (gtk_shortcuts_window_get_type ());
  g_type_ensure (gtk_signal_action_get_type ());
  g_type_ensure (gtk_signal_list_item_factory_get_type ());
  g_type_ensure (gtk_single_selection_get_type ());
  g_type_ensure (gtk_size_group_get_type ());
  g_type_ensure (gtk_size_group_mode_get_type ());
  g_type_ensure (gtk_size_request_mode_get_type ());
  g_type_ensure (gtk_slice_list_model_get_type ());
  g_type_ensure (gtk_snapshot_get_type ());
  g_type_ensure (gtk_sort_list_model_get_type ());
  g_type_ensure (gtk_sort_type_get_type ());
  g_type_ensure (gtk_sorter_get_type ());
  g_type_ensure (gtk_sorter_change_get_type ());
  g_type_ensure (gtk_sorter_order_get_type ());
  g_type_ensure (gtk_spin_button_get_type ());
  g_type_ensure (gtk_spin_button_update_policy_get_type ());
  g_type_ensure (gtk_spin_type_get_type ());
  g_type_ensure (gtk_spinner_get_type ());
  g_type_ensure (gtk_stack_get_type ());
  g_type_ensure (gtk_stack_page_get_type ());
  g_type_ensure (gtk_stack_sidebar_get_type ());
  g_type_ensure (gtk_stack_switcher_get_type ());
  g_type_ensure (gtk_stack_transition_type_get_type ());
  g_type_ensure (gtk_state_flags_get_type ());
  g_type_ensure (gtk_statusbar_get_type ());
  g_type_ensure (gtk_string_filter_get_type ());
  g_type_ensure (gtk_string_filter_match_mode_get_type ());
  g_type_ensure (gtk_string_list_get_type ());
  g_type_ensure (gtk_string_object_get_type ());
  g_type_ensure (gtk_string_sorter_get_type ());
  g_type_ensure (gtk_style_context_get_type ());
  g_type_ensure (gtk_style_context_print_flags_get_type ());
  g_type_ensure (gtk_style_provider_get_type ());
  g_type_ensure (gtk_switch_get_type ());
  g_type_ensure (gtk_symbolic_color_get_type ());
  g_type_ensure (gtk_symbolic_paintable_get_type ());
  g_type_ensure (gtk_system_setting_get_type ());
  g_type_ensure (gtk_text_get_type ());
  g_type_ensure (gtk_text_buffer_get_type ());
  g_type_ensure (gtk_text_buffer_notify_flags_get_type ());
  g_type_ensure (gtk_text_child_anchor_get_type ());
  g_type_ensure (gtk_text_direction_get_type ());
  g_type_ensure (gtk_text_extend_selection_get_type ());
  g_type_ensure (gtk_text_iter_get_type ());
  g_type_ensure (gtk_text_mark_get_type ());
  g_type_ensure (gtk_text_search_flags_get_type ());
  g_type_ensure (gtk_text_tag_get_type ());
  g_type_ensure (gtk_text_tag_table_get_type ());
  g_type_ensure (gtk_text_view_get_type ());
  g_type_ensure (gtk_text_view_layer_get_type ());
  g_type_ensure (gtk_text_window_type_get_type ());
  g_type_ensure (gtk_toggle_button_get_type ());
  g_type_ensure (gtk_tooltip_get_type ());
  g_type_ensure (gtk_tree_expander_get_type ());
  g_type_ensure (gtk_tree_iter_get_type ());
  g_type_ensure (gtk_tree_list_model_get_type ());
  g_type_ensure (gtk_tree_list_row_get_type ());
  g_type_ensure (gtk_tree_list_row_sorter_get_type ());
  g_type_ensure (gtk_tree_model_get_type ());
  g_type_ensure (gtk_tree_model_filter_get_type ());
  g_type_ensure (gtk_tree_model_flags_get_type ());
  g_type_ensure (gtk_tree_model_sort_get_type ());
  g_type_ensure (gtk_tree_path_get_type ());
  g_type_ensure (gtk_tree_row_reference_get_type ());
  g_type_ensure (gtk_tree_selection_get_type ());
  g_type_ensure (gtk_tree_sortable_get_type ());
  g_type_ensure (gtk_tree_store_get_type ());
  g_type_ensure (gtk_tree_view_get_type ());
  g_type_ensure (gtk_tree_view_column_get_type ());
  g_type_ensure (gtk_tree_view_column_sizing_get_type ());
  g_type_ensure (gtk_tree_view_drop_position_get_type ());
  g_type_ensure (gtk_tree_view_grid_lines_get_type ());
  g_type_ensure (gtk_unit_get_type ());
  g_type_ensure (gtk_uri_launcher_get_type ());
  g_type_ensure (gtk_video_get_type ());
  g_type_ensure (gtk_viewport_get_type ());
  g_type_ensure (gtk_volume_button_get_type ());
  g_type_ensure (gtk_widget_get_type ());
  g_type_ensure (gtk_widget_paintable_get_type ());
  g_type_ensure (gtk_window_get_type ());
  g_type_ensure (gtk_window_controls_get_type ());
  g_type_ensure (gtk_window_gravity_get_type ());
  g_type_ensure (gtk_window_group_get_type ());
  g_type_ensure (gtk_window_handle_get_type ());
  g_type_ensure (gtk_wrap_mode_get_type ());
}

static void
bge_wdgt_spec_init (BgeWdgtSpec *self)
{
  self->values      = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, value_data_unref);
  self->anon_values = g_ptr_array_new_with_free_func (value_data_unref);
  self->states      = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, state_data_unref);
  self->children    = g_ptr_array_new_with_free_func (value_data_unref);
  self->nonchildren = g_ptr_array_new_with_free_func (value_data_unref);

  self->init_state          = state_data_new ();
  self->init_state->name    = g_strdup ("init");
  self->init_state->setters = g_hash_table_new_full (
      g_direct_hash, g_direct_equal,
      value_data_unref, value_data_unref);
  self->init_state->transitions = g_hash_table_new_full (
      g_direct_hash, g_direct_equal,
      value_data_unref, transition_data_unref);
  ensure_state_snapshot (self->init_state);
  g_hash_table_replace (self->states,
                        g_strdup ("init"),
                        state_data_ref (self->init_state));
}

BgeWdgtSpec *
bge_wdgt_spec_new (void)
{
  return g_object_new (BGE_TYPE_WDGT_SPEC, NULL);
}

BgeWdgtSpec *
bge_wdgt_spec_new_for_string (const char *string,
                              GError    **error)
{
  g_return_val_if_fail (string != NULL, NULL);
  return bge_wdgt_parse_string (string, error);
}

BgeWdgtSpec *
bge_wdgt_spec_new_for_resource (const char *resource,
                                GError    **error)
{
  g_autoptr (GBytes) bytes = NULL;
  gsize         size       = 0;
  gconstpointer data       = NULL;

  g_return_val_if_fail (resource != NULL, NULL);

  bytes = g_resources_lookup_data (resource, G_RESOURCE_LOOKUP_FLAGS_NONE, error);
  if (bytes == NULL)
    return NULL;

  data = g_bytes_get_data (bytes, &size);
  return bge_wdgt_parse_string ((const char *) data, error);
}

const char *
bge_wdgt_spec_get_name (BgeWdgtSpec *self)
{
  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), NULL);
  return self->name;
}

void
bge_wdgt_spec_set_name (BgeWdgtSpec *self,
                        const char  *name)
{
  g_return_if_fail (BGE_IS_WDGT_SPEC (self));

  if (name == self->name || (name != NULL && self->name != NULL && g_strcmp0 (name, self->name) == 0))
    return;

  g_clear_pointer (&self->name, g_free);
  if (name != NULL)
    self->name = g_strdup (name);

  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NAME]);
}

void
bge_wdgt_spec_set_name_take (BgeWdgtSpec *self,
                             char        *name)
{
  g_return_if_fail (BGE_IS_WDGT_SPEC (self));

  if (name != NULL && self->name != NULL && g_strcmp0 (name, self->name) == 0)
    {
      g_free (name);
      return;
    }

  g_clear_pointer (&self->name, g_free);
  if (name != NULL)
    self->name = name;

  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NAME]);
}

gboolean
bge_wdgt_spec_add_constant_source_value (BgeWdgtSpec  *self,
                                         const char   *name,
                                         const GValue *constant,
                                         GError      **error)
{
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);
  g_return_val_if_fail (constant != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }

  value       = value_data_new ();
  value->name = g_strdup (name);
  value->type = constant->g_type;
  value->kind = VALUE_CONSTANT;
  g_value_copy (constant, g_value_init (&value->constant, constant->g_type));

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_component_source_value (BgeWdgtSpec       *self,
                                          const char        *name,
                                          GType              type,
                                          const char *const *components,
                                          guint              n_components,
                                          GError           **error)
{
  GType expected_types[ARGBUF_SIZE] = { 0 };
  g_autoptr (ValueData) value       = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);
  g_return_val_if_fail (components != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }

  if (type == GRAPHENE_TYPE_POINT)
    {
      if (n_components != 2)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "composed point value needs 2 arguments");
          return FALSE;
        }
      expected_types[0] = G_TYPE_DOUBLE;
      expected_types[1] = G_TYPE_DOUBLE;
    }
  else if (type == GRAPHENE_TYPE_POINT3D)
    {
      if (n_components != 3)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "composed 3d point value needs 3 arguments");
          return FALSE;
        }
      expected_types[0] = G_TYPE_DOUBLE;
      expected_types[1] = G_TYPE_DOUBLE;
      expected_types[2] = G_TYPE_DOUBLE;
    }
  else if (type == GRAPHENE_TYPE_SIZE)
    {
      if (n_components != 2)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "composed 2d size value needs 2 arguments");
          return FALSE;
        }
      expected_types[0] = G_TYPE_DOUBLE;
      expected_types[1] = G_TYPE_DOUBLE;
    }
  else if (type == GRAPHENE_TYPE_RECT)
    {
      if (n_components != 4)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "composed rectangle value needs 4 arguments");
          return FALSE;
        }
      expected_types[0] = G_TYPE_DOUBLE;
      expected_types[1] = G_TYPE_DOUBLE;
      expected_types[2] = G_TYPE_DOUBLE;
      expected_types[3] = G_TYPE_DOUBLE;
    }
  else
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "type '%s' is not elligible for a component value",
                   g_type_name (type));
      return FALSE;
    }

  value            = value_data_new ();
  value->name      = g_strdup (name);
  value->type      = type;
  value->kind      = VALUE_COMPONENT;
  value->component = g_ptr_array_new_with_free_func (value_data_unref);

  for (guint i = 0; i < n_components; i++)
    {
      ValueData *value_data = NULL;

      value_data = g_hash_table_lookup (self->values, components[i]);
      if (value_data == NULL)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "value '%s' is undefined", components[i]);
          return FALSE;
        }
      if (!g_type_is_a (value_data->type, expected_types[i]))
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "component %u for type %s "
                       "must be of type %s, got %s",
                       i, g_type_name (type),
                       g_type_name (expected_types[i]),
                       g_type_name (value_data->type));
          return FALSE;
        }

      g_ptr_array_add (value->component, value_data_ref (value_data));
    }

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_transform_source_value (BgeWdgtSpec       *self,
                                          const char        *name,
                                          const char        *next,
                                          const char        *instruction,
                                          const char *const *args,
                                          guint              n_args,
                                          GError           **error)
{
  gboolean   result           = FALSE;
  ValueData *next_data        = NULL;
  g_autoptr (ValueData) value = NULL;
  TransformInstr match        = { 0 };

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);
  g_return_val_if_fail (next != NULL, FALSE);
  g_return_val_if_fail (instruction != NULL, FALSE);
  g_return_val_if_fail (n_args == 0 || args != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }

  next_data = g_hash_table_lookup (self->values, next);
  if (next_data == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", next);
      return FALSE;
    }
  if (next_data->type != GSK_TYPE_TRANSFORM)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' must be of type %s to "
                   "build a transform, got %s",
                   next,
                   g_type_name (GSK_TYPE_TRANSFORM),
                   g_type_name (next_data->type));
      return FALSE;
    }

  result = lookup_transform_instr (instruction, &match);
  if (!result)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "\"%s\" is not a valid transform builder instruction",
                   instruction);
      return FALSE;
    }
  if (n_args != match.n_args)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "transform builder function %s requires %u "
                   "arguments, got %u",
                   match.name, match.n_args, n_args);
      return FALSE;
    }

  value                 = value_data_new ();
  value->name           = g_strdup (name);
  value->type           = GSK_TYPE_TRANSFORM;
  value->kind           = VALUE_TRANSFORM;
  value->transform.func = match.call;
  value->transform.next = value_data_ref (next_data);
  value->transform.args = g_ptr_array_new_with_free_func (value_data_unref);

  for (guint i = 0; i < n_args; i++)
    {
      ValueData *value_data = NULL;

      value_data = g_hash_table_lookup (self->values, args[i]);
      if (value_data == NULL)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "value '%s' is undefined", args[i]);
          return FALSE;
        }

      if (!g_type_is_a (value_data->type, match.args[i]))
        {
          if (check_can_coerce_type (match.args[i], value_data->type))
            g_ptr_array_add (value->transform.args,
                             wrap_coerce_value (self, value_data, match.args[i]));
          else
            {
              g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                           "argument %u for snapshot instruction %s "
                           "must be of type %s, got %s",
                           i, match.name,
                           g_type_name (match.args[i]),
                           g_type_name (value_data->type));
              return FALSE;
            }
        }
      else
        g_ptr_array_add (value->transform.args, value_data_ref (value_data));
    }

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_instance_source_value (BgeWdgtSpec *self,
                                         const char  *name,
                                         GType        type,
                                         GError     **error)
{
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }
  if (!g_type_is_a (type, G_TYPE_OBJECT))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "type '%s' is not an object type",
                   g_type_name (type));
      return FALSE;
    }
  if (!G_TYPE_IS_INSTANTIATABLE (type) ||
      G_TYPE_IS_ABSTRACT (type))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "type '%s' is not instantiable",
                   g_type_name (type));
      return FALSE;
    }

  value       = value_data_new ();
  value->kind = VALUE_OBJECT;
  value->type = type;
  value->name = g_strdup (name);

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  g_ptr_array_add (self->nonchildren, value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_child_source_value (BgeWdgtSpec       *self,
                                      const char        *name,
                                      GType              type,
                                      const char        *parent,
                                      const char        *builder_type,
                                      const char *const *css_classes,
                                      guint              n_css_classes,
                                      GError           **error)
{
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }
  if (!g_type_is_a (type, GTK_TYPE_WIDGET))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "type '%s' does not derive from GtkWidget",
                   g_type_name (type));
      return FALSE;
    }
  if (!G_TYPE_IS_INSTANTIATABLE (type) ||
      G_TYPE_IS_ABSTRACT (type))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "type '%s' is not instantiable",
                   g_type_name (type));
      return FALSE;
    }

  value                     = value_data_new ();
  value->kind               = VALUE_CHILD;
  value->type               = type;
  value->name               = g_strdup (name);
  value->child.builder_type = builder_type != NULL ? g_strdup (builder_type) : NULL;

  if (n_css_classes > 0 &&
      css_classes != NULL)
    {
      value->child.css_classes = g_ptr_array_new_with_free_func (g_free);
      g_ptr_array_set_size (value->child.css_classes, n_css_classes);

      for (guint i = 0; i < n_css_classes; i++)
        {
          g_ptr_array_index (value->child.css_classes, i) = g_strdup (css_classes[i]);
        }
    }

  if (parent != NULL)
    {
      ValueData *parent_data = NULL;

      parent_data = g_hash_table_lookup (self->values, parent);
      if (parent_data == NULL)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "value '%s' is undefined", parent);
          return FALSE;
        }
      if (!g_type_is_a (parent_data->type, GTK_TYPE_BUILDABLE))
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "parent widget '%s' is not of type %s",
                       g_type_name (GTK_TYPE_BUILDABLE), parent);
          return FALSE;
        }

      value->child.parent_widget = value_data_ref (parent_data);
    }

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  g_ptr_array_add (self->children, value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_cclosure_source_value (BgeWdgtSpec       *self,
                                         const char        *name,
                                         GType              type,
                                         GClosureMarshal    marshal,
                                         GCallback          func,
                                         const char *const *args,
                                         const GType       *arg_types,
                                         guint              n_args,
                                         gpointer           user_data,
                                         GDestroyNotify     destroy_user_data,
                                         GError           **error)
{
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);
  g_return_val_if_fail (func != NULL, FALSE);
  g_return_val_if_fail (args != NULL, FALSE);
  g_return_val_if_fail (n_args > 0, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      if (user_data != NULL &&
          destroy_user_data != NULL)
        destroy_user_data (user_data);
      return FALSE;
    }
  if (!G_TYPE_IS_VALUE (type))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "invalid type '%s'", g_type_name (type));
      if (user_data != NULL &&
          destroy_user_data != NULL)
        destroy_user_data (user_data);
      return FALSE;
    }

  value                  = value_data_new ();
  value->kind            = VALUE_CLOSURE;
  value->name            = g_strdup (name);
  value->type            = type;
  value->closure.marshal = marshal;
  value->closure.func    = func;
  value->closure.args    = g_ptr_array_new_with_free_func (value_data_unref);

  value->closure.user_data         = user_data;
  value->closure.destroy_user_data = destroy_user_data;

  for (guint i = 0; i < n_args; i++)
    {
      ValueData *value_data = NULL;

      value_data = g_hash_table_lookup (self->values, args[i]);
      if (value_data == NULL)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "value '%s' is undefined", args[i]);
          return FALSE;
        }

      if (arg_types != NULL &&
          !g_type_is_a (value_data->type, arg_types[i]))
        {
          if (check_can_coerce_type (arg_types[i], value_data->type))
            g_ptr_array_add (value->closure.args,
                             wrap_coerce_value (self, value_data, arg_types[i]));
          else
            {
              g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                           "component %u for type %s "
                           "must be of type %s, got %s",
                           i, g_type_name (type),
                           g_type_name (arg_types[i]),
                           g_type_name (value_data->type));
              return FALSE;
            }
        }
      else
        g_ptr_array_add (value->closure.args, value_data_ref (value_data));
    }

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_measure_for_size_source_value (BgeWdgtSpec *self,
                                                 const char  *name,
                                                 GError     **error)
{
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }

  value       = value_data_new ();
  value->name = g_strdup (name);
  value->kind = VALUE_MEASURE_FOR_SIZE;
  value->type = G_TYPE_INT;

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_widget_width_source_value (BgeWdgtSpec *self,
                                             const char  *name,
                                             GError     **error)
{
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }

  value       = value_data_new ();
  value->name = g_strdup (name);
  value->kind = VALUE_WIDGET_WIDTH;
  value->type = G_TYPE_DOUBLE;

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_widget_height_source_value (BgeWdgtSpec *self,
                                              const char  *name,
                                              GError     **error)
{
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }

  value       = value_data_new ();
  value->name = g_strdup (name);
  value->kind = VALUE_WIDGET_HEIGHT;
  value->type = G_TYPE_DOUBLE;

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_track_transition_source_value (BgeWdgtSpec *self,
                                                 const char  *name,
                                                 const char  *src,
                                                 const char  *damping_ratio,
                                                 const char  *mass,
                                                 const char  *stiffness,
                                                 GError     **error)
{
  ValueData *src_value                              = NULL;
  g_autoptr (ValueData) coerced_src_value           = NULL;
  ValueData *damping_ratio_value                    = NULL;
  g_autoptr (ValueData) coerced_damping_ratio_value = NULL;
  ValueData *mass_value                             = NULL;
  g_autoptr (ValueData) coerced_mass_value          = NULL;
  ValueData *stiffness_value                        = NULL;
  g_autoptr (ValueData) coerced_stiffness_value     = NULL;
  g_autoptr (ValueData) value                       = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);
  g_return_val_if_fail (src != NULL, FALSE);
  g_return_val_if_fail (damping_ratio != NULL, FALSE);
  g_return_val_if_fail (mass != NULL, FALSE);
  g_return_val_if_fail (stiffness != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }

  /* TODO clean up type checking process */

  src_value = g_hash_table_lookup (self->values, src);
  if (src_value == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", src);
      return FALSE;
    }
  if (src_value->type != G_TYPE_DOUBLE)
    {
      if (check_can_coerce_type (G_TYPE_DOUBLE, src_value->type))
        coerced_src_value = wrap_coerce_value (self, src_value, src_value->type);
      else
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "source type %s cannot be transitioned",
                       g_type_name (src_value->type));
          return FALSE;
        }
    }

  damping_ratio_value = g_hash_table_lookup (self->values, damping_ratio);
  if (damping_ratio_value == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", damping_ratio);
      return FALSE;
    }
  if (damping_ratio_value->type != G_TYPE_DOUBLE)
    {
      if (check_can_coerce_type (G_TYPE_DOUBLE, damping_ratio_value->type))
        coerced_damping_ratio_value = wrap_coerce_value (self, damping_ratio_value, damping_ratio_value->type);
      else
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "damping ratio must be a double, got type %s",
                       g_type_name (damping_ratio_value->type));
          return FALSE;
        }
    }

  mass_value = g_hash_table_lookup (self->values, mass);
  if (mass_value == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", mass);
      return FALSE;
    }
  if (mass_value->type != G_TYPE_DOUBLE)
    {
      if (check_can_coerce_type (G_TYPE_DOUBLE, mass_value->type))
        coerced_mass_value = wrap_coerce_value (self, mass_value, mass_value->type);
      else
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "mass must be a double, got type %s",
                       g_type_name (mass_value->type));
          return FALSE;
        }
    }

  stiffness_value = g_hash_table_lookup (self->values, stiffness);
  if (stiffness_value == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", stiffness);
      return FALSE;
    }
  if (stiffness_value->type != G_TYPE_DOUBLE)
    {
      if (check_can_coerce_type (G_TYPE_DOUBLE, stiffness_value->type))
        coerced_stiffness_value = wrap_coerce_value (self, stiffness_value, stiffness_value->type);
      else
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "stiffness must be a double, got type %s",
                       g_type_name (stiffness_value->type));
          return FALSE;
        }
    }

  value       = value_data_new ();
  value->name = g_strdup (name);
  value->kind = VALUE_TRACK_TRANSITION;
  value->type = G_TYPE_DOUBLE;

  value->track_transition.src =
      coerced_src_value != NULL
          ? value_data_ref (coerced_src_value)
          : value_data_ref (src_value);
  value->track_transition.spring.damping_ratio =
      coerced_damping_ratio_value != NULL
          ? value_data_ref (coerced_damping_ratio_value)
          : value_data_ref (damping_ratio_value);
  value->track_transition.spring.mass =
      coerced_mass_value != NULL
          ? value_data_ref (coerced_mass_value)
          : value_data_ref (mass_value);
  value->track_transition.spring.stiffness =
      coerced_stiffness_value != NULL
          ? value_data_ref (coerced_stiffness_value)
          : value_data_ref (stiffness_value);

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_special_source_value (BgeWdgtSpec        *self,
                                        const char         *name,
                                        BgeWdgtSpecialValue kind,
                                        GError            **error)
{
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }

  switch (kind)
    {
    case BGE_WDGT_SPECIAL_VALUE_MOTION_X:
      if (self->special_values.motion_x != NULL)
        value = value_data_ref (self->special_values.motion_x);
      break;
    case BGE_WDGT_SPECIAL_VALUE_MOTION_Y:
      if (self->special_values.motion_y != NULL)
        value = value_data_ref (self->special_values.motion_y);
      break;
    default:
      g_critical ("Invalid special value specified");
      return FALSE;
    }

  if (value == NULL)
    {
      value          = value_data_new ();
      value->name    = g_strdup (name);
      value->kind    = VALUE_SPECIAL;
      value->special = kind;

      switch (kind)
        {
        case BGE_WDGT_SPECIAL_VALUE_MOTION_X:
          self->special_values.motion_x = value_data_ref (value);
          break;
        case BGE_WDGT_SPECIAL_VALUE_MOTION_Y:
          self->special_values.motion_y = value_data_ref (value);
          break;
        default:
          g_assert_not_reached ();
        }
    }

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_variable_value (BgeWdgtSpec *self,
                                  GType        type,
                                  const char  *name,
                                  GError     **error)
{
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);

  if (!G_TYPE_IS_VALUE (type))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "type '%s' cannot be stored in value '%s'",
                   g_type_name (type), name);
      return FALSE;
    }
  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }

  value       = value_data_new ();
  value->name = g_strdup (name);
  value->type = type;
  value->kind = VALUE_VARIABLE;

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_reference_object_value (BgeWdgtSpec *self,
                                          GType        type,
                                          const char  *name,
                                          GError     **error)
{
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);

  if (self->reference != NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "an object reference value has already been defined");
      return FALSE;
    }
  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }
  if (!g_type_is_a (type, G_TYPE_OBJECT))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "type '%s' cannot be stored in reference object value '%s'",
                   g_type_name (type), name);
      return FALSE;
    }

  value       = value_data_new ();
  value->name = g_strdup (name);
  value->type = type;
  value->kind = VALUE_REFERENCE_OBJECT;

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  self->reference = value_data_ref (value);
  return TRUE;
}

gboolean
bge_wdgt_spec_add_property_value (BgeWdgtSpec *self,
                                  const char  *name,
                                  const char  *object,
                                  const char  *property,
                                  GType       *type_out,
                                  GError     **error)
{
  ValueData *object_value           = NULL;
  ValueData *existing_value         = NULL;
  g_autoptr (GTypeClass) type_class = NULL;
  GParamSpec *pspec                 = NULL;
  g_autoptr (ValueData) value       = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);
  g_return_val_if_fail (object != NULL, FALSE);
  g_return_val_if_fail (property != NULL, FALSE);

  object_value = g_hash_table_lookup (self->values, object);
  if (object_value == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", object);
      return FALSE;
    }
  if (!g_type_is_a (object_value->type, G_TYPE_OBJECT))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is not an object", object);
      return FALSE;
    }

  existing_value = g_hash_table_lookup (self->values, name);
  if (existing_value != NULL)
    {
      if (existing_value->kind == VALUE_PROPERTY &&
          existing_value->property.object == object_value &&
          g_strcmp0 (existing_value->property.prop_name, property) == 0)
        return TRUE;
      else
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "value '%s' already exists", name);
          return FALSE;
        }
    }

  type_class = g_type_class_ref (object_value->type);
  pspec      = g_object_class_find_property (G_OBJECT_CLASS (type_class), property);
  if (pspec == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "property '%s' doesn't exist on type %s",
                   property, g_type_name (object_value->type));
      return FALSE;
    }

  value                       = value_data_new ();
  value->name                 = g_strdup (name);
  value->type                 = pspec->value_type;
  value->kind                 = VALUE_PROPERTY;
  value->property.object      = value_data_ref (object_value);
  value->property.prop_name   = g_strdup (property);
  value->property.pspec_flags = pspec->flags;

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));

  if (type_out != NULL)
    *type_out = pspec->value_type;
  return TRUE;
}

gboolean
bge_wdgt_spec_add_allocation_width_value (BgeWdgtSpec *self,
                                          const char  *name,
                                          const char  *child,
                                          GError     **error)
{
  ValueData *child_value      = NULL;
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);
  g_return_val_if_fail (child != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }

  child_value = g_hash_table_lookup (self->values, child);
  if (child_value == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", child);
      return FALSE;
    }
  if (!g_type_is_a (child_value->type, GTK_TYPE_WIDGET))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is not a widget", child);
      return FALSE;
    }

  value                    = value_data_new ();
  value->name              = g_strdup (name);
  value->type              = G_TYPE_INT;
  value->kind              = VALUE_ALLOCATION_WIDTH;
  value->allocation.widget = value_data_ref (child_value);

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_allocation_height_value (BgeWdgtSpec *self,
                                           const char  *name,
                                           const char  *child,
                                           GError     **error)
{
  ValueData *child_value      = NULL;
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);
  g_return_val_if_fail (child != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }

  child_value = g_hash_table_lookup (self->values, child);
  if (child_value == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", child);
      return FALSE;
    }
  if (!g_type_is_a (child_value->type, GTK_TYPE_WIDGET))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is not a widget", child);
      return FALSE;
    }

  value                    = value_data_new ();
  value->name              = g_strdup (name);
  value->type              = G_TYPE_INT;
  value->kind              = VALUE_ALLOCATION_HEIGHT;
  value->allocation.widget = value_data_ref (child_value);

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_allocation_transform_value (BgeWdgtSpec *self,
                                              const char  *name,
                                              const char  *child,
                                              GError     **error)
{
  ValueData *child_value      = NULL;
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);
  g_return_val_if_fail (child != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }

  child_value = g_hash_table_lookup (self->values, child);
  if (child_value == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", child);
      return FALSE;
    }
  if (!g_type_is_a (child_value->type, GTK_TYPE_WIDGET))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is not a widget", child);
      return FALSE;
    }

  value                    = value_data_new ();
  value->name              = g_strdup (name);
  value->type              = GSK_TYPE_TRANSFORM;
  value->kind              = VALUE_ALLOCATION_TRANSFORM;
  value->allocation.widget = value_data_ref (child_value);

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_measure_value (BgeWdgtSpec       *self,
                                 const char        *name,
                                 BgeWdgtMeasureKind kind,
                                 GError           **error)
{
  g_autoptr (ValueData) value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);

  if (g_hash_table_contains (self->values, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' already exists", name);
      return FALSE;
    }

  value       = value_data_new ();
  value->name = g_strdup (name);
  value->type = G_TYPE_INT;

  switch (kind)
    {
    case BGE_WDGT_MEASURE_MINIMUM_WIDTH:
      value->kind = VALUE_MEASURE_MINIMUM_WIDTH;
      break;
    case BGE_WDGT_MEASURE_NATURAL_WIDTH:
      value->kind = VALUE_MEASURE_NATURAL_WIDTH;
      break;
    case BGE_WDGT_MEASURE_MINIMUM_HEIGHT:
      value->kind = VALUE_MEASURE_MINIMUM_HEIGHT;
      break;
    case BGE_WDGT_MEASURE_NATURAL_HEIGHT:
      value->kind = VALUE_MEASURE_NATURAL_HEIGHT;
      break;
    default:
      {
        g_critical ("Passed an invalid measure kind");
        return FALSE;
      }
    }

  g_hash_table_replace (self->values, g_strdup (name), value_data_ref (value));
  return TRUE;
}

gboolean
bge_wdgt_spec_add_state (BgeWdgtSpec *self,
                         const char  *name,
                         gboolean     default_state,
                         GError     **error)
{
  g_autoptr (StateData) state = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (name != NULL, FALSE);

  if (g_hash_table_contains (self->states, name))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "state '%s' already exists", name);
      return FALSE;
    }
  if (default_state && self->default_state != NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "default state already specified");
      return FALSE;
    }

  state          = state_data_new ();
  state->name    = g_strdup (name);
  state->setters = g_hash_table_new_full (
      g_direct_hash, g_direct_equal,
      value_data_unref, value_data_unref);
  state->transitions = g_hash_table_new_full (
      g_direct_hash, g_direct_equal,
      value_data_unref, transition_data_unref);

  g_hash_table_replace (self->states, g_strdup (name), state_data_ref (state));
  if (default_state)
    self->default_state = state_data_ref (state);

  return TRUE;
}

gboolean
bge_wdgt_spec_set_value (BgeWdgtSpec *self,
                         const char  *state,
                         const char  *dest_value,
                         const char  *src_value,
                         GError     **error)
{
  StateData *state_data                  = NULL;
  ValueData *dest_data                   = NULL;
  ValueData *src_data                    = NULL;
  g_autoptr (ValueData) coerced_src_data = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (dest_value != NULL, FALSE);
  g_return_val_if_fail (src_value != NULL, FALSE);

  if (state != NULL)
    state_data = g_hash_table_lookup (self->states, state);
  else
    state_data = self->init_state;

  dest_data = g_hash_table_lookup (self->values, dest_value);
  src_data  = g_hash_table_lookup (self->values, src_value);

  if (state_data == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "state '%s' is undefined", state);
      return FALSE;
    }
  if (dest_data == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", dest_value);
      return FALSE;
    }
  if (src_data == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", src_value);
      return FALSE;
    }
  if (dest_data == src_data)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "cannot assign '%s' to itself", src_value);
      return FALSE;
    }
  if (!g_type_is_a (src_data->type, dest_data->type))
    {
      if (check_can_coerce_type (dest_data->type, src_data->type))
        coerced_src_data = wrap_coerce_value (self, src_data, dest_data->type);
      else
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "source type %s cannot be assigned to destination type %s",
                       g_type_name (src_data->type), g_type_name (dest_data->type));
          return FALSE;
        }
    }
  if (dest_data->kind == VALUE_CONSTANT)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "cannot assign to a constant source value");
      return FALSE;
    }
  if (dest_data->kind == VALUE_SPECIAL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "cannot assign to a special source value");
      return FALSE;
    }
  if (dest_data->kind == VALUE_PROPERTY &&
      !(dest_data->property.pspec_flags & G_PARAM_WRITABLE))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "property %s on type %s is not writable",
                   dest_data->property.prop_name,
                   g_type_name (dest_data->type));
      return FALSE;
    }
  if (src_data->kind == VALUE_PROPERTY &&
      !(src_data->property.pspec_flags & G_PARAM_READABLE))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "property %s on type %s is not writable",
                   src_data->property.prop_name,
                   g_type_name (src_data->type));
      return FALSE;
    }

  g_hash_table_replace (
      state_data->setters,
      value_data_ref (dest_data),
      coerced_src_data != NULL
          ? value_data_ref (coerced_src_data)
          : value_data_ref (src_data));
  return TRUE;
}

gboolean
bge_wdgt_spec_transition_value (BgeWdgtSpec *self,
                                const char  *state,
                                const char  *value,
                                double       seconds,
                                BgeEasing    easing,
                                GError     **error)
{
  StateData *state_data                 = NULL;
  ValueData *value_data                 = NULL;
  g_autoptr (TransitionData) transition = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  /* state is required for transitions */
  g_return_val_if_fail (state != NULL, FALSE);
  g_return_val_if_fail (value != NULL, FALSE);

  if (seconds <= 0.0)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "transition period must be greater than 0.0");
      return FALSE;
    }

  state_data = g_hash_table_lookup (self->states, state);
  value_data = g_hash_table_lookup (self->values, value);

  if (state_data == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "state '%s' is undefined", state);
      return FALSE;
    }
  if (value_data == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", value);
      return FALSE;
    }
  if (value_data->type != G_TYPE_DOUBLE)
    /* TODO: support more types */
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "type %s cannot be transitioned",
                   g_type_name (value_data->type));
      return FALSE;
    }

  transition               = transition_data_new ();
  transition->kind         = TRANSITION_EASE;
  transition->ease.seconds = seconds;
  transition->ease.easing  = easing;

  g_hash_table_replace (
      state_data->transitions,
      value_data_ref (value_data),
      transition_data_ref (transition));
  /* We want the init state to track the transition as well in case of value
     overlays */
  g_hash_table_replace (
      self->init_state->transitions,
      value_data_ref (value_data),
      transition_data_ref (transition));
  return TRUE;
}

gboolean
bge_wdgt_spec_transition_value_spring (BgeWdgtSpec *self,
                                       const char  *state,
                                       const char  *value,
                                       const char  *damping_ratio,
                                       const char  *mass,
                                       const char  *stiffness,
                                       GError     **error)
{
  StateData *state_data                 = NULL;
  ValueData *value_data                 = NULL;
  ValueData *damping_ratio_data         = NULL;
  ValueData *mass_data                  = NULL;
  ValueData *stiffness_data             = NULL;
  g_autoptr (TransitionData) transition = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  /* state is required for transitions */
  g_return_val_if_fail (state != NULL, FALSE);
  g_return_val_if_fail (value != NULL, FALSE);
  g_return_val_if_fail (damping_ratio != NULL, FALSE);
  g_return_val_if_fail (mass != NULL, FALSE);
  g_return_val_if_fail (stiffness != NULL, FALSE);

  state_data         = g_hash_table_lookup (self->states, state);
  value_data         = g_hash_table_lookup (self->values, value);
  damping_ratio_data = g_hash_table_lookup (self->values, damping_ratio);
  mass_data          = g_hash_table_lookup (self->values, mass);
  stiffness_data     = g_hash_table_lookup (self->values, stiffness);

  if (state_data == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "state '%s' is undefined", state);
      return FALSE;
    }
  if (value_data == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", value);
      return FALSE;
    }
  if (damping_ratio_data == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", damping_ratio);
      return FALSE;
    }
  if (mass_data == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", mass);
      return FALSE;
    }
  if (stiffness_data == NULL)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "value '%s' is undefined", stiffness);
      return FALSE;
    }

  if (value_data->type != G_TYPE_DOUBLE)
    /* TODO: support more types */
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "type %s cannot be transitioned",
                   g_type_name (value_data->type));
      return FALSE;
    }

  if (damping_ratio_data->type != G_TYPE_DOUBLE)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "transition damping ratio must of type %s, got %s",
                   g_type_name (G_TYPE_DOUBLE),
                   g_type_name (damping_ratio_data->type));
      return FALSE;
    }
  if (mass_data->type != G_TYPE_DOUBLE)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "transition mass must of type %s, got %s",
                   g_type_name (G_TYPE_DOUBLE),
                   g_type_name (mass_data->type));
      return FALSE;
    }
  if (stiffness_data->type != G_TYPE_DOUBLE)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                   "transition stiffness must of type %s, got %s",
                   g_type_name (G_TYPE_DOUBLE),
                   g_type_name (stiffness_data->type));
      return FALSE;
    }

  transition                       = transition_data_new ();
  transition->kind                 = TRANSITION_SPRING;
  transition->spring.damping_ratio = value_data_ref (damping_ratio_data);
  transition->spring.mass          = value_data_ref (mass_data);
  transition->spring.stiffness     = value_data_ref (stiffness_data);

  g_hash_table_replace (
      state_data->transitions,
      value_data_ref (value_data),
      transition_data_ref (transition));
  /* We want the init state to track the transition as well in case of value
     overlays */
  g_hash_table_replace (
      self->init_state->transitions,
      value_data_ref (value_data),
      transition_data_ref (transition));
  return TRUE;
}

gboolean
bge_wdgt_spec_append_snapshot_instr (BgeWdgtSpec             *self,
                                     const char              *state,
                                     BgeWdgtSnapshotInstrKind kind,
                                     const char              *instr,
                                     const char *const       *args,
                                     guint                    n_args,
                                     GError                 **error)
{
  gboolean      result              = FALSE;
  StateData    *state_data          = NULL;
  SnapshotInstr match               = { 0 };
  guint         match_rest_start    = 0;
  g_autoptr (SnapshotCallData) call = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_SPEC (self), FALSE);
  g_return_val_if_fail (instr != NULL, FALSE);
  g_return_val_if_fail (n_args == 0 || args != NULL, FALSE);

  if (state != NULL)
    {
      state_data = g_hash_table_lookup (self->states, state);
      if (state_data == NULL)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "state '%s' is undefined", state);
          return FALSE;
        }
    }
  else
    state_data = self->init_state;

  switch (kind)
    {
    case BGE_WDGT_SNAPSHOT_INSTR_PUSH:
      result = lookup_snapshot_push_instr (instr, &match);
      if (!result)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "\"%s\" is not a valid snapshot push instruction",
                       instr);
          return FALSE;
        }
      break;
    case BGE_WDGT_SNAPSHOT_INSTR_TRANSFORM:
      result = lookup_snapshot_transform_instr (instr, &match);
      if (!result)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "\"%s\" is not a valid snapshot transform instruction",
                       instr);
          return FALSE;
        }
      break;
    case BGE_WDGT_SNAPSHOT_INSTR_APPEND:
      result = lookup_snapshot_append_instr (instr, &match);
      if (!result)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "\"%s\" is not a valid snapshot append instruction",
                       instr);
          return FALSE;
        }
      break;
    case BGE_WDGT_SNAPSHOT_INSTR_POP:
    case BGE_WDGT_SNAPSHOT_INSTR_SAVE:
    case BGE_WDGT_SNAPSHOT_INSTR_RESTORE:
      {
        call       = snapshot_call_data_new ();
        call->kind = kind;

        ensure_state_snapshot (state_data);
        g_ptr_array_add (state_data->snapshot->calls, snapshot_call_data_ref (call));
      }
      return TRUE;
    case BGE_WDGT_SNAPSHOT_INSTR_SNAPSHOT_CHILD:
      {
        ValueData *child = NULL;

        if (n_args != 1)
          {
            g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                         "child snapshot instruction requires "
                         "a single argument");
            return FALSE;
          }

        child = g_hash_table_lookup (self->values, args[0]);
        if (child == NULL)
          {
            g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                         "value '%s' is undefined", args[0]);
            return FALSE;
          }
        if (!g_type_is_a (child->type, GTK_TYPE_WIDGET))
          {
            g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                         "value '%s' is not a child", args[0]);
            return FALSE;
          }

        call        = snapshot_call_data_new ();
        call->kind  = kind;
        call->child = value_data_ref (child);

        ensure_state_snapshot (state_data);
        g_ptr_array_add (state_data->snapshot->calls, snapshot_call_data_ref (call));
      }
      return TRUE;
    default:
      g_critical ("invalid snapshot instruction kind specified");
      return FALSE;
    }
  match_rest_start = match.n_args - match.n_rest;

  if (n_args != match.n_args)
    {
      if (match.n_rest > 0 &&
          n_args > match.n_args)
        {
          if ((n_args - match.n_args) % match.n_rest != 0)
            g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                         "snapshot instruction %s cannot handle %u "
                         "trailing arguments",
                         match.name, n_args - match.n_args);
        }
      else
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "snapshot instruction %s requires %u "
                       "arguments, got %u",
                       match.name, match.n_args, n_args);
          return FALSE;
        }
    }

  call       = snapshot_call_data_new ();
  call->kind = kind;
  call->func = match.call;
  call->args = g_ptr_array_new_with_free_func (value_data_unref);
  call->rest = g_ptr_array_new_with_free_func (value_data_unref);

  for (guint i = 0; i < n_args; i++)
    {
      ValueData *value_data    = NULL;
      gboolean   in_rest       = FALSE;
      GType      expected_type = 0;

      value_data = g_hash_table_lookup (self->values, args[i]);
      if (value_data == NULL)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "value '%s' is undefined", args[i]);
          return FALSE;
        }

      in_rest = i >= match_rest_start;
      if (in_rest)
        expected_type = match.args[match_rest_start +
                                   ((i - match_rest_start) %
                                    match.n_rest)];
      else
        expected_type = match.args[i];

      if (!g_type_is_a (value_data->type, expected_type))
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_UNKNOWN,
                       "argument %u for snapshot instruction %s "
                       "must be of type %s, got %s",
                       i, match.name,
                       g_type_name (expected_type),
                       g_type_name (value_data->type));
          return FALSE;
        }

      if (in_rest)
        g_ptr_array_add (call->rest, value_data_ref (value_data));
      else
        g_ptr_array_add (call->args, value_data_ref (value_data));
    }

  ensure_state_snapshot (state_data);
  g_ptr_array_add (state_data->snapshot->calls, snapshot_call_data_ref (call));

  return TRUE;
}

static gboolean
check_can_coerce_type (GType dest,
                       GType src)
{
  const struct
  {
    GType dest;
    GType src;
  } valid_pairings[] = {
    {    G_TYPE_INT,   G_TYPE_UINT },
    {    G_TYPE_INT,  G_TYPE_FLOAT },
    {    G_TYPE_INT, G_TYPE_DOUBLE },

    {   G_TYPE_UINT,    G_TYPE_INT },
    {   G_TYPE_UINT,  G_TYPE_FLOAT },
    {   G_TYPE_UINT, G_TYPE_DOUBLE },

    {  G_TYPE_INT64,    G_TYPE_INT },
    {  G_TYPE_INT64,   G_TYPE_UINT },
    {  G_TYPE_INT64,  G_TYPE_FLOAT },
    {  G_TYPE_INT64, G_TYPE_DOUBLE },

    { G_TYPE_UINT64,    G_TYPE_INT },
    { G_TYPE_UINT64,   G_TYPE_UINT },
    { G_TYPE_UINT64,  G_TYPE_INT64 },
    { G_TYPE_UINT64,  G_TYPE_FLOAT },
    { G_TYPE_UINT64, G_TYPE_DOUBLE },

    {  G_TYPE_FLOAT,    G_TYPE_INT },
    {  G_TYPE_FLOAT,   G_TYPE_UINT },
    {  G_TYPE_FLOAT, G_TYPE_DOUBLE },

    { G_TYPE_DOUBLE,    G_TYPE_INT },
    { G_TYPE_DOUBLE,   G_TYPE_UINT },
    { G_TYPE_DOUBLE,  G_TYPE_FLOAT },

    { G_TYPE_STRING,    G_TYPE_INT },
    { G_TYPE_STRING,   G_TYPE_UINT },
    { G_TYPE_STRING,  G_TYPE_INT64 },
    { G_TYPE_STRING, G_TYPE_UINT64 },
    { G_TYPE_STRING,  G_TYPE_FLOAT },
    { G_TYPE_STRING, G_TYPE_DOUBLE },
  };

  for (guint i = 0; i < G_N_ELEMENTS (valid_pairings); i++)
    {
      if (dest == valid_pairings[i].dest &&
          src == valid_pairings[i].src)
        return TRUE;
    }
  return FALSE;
}

static void
coerce_value (const GValue *in,
              GType         dest_type,
              GValue       *out)
{
  /* Not a switch for style, though not the case here GType macros can sometimes
     be non-compile-time constants */
  if (dest_type == G_TYPE_INT)
    {
      int val = 0;

      if (in->g_type == G_TYPE_UINT)
        val = g_value_get_uint (in);
      else if (in->g_type == G_TYPE_FLOAT)
        val = round (g_value_get_float (in));
      else if (in->g_type == G_TYPE_DOUBLE)
        val = round (g_value_get_double (in));

      g_value_set_int (out, val);
    }
  else if (dest_type == G_TYPE_UINT)
    {
      guint val = 0;

      if (in->g_type == G_TYPE_INT)
        val = g_value_get_int (in);
      else if (in->g_type == G_TYPE_FLOAT)
        val = round (g_value_get_float (in));
      else if (in->g_type == G_TYPE_DOUBLE)
        val = round (g_value_get_double (in));

      g_value_set_uint (out, val);
    }
  else if (dest_type == G_TYPE_INT64)
    {
      gint64 val = 0;

      if (in->g_type == G_TYPE_INT)
        val = g_value_get_int (in);
      else if (in->g_type == G_TYPE_UINT)
        val = g_value_get_uint (in);
      else if (in->g_type == G_TYPE_FLOAT)
        val = round (g_value_get_float (in));
      else if (in->g_type == G_TYPE_DOUBLE)
        val = round (g_value_get_double (in));

      g_value_set_int64 (out, val);
    }
  else if (dest_type == G_TYPE_UINT64)
    {
      guint64 val = 0;

      if (in->g_type == G_TYPE_INT)
        val = g_value_get_int (in);
      else if (in->g_type == G_TYPE_UINT)
        val = g_value_get_uint (in);
      else if (in->g_type == G_TYPE_INT64)
        val = g_value_get_int64 (in);
      else if (in->g_type == G_TYPE_UINT64)
        val = round (g_value_get_float (in));
      else if (in->g_type == G_TYPE_DOUBLE)
        val = round (g_value_get_double (in));

      g_value_set_uint64 (out, val);
    }
  else if (dest_type == G_TYPE_FLOAT)
    {
      float val = 0;

      if (in->g_type == G_TYPE_INT)
        val = g_value_get_int (in);
      else if (in->g_type == G_TYPE_UINT)
        val = g_value_get_uint (in);
      else if (in->g_type == G_TYPE_DOUBLE)
        val = round (g_value_get_double (in));

      g_value_set_float (out, val);
    }
  else if (dest_type == G_TYPE_DOUBLE)
    {
      double val = 0;

      if (in->g_type == G_TYPE_INT)
        val = g_value_get_int (in);
      else if (in->g_type == G_TYPE_UINT)
        val = g_value_get_uint (in);
      else if (in->g_type == G_TYPE_FLOAT)
        val = round (g_value_get_float (in));

      g_value_set_double (out, val);
    }
  else if (dest_type == G_TYPE_STRING)
    {
      g_autofree char *val = NULL;

      if (in->g_type == G_TYPE_INT)
        val = g_strdup_printf ("%d", g_value_get_int (in));
      else if (in->g_type == G_TYPE_UINT)
        val = g_strdup_printf ("%u", g_value_get_uint (in));
      else if (in->g_type == G_TYPE_INT64)
        val = g_strdup_printf ("%zd", g_value_get_int64 (in));
      else if (in->g_type == G_TYPE_UINT64)
        val = g_strdup_printf ("%zu", g_value_get_uint64 (in));
      else if (in->g_type == G_TYPE_FLOAT)
        val = g_strdup_printf ("%f", g_value_get_float (in));
      else if (in->g_type == G_TYPE_DOUBLE)
        val = g_strdup_printf ("%f", g_value_get_double (in));

      g_value_take_string (out, g_steal_pointer (&val));
    }
}

static ValueData *
wrap_coerce_value (BgeWdgtSpec *self,
                   ValueData   *value,
                   GType        dest_type)
{
  g_autoptr (ValueData) coerced = NULL;

  g_assert (!g_type_is_a (value->type, dest_type));

  coerced                 = value_data_new ();
  coerced->type           = dest_type;
  coerced->kind           = VALUE_COERCION;
  coerced->coercion.value = value_data_ref (value);

  g_ptr_array_add (self->anon_values, value_data_ref (coerced));
  return g_steal_pointer (&coerced);
}

static GskTransform *
transform_instr_transform (GskTransform *next,
                           const GValue  args[])
{
  return gsk_transform_transform (
      next,
      g_value_get_boxed (&args[0]));
}

static GskTransform *
transform_instr_invert (GskTransform *next,
                        const GValue  args[])
{
  return gsk_transform_transform (
      next,
      g_value_get_boxed (&args[0]));
}

static GskTransform *
transform_instr_matrix (GskTransform *next,
                        const GValue  args[])
{
  return gsk_transform_matrix (
      next,
      g_value_get_boxed (&args[0]));
}

static GskTransform *
transform_instr_matrix_2d (GskTransform *next,
                           const GValue  args[])
{
  return gsk_transform_matrix_2d (
      next,
      g_value_get_double (&args[0]),
      g_value_get_double (&args[1]),
      g_value_get_double (&args[2]),
      g_value_get_double (&args[3]),
      g_value_get_double (&args[4]),
      g_value_get_double (&args[5]));
}

static GskTransform *
transform_instr_translate (GskTransform *next,
                           const GValue  args[])
{
  return gsk_transform_translate (
      next,
      g_value_get_boxed (&args[0]));
}

static GskTransform *
transform_instr_translate_3d (GskTransform *next,
                              const GValue  args[])
{
  return gsk_transform_translate_3d (
      next,
      g_value_get_boxed (&args[0]));
}

static GskTransform *
transform_instr_skew (GskTransform *next,
                      const GValue  args[])
{
  return gsk_transform_skew (
      next,
      g_value_get_double (&args[0]),
      g_value_get_double (&args[1]));
}

static GskTransform *
transform_instr_rotate (GskTransform *next,
                        const GValue  args[])
{
  return gsk_transform_rotate (
      next,
      g_value_get_double (&args[0]));
}

static GskTransform *
transform_instr_rotate_3d (GskTransform *next,
                           const GValue  args[])
{
  return gsk_transform_rotate_3d (
      next,
      g_value_get_double (&args[0]),
      g_value_get_boxed (&args[1]));
}

static GskTransform *
transform_instr_scale (GskTransform *next,
                       const GValue  args[])
{
  return gsk_transform_scale (
      next,
      g_value_get_double (&args[0]),
      g_value_get_double (&args[1]));
}

static GskTransform *
transform_instr_scale_3d (GskTransform *next,
                          const GValue  args[])
{
  return gsk_transform_scale_3d (
      next,
      g_value_get_double (&args[0]),
      g_value_get_double (&args[1]),
      g_value_get_double (&args[2]));
}

static GskTransform *
transform_instr_perspective (GskTransform *next,
                             const GValue  args[])
{
  return gsk_transform_perspective (
      next,
      g_value_get_double (&args[0]));
}

static gboolean
lookup_transform_instr (const char     *lookup_name,
                        TransformInstr *out)
{
  TransformInstr instrs[] = {
    {
     "transform",
     1,
     {
     GSK_TYPE_TRANSFORM,
     },
     gsk_transform_transform,
     transform_instr_transform,
     },
    {
     "invert",
     0,
     {},
     gsk_transform_invert,
     transform_instr_invert,
     },
    {
     "matrix",
     1,
     {
     GRAPHENE_TYPE_MATRIX,
     },
     gsk_transform_matrix,
     transform_instr_matrix,
     },
    {
     "matrix-2d",
     6,
     {
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     },
     gsk_transform_matrix_2d,
     transform_instr_matrix_2d,
     },
    {
     "translate",
     1,
     {
     GRAPHENE_TYPE_POINT,
     },
     gsk_transform_translate,
     transform_instr_translate,
     },
    {
     "translate-3d",
     1,
     {
     GRAPHENE_TYPE_POINT3D,
     },
     gsk_transform_translate_3d,
     transform_instr_translate_3d,
     },
    {
     "skew",
     2,
     {
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     },
     gsk_transform_skew,
     transform_instr_skew,
     },
    {
     "rotate",
     1,
     {
     G_TYPE_DOUBLE,
     },
     gsk_transform_rotate,
     transform_instr_rotate,
     },
    {
     "rotate-3d",
     2,
     {
     G_TYPE_DOUBLE,
     GRAPHENE_TYPE_VEC3,
     },
     gsk_transform_rotate_3d,
     transform_instr_rotate_3d,
     },
    {
     "scale",
     2,
     {
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     },
     gsk_transform_scale,
     transform_instr_scale,
     },
    {
     "scale-3d",
     3,
     {
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     },
     gsk_transform_scale_3d,
     transform_instr_scale_3d,
     },
    {
     "perspective",
     1,
     {
     G_TYPE_DOUBLE,
     },
     gsk_transform_perspective,
     transform_instr_perspective,
     },
  };

  for (guint i = 0; i < G_N_ELEMENTS (instrs); i++)
    {
      if (g_strcmp0 (lookup_name, instrs[i].name) == 0)
        {
          *out = instrs[i];
          return TRUE;
        }
    }
  return FALSE;
}

static void
snapshot_push_instr_opacity (GtkSnapshot *snapshot,
                             const GValue args[],
                             const GValue rest[],
                             guint        n_rest)
{
  gtk_snapshot_push_opacity (
      snapshot,
      g_value_get_double (&args[0]));
}

static void
snapshot_push_instr_isolation (GtkSnapshot *snapshot,
                               const GValue args[],
                               const GValue rest[],
                               guint        n_rest)
{
  gtk_snapshot_push_isolation (
      snapshot,
      g_value_get_flags (&args[0]));
}

static void
snapshot_push_instr_blur (GtkSnapshot *snapshot,
                          const GValue args[],
                          const GValue rest[],
                          guint        n_rest)
{
  gtk_snapshot_push_blur (
      snapshot,
      g_value_get_double (&args[0]));
}

static void
snapshot_push_instr_color_matrix (GtkSnapshot *snapshot,
                                  const GValue args[],
                                  const GValue rest[],
                                  guint        n_rest)
{
  gtk_snapshot_push_color_matrix (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_boxed (&args[1]));
}

static void
snapshot_push_instr_component_transfer (GtkSnapshot *snapshot,
                                        const GValue args[],
                                        const GValue rest[],
                                        guint        n_rest)
{
  gtk_snapshot_push_component_transfer (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_boxed (&args[1]),
      g_value_get_boxed (&args[2]),
      g_value_get_boxed (&args[3]));
}

static void
snapshot_push_instr_repeat (GtkSnapshot *snapshot,
                            const GValue args[],
                            const GValue rest[],
                            guint        n_rest)
{
  gtk_snapshot_push_repeat (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_boxed (&args[1]));
}

static void
snapshot_push_instr_clip (GtkSnapshot *snapshot,
                          const GValue args[],
                          const GValue rest[],
                          guint        n_rest)
{
  gtk_snapshot_push_clip (
      snapshot,
      g_value_get_boxed (&args[0]));
}

static void
snapshot_push_instr_rounded_clip (GtkSnapshot *snapshot,
                                  const GValue args[],
                                  const GValue rest[],
                                  guint        n_rest)
{
  GskRoundedRect rrect = { 0 };

  rrect.bounds    = *(graphene_rect_t *) g_value_get_boxed (&args[0]);
  rrect.corner[0] = *(graphene_size_t *) g_value_get_boxed (&args[1]);
  rrect.corner[1] = *(graphene_size_t *) g_value_get_boxed (&args[2]);
  rrect.corner[2] = *(graphene_size_t *) g_value_get_boxed (&args[3]);
  rrect.corner[3] = *(graphene_size_t *) g_value_get_boxed (&args[4]);

  gtk_snapshot_push_rounded_clip (
      snapshot,
      &rrect);
}

static void
snapshot_push_instr_fill (GtkSnapshot *snapshot,
                          const GValue args[],
                          const GValue rest[],
                          guint        n_rest)
{
  gtk_snapshot_push_fill (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_enum (&args[1]));
}

static void
snapshot_push_instr_stroke (GtkSnapshot *snapshot,
                            const GValue args[],
                            const GValue rest[],
                            guint        n_rest)
{
  gtk_snapshot_push_stroke (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_boxed (&args[1]));
}

static void
snapshot_push_instr_shadow (GtkSnapshot *snapshot,
                            const GValue args[],
                            const GValue rest[],
                            guint        n_rest)
{
  guint     n_shadows            = 0;
  GskShadow shadows[ARGBUF_SIZE] = { 0 };

  n_shadows = MIN (n_rest / 4, G_N_ELEMENTS (shadows));

  for (guint i = 0; i < n_shadows; i++)
    {
      shadows[i].color  = *(GdkRGBA *) g_value_get_boxed (&rest[i * 4 + 0]);
      shadows[i].dx     = g_value_get_double (&rest[i * 4 + 1]);
      shadows[i].dy     = g_value_get_double (&rest[i * 4 + 2]);
      shadows[i].radius = g_value_get_double (&rest[i * 4 + 3]);
    }

  gtk_snapshot_push_shadow (
      snapshot,
      shadows,
      n_shadows);
}

static void
snapshot_push_instr_blend (GtkSnapshot *snapshot,
                           const GValue args[],
                           const GValue rest[],
                           guint        n_rest)
{
  gtk_snapshot_push_blend (
      snapshot,
      g_value_get_enum (&args[0]));
}

static void
snapshot_push_instr_mask (GtkSnapshot *snapshot,
                          const GValue args[],
                          const GValue rest[],
                          guint        n_rest)
{
  gtk_snapshot_push_mask (
      snapshot,
      g_value_get_enum (&args[0]));
}

static void
snapshot_push_instr_copy (GtkSnapshot *snapshot,
                          const GValue args[],
                          const GValue rest[],
                          guint        n_rest)
{
  gtk_snapshot_push_copy (
      snapshot);
}

static void
snapshot_push_instr_composite (GtkSnapshot *snapshot,
                               const GValue args[],
                               const GValue rest[],
                               guint        n_rest)
{
  gtk_snapshot_push_composite (
      snapshot,
      g_value_get_enum (&args[0]));
}

static void
snapshot_push_instr_cross_fade (GtkSnapshot *snapshot,
                                const GValue args[],
                                const GValue rest[],
                                guint        n_rest)
{
  gtk_snapshot_push_cross_fade (
      snapshot,
      g_value_get_double (&args[0]));
}

static gboolean
lookup_snapshot_push_instr (const char    *lookup_name,
                            SnapshotInstr *out)
{
  SnapshotInstr instrs[] = {
    {
     "opacity",
     1,
     0,
     {
     G_TYPE_DOUBLE,
     },
     gtk_snapshot_push_opacity,
     snapshot_push_instr_opacity,
     },
    {
     "isolation",
     1,
     0,
     {
     GSK_TYPE_ISOLATION,
     },
     gtk_snapshot_push_isolation,
     snapshot_push_instr_isolation,
     },
    {
     "blur",
     1,
     0,
     {
     G_TYPE_DOUBLE,
     },
     gtk_snapshot_push_blur,
     snapshot_push_instr_blur,
     },
    {
     "color-matrix",
     2,
     0,
     {
     GRAPHENE_TYPE_MATRIX,
     GRAPHENE_TYPE_VEC4,
     },
     gtk_snapshot_push_color_matrix,
     snapshot_push_instr_color_matrix,
     },
    {
     "component-transfer",
     4,
     0,
     {
     GSK_TYPE_COMPONENT_TRANSFER,
     GSK_TYPE_COMPONENT_TRANSFER,
     GSK_TYPE_COMPONENT_TRANSFER,
     GSK_TYPE_COMPONENT_TRANSFER,
     },
     gtk_snapshot_push_component_transfer,
     snapshot_push_instr_component_transfer,
     },
    {
     "repeat",
     2,
     0,
     {
     GRAPHENE_TYPE_RECT,
     GRAPHENE_TYPE_RECT,
     },
     gtk_snapshot_push_repeat,
     snapshot_push_instr_repeat,
     },
    {
     "clip",
     1,
     0,
     {
     GRAPHENE_TYPE_RECT,
     },
     gtk_snapshot_push_clip,
     snapshot_push_instr_clip,
     },
    {
     "rounded-clip",
     5,
     0,
     {
     GRAPHENE_TYPE_RECT,
     GRAPHENE_TYPE_SIZE,
     GRAPHENE_TYPE_SIZE,
     GRAPHENE_TYPE_SIZE,
     GRAPHENE_TYPE_SIZE,
     },
     gtk_snapshot_push_rounded_clip,
     snapshot_push_instr_rounded_clip,
     },
    {
     "fill",
     2,
     0,
     {
     GSK_TYPE_PATH,
     GSK_TYPE_FILL_RULE,
     },
     gtk_snapshot_push_fill,
     snapshot_push_instr_fill,
     },
    {
     "stroke",
     2,
     0,
     {
     GSK_TYPE_PATH,
     GSK_TYPE_STROKE,
     },
     gtk_snapshot_push_stroke,
     snapshot_push_instr_stroke,
     },
    {
     "shadow",
     4,
     4,
     {
     GDK_TYPE_RGBA,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     },
     gtk_snapshot_push_shadow,
     snapshot_push_instr_shadow,
     },
    {
     "blend",
     1,
     0,
     {
     GSK_TYPE_BLEND_MODE,
     },
     gtk_snapshot_push_blend,
     snapshot_push_instr_blend,
     },
    {
     "mask",
     1,
     0,
     {
     GSK_TYPE_MASK_MODE,
     },
     gtk_snapshot_push_mask,
     snapshot_push_instr_mask,
     },
    {
     "copy",
     0,
     0,
     {},
     gtk_snapshot_push_copy,
     snapshot_push_instr_copy,
     },
    {
     "composite",
     1,
     0,
     {
     GSK_TYPE_PORTER_DUFF,
     },
     gtk_snapshot_push_composite,
     snapshot_push_instr_composite,
     },
    {
     "cross-fade",
     1,
     0,
     {
     G_TYPE_DOUBLE,
     },
     gtk_snapshot_push_cross_fade,
     snapshot_push_instr_cross_fade,
     },
  };

  for (guint i = 0; i < G_N_ELEMENTS (instrs); i++)
    {
      if (g_strcmp0 (lookup_name, instrs[i].name) == 0)
        {
          *out = instrs[i];
          return TRUE;
        }
    }
  return FALSE;
}

static void
snapshot_transform_instr_transform (GtkSnapshot *snapshot,
                                    const GValue args[],
                                    const GValue rest[],
                                    guint        n_rest)
{
  gtk_snapshot_transform (
      snapshot,
      g_value_get_boxed (&args[0]));
}

static void
snapshot_transform_instr_transform_matrix (GtkSnapshot *snapshot,
                                           const GValue args[],
                                           const GValue rest[],
                                           guint        n_rest)
{
  gtk_snapshot_transform_matrix (
      snapshot,
      g_value_get_boxed (&args[0]));
}

static void
snapshot_transform_instr_translate (GtkSnapshot *snapshot,
                                    const GValue args[],
                                    const GValue rest[],
                                    guint        n_rest)
{
  gtk_snapshot_translate (
      snapshot,
      g_value_get_boxed (&args[0]));
}

static void
snapshot_transform_instr_translate_3d (GtkSnapshot *snapshot,
                                       const GValue args[],
                                       const GValue rest[],
                                       guint        n_rest)
{
  gtk_snapshot_translate_3d (
      snapshot,
      g_value_get_boxed (&args[0]));
}

static void
snapshot_transform_instr_rotate (GtkSnapshot *snapshot,
                                 const GValue args[],
                                 const GValue rest[],
                                 guint        n_rest)
{
  gtk_snapshot_rotate (
      snapshot,
      g_value_get_double (&args[0]));
}

static void
snapshot_transform_instr_rotate_3d (GtkSnapshot *snapshot,
                                    const GValue args[],
                                    const GValue rest[],
                                    guint        n_rest)
{
  gtk_snapshot_rotate_3d (
      snapshot,
      g_value_get_double (&args[0]),
      g_value_get_boxed (&args[1]));
}

static void
snapshot_transform_instr_scale (GtkSnapshot *snapshot,
                                const GValue args[],
                                const GValue rest[],
                                guint        n_rest)
{
  gtk_snapshot_scale (
      snapshot,
      g_value_get_double (&args[0]),
      g_value_get_double (&args[1]));
}

static void
snapshot_transform_instr_scale_3d (GtkSnapshot *snapshot,
                                   const GValue args[],
                                   const GValue rest[],
                                   guint        n_rest)
{
  gtk_snapshot_scale_3d (
      snapshot,
      g_value_get_double (&args[0]),
      g_value_get_double (&args[1]),
      g_value_get_double (&args[2]));
}

static void
snapshot_transform_instr_perspective (GtkSnapshot *snapshot,
                                      const GValue args[],
                                      const GValue rest[],
                                      guint        n_rest)
{
  gtk_snapshot_scale_3d (
      snapshot,
      g_value_get_double (&args[0]),
      g_value_get_double (&args[1]),
      g_value_get_double (&args[2]));
}

static gboolean
lookup_snapshot_transform_instr (const char    *lookup_name,
                                 SnapshotInstr *out)
{
  SnapshotInstr instrs[] = {
    {
     "transform",
     1,
     0,
     {
     GSK_TYPE_TRANSFORM,
     },
     gtk_snapshot_transform,
     snapshot_transform_instr_transform,
     },
    {
     "transform-matrix",
     1,
     0,
     {
     GRAPHENE_TYPE_MATRIX,
     },
     gtk_snapshot_transform_matrix,
     snapshot_transform_instr_transform_matrix,
     },
    {
     "translate",
     1,
     0,
     {
     GRAPHENE_TYPE_POINT,
     },
     gtk_snapshot_translate,
     snapshot_transform_instr_translate,
     },
    {
     "translate-3d",
     1,
     0,
     {
     GRAPHENE_TYPE_POINT3D,
     },
     gtk_snapshot_translate_3d,
     snapshot_transform_instr_translate_3d,
     },
    {
     "rotate",
     1,
     0,
     {
     G_TYPE_DOUBLE,
     },
     gtk_snapshot_rotate,
     snapshot_transform_instr_rotate,
     },
    {
     "rotate-3d",
     2,
     0,
     {
     G_TYPE_DOUBLE,
     GRAPHENE_TYPE_VEC3,
     },
     gtk_snapshot_rotate_3d,
     snapshot_transform_instr_rotate_3d,
     },
    {
     "scale",
     2,
     0,
     {
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     },
     gtk_snapshot_scale,
     snapshot_transform_instr_scale,
     },
    {
     "scale-3d",
     3,
     0,
     {
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     },
     gtk_snapshot_scale_3d,
     snapshot_transform_instr_scale_3d,
     },
    {
     "perspective",
     1,
     0,
     {
     G_TYPE_DOUBLE,
     },
     gtk_snapshot_perspective,
     snapshot_transform_instr_perspective,
     },
  };

  for (guint i = 0; i < G_N_ELEMENTS (instrs); i++)
    {
      if (g_strcmp0 (lookup_name, instrs[i].name) == 0)
        {
          *out = instrs[i];
          return TRUE;
        }
    }
  return FALSE;
}

static void
snapshot_append_instr_node (GtkSnapshot *snapshot,
                            const GValue args[],
                            const GValue rest[],
                            guint        n_rest)
{
  gtk_snapshot_append_node (
      snapshot,
      g_value_get_boxed (&args[0]));
}

static void
snapshot_append_instr_texture (GtkSnapshot *snapshot,
                               const GValue args[],
                               const GValue rest[],
                               guint        n_rest)
{
  gtk_snapshot_append_texture (
      snapshot,
      g_value_get_object (&args[0]),
      g_value_get_boxed (&args[1]));
}

static void
snapshot_append_instr_scaled_texture (GtkSnapshot *snapshot,
                                      const GValue args[],
                                      const GValue rest[],
                                      guint        n_rest)
{
  gtk_snapshot_append_scaled_texture (
      snapshot,
      g_value_get_object (&args[0]),
      g_value_get_enum (&args[1]),
      g_value_get_boxed (&args[2]));
}

static void
snapshot_append_instr_color (GtkSnapshot *snapshot,
                             const GValue args[],
                             const GValue rest[],
                             guint        n_rest)
{
  gtk_snapshot_append_color (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_boxed (&args[1]));
}

static void
snapshot_append_instr_linear_gradient (GtkSnapshot *snapshot,
                                       const GValue args[],
                                       const GValue rest[],
                                       guint        n_rest)
{
  guint        n_stops            = 0;
  GskColorStop stops[ARGBUF_SIZE] = { 0 };

  n_stops = MIN (n_rest / 2, G_N_ELEMENTS (stops));
  for (guint i = 0; i < n_stops; i++)
    {
      stops[i].offset = g_value_get_double (&rest[i * 2 + 0]);
      stops[i].color  = *(GdkRGBA *) g_value_get_boxed (&rest[i * 2 + 1]);
    }

  gtk_snapshot_append_linear_gradient (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_boxed (&args[1]),
      g_value_get_boxed (&args[2]),
      stops,
      n_stops);
}

static void
snapshot_append_instr_repeating_linear_gradient (GtkSnapshot *snapshot,
                                                 const GValue args[],
                                                 const GValue rest[],
                                                 guint        n_rest)
{
  guint        n_stops            = 0;
  GskColorStop stops[ARGBUF_SIZE] = { 0 };

  n_stops = MIN (n_rest / 2, G_N_ELEMENTS (stops));
  for (guint i = 0; i < n_stops; i++)
    {
      stops[i].offset = g_value_get_double (&rest[i * 2 + 0]);
      stops[i].color  = *(GdkRGBA *) g_value_get_boxed (&rest[i * 2 + 1]);
    }

  gtk_snapshot_append_repeating_linear_gradient (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_boxed (&args[1]),
      g_value_get_boxed (&args[2]),
      stops,
      n_stops);
}

static void
snapshot_append_instr_radial_gradient (GtkSnapshot *snapshot,
                                       const GValue args[],
                                       const GValue rest[],
                                       guint        n_rest)
{
  guint        n_stops            = 0;
  GskColorStop stops[ARGBUF_SIZE] = { 0 };

  n_stops = MIN (n_rest / 2, G_N_ELEMENTS (stops));
  for (guint i = 0; i < n_stops; i++)
    {
      stops[i].offset = g_value_get_double (&rest[i * 2 + 0]);
      stops[i].color  = *(GdkRGBA *) g_value_get_boxed (&rest[i * 2 + 1]);
    }

  gtk_snapshot_append_radial_gradient (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_boxed (&args[1]),
      g_value_get_double (&args[2]),
      g_value_get_double (&args[3]),
      g_value_get_double (&args[4]),
      g_value_get_double (&args[5]),
      stops,
      n_stops);
}

static void
snapshot_append_instr_repeating_radial_gradient (GtkSnapshot *snapshot,
                                                 const GValue args[],
                                                 const GValue rest[],
                                                 guint        n_rest)
{
  guint        n_stops            = 0;
  GskColorStop stops[ARGBUF_SIZE] = { 0 };

  n_stops = MIN (n_rest / 2, G_N_ELEMENTS (stops));
  for (guint i = 0; i < n_stops; i++)
    {
      stops[i].offset = g_value_get_double (&rest[i * 2 + 0]);
      stops[i].color  = *(GdkRGBA *) g_value_get_boxed (&rest[i * 2 + 1]);
    }

  gtk_snapshot_append_repeating_radial_gradient (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_boxed (&args[1]),
      g_value_get_double (&args[2]),
      g_value_get_double (&args[3]),
      g_value_get_double (&args[4]),
      g_value_get_double (&args[5]),
      stops,
      n_stops);
}

static void
snapshot_append_instr_conic_gradient (GtkSnapshot *snapshot,
                                      const GValue args[],
                                      const GValue rest[],
                                      guint        n_rest)
{
  guint        n_stops            = 0;
  GskColorStop stops[ARGBUF_SIZE] = { 0 };

  n_stops = MIN (n_rest / 2, G_N_ELEMENTS (stops));
  for (guint i = 0; i < n_stops; i++)
    {
      stops[i].offset = g_value_get_double (&rest[i * 2 + 0]);
      stops[i].color  = *(GdkRGBA *) g_value_get_boxed (&rest[i * 2 + 1]);
    }

  gtk_snapshot_append_conic_gradient (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_boxed (&args[1]),
      g_value_get_double (&args[2]),
      stops,
      n_stops);
}

static void
snapshot_append_instr_border (GtkSnapshot *snapshot,
                              const GValue args[],
                              const GValue rest[],
                              guint        n_rest)
{
  GskRoundedRect rrect           = { 0 };
  float          border_width[4] = { 0 };
  GdkRGBA        border_color[4] = { 0 };

  rrect.bounds    = *(graphene_rect_t *) g_value_get_boxed (&args[0]);
  rrect.corner[0] = *(graphene_size_t *) g_value_get_boxed (&args[1]);
  rrect.corner[1] = *(graphene_size_t *) g_value_get_boxed (&args[2]);
  rrect.corner[2] = *(graphene_size_t *) g_value_get_boxed (&args[3]);
  rrect.corner[3] = *(graphene_size_t *) g_value_get_boxed (&args[4]);

  border_width[0] = g_value_get_double (&args[5]);
  border_width[1] = g_value_get_double (&args[6]);
  border_width[2] = g_value_get_double (&args[7]);
  border_width[3] = g_value_get_double (&args[8]);

  border_color[0] = *(GdkRGBA *) g_value_get_boxed (&args[9]);
  border_color[1] = *(GdkRGBA *) g_value_get_boxed (&args[10]);
  border_color[2] = *(GdkRGBA *) g_value_get_boxed (&args[11]);
  border_color[3] = *(GdkRGBA *) g_value_get_boxed (&args[12]);

  gtk_snapshot_append_border (
      snapshot,
      &rrect,
      border_width,
      border_color);
}

static void
snapshot_append_instr_inset_shadow (GtkSnapshot *snapshot,
                                    const GValue args[],
                                    const GValue rest[],
                                    guint        n_rest)
{
  GskRoundedRect rrect = { 0 };

  rrect.bounds    = *(graphene_rect_t *) g_value_get_boxed (&args[0]);
  rrect.corner[0] = *(graphene_size_t *) g_value_get_boxed (&args[1]);
  rrect.corner[1] = *(graphene_size_t *) g_value_get_boxed (&args[2]);
  rrect.corner[2] = *(graphene_size_t *) g_value_get_boxed (&args[3]);
  rrect.corner[3] = *(graphene_size_t *) g_value_get_boxed (&args[4]);

  gtk_snapshot_append_inset_shadow (
      snapshot,
      &rrect,
      g_value_get_boxed (&args[5]),
      g_value_get_double (&args[6]),
      g_value_get_double (&args[7]),
      g_value_get_double (&args[8]),
      g_value_get_double (&args[9]));
}

static void
snapshot_append_instr_outset_shadow (GtkSnapshot *snapshot,
                                     const GValue args[],
                                     const GValue rest[],
                                     guint        n_rest)
{
  GskRoundedRect rrect = { 0 };

  rrect.bounds    = *(graphene_rect_t *) g_value_get_boxed (&args[0]);
  rrect.corner[0] = *(graphene_size_t *) g_value_get_boxed (&args[1]);
  rrect.corner[1] = *(graphene_size_t *) g_value_get_boxed (&args[2]);
  rrect.corner[2] = *(graphene_size_t *) g_value_get_boxed (&args[3]);
  rrect.corner[3] = *(graphene_size_t *) g_value_get_boxed (&args[4]);

  gtk_snapshot_append_outset_shadow (
      snapshot,
      &rrect,
      g_value_get_boxed (&args[5]),
      g_value_get_double (&args[6]),
      g_value_get_double (&args[7]),
      g_value_get_double (&args[8]),
      g_value_get_double (&args[9]));
}

static void
snapshot_append_instr_layout (GtkSnapshot *snapshot,
                              const GValue args[],
                              const GValue rest[],
                              guint        n_rest)
{
  gtk_snapshot_append_layout (
      snapshot,
      g_value_get_object (&args[0]),
      g_value_get_boxed (&args[1]));
}

static void
snapshot_append_instr_fill (GtkSnapshot *snapshot,
                            const GValue args[],
                            const GValue rest[],
                            guint        n_rest)
{
  gtk_snapshot_append_fill (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_enum (&args[1]),
      g_value_get_boxed (&args[2]));
}

static void
snapshot_append_instr_stroke (GtkSnapshot *snapshot,
                              const GValue args[],
                              const GValue rest[],
                              guint        n_rest)
{
  gtk_snapshot_append_stroke (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_boxed (&args[1]),
      g_value_get_boxed (&args[2]));
}

static void
snapshot_append_instr_paste (GtkSnapshot *snapshot,
                             const GValue args[],
                             const GValue rest[],
                             guint        n_rest)
{
  gtk_snapshot_append_paste (
      snapshot,
      g_value_get_boxed (&args[0]),
      g_value_get_uint64 (&args[1]));
}

static gboolean
lookup_snapshot_append_instr (const char    *lookup_name,
                              SnapshotInstr *out)
{
  SnapshotInstr instrs[] = {
    {
     "node",
     1,
     0,
     {
     GSK_TYPE_RENDER_NODE,
     },
     gtk_snapshot_append_node,
     snapshot_append_instr_node,
     },
    {
     "texture",
     2,
     0,
     {
     GDK_TYPE_TEXTURE,
     GRAPHENE_TYPE_RECT,
     },
     gtk_snapshot_append_texture,
     snapshot_append_instr_texture,
     },
    {
     "scaled-texture",
     3,
     0,
     {
     GDK_TYPE_TEXTURE,
     GSK_TYPE_SCALING_FILTER,
     GRAPHENE_TYPE_RECT,
     },
     gtk_snapshot_append_scaled_texture,
     snapshot_append_instr_scaled_texture,
     },
    {
     "color",
     2,
     0,
     {
     GDK_TYPE_RGBA,
     GRAPHENE_TYPE_RECT,
     },
     gtk_snapshot_append_color,
     snapshot_append_instr_color,
     },
    {
     "linear-gradient",
     5,
     2,
     {
     GRAPHENE_TYPE_RECT,
     GRAPHENE_TYPE_POINT,
     GRAPHENE_TYPE_POINT,
     G_TYPE_DOUBLE,
     GDK_TYPE_RGBA,
     },
     gtk_snapshot_append_linear_gradient,
     snapshot_append_instr_linear_gradient,
     },
    {
     "repeating-linear-gradient",
     5,
     2,
     {
     GRAPHENE_TYPE_RECT,
     GRAPHENE_TYPE_POINT,
     GRAPHENE_TYPE_POINT,
     G_TYPE_DOUBLE,
     GDK_TYPE_RGBA,
     },
     gtk_snapshot_append_repeating_linear_gradient,
     snapshot_append_instr_repeating_linear_gradient,
     },
    {
     "radial-gradient",
     8,
     2,
     {
     GRAPHENE_TYPE_RECT,
     GRAPHENE_TYPE_POINT,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     GDK_TYPE_RGBA,
     },
     gtk_snapshot_append_radial_gradient,
     snapshot_append_instr_radial_gradient,
     },
    {
     "repeating-radial-gradient",
     8,
     2,
     {
     GRAPHENE_TYPE_RECT,
     GRAPHENE_TYPE_POINT,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     GDK_TYPE_RGBA,
     },
     gtk_snapshot_append_repeating_radial_gradient,
     snapshot_append_instr_repeating_radial_gradient,
     },
    {
     "conic-gradient",
     5,
     2,
     {
     GRAPHENE_TYPE_RECT,
     GRAPHENE_TYPE_POINT,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     GDK_TYPE_RGBA,
     },
     gtk_snapshot_append_conic_gradient,
     snapshot_append_instr_conic_gradient,
     },
    {
     "border",
     13,
     0,
     {
     GRAPHENE_TYPE_RECT,
     GRAPHENE_TYPE_SIZE,
     GRAPHENE_TYPE_SIZE,
     GRAPHENE_TYPE_SIZE,
     GRAPHENE_TYPE_SIZE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     GDK_TYPE_RGBA,
     GDK_TYPE_RGBA,
     GDK_TYPE_RGBA,
     GDK_TYPE_RGBA,
     },
     gtk_snapshot_append_border,
     snapshot_append_instr_border,
     },
    {
     "inset-shadow",
     10,
     0,
     {
     GRAPHENE_TYPE_RECT,
     GRAPHENE_TYPE_SIZE,
     GRAPHENE_TYPE_SIZE,
     GRAPHENE_TYPE_SIZE,
     GRAPHENE_TYPE_SIZE,
     GDK_TYPE_RGBA,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     },
     gtk_snapshot_append_inset_shadow,
     snapshot_append_instr_inset_shadow,
     },
    {
     "outset-shadow",
     10,
     0,
     {
     GRAPHENE_TYPE_RECT,
     GRAPHENE_TYPE_SIZE,
     GRAPHENE_TYPE_SIZE,
     GRAPHENE_TYPE_SIZE,
     GRAPHENE_TYPE_SIZE,
     GDK_TYPE_RGBA,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     G_TYPE_DOUBLE,
     },
     gtk_snapshot_append_outset_shadow,
     snapshot_append_instr_outset_shadow,
     },
    {
     "layout",
     2,
     0,
     {
     PANGO_TYPE_LAYOUT,
     GDK_TYPE_RGBA,
     },
     gtk_snapshot_append_layout,
     snapshot_append_instr_layout,
     },
    {
     "fill",
     3,
     0,
     {
     GSK_TYPE_PATH,
     GSK_TYPE_FILL_RULE,
     GDK_TYPE_RGBA,
     },
     gtk_snapshot_append_fill,
     snapshot_append_instr_fill,
     },
    {
     "stroke",
     3,
     0,
     {
     GSK_TYPE_PATH,
     GSK_TYPE_STROKE,
     GDK_TYPE_RGBA,
     },
     gtk_snapshot_append_stroke,
     snapshot_append_instr_stroke,
     },
    {
     "paste",
     2,
     0,
     {
     GRAPHENE_TYPE_RECT,
     G_TYPE_UINT64,
     },
     gtk_snapshot_append_paste,
     snapshot_append_instr_paste,
     },
  };

  for (guint i = 0; i < G_N_ELEMENTS (instrs); i++)
    {
      if (g_strcmp0 (lookup_name, instrs[i].name) == 0)
        {
          *out = instrs[i];
          return TRUE;
        }
    }
  return FALSE;
}

static void
ensure_state_snapshot (StateData *state)
{
  if (state->snapshot != NULL)
    return;

  state->snapshot        = snapshot_data_new ();
  state->snapshot->calls = g_ptr_array_new_with_free_func (
      snapshot_call_data_unref);
}

/* ------------------------------ */
/* Widget Renderer Implementation */
/* ------------------------------ */

#define BGE_TYPE_WDGT_NOTIFIER (bge_wdgt_notifier_get_type ())
G_DECLARE_FINAL_TYPE (BgeWdgtNotifier, bge_wdgt_notifier, BGE, WDGT_NOTIFIER, GObject)

struct _BgeWdgtNotifier
{
  GObject parent_instance;

  double value;
};

G_DEFINE_FINAL_TYPE (BgeWdgtNotifier, bge_wdgt_notifier, G_TYPE_OBJECT);

enum
{
  NOTIFIER_PROP_0,

  NOTIFIER_PROP_VALUE,

  LAST_NOTIFIER_PROP
};
static GParamSpec *notifier_props[LAST_NOTIFIER_PROP] = { 0 };

static void
bge_wdgt_notifier_dispose (GObject *object)
{
  BgeWdgtNotifier *self = BGE_WDGT_NOTIFIER (object);

  (void) self;

  G_OBJECT_CLASS (bge_wdgt_notifier_parent_class)->dispose (object);
}

static void
bge_wdgt_notifier_get_property (GObject    *object,
                                guint       prop_id,
                                GValue     *value,
                                GParamSpec *pspec)
{
  BgeWdgtNotifier *self = BGE_WDGT_NOTIFIER (object);

  switch (prop_id)
    {
    case NOTIFIER_PROP_VALUE:
      g_value_set_double (value, self->value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
bge_wdgt_notifier_set_property (GObject      *object,
                                guint         prop_id,
                                const GValue *value,
                                GParamSpec   *pspec)
{
  BgeWdgtNotifier *self = BGE_WDGT_NOTIFIER (object);

  switch (prop_id)
    {
    case NOTIFIER_PROP_VALUE:
      self->value = g_value_get_double (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
bge_wdgt_notifier_class_init (BgeWdgtNotifierClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->set_property = bge_wdgt_notifier_set_property;
  object_class->get_property = bge_wdgt_notifier_get_property;
  object_class->dispose      = bge_wdgt_notifier_dispose;

  notifier_props[NOTIFIER_PROP_VALUE] =
      g_param_spec_double (
          "value",
          NULL, NULL,
          G_MININT, G_MAXINT, 0.0,
          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, LAST_PROP, notifier_props);
}

static void
bge_wdgt_notifier_init (BgeWdgtNotifier *self)
{
}

/* --- */

BGE_DEFINE_DATA (
    state_instance,
    StateInstance,
    {
      GHashTable *expressions;
      GHashTable *transitions;
      GPtrArray  *snapshot_deps;
    },
    BGE_RELEASE_DATA (expressions, g_hash_table_unref);
    BGE_RELEASE_DATA (transitions, g_hash_table_unref);
    BGE_RELEASE_DATA (snapshot_deps, g_ptr_array_unref))

struct _BgeWdgtRenderer
{
  GtkWidget parent_instance;

  BgeWdgtSpec *spec;
  char        *state;
  GObject     *reference;
  GtkWidget   *child;

  StateInstanceData *init_instance;
  GPtrArray         *init_watches;

  StateData         *active_state;
  StateInstanceData *active_instance;
  SnapshotData      *active_snapshot;

  StateData         *last_state;
  StateInstanceData *last_instance;
  GTimer            *since_last_state;
  guint              tick;
  gboolean           had_tick_since_state_switch;

  GHashTable *objects;
  GPtrArray  *children;
  GHashTable *allocations;
  GPtrArray  *nonchildren;
  GHashTable *state_instances;
  GPtrArray  *track_transitions;

  GPtrArray *bindings;
  GPtrArray *watches;

  BgeWdgtNotifier *reference_notifier;

  int              measure_minimum_width;
  int              measure_natural_width;
  int              measure_minimum_height;
  int              measure_natural_height;
  int              current_measure_for_size;
  BgeWdgtNotifier *measure_for_size_notifier;

  int              widget_width;
  int              widget_height;
  BgeWdgtNotifier *widget_width_notifier;
  BgeWdgtNotifier *widget_height_notifier;
};

G_DEFINE_FINAL_TYPE (BgeWdgtRenderer, bge_wdgt_renderer, GTK_TYPE_WIDGET);

enum
{
  RENDERER_PROP_0,

  RENDERER_PROP_SPEC,
  RENDERER_PROP_STATE,
  RENDERER_PROP_REFERENCE,
  RENDERER_PROP_CHILD,

  LAST_RENDERER_PROP
};
static GParamSpec *renderer_props[LAST_RENDERER_PROP] = { 0 };

BGE_DEFINE_DATA (
    watch_setter,
    WatchSetter,
    {
      BgeWdgtRenderer   *self;
      StateData         *state;
      StateInstanceData *instance;
      ValueData         *dest;
      ValueData         *src;
    },
    BGE_RELEASE_DATA (state, state_data_unref);
    BGE_RELEASE_DATA (instance, state_instance_data_unref);
    BGE_RELEASE_DATA (dest, value_data_unref);
    BGE_RELEASE_DATA (src, value_data_unref))

BGE_DEFINE_DATA (
    allocation,
    Allocation,
    {
      ValueData *width;
      ValueData *height;
      ValueData *transform;
    },
    BGE_RELEASE_DATA (width, value_data_unref);
    BGE_RELEASE_DATA (height, value_data_unref);
    BGE_RELEASE_DATA (transform, value_data_unref))

typedef struct
{
  double velocity;
  double est_duration;
  double cache_damping_ratio;
  double cache_mass;
  double cache_stiffness;
} SpringCache;

BGE_DEFINE_DATA (
    transition_instance,
    TransitionInstance,
    {
      double           value;
      double           elapsed;
      BgeWdgtNotifier *notifier;
      SpringCache      spring;
    },
    BGE_RELEASE_DATA (notifier, g_object_unref))

BGE_DEFINE_DATA (
    transition_closure,
    TransitionClosure,
    {
      StateData         *state;
      StateInstanceData *instance;
      ValueData         *value;
    },
    BGE_RELEASE_DATA (state, state_data_unref);
    BGE_RELEASE_DATA (instance, state_instance_data_unref);
    BGE_RELEASE_DATA (value, value_data_unref));

BGE_DEFINE_DATA (
    track_transition_closure,
    TrackTransitionClosure,
    {
      ValueData       *value;
      GTimer          *timer;
      BgeWdgtNotifier *notifier;
      double           elapsed;
      double           current;
      double           target;
      double           last;
      SpringCache      spring;
    },
    BGE_RELEASE_DATA (value, value_data_unref);
    BGE_RELEASE_DATA (notifier, g_object_unref);
    BGE_RELEASE_DATA (timer, g_timer_destroy));

static void
regenerate (BgeWdgtRenderer *self);

static void
apply_state (BgeWdgtRenderer *self);

static GtkExpression *
ensure_expressions (BgeWdgtRenderer   *self,
                    ValueData         *value,
                    StateData         *state,
                    StateInstanceData *instance);

static void
set_value (BgeWdgtRenderer   *self,
           StateData         *state,
           StateInstanceData *instance,
           ValueData         *dest,
           ValueData         *src,
           GPtrArray         *watches);

static int
resolve_value_int (BgeWdgtRenderer   *self,
                   ValueData         *value,
                   StateInstanceData *instance);

static double
resolve_value_double (BgeWdgtRenderer   *self,
                      ValueData         *value,
                      StateInstanceData *instance);

static gpointer
resolve_value_boxed_dup (BgeWdgtRenderer   *self,
                         ValueData         *value,
                         StateInstanceData *instance);

static gpointer
resolve_value_object_dup (BgeWdgtRenderer   *self,
                          ValueData         *value,
                          StateInstanceData *instance);

static void
discard_binding (gpointer ptr);

static void
discard_watch (gpointer ptr);

static void
prop_change_queue_draw (BgeWdgtRenderer *self);

static void
reset_setter (WatchSetterData *data);

static void
bge_wdgt_renderer_dispose (GObject *object)
{
  BgeWdgtRenderer *self = BGE_WDGT_RENDERER (object);

  if (self->tick > 0)
    {
      gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick);
      self->tick = 0;
    }

  g_clear_pointer (&self->spec, g_object_unref);
  g_clear_pointer (&self->state, g_free);
  g_clear_pointer (&self->reference, g_object_unref);
  g_clear_pointer (&self->child, gtk_widget_unparent);

  g_clear_pointer (&self->init_instance, state_instance_data_unref);
  g_clear_pointer (&self->init_watches, g_ptr_array_unref);

  g_clear_pointer (&self->active_state, state_data_unref);
  g_clear_pointer (&self->active_instance, state_instance_data_unref);
  g_clear_pointer (&self->active_snapshot, snapshot_data_unref);

  g_clear_pointer (&self->last_state, state_data_unref);
  g_clear_pointer (&self->last_instance, state_instance_data_unref);
  g_clear_pointer (&self->since_last_state, g_timer_destroy);

  g_clear_pointer (&self->objects, g_hash_table_unref);
  g_clear_pointer (&self->children, g_ptr_array_unref);
  g_clear_pointer (&self->allocations, g_hash_table_unref);
  g_clear_pointer (&self->nonchildren, g_ptr_array_unref);
  g_clear_pointer (&self->state_instances, g_hash_table_unref);
  g_clear_pointer (&self->track_transitions, g_ptr_array_unref);

  g_clear_pointer (&self->bindings, g_ptr_array_unref);
  g_clear_pointer (&self->watches, g_ptr_array_unref);

  g_clear_pointer (&self->reference_notifier, g_object_unref);
  g_clear_pointer (&self->measure_for_size_notifier, g_object_unref);
  g_clear_pointer (&self->widget_width_notifier, g_object_unref);
  g_clear_pointer (&self->widget_height_notifier, g_object_unref);

  G_OBJECT_CLASS (bge_wdgt_renderer_parent_class)->dispose (object);
}

static void
bge_wdgt_renderer_get_property (GObject    *object,
                                guint       prop_id,
                                GValue     *value,
                                GParamSpec *pspec)
{
  BgeWdgtRenderer *self = BGE_WDGT_RENDERER (object);

  switch (prop_id)
    {
    case RENDERER_PROP_SPEC:
      g_value_set_object (value, bge_wdgt_renderer_get_spec (self));
      break;
    case RENDERER_PROP_STATE:
      g_value_set_string (value, bge_wdgt_renderer_get_state (self));
      break;
    case RENDERER_PROP_REFERENCE:
      g_value_set_object (value, bge_wdgt_renderer_get_reference (self));
      break;
    case RENDERER_PROP_CHILD:
      g_value_set_object (value, bge_wdgt_renderer_get_child (self));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
bge_wdgt_renderer_set_property (GObject      *object,
                                guint         prop_id,
                                const GValue *value,
                                GParamSpec   *pspec)
{
  BgeWdgtRenderer *self = BGE_WDGT_RENDERER (object);

  switch (prop_id)
    {
    case RENDERER_PROP_SPEC:
      bge_wdgt_renderer_set_spec (self, g_value_get_object (value));
      break;
    case RENDERER_PROP_STATE:
      bge_wdgt_renderer_set_state (self, g_value_get_string (value));
      break;
    case RENDERER_PROP_REFERENCE:
      bge_wdgt_renderer_set_reference (self, g_value_get_object (value));
      break;
    case RENDERER_PROP_CHILD:
      bge_wdgt_renderer_set_child (self, g_value_get_object (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
bge_wdgt_renderer_measure (GtkWidget     *widget,
                           GtkOrientation orientation,
                           int            for_size,
                           int           *minimum,
                           int           *natural,
                           int           *minimum_baseline,
                           int           *natural_baseline)
{
  BgeWdgtRenderer *self = BGE_WDGT_RENDERER (widget);

  self->current_measure_for_size = for_size;
  /* This will update our measurement values in self */
  g_object_notify_by_pspec (
      G_OBJECT (self->measure_for_size_notifier),
      notifier_props[NOTIFIER_PROP_VALUE]);

  switch (orientation)
    {
    case GTK_ORIENTATION_HORIZONTAL:
      *minimum = self->measure_minimum_width;
      *natural = self->measure_natural_width;
      break;
    case GTK_ORIENTATION_VERTICAL:
      *minimum = self->measure_minimum_height;
      *natural = self->measure_natural_height;
      break;
    default:
      g_assert_not_reached ();
    }

  // if (self->child != NULL)
  //   gtk_widget_measure (
  //       GTK_WIDGET (self->child),
  //       orientation, for_size,
  //       minimum, natural,
  //       minimum_baseline, natural_baseline);
}

static void
bge_wdgt_renderer_size_allocate (GtkWidget *widget,
                                 int        width,
                                 int        height,
                                 int        baseline)
{
  BgeWdgtRenderer *self = BGE_WDGT_RENDERER (widget);

  self->widget_width  = width;
  self->widget_height = height;
  g_object_notify_by_pspec (
      G_OBJECT (self->widget_width_notifier),
      notifier_props[NOTIFIER_PROP_VALUE]);
  g_object_notify_by_pspec (
      G_OBJECT (self->widget_height_notifier),
      notifier_props[NOTIFIER_PROP_VALUE]);

  if (self->child != NULL &&
      gtk_widget_should_layout (self->child))
    gtk_widget_allocate (self->child, width, height, baseline, NULL);

  for (guint i = 0; i < self->children->len; i++)
    {
      GtkWidget      *child              = NULL;
      AllocationData *allocation         = NULL;
      int             alloc_width        = width;
      int             alloc_height       = height;
      g_autoptr (GskTransform) transform = NULL;

      child      = g_ptr_array_index (self->children, i);
      allocation = g_hash_table_lookup (self->allocations, child);

      alloc_width = resolve_value_int (
          self,
          allocation->width,
          self->active_instance != NULL
              ? self->active_instance
              : self->init_instance);
      alloc_height = resolve_value_int (
          self,
          allocation->height,
          self->active_instance != NULL
              ? self->active_instance
              : self->init_instance);
      transform = resolve_value_boxed_dup (
          self,
          allocation->transform,
          self->active_instance != NULL
              ? self->active_instance
              : self->init_instance);

      gtk_widget_allocate (
          child,
          alloc_width,
          alloc_height,
          baseline,
          g_steal_pointer (&transform));
    }
}

static void
bge_wdgt_renderer_snapshot (GtkWidget   *widget,
                            GtkSnapshot *snapshot)
{
  BgeWdgtRenderer *self  = BGE_WDGT_RENDERER (widget);
  GPtrArray       *calls = NULL;

  if (self->child != NULL)
    gtk_widget_snapshot_child (GTK_WIDGET (self), self->child, snapshot);

  if (self->active_state == NULL)
    return;

  if (self->active_snapshot != NULL)
    calls = self->active_snapshot->calls;

  if (calls != NULL)
    {
      for (guint i = 0; i < calls->len; i++)
        {
          SnapshotCallData *call = NULL;

          call = g_ptr_array_index (calls, i);
          switch (call->kind)
            {
            case BGE_WDGT_SNAPSHOT_INSTR_POP:
              gtk_snapshot_pop (snapshot);
              break;
            case BGE_WDGT_SNAPSHOT_INSTR_SAVE:
              gtk_snapshot_save (snapshot);
              break;
            case BGE_WDGT_SNAPSHOT_INSTR_RESTORE:
              gtk_snapshot_restore (snapshot);
              break;
            case BGE_WDGT_SNAPSHOT_INSTR_APPEND:
            case BGE_WDGT_SNAPSHOT_INSTR_PUSH:
            case BGE_WDGT_SNAPSHOT_INSTR_TRANSFORM:
              {
                GValue arg_values[ARGBUF_SIZE]  = { 0 };
                guint  n_arg_values             = 0;
                GValue rest_values[ARGBUF_SIZE] = { 0 };
                guint  n_rest_values            = 0;

                n_arg_values  = MIN (call->args->len, G_N_ELEMENTS (arg_values));
                n_rest_values = MIN (call->rest->len, G_N_ELEMENTS (rest_values));

                for (guint j = 0; j < n_arg_values; j++)
                  {
                    ValueData     *value      = NULL;
                    GtkExpression *expression = NULL;

                    value      = g_ptr_array_index (call->args, j);
                    expression = g_hash_table_lookup (self->active_instance->expressions, value);
                    gtk_expression_evaluate (expression, self, &arg_values[j]);
                  }
                for (guint j = 0; j < n_rest_values; j++)
                  {
                    ValueData     *value      = NULL;
                    GtkExpression *expression = NULL;

                    value      = g_ptr_array_index (call->rest, j);
                    expression = g_hash_table_lookup (self->active_instance->expressions, value);
                    gtk_expression_evaluate (expression, self, &rest_values[j]);
                  }

                call->func (snapshot, arg_values, rest_values, n_rest_values);

                for (guint j = 0; j < n_rest_values; j++)
                  {
                    g_value_unset (&rest_values[j]);
                  }
                for (guint j = 0; j < n_arg_values; j++)
                  {
                    g_value_unset (&arg_values[j]);
                  }
              }
              break;
            case BGE_WDGT_SNAPSHOT_INSTR_SNAPSHOT_CHILD:
              {
                GtkExpression *expression  = NULL;
                GValue         child_value = G_VALUE_INIT;
                GtkWidget     *child       = NULL;

                expression = g_hash_table_lookup (
                    self->active_instance->expressions,
                    call->child);
                gtk_expression_evaluate (expression, self, &child_value);

                child = g_value_get_object (&child_value);
                if (child != NULL)
                  {
                    if (gtk_widget_get_parent (child) == GTK_WIDGET (self))
                      gtk_widget_snapshot_child (GTK_WIDGET (self), child, snapshot);
                    else
                      g_critical ("Trying to snapshot a widget which is "
                                  "not a direct child of this spec! Skipping");
                  }

                g_value_unset (&child_value);
              }
              break;
            default:
              break;
            }
        }
    }

  // for (guint i = 0; i < self->children->len; i++)
  //   {
  //     GtkWidget *child = NULL;
  //
  //     child = g_ptr_array_index (self->children, i);
  //     gtk_widget_snapshot_child (GTK_WIDGET (self), child, snapshot);
  //   }
}

static void
bge_wdgt_renderer_class_init (BgeWdgtRendererClass *klass)
{
  GObjectClass   *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->set_property = bge_wdgt_renderer_set_property;
  object_class->get_property = bge_wdgt_renderer_get_property;
  object_class->dispose      = bge_wdgt_renderer_dispose;

  renderer_props[RENDERER_PROP_SPEC] =
      g_param_spec_object (
          "spec",
          NULL, NULL,
          BGE_TYPE_WDGT_SPEC,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  renderer_props[RENDERER_PROP_STATE] =
      g_param_spec_string (
          "state",
          NULL, NULL, NULL,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  renderer_props[RENDERER_PROP_REFERENCE] =
      g_param_spec_object (
          "reference",
          NULL, NULL,
          G_TYPE_OBJECT,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  renderer_props[RENDERER_PROP_CHILD] =
      g_param_spec_object (
          "child",
          NULL, NULL,
          GTK_TYPE_WIDGET,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, LAST_RENDERER_PROP, renderer_props);

  widget_class->measure       = bge_wdgt_renderer_measure;
  widget_class->size_allocate = bge_wdgt_renderer_size_allocate;
  widget_class->snapshot      = bge_wdgt_renderer_snapshot;

  g_type_ensure (BGE_TYPE_WDGT_NOTIFIER);
}

static gboolean
tick_cb (BgeWdgtRenderer *self,
         GdkFrameClock   *frame_clock,
         gpointer         user_data)
{
  GHashTableIter iter                           = { 0 };
  double         elapsed                        = 0.0;
  gboolean       finished_all_state_transitions = TRUE;

  self->had_tick_since_state_switch = TRUE;

  for (guint i = 0; i < self->track_transitions->len;)
    {
      TrackTransitionClosureData *data = NULL;

      data    = g_ptr_array_index (self->track_transitions, i);
      elapsed = g_timer_elapsed (data->timer, NULL);

      g_object_notify_by_pspec (
          G_OBJECT (data->notifier),
          notifier_props[NOTIFIER_PROP_VALUE]);

      if (elapsed <= data->spring.est_duration)
        i++;
      else
        g_ptr_array_remove_index (self->track_transitions, i);
    }

  if (self->spec == NULL ||
      self->active_state == NULL ||
      self->active_instance == NULL ||
      self->last_state == NULL ||
      self->last_instance == NULL)
    return G_SOURCE_CONTINUE;

  elapsed = g_timer_elapsed (self->since_last_state, NULL);

  g_hash_table_iter_init (&iter, self->active_state->transitions);
  for (;;)
    {
      ValueData              *value                    = NULL;
      TransitionData         *transition               = NULL;
      TransitionInstanceData *transition_instance      = NULL;
      TransitionInstanceData *init_transition_instance = NULL;

      if (!g_hash_table_iter_next (
              &iter,
              (gpointer *) &value,
              (gpointer *) &transition))
        break;

      transition_instance = g_hash_table_lookup (self->active_instance->transitions, value);
      if (transition_instance != NULL)
        g_object_notify_by_pspec (
            G_OBJECT (transition_instance->notifier),
            notifier_props[NOTIFIER_PROP_VALUE]);
      init_transition_instance = g_hash_table_lookup (self->init_instance->transitions, value);
      if (init_transition_instance != NULL)
        g_object_notify_by_pspec (
            G_OBJECT (init_transition_instance->notifier),
            notifier_props[NOTIFIER_PROP_VALUE]);

      switch (transition->kind)
        {
        case TRANSITION_EASE:
          if (elapsed <= transition->ease.seconds)
            finished_all_state_transitions = FALSE;
          break;
        case TRANSITION_SPRING:
          if (transition_instance->spring.est_duration < 0.0 ||
              elapsed <= transition_instance->spring.est_duration)
            finished_all_state_transitions = FALSE;
          break;
        default:
          g_assert_not_reached ();
        }
    }

  if (finished_all_state_transitions)
    {
      g_clear_pointer (&self->last_state, state_data_unref);
      g_clear_pointer (&self->last_instance, state_instance_data_unref);
    }

  return G_SOURCE_CONTINUE;
}

static void
bge_wdgt_renderer_init (BgeWdgtRenderer *self)
{
  self->init_watches = g_ptr_array_new_with_free_func (discard_watch);

  self->objects = g_hash_table_new_full (
      g_direct_hash, g_direct_equal,
      value_data_unref, g_object_unref);
  self->children = g_ptr_array_new_with_free_func (
      (GDestroyNotify) gtk_widget_unparent);
  self->allocations = g_hash_table_new_full (
      g_direct_hash, g_direct_equal,
      g_object_unref, allocation_data_unref);
  self->nonchildren = g_ptr_array_new_with_free_func (
      (GDestroyNotify) g_object_unref);
  self->state_instances = g_hash_table_new_full (
      g_direct_hash, g_direct_equal,
      state_data_unref, state_instance_data_unref);
  self->track_transitions = g_ptr_array_new_with_free_func (
      track_transition_closure_data_unref);

  self->bindings = g_ptr_array_new_with_free_func (discard_binding);
  self->watches  = g_ptr_array_new_with_free_func (discard_watch);

  self->since_last_state = g_timer_new ();

  self->reference_notifier        = g_object_new (BGE_TYPE_WDGT_NOTIFIER, NULL);
  self->measure_for_size_notifier = g_object_new (BGE_TYPE_WDGT_NOTIFIER, NULL);
  self->widget_width_notifier     = g_object_new (BGE_TYPE_WDGT_NOTIFIER, NULL);
  self->widget_height_notifier    = g_object_new (BGE_TYPE_WDGT_NOTIFIER, NULL);

  self->tick = gtk_widget_add_tick_callback (
      GTK_WIDGET (self),
      (GtkTickCallback) tick_cb,
      NULL, NULL);
}

BgeWdgtRenderer *
bge_wdgt_renderer_new (void)
{
  return g_object_new (BGE_TYPE_WDGT_RENDERER, NULL);
}

BgeWdgtSpec *
bge_wdgt_renderer_get_spec (BgeWdgtRenderer *self)
{
  g_return_val_if_fail (BGE_IS_WDGT_RENDERER (self), NULL);
  return self->spec;
}

const char *
bge_wdgt_renderer_get_state (BgeWdgtRenderer *self)
{
  g_return_val_if_fail (BGE_IS_WDGT_RENDERER (self), NULL);
  return self->state;
}

GObject *
bge_wdgt_renderer_get_reference (BgeWdgtRenderer *self)
{
  g_return_val_if_fail (BGE_IS_WDGT_RENDERER (self), NULL);
  return self->reference;
}

GtkWidget *
bge_wdgt_renderer_get_child (BgeWdgtRenderer *self)
{
  g_return_val_if_fail (BGE_IS_WDGT_RENDERER (self), NULL);
  return self->child;
}

void
bge_wdgt_renderer_set_spec (BgeWdgtRenderer *self,
                            BgeWdgtSpec     *spec)
{
  GBinding *name_binding = NULL;

  g_return_if_fail (BGE_IS_WDGT_RENDERER (self));
  g_return_if_fail (spec == NULL || BGE_IS_WDGT_SPEC (spec));

  if (spec == self->spec)
    return;

  g_clear_pointer (&self->spec, g_object_unref);
  if (spec != NULL)
    self->spec = g_object_ref (spec);

  regenerate (self);
  apply_state (self);

  if (spec != NULL)
    {
      name_binding = g_object_bind_property (spec, "name", self, "name", G_BINDING_SYNC_CREATE);
      g_ptr_array_add (self->bindings, g_object_ref (name_binding));
    }
  else
    gtk_widget_set_name (GTK_WIDGET (self), NULL);

  g_object_notify_by_pspec (G_OBJECT (self), renderer_props[RENDERER_PROP_SPEC]);
}

void
bge_wdgt_renderer_set_state (BgeWdgtRenderer *self,
                             const char      *state)
{
  g_return_if_fail (BGE_IS_WDGT_RENDERER (self));

  if (state == self->state || (state != NULL && self->state != NULL && g_strcmp0 (state, self->state) == 0))
    return;

  g_clear_pointer (&self->state, g_free);
  if (state != NULL)
    self->state = g_strdup (state);

  apply_state (self);

  g_object_notify_by_pspec (G_OBJECT (self), renderer_props[RENDERER_PROP_STATE]);
}

void
bge_wdgt_renderer_set_reference (BgeWdgtRenderer *self,
                                 GObject         *reference)
{
  g_return_if_fail (BGE_IS_WDGT_RENDERER (self));

  if (reference == self->reference)
    return;

  g_clear_pointer (&self->reference, g_object_unref);
  if (reference != NULL)
    self->reference = g_object_ref (reference);

  g_object_notify_by_pspec (G_OBJECT (self), renderer_props[RENDERER_PROP_REFERENCE]);
  g_object_notify_by_pspec (G_OBJECT (self->reference_notifier), notifier_props[NOTIFIER_PROP_VALUE]);
}

void
bge_wdgt_renderer_set_child (BgeWdgtRenderer *self,
                             GtkWidget       *child)
{
  g_return_if_fail (BGE_IS_WDGT_RENDERER (self));

  if (self->child == child)
    return;

  if (child != NULL)
    g_return_if_fail (gtk_widget_get_parent (child) == NULL);

  g_clear_pointer (&self->child, gtk_widget_unparent);
  self->child = child;

  if (child != NULL)
    gtk_widget_set_parent (child, GTK_WIDGET (self));

  g_object_notify_by_pspec (G_OBJECT (self), renderer_props[RENDERER_PROP_CHILD]);
}

void
bge_wdgt_renderer_set_state_take (BgeWdgtRenderer *self,
                                  char            *state)
{
  g_return_if_fail (BGE_IS_WDGT_RENDERER (self));

  if (state != NULL && self->state != NULL && g_strcmp0 (state, self->state) == 0)
    {
      g_free (state);
      return;
    }

  g_clear_pointer (&self->state, g_free);
  if (state != NULL)
    self->state = state;

  g_object_notify_by_pspec (G_OBJECT (self), renderer_props[RENDERER_PROP_STATE]);
}

gpointer
bge_wdgt_renderer_lookup_object (BgeWdgtRenderer *self,
                                 const char      *name)
{
  ValueData *value = NULL;

  g_return_val_if_fail (BGE_IS_WDGT_RENDERER (self), NULL);
  g_return_val_if_fail (name != NULL, NULL);

  if (self->spec == NULL ||
      self->active_instance == NULL)
    return NULL;

  value = g_hash_table_lookup (self->spec->values, name);
  if (value == NULL)
    return NULL;

  if (!g_type_is_a (value->type, G_TYPE_OBJECT))
    return NULL;

  return resolve_value_object_dup (self, value, self->active_instance);
}

static void
regenerate (BgeWdgtRenderer *self)
{
  BgeWdgtSpec *spec                    = self->spec;
  g_autoptr (GtkBuilder) dummy_builder = NULL;
  GHashTableIter state_iter            = { 0 };
  GHashTableIter setters_iter          = { 0 };

  g_clear_pointer (&self->init_instance, state_instance_data_unref);
  g_ptr_array_set_size (self->init_watches, 0);
  g_hash_table_remove_all (self->state_instances);
  g_hash_table_remove_all (self->objects);
  g_ptr_array_set_size (self->children, 0);
  g_hash_table_remove_all (self->allocations);
  g_ptr_array_set_size (self->nonchildren, 0);
  g_ptr_array_set_size (self->track_transitions, 0);

  g_clear_pointer (&self->last_state, state_data_unref);
  g_clear_pointer (&self->last_instance, state_instance_data_unref);
  g_clear_pointer (&self->active_state, state_data_unref);
  g_clear_pointer (&self->active_instance, state_instance_data_unref);
  g_clear_pointer (&self->active_snapshot, snapshot_data_unref);
  g_ptr_array_set_size (self->bindings, 0);
  g_ptr_array_set_size (self->watches, 0);

  if (self->spec == NULL)
    return;

  for (guint i = 0; i < spec->nonchildren->len; i++)
    {
      ValueData *value           = NULL;
      g_autoptr (GObject) object = NULL;

      value = g_ptr_array_index (spec->nonchildren, i);
      g_assert (value->kind == VALUE_OBJECT);

      object = g_object_new (value->type, NULL);
      if (g_type_is_a (value->type, G_TYPE_INITIALLY_UNOWNED))
        g_object_ref_sink (object);

      g_hash_table_replace (self->objects,
                            value_data_ref (value),
                            g_object_ref (object));
      g_ptr_array_add (self->nonchildren, g_object_ref (object));
    }

  dummy_builder = gtk_builder_new ();
  for (guint i = 0; i < spec->children->len; i++)
    {
      ValueData *value                      = NULL;
      GtkWidget *widget                     = NULL;
      g_autoptr (AllocationData) allocation = NULL;

      value = g_ptr_array_index (spec->children, i);
      g_assert (value->kind == VALUE_CHILD);

      widget = g_object_new (
          value->type,
          "name", value->name,
          NULL);
      if (value->child.css_classes != NULL)
        {
          for (guint j = 0; j < value->child.css_classes->len; j++)
            {
              const char *class = NULL;

              class = g_ptr_array_index (value->child.css_classes, j);
              gtk_widget_add_css_class (widget, class);
            }
        }

      if (value->child.parent_widget != NULL)
        {
          GtkWidget *parent_widget = NULL;

          parent_widget = g_hash_table_lookup (
              self->objects,
              value->child.parent_widget);
          g_assert (parent_widget != NULL);

          GTK_BUILDABLE_GET_IFACE (parent_widget)
              ->add_child (
                  GTK_BUILDABLE (parent_widget),
                  dummy_builder,
                  G_OBJECT (widget),
                  value->child.builder_type);
        }
      else
        {
          gtk_widget_set_parent (widget, GTK_WIDGET (self));
          g_ptr_array_add (self->children, g_object_ref (widget));
        }

      g_hash_table_replace (self->objects,
                            value_data_ref (value),
                            g_object_ref (widget));

      allocation = allocation_data_new ();
      g_hash_table_replace (self->allocations,
                            g_object_ref (widget),
                            allocation_data_ref (allocation));
    }

  g_hash_table_iter_init (&state_iter, spec->states);
  for (;;)
    {
      char      *state_name                  = NULL;
      StateData *state                       = NULL;
      g_autoptr (StateInstanceData) instance = NULL;

      if (!g_hash_table_iter_next (
              &state_iter,
              (gpointer *) &state_name,
              (gpointer *) &state))
        break;

      instance              = state_instance_data_new ();
      instance->expressions = g_hash_table_new_full (
          g_direct_hash, g_direct_equal,
          value_data_unref, (GDestroyNotify) gtk_expression_unref);
      instance->transitions = g_hash_table_new_full (
          g_direct_hash, g_direct_equal,
          value_data_unref, transition_instance_data_unref);
      instance->snapshot_deps = g_ptr_array_new_with_free_func (
          (GDestroyNotify) gtk_expression_unref);

      g_hash_table_replace (self->state_instances,
                            state_data_ref (state),
                            state_instance_data_ref (instance));

      if (state == spec->init_state)
        self->init_instance = state_instance_data_ref (instance);
    }

  /* Separate to ensure `self->init_instance` has been set */

  g_hash_table_iter_init (&state_iter, spec->states);
  for (;;)
    {
      GHashTableIter     value_iter = { 0 };
      char              *state_name = NULL;
      StateData         *state      = NULL;
      StateInstanceData *instance   = NULL;

      if (!g_hash_table_iter_next (
              &state_iter,
              (gpointer *) &state_name,
              (gpointer *) &state))
        break;

      instance = g_hash_table_lookup (self->state_instances, state);
      g_assert (instance != NULL);

      g_hash_table_iter_init (&value_iter, spec->values);
      for (;;)
        {
          char      *value_name                = NULL;
          ValueData *value                     = NULL;
          g_autoptr (GtkExpression) expression = NULL;

          if (!g_hash_table_iter_next (
                  &value_iter,
                  (gpointer *) &value_name,
                  (gpointer *) &value))
            break;

          expression = ensure_expressions (self, value, state, instance);
        }

      for (guint i = 0; i < spec->anon_values->len; i++)
        {
          ValueData *value                     = NULL;
          g_autoptr (GtkExpression) expression = NULL;

          value      = g_ptr_array_index (spec->anon_values, i);
          expression = ensure_expressions (self, value, state, instance);
        }
    }

  g_hash_table_iter_init (&state_iter, spec->states);
  for (;;)
    {
      char              *state_name     = NULL;
      StateData         *state          = NULL;
      StateInstanceData *instance       = NULL;
      StateData         *snapshot_state = NULL;

      if (!g_hash_table_iter_next (
              &state_iter,
              (gpointer *) &state_name,
              (gpointer *) &state))
        break;

      instance = g_hash_table_lookup (self->state_instances, state);
      g_assert (instance != NULL);

      if (state->snapshot != NULL)
        snapshot_state = state;
      else
        /* If this state doesn't have snapshot instructions specified, fallback
           on using the init state */
        snapshot_state = self->spec->init_state;

      for (guint i = 0; i < snapshot_state->snapshot->calls->len; i++)
        {
          SnapshotCallData *call = NULL;

          call = g_ptr_array_index (snapshot_state->snapshot->calls, i);
          if (call->args == NULL)
            /* pop, save, restore... */
            continue;

          for (guint j = 0; j < call->args->len; j++)
            {
              ValueData     *arg        = NULL;
              gboolean       found      = FALSE;
              GtkExpression *expression = NULL;

              arg        = g_ptr_array_index (call->args, j);
              expression = g_hash_table_lookup (instance->expressions, arg);
              g_assert (expression != NULL);

              for (guint k = 0; k < instance->snapshot_deps->len; k++)
                {
                  GtkExpression *other = NULL;

                  other = g_ptr_array_index (instance->snapshot_deps, k);
                  if (expression == other)
                    {
                      found = TRUE;
                      break;
                    }
                }
              if (found)
                /* Ensure we don't duplicate deps */
                continue;

              g_ptr_array_add (instance->snapshot_deps,
                               gtk_expression_ref (expression));
            }
        }
    }

  g_hash_table_iter_init (
      &setters_iter,
      self->spec->init_state->setters);
  for (;;)
    {
      ValueData *dest = NULL;
      ValueData *src  = NULL;

      if (!g_hash_table_iter_next (
              &setters_iter,
              (gpointer *) &dest,
              (gpointer *) &src))
        break;

      set_value (
          self,
          self->spec->init_state,
          self->init_instance,
          dest,
          src,
          self->init_watches);
    }

  gtk_widget_queue_allocate (GTK_WIDGET (self));
  gtk_widget_queue_draw (GTK_WIDGET (self));
}

static void
apply_state (BgeWdgtRenderer *self)
{
  BgeWdgtSpec       *spec     = self->spec;
  StateData         *state    = NULL;
  StateInstanceData *instance = NULL;
  GHashTableIter     iter     = { 0 };

  if (self->had_tick_since_state_switch)
    {
      g_clear_pointer (&self->last_state, state_data_unref);
      g_clear_pointer (&self->last_instance, state_instance_data_unref);
      if (self->active_state != NULL &&
          self->active_instance != NULL)
        {
          self->last_state    = g_steal_pointer (&self->active_state);
          self->last_instance = g_steal_pointer (&self->active_instance);
        }
      g_timer_start (self->since_last_state);
      self->had_tick_since_state_switch = FALSE;
    }

  g_clear_pointer (&self->active_state, state_data_unref);
  g_clear_pointer (&self->active_instance, state_instance_data_unref);
  g_clear_pointer (&self->active_snapshot, snapshot_data_unref);

  g_ptr_array_set_size (self->bindings, 0);
  g_ptr_array_set_size (self->watches, 0);
  gtk_widget_queue_draw (GTK_WIDGET (self));

  if (self->spec == NULL ||
      self->state == NULL)
    return;

  state = g_hash_table_lookup (spec->states, self->state);
  if (state == NULL)
    {
      g_critical ("state \"%s\" doesn't exist on spec \"%s\"",
                  self->state, spec->name);
      return;
    }
  instance = g_hash_table_lookup (self->state_instances, state);
  g_assert (instance != NULL);

  self->active_state    = state_data_ref (state);
  self->active_instance = state_instance_data_ref (instance);

  if (state->snapshot != NULL)
    self->active_snapshot = snapshot_data_ref (state->snapshot);
  else if (self->spec->init_state->snapshot != NULL)
    self->active_snapshot = snapshot_data_ref (self->spec->init_state->snapshot);

  g_hash_table_iter_init (&iter, state->setters);
  for (;;)
    {
      ValueData              *dest                = NULL;
      ValueData              *src                 = NULL;
      TransitionInstanceData *transition_instance = NULL;

      if (!g_hash_table_iter_next (
              &iter,
              (gpointer *) &dest,
              (gpointer *) &src))
        break;

      set_value (
          self,
          state,
          instance,
          dest,
          src,
          self->watches);

      transition_instance = g_hash_table_lookup (instance->transitions, dest);
      if (transition_instance != NULL)
        transition_instance->spring.est_duration = -1.0;
    }

  g_hash_table_iter_init (&iter, spec->init_state->setters);
  for (;;)
    {
      ValueData *dest = NULL;
      ValueData *src  = NULL;

      if (!g_hash_table_iter_next (
              &iter,
              (gpointer *) &dest,
              (gpointer *) &src))
        break;

      set_value (
          self,
          state,
          instance,
          dest,
          src,
          self->watches);
    }

  for (guint i = 0; i < instance->snapshot_deps->len; i++)
    {
      GtkExpression      *expression = NULL;
      GtkExpressionWatch *watch      = NULL;

      expression = g_ptr_array_index (instance->snapshot_deps, i);
      watch      = gtk_expression_watch (
          expression,
          self,
          (GtkExpressionNotify) prop_change_queue_draw,
          self, NULL);
      g_ptr_array_add (self->watches, watch);
    }

  gtk_widget_queue_allocate (GTK_WIDGET (self));
  gtk_widget_queue_draw (GTK_WIDGET (self));
}

static graphene_point_t *
expression_create_point (gpointer object,
                         double   x,
                         double   y,
                         gpointer user_data)
{
  graphene_point_t *point = NULL;

  point    = graphene_point_alloc ();
  point->x = x;
  point->y = y;

  return point;
}

static graphene_size_t *
expression_create_size (gpointer object,
                        double   width,
                        double   height,
                        gpointer user_data)
{
  graphene_size_t *size = NULL;

  size         = graphene_size_alloc ();
  size->width  = width;
  size->height = height;

  return size;
}

static graphene_point3d_t *
expression_create_point3d (gpointer object,
                           double   x,
                           double   y,
                           double   z,
                           gpointer user_data)
{
  graphene_point3d_t *point3d = NULL;

  point3d    = graphene_point3d_alloc ();
  point3d->x = x;
  point3d->y = y;
  point3d->z = z;

  return point3d;
}

static graphene_rect_t *
expression_create_rect (gpointer object,
                        double   x,
                        double   y,
                        double   width,
                        double   height,
                        gpointer user_data)
{
  graphene_rect_t *rect = NULL;

  rect              = graphene_rect_alloc ();
  rect->origin.x    = x;
  rect->origin.y    = y;
  rect->size.width  = width;
  rect->size.height = height;

  return rect;
}

static void
expression_coerce_type (gpointer      this,
                        GValue       *return_value,
                        guint         n_param_values,
                        const GValue *param_values,
                        gpointer      dest_type_ptr)
{
  GType dest_type = GPOINTER_TO_SIZE (dest_type_ptr);
  coerce_value (&param_values[0], dest_type, return_value);
}

static int
expression_get_measure_for_size (BgeWdgtRenderer *this,
                                 double           notify,
                                 gpointer         user_data)
{
  return this->current_measure_for_size;
}

static double
expression_get_widget_width (BgeWdgtRenderer *this,
                             double           notify,
                             gpointer         user_data)
{
  return (double) this->widget_width;
}

static double
expression_get_widget_height (BgeWdgtRenderer *this,
                              double           notify,
                              gpointer         user_data)
{
  return (double) this->widget_height;
}

static gpointer
expression_get_reference_object (BgeWdgtRenderer *this,
                                 double           notify,
                                 gpointer         user_data)
{
  return this->reference != NULL
             ? g_object_ref (this->reference)
             : NULL;
}

static double
expression_adjust_state_transition (BgeWdgtRenderer       *this,
                                    double                 in,
                                    double                 notifier,
                                    TransitionClosureData *data)
{
  gboolean                result                   = FALSE;
  TransitionData         *transition               = NULL;
  TransitionData         *last_transition          = NULL;
  TransitionInstanceData *transition_instance      = NULL;
  TransitionInstanceData *last_transition_instance = NULL;
  ValueData              *last_in_value            = NULL;
  double                  last_in                  = 0.0;
  double                  elapsed                  = 0.0;
  double                  progress                 = 0.0;
  double                  interpolated_number      = 0.0;

  g_assert (data->value->type == G_TYPE_DOUBLE);

  if (this->active_state == NULL ||
      this->active_instance == NULL ||
      this->last_state == NULL ||
      this->last_instance == NULL)
    return in;

  if (data->instance != this->active_instance)
    {
      GtkExpression *corrected_in_expression = NULL;
      GValue         corrected_in_resolved   = G_VALUE_INIT;

      corrected_in_expression = g_hash_table_lookup (
          this->active_instance->expressions, data->value);
      g_assert (corrected_in_expression != NULL);
      result = gtk_expression_evaluate (
          corrected_in_expression,
          this,
          &corrected_in_resolved);
      if (result)
        in = g_value_get_double (&corrected_in_resolved);
      g_value_unset (&corrected_in_resolved);
    }

  transition = g_hash_table_lookup (
      this->active_state->transitions, data->value);
  if (transition == NULL)
    return in;
  last_transition = g_hash_table_lookup (
      this->last_state->transitions, data->value);

  transition_instance = g_hash_table_lookup (
      this->active_instance->transitions, data->value);
  g_assert (transition_instance != NULL);
  last_transition_instance = g_hash_table_lookup (
      this->last_instance->transitions, data->value);

  if (last_transition != NULL &&
      last_transition_instance != NULL &&
      last_transition->kind == TRANSITION_EASE &&
      last_transition_instance->elapsed < last_transition->ease.seconds)
    last_in = last_transition_instance->value;
  else
    {
      last_in_value = g_hash_table_lookup (this->last_state->setters, data->value);
      if (last_in_value == NULL)
        return in;
      last_in = resolve_value_double (this, last_in_value, this->last_instance);
    }

  elapsed                      = g_timer_elapsed (this->since_last_state, NULL);
  transition_instance->elapsed = elapsed;

  switch (transition->kind)
    {
    case TRANSITION_SPRING:
      {
        double damping_ratio = 0.0;
        double mass          = 0.0;
        double stiffness     = 0.0;
        double damping       = 0.0;

        damping_ratio = resolve_value_double (
            this,
            transition->spring.damping_ratio,
            this->active_instance);
        mass = resolve_value_double (
            this,
            transition->spring.mass,
            this->active_instance);
        stiffness = resolve_value_double (
            this,
            transition->spring.stiffness,
            this->active_instance);

        damping = damping_ratio *
                  (/* critical damping */
                   2 * sqrt (mass * stiffness));

        if (transition_instance->spring.est_duration < 0.0 ||
            damping_ratio != transition_instance->spring.cache_damping_ratio ||
            mass != transition_instance->spring.cache_mass ||
            stiffness != transition_instance->spring.cache_stiffness)
          {
            if (last_transition != NULL &&
                last_transition->kind == TRANSITION_SPRING)
              transition_instance->spring.velocity = last_transition_instance->spring.velocity;
            else
              transition_instance->spring.velocity = 0.0;

            transition_instance->spring.est_duration = spring_calculate_duration (
                damping,
                mass,
                stiffness,
                last_in,
                in,
                FALSE);

            transition_instance->spring.cache_damping_ratio = damping_ratio;
            transition_instance->spring.cache_mass          = mass;
            transition_instance->spring.cache_stiffness     = stiffness;
          }

        if (elapsed >= transition_instance->spring.est_duration)
          interpolated_number = in;
        else
          interpolated_number = spring_oscillate (
              damping,
              mass,
              stiffness,
              last_in,
              in,
              elapsed,
              &transition_instance->spring.velocity);
      }
      break;
    case TRANSITION_EASE:
      {
        if (elapsed >= transition->ease.seconds)
          interpolated_number = in;
        else
          {
            progress            = bge_easing_ease (transition->ease.easing,
                                                   elapsed / transition->ease.seconds);
            interpolated_number = last_in + progress * (in - last_in);
          }
      }
      break;
    default:
      g_assert_not_reached ();
    }

  transition_instance->value = interpolated_number;
  return interpolated_number;
}

static double
expression_adjust_track_transition (BgeWdgtRenderer            *this,
                                    double                      in,
                                    double                      damping_ratio,
                                    double                      mass,
                                    double                      stiffness,
                                    double                      notifier,
                                    TrackTransitionClosureData *data)
{
  guint    idx        = 0;
  gboolean registered = FALSE;
  double   damping    = 0.0;

  g_assert (data->value->type == G_TYPE_DOUBLE);

  registered = g_ptr_array_find (this->track_transitions, data, &idx);
  if (!registered)
    g_ptr_array_add (this->track_transitions,
                     track_transition_closure_data_ref (data));

  damping = damping_ratio *
            (/* critical damping */
             2 * sqrt (mass * stiffness));

  if (!registered ||
      data->target != in ||
      damping_ratio != data->spring.cache_damping_ratio ||
      mass != data->spring.cache_mass ||
      stiffness != data->spring.cache_stiffness)
    {
      data->target  = in;
      data->last    = data->current;
      data->elapsed = 0.0;
      g_timer_start (data->timer);

      data->spring.est_duration = spring_calculate_duration (
          damping,
          mass,
          stiffness,
          data->last,
          data->target,
          FALSE);

      data->spring.cache_damping_ratio = damping_ratio;
      data->spring.cache_mass          = mass;
      data->spring.cache_stiffness     = stiffness;
    }
  else
    data->elapsed = g_timer_elapsed (data->timer, NULL);

  if (data->elapsed >= data->spring.est_duration)
    {
      data->current = data->target;
      data->last    = data->target;
    }
  else
    data->current = spring_oscillate (
        damping,
        mass,
        stiffness,
        data->last,
        data->target,
        data->elapsed,
        &data->spring.velocity);

  return data->current;
}

static GskTransform *
expression_perform_transform (gpointer          this,
                              guint             n_param_values,
                              const GValue     *param_values,
                              TransformCallFunc func)
{
  return func (g_value_dup_boxed (&param_values[0]), param_values + 1);
}

static GtkExpression *
ensure_expressions (BgeWdgtRenderer   *self,
                    ValueData         *value,
                    StateData         *state,
                    StateInstanceData *instance)
{
  GtkExpression *cached                = NULL;
  g_autoptr (GtkExpression) expression = NULL;
  TransitionData *transition           = NULL;

  cached = g_hash_table_lookup (instance->expressions, value);
  if (cached != NULL)
    return gtk_expression_ref (cached);

  switch (value->kind)
    {
    case VALUE_CONSTANT:
      expression = gtk_constant_expression_new_for_value (&value->constant);
      break;
    case VALUE_OBJECT:
    case VALUE_CHILD:
      {
        gpointer object = NULL;

        object = g_hash_table_lookup (self->objects, value);
        g_assert (object != NULL);
        expression = gtk_constant_expression_new (value->type, object);
      }
      break;
    case VALUE_REFERENCE_OBJECT:
      {
        GtkExpression *notifier_constant = NULL;
        GtkExpression *notify_expression = NULL;

        notifier_constant = gtk_constant_expression_new (
            BGE_TYPE_WDGT_NOTIFIER, self->reference_notifier);
        notify_expression = gtk_property_expression_new_for_pspec (
            notifier_constant, notifier_props[NOTIFIER_PROP_VALUE]);

        expression = gtk_cclosure_expression_new (
            value->type,
            bge_marshal_OBJECT__DOUBLE,
            1, (GtkExpression *[]){ notify_expression },
            G_CALLBACK (expression_get_reference_object),
            GSIZE_TO_POINTER (value->type),
            NULL);
      }
      break;
    case VALUE_VARIABLE:
    case VALUE_ALLOCATION_WIDTH:
    case VALUE_ALLOCATION_HEIGHT:
    case VALUE_ALLOCATION_TRANSFORM:
    case VALUE_MEASURE_MINIMUM_WIDTH:
    case VALUE_MEASURE_NATURAL_WIDTH:
    case VALUE_MEASURE_MINIMUM_HEIGHT:
    case VALUE_MEASURE_NATURAL_HEIGHT:
      {
        ValueData *holds = NULL;

        holds = g_hash_table_lookup (state->setters, value);
        if (holds == NULL)
          holds = g_hash_table_lookup (self->spec->init_state->setters, value);

        if (holds != NULL)
          expression = ensure_expressions (self, holds, state, instance);
        else
          {
            GValue empty_value = G_VALUE_INIT;

            g_value_init (&empty_value, value->type);
            expression = gtk_constant_expression_new_for_value (&empty_value);
            g_value_unset (&empty_value);
          }
      }
      break;
    case VALUE_MEASURE_FOR_SIZE:
      {
        GtkExpression *notifier_constant = NULL;
        GtkExpression *notify_expression = NULL;

        notifier_constant = gtk_constant_expression_new (
            BGE_TYPE_WDGT_NOTIFIER, self->measure_for_size_notifier);
        notify_expression = gtk_property_expression_new_for_pspec (
            notifier_constant, notifier_props[NOTIFIER_PROP_VALUE]);

        expression = gtk_cclosure_expression_new (
            value->type,
            bge_marshal_INT__DOUBLE,
            1, (GtkExpression *[]){ notify_expression },
            G_CALLBACK (expression_get_measure_for_size),
            GSIZE_TO_POINTER (value->type),
            NULL);
      }
      break;
    case VALUE_WIDGET_WIDTH:
      {
        GtkExpression *notifier_constant = NULL;
        GtkExpression *notify_expression = NULL;

        notifier_constant = gtk_constant_expression_new (
            BGE_TYPE_WDGT_NOTIFIER, self->widget_width_notifier);
        notify_expression = gtk_property_expression_new_for_pspec (
            notifier_constant, notifier_props[NOTIFIER_PROP_VALUE]);

        expression = gtk_cclosure_expression_new (
            value->type,
            bge_marshal_DOUBLE__DOUBLE,
            1, (GtkExpression *[]){ notify_expression },
            G_CALLBACK (expression_get_widget_width),
            GSIZE_TO_POINTER (value->type),
            NULL);
      }
      break;
    case VALUE_WIDGET_HEIGHT:
      {
        GtkExpression *notifier_constant = NULL;
        GtkExpression *notify_expression = NULL;

        notifier_constant = gtk_constant_expression_new (
            BGE_TYPE_WDGT_NOTIFIER, self->widget_height_notifier);
        notify_expression = gtk_property_expression_new_for_pspec (
            notifier_constant, notifier_props[NOTIFIER_PROP_VALUE]);

        expression = gtk_cclosure_expression_new (
            value->type,
            bge_marshal_DOUBLE__DOUBLE,
            1, (GtkExpression *[]){ notify_expression },
            G_CALLBACK (expression_get_widget_height),
            GSIZE_TO_POINTER (value->type),
            NULL);
      }
      break;
    case VALUE_COERCION:
      {
        expression = ensure_expressions (
            self, value->coercion.value, state, instance);
        expression = gtk_cclosure_expression_new (
            value->type,
            _marshal_DIRECT__ARGS_DIRECT,
            1, (GtkExpression *[]){ expression },
            G_CALLBACK (expression_coerce_type),
            GSIZE_TO_POINTER (value->type),
            NULL);
      }
      break;
    case VALUE_TRACK_TRANSITION:
      {
        g_autoptr (BgeWdgtNotifier) notifier_object         = NULL;
        GtkExpression *notifier_constant                    = NULL;
        GtkExpression *notify_expression                    = NULL;
        GtkExpression *damping_ratio_expression             = NULL;
        GtkExpression *mass_expression                      = NULL;
        GtkExpression *stiffness_expression                 = NULL;
        g_autoptr (TrackTransitionClosureData) closure_data = NULL;

        notifier_object   = g_object_new (BGE_TYPE_WDGT_NOTIFIER, NULL);
        notifier_constant = gtk_constant_expression_new (
            BGE_TYPE_WDGT_NOTIFIER, notifier_object);
        notify_expression = gtk_property_expression_new_for_pspec (
            notifier_constant, notifier_props[NOTIFIER_PROP_VALUE]);

        damping_ratio_expression = ensure_expressions (
            self, value->track_transition.spring.damping_ratio, state, instance);
        mass_expression = ensure_expressions (
            self, value->track_transition.spring.mass, state, instance);
        stiffness_expression = ensure_expressions (
            self, value->track_transition.spring.stiffness, state, instance);

        closure_data           = track_transition_closure_data_new ();
        closure_data->value    = value_data_ref (value);
        closure_data->timer    = g_timer_new ();
        closure_data->notifier = g_object_ref (notifier_object);

        expression = ensure_expressions (
            self, value->track_transition.src, state, instance);
        expression = gtk_cclosure_expression_new (
            value->type,
            bge_marshal_DOUBLE__DOUBLE_DOUBLE_DOUBLE_DOUBLE_DOUBLE,
            5, (GtkExpression *[]){ expression, damping_ratio_expression, mass_expression, stiffness_expression, notify_expression },
            G_CALLBACK (expression_adjust_track_transition),
            track_transition_closure_data_ref (closure_data),
            track_transition_closure_data_unref_closure);
      }
      break;
    case VALUE_COMPONENT:
      {
        g_autoptr (GPtrArray) params = NULL;
        GClosureMarshal marshal      = NULL;
        GCallback       callback     = NULL;

        params = g_ptr_array_new ();

        for (guint i = 0; i < value->component->len; i++)
          {
            ValueData *member                     = NULL;
            g_autoptr (GtkExpression) member_expr = NULL;

            member      = g_ptr_array_index (value->component, i);
            member_expr = ensure_expressions (self, member, state, instance);
            g_ptr_array_add (params, g_steal_pointer (&member_expr));
          }

        if (value->type == GRAPHENE_TYPE_POINT)
          {
            marshal  = bge_marshal_BOXED__DOUBLE_DOUBLE;
            callback = G_CALLBACK (expression_create_point);
          }
        else if (value->type == GRAPHENE_TYPE_SIZE)
          {
            marshal  = bge_marshal_BOXED__DOUBLE_DOUBLE;
            callback = G_CALLBACK (expression_create_size);
          }
        else if (value->type == GRAPHENE_TYPE_POINT3D)
          {
            marshal  = bge_marshal_BOXED__DOUBLE_DOUBLE_DOUBLE;
            callback = G_CALLBACK (expression_create_point3d);
          }
        else if (value->type == GRAPHENE_TYPE_RECT)
          {
            marshal  = bge_marshal_BOXED__DOUBLE_DOUBLE_DOUBLE_DOUBLE;
            callback = G_CALLBACK (expression_create_rect);
          }
        else
          g_assert (FALSE);

        expression = gtk_cclosure_expression_new (
            value->type,
            marshal,
            params->len,
            (GtkExpression **) params->pdata,
            callback,
            self,
            NULL);
      }
      break;
    case VALUE_TRANSFORM:
      {
        g_autoptr (GPtrArray) params        = NULL;
        g_autoptr (GtkExpression) next_expr = NULL;

        params = g_ptr_array_new ();

        next_expr = ensure_expressions (self, value->transform.next, state, instance);
        g_ptr_array_add (params, g_steal_pointer (&next_expr));

        for (guint i = 0; i < value->transform.args->len; i++)
          {
            ValueData *arg                     = NULL;
            g_autoptr (GtkExpression) arg_expr = NULL;

            arg      = g_ptr_array_index (value->transform.args, i);
            arg_expr = ensure_expressions (self, arg, state, instance);
            g_ptr_array_add (params, g_steal_pointer (&arg_expr));
          }

        expression = gtk_cclosure_expression_new (
            value->type,
            _marshal_BOXED__ARGS_DIRECT,
            params->len,
            (GtkExpression **) params->pdata,
            G_CALLBACK (expression_perform_transform),
            value->transform.func,
            NULL);
      }
      break;
    case VALUE_CLOSURE:
      {
        g_autoptr (GPtrArray) params = NULL;

        params = g_ptr_array_new ();

        for (guint i = 0; i < value->closure.args->len; i++)
          {
            ValueData *arg                     = NULL;
            g_autoptr (GtkExpression) arg_expr = NULL;

            arg      = g_ptr_array_index (value->closure.args, i);
            arg_expr = ensure_expressions (self, arg, state, instance);
            g_ptr_array_add (params, g_steal_pointer (&arg_expr));
          }

        expression = gtk_cclosure_expression_new (
            value->type,
            value->closure.marshal,
            params->len,
            (GtkExpression **) params->pdata,
            value->closure.func,
            value->closure.user_data,
            NULL);
      }
      break;
    case VALUE_PROPERTY:
      {
        g_autoptr (GtkExpression) object_expression = NULL;

        /* Mark subproperty values as dependencies as well */
        object_expression = ensure_expressions (
            self, value->property.object, state, instance);
        expression = gtk_property_expression_new (
            value->property.object->type,
            g_steal_pointer (&object_expression),
            value->property.prop_name);
      }
      break;
    case VALUE_SPECIAL:
      g_assert (FALSE);
      break;
    default:
      g_assert_not_reached ();
      break;
    }

  transition = g_hash_table_lookup (state->transitions, value);
  if (value->type == G_TYPE_DOUBLE &&
      transition != NULL)
    {
      g_autoptr (BgeWdgtNotifier) notifier_object      = NULL;
      g_autoptr (TransitionInstanceData) instance_data = NULL;
      GtkExpression *notifier_constant                 = NULL;
      GtkExpression *notify_expression                 = NULL;
      g_autoptr (TransitionClosureData) closure_data   = NULL;

      notifier_object = g_object_new (BGE_TYPE_WDGT_NOTIFIER, NULL);

      instance_data = transition_instance_data_new ();
      switch (transition->kind)
        {
        case TRANSITION_EASE:
          instance_data->elapsed = transition->ease.seconds;
          break;
        case TRANSITION_SPRING:
          instance_data->spring.est_duration = -1.0;
          break;
        default:
          g_assert_not_reached ();
        }
      instance_data->notifier = g_object_ref (notifier_object);
      g_hash_table_replace (instance->transitions,
                            value_data_ref (value),
                            transition_instance_data_ref (instance_data));

      notifier_constant = gtk_constant_expression_new (
          BGE_TYPE_WDGT_NOTIFIER, notifier_object);
      notify_expression = gtk_property_expression_new_for_pspec (
          notifier_constant, notifier_props[NOTIFIER_PROP_VALUE]);

      closure_data           = transition_closure_data_new ();
      closure_data->state    = state_data_ref (state);
      closure_data->instance = state_instance_data_ref (instance);
      closure_data->value    = value_data_ref (value);

      expression = gtk_cclosure_expression_new (
          value->type,
          bge_marshal_DOUBLE__DOUBLE_DOUBLE,
          2, (GtkExpression *[]){ expression, notify_expression },
          G_CALLBACK (expression_adjust_state_transition),
          transition_closure_data_ref (closure_data),
          transition_closure_data_unref_closure);
    }

  g_hash_table_replace (
      instance->expressions,
      value_data_ref (value),
      gtk_expression_ref (expression));
  return gtk_expression_ref (expression);
}

static void
set_value (BgeWdgtRenderer   *self,
           StateData         *state,
           StateInstanceData *instance,
           ValueData         *dest,
           ValueData         *src,
           GPtrArray         *watches)
{
  GtkExpression *src_expression  = NULL;
  GtkExpression *dest_expression = NULL;

  src_expression = g_hash_table_lookup (
      instance->expressions, src);
  g_assert (src_expression != NULL);
  dest_expression = g_hash_table_lookup (
      instance->expressions, dest);
  g_assert (dest_expression != NULL);

  switch (dest->kind)
    {
    case VALUE_PROPERTY:
      {
        GtkExpression *dest_obj_expression = NULL;
        GValue         dest_obj_resolved   = G_VALUE_INIT;
        GObject       *dest_obj            = NULL;

        dest_obj_expression = g_hash_table_lookup (
            instance->expressions, dest->property.object);
        g_assert (dest_obj_expression != NULL);
        gtk_expression_evaluate (
            dest_obj_expression,
            self,
            &dest_obj_resolved);
        dest_obj = g_value_get_object (&dest_obj_resolved);

        if (dest_obj != NULL)
          {
            GValue src_resolved = G_VALUE_INIT;

            gtk_expression_evaluate (
                src_expression,
                self,
                &src_resolved);

            g_object_set_property (
                dest_obj,
                dest->property.prop_name,
                &src_resolved);
            g_value_unset (&src_resolved);
          }

        g_value_unset (&dest_obj_resolved);
      }
      break;
    case VALUE_ALLOCATION_WIDTH:
    case VALUE_ALLOCATION_HEIGHT:
    case VALUE_ALLOCATION_TRANSFORM:
      {
        GtkExpression *dest_widget_expression = NULL;
        GValue         dest_widget_resolved   = G_VALUE_INIT;
        GtkWidget     *dest_widget            = NULL;

        dest_widget_expression = g_hash_table_lookup (
            instance->expressions, dest->allocation.widget);
        g_assert (dest_widget_expression != NULL);
        gtk_expression_evaluate (
            dest_widget_expression,
            self,
            &dest_widget_resolved);
        dest_widget = g_value_get_object (&dest_widget_resolved);

        if (dest_widget != NULL)
          {
            AllocationData *allocation = NULL;

            allocation = g_hash_table_lookup (self->allocations, dest_widget);
            if (allocation != NULL)
              {
                switch (dest->kind)
                  {
                  case VALUE_ALLOCATION_WIDTH:
                    g_clear_pointer (&allocation->width, value_data_unref);
                    allocation->width = value_data_ref (src);
                    break;
                  case VALUE_ALLOCATION_HEIGHT:
                    g_clear_pointer (&allocation->height, value_data_unref);
                    allocation->height = value_data_ref (src);
                    break;
                  case VALUE_ALLOCATION_TRANSFORM:
                    g_clear_pointer (&allocation->transform, value_data_unref);
                    allocation->transform = value_data_ref (src);
                    break;
                  case VALUE_CHILD:
                  case VALUE_CLOSURE:
                  case VALUE_COERCION:
                  case VALUE_COMPONENT:
                  case VALUE_CONSTANT:
                  case VALUE_MEASURE_FOR_SIZE:
                  case VALUE_MEASURE_MINIMUM_HEIGHT:
                  case VALUE_MEASURE_MINIMUM_WIDTH:
                  case VALUE_MEASURE_NATURAL_HEIGHT:
                  case VALUE_MEASURE_NATURAL_WIDTH:
                  case VALUE_OBJECT:
                  case VALUE_PROPERTY:
                  case VALUE_REFERENCE_OBJECT:
                  case VALUE_SPECIAL:
                  case VALUE_TRACK_TRANSITION:
                  case VALUE_TRANSFORM:
                  case VALUE_VARIABLE:
                  case VALUE_WIDGET_HEIGHT:
                  case VALUE_WIDGET_WIDTH:
                  default:
                    g_assert_not_reached ();
                  }

                gtk_widget_queue_allocate (GTK_WIDGET (self));
              }
          }

        g_value_unset (&dest_widget_resolved);
      }
      break;
    case VALUE_MEASURE_MINIMUM_WIDTH:
    case VALUE_MEASURE_NATURAL_WIDTH:
    case VALUE_MEASURE_MINIMUM_HEIGHT:
    case VALUE_MEASURE_NATURAL_HEIGHT:
      {
        GValue src_resolved = G_VALUE_INIT;

        gtk_expression_evaluate (
            src_expression,
            self,
            &src_resolved);

        switch (dest->kind)
          {
          case VALUE_MEASURE_MINIMUM_WIDTH:
            self->measure_minimum_width = g_value_get_int (&src_resolved);
            break;
          case VALUE_MEASURE_NATURAL_WIDTH:
            self->measure_natural_width = g_value_get_int (&src_resolved);
            break;
          case VALUE_MEASURE_MINIMUM_HEIGHT:
            self->measure_minimum_height = g_value_get_int (&src_resolved);
            break;
          case VALUE_MEASURE_NATURAL_HEIGHT:
            self->measure_natural_height = g_value_get_int (&src_resolved);
            break;
          case VALUE_ALLOCATION_HEIGHT:
          case VALUE_ALLOCATION_TRANSFORM:
          case VALUE_ALLOCATION_WIDTH:
          case VALUE_CHILD:
          case VALUE_CLOSURE:
          case VALUE_COERCION:
          case VALUE_COMPONENT:
          case VALUE_CONSTANT:
          case VALUE_MEASURE_FOR_SIZE:
          case VALUE_OBJECT:
          case VALUE_PROPERTY:
          case VALUE_REFERENCE_OBJECT:
          case VALUE_SPECIAL:
          case VALUE_TRACK_TRANSITION:
          case VALUE_TRANSFORM:
          case VALUE_VARIABLE:
          case VALUE_WIDGET_HEIGHT:
          case VALUE_WIDGET_WIDTH:
          default:
            g_assert_not_reached ();
          }

        g_value_unset (&src_resolved);

        gtk_widget_queue_resize (GTK_WIDGET (self));
      }
      break;
    case VALUE_CHILD:
    case VALUE_CLOSURE:
    case VALUE_COERCION:
    case VALUE_COMPONENT:
    case VALUE_CONSTANT:
    case VALUE_MEASURE_FOR_SIZE:
    case VALUE_OBJECT:
    case VALUE_REFERENCE_OBJECT:
    case VALUE_SPECIAL:
    case VALUE_TRACK_TRANSITION:
    case VALUE_TRANSFORM:
    case VALUE_VARIABLE:
    case VALUE_WIDGET_HEIGHT:
    case VALUE_WIDGET_WIDTH:
      break;
    default:
      g_assert_not_reached ();
    }

  if (watches != NULL)
    {
      g_autoptr (WatchSetterData) watch_data = NULL;
      GtkExpressionWatch *watch              = NULL;

      watch_data           = watch_setter_data_new ();
      watch_data->self     = self;
      watch_data->state    = state_data_ref (state);
      watch_data->instance = state_instance_data_ref (instance);
      watch_data->dest     = value_data_ref (dest);
      watch_data->src      = value_data_ref (src);

      watch = gtk_expression_watch (
          src_expression,
          self,
          (GtkExpressionNotify) reset_setter,
          watch_setter_data_ref (watch_data),
          watch_setter_data_unref);
      g_ptr_array_add (watches, watch);
    }
}

static int
resolve_value_int (BgeWdgtRenderer   *self,
                   ValueData         *value,
                   StateInstanceData *instance)
{
  GtkExpression *expression = NULL;
  GValue         resolved   = G_VALUE_INIT;
  int            ret        = 0.0;

  expression = g_hash_table_lookup (instance->expressions, value);
  g_assert (expression != NULL);
  gtk_expression_evaluate (
      expression,
      self,
      &resolved);
  ret = g_value_get_int (&resolved);
  g_value_unset (&resolved);

  return ret;
}

static double
resolve_value_double (BgeWdgtRenderer   *self,
                      ValueData         *value,
                      StateInstanceData *instance)
{
  GtkExpression *expression = NULL;
  GValue         resolved   = G_VALUE_INIT;
  double         ret        = 0.0;

  expression = g_hash_table_lookup (instance->expressions, value);
  g_assert (expression != NULL);
  gtk_expression_evaluate (
      expression,
      self,
      &resolved);
  ret = g_value_get_double (&resolved);
  g_value_unset (&resolved);

  return ret;
}

static gpointer
resolve_value_boxed_dup (BgeWdgtRenderer   *self,
                         ValueData         *value,
                         StateInstanceData *instance)
{
  GtkExpression *expression = NULL;
  GValue         resolved   = G_VALUE_INIT;
  gpointer       ret        = NULL;

  expression = g_hash_table_lookup (instance->expressions, value);
  g_assert (expression != NULL);
  gtk_expression_evaluate (
      expression,
      self,
      &resolved);
  ret = g_value_dup_boxed (&resolved);
  g_value_unset (&resolved);

  return ret;
}

static gpointer
resolve_value_object_dup (BgeWdgtRenderer   *self,
                          ValueData         *value,
                          StateInstanceData *instance)
{
  GtkExpression *expression = NULL;
  GValue         resolved   = G_VALUE_INIT;
  gpointer       ret        = NULL;

  expression = g_hash_table_lookup (instance->expressions, value);
  g_assert (expression != NULL);
  gtk_expression_evaluate (
      expression,
      self,
      &resolved);
  ret = g_value_dup_object (&resolved);
  g_value_unset (&resolved);

  return ret;
}

static void
discard_binding (gpointer ptr)
{
  GBinding *binding = ptr;

  g_binding_unbind (binding);
  g_object_unref (binding);
}

static void
discard_watch (gpointer ptr)
{
  GtkExpressionWatch *watch = ptr;

  /* unrefs for us */
  gtk_expression_watch_unwatch (watch);
}

static void
prop_change_queue_draw (BgeWdgtRenderer *self)
{
  gtk_widget_queue_draw (GTK_WIDGET (self));
}

static void
reset_setter (WatchSetterData *data)
{
  BgeWdgtRenderer *self = data->self;

  /* Active state watchers are "overlayed" over the permanent init state. Thus,
     if this is the init state and the active state also sets this value, we
     should avoid touching it */
  if (self->active_state != NULL /* ? */ &&
      data->state == self->spec->init_state &&
      g_hash_table_contains (
          self->active_state->setters,
          data->dest))
    return;

  set_value (
      data->self,
      data->state,
      data->instance,
      data->dest,
      data->src,
      NULL);
}

static void
_marshal_DIRECT__ARGS_DIRECT (GClosure                *closure,
                              GValue                  *return_value,
                              guint                    n_param_values,
                              const GValue            *param_values,
                              gpointer invocation_hint G_GNUC_UNUSED,
                              gpointer                 marshal_data)
{
  typedef void (*GMarshalFunc_DIRECT__ARGS_DIRECT) (gpointer      data1,
                                                    GValue       *return_value,
                                                    guint         n_param_values,
                                                    const GValue *param_values,
                                                    gpointer      data2);
  GCClosure                       *cc = (GCClosure *) closure;
  gpointer                         data1, data2;
  GMarshalFunc_DIRECT__ARGS_DIRECT callback;

  g_return_if_fail (return_value != NULL);
  g_return_if_fail (n_param_values >= 1);

  if (G_CCLOSURE_SWAP_DATA (closure))
    {
      data1 = closure->data;
      data2 = g_value_peek_pointer (param_values + 0);
    }
  else
    {
      data1 = g_value_peek_pointer (param_values + 0);
      data2 = closure->data;
    }
  callback = (GMarshalFunc_DIRECT__ARGS_DIRECT) (marshal_data ? marshal_data : cc->callback);

  callback (data1,
            return_value,
            n_param_values - 1,
            param_values + 1,
            data2);
}

static void
_marshal_BOXED__ARGS_DIRECT (GClosure                *closure,
                             GValue                  *return_value,
                             guint                    n_param_values,
                             const GValue            *param_values,
                             gpointer invocation_hint G_GNUC_UNUSED,
                             gpointer                 marshal_data)
{
  typedef gpointer (*GMarshalFunc_BOXED__ARGS_DIRECT) (gpointer      data1,
                                                       guint         n_param_values,
                                                       const GValue *param_values,
                                                       gpointer      data2);
  GCClosure                      *cc = (GCClosure *) closure;
  gpointer                        data1, data2;
  GMarshalFunc_BOXED__ARGS_DIRECT callback;
  gpointer                        v_return;

  g_return_if_fail (return_value != NULL);
  g_return_if_fail (n_param_values >= 1);

  if (G_CCLOSURE_SWAP_DATA (closure))
    {
      data1 = closure->data;
      data2 = g_value_peek_pointer (param_values + 0);
    }
  else
    {
      data1 = g_value_peek_pointer (param_values + 0);
      data2 = closure->data;
    }
  callback = (GMarshalFunc_BOXED__ARGS_DIRECT) (marshal_data ? marshal_data : cc->callback);

  v_return = callback (data1,
                       n_param_values - 1,
                       param_values + 1,
                       data2);

  g_value_take_boxed (return_value, v_return);
}

/* End of bge-wdgt-spec.c */
