#include <clutter/clutter.h>
#include <cairo/cairo.h>
#include <string.h>
#include <math.h>

#include "test-conform-common.h"

#define MAX_NODES 128

#define FLOAT_FUZZ_AMOUNT 5.0f

typedef struct _CallbackData CallbackData;

typedef gboolean (* PathTestFunc) (CallbackData *data);

static void compare_node (const ClutterPathNode *node, gpointer data_p);

struct _CallbackData
{
  ClutterPath *path;

  guint n_nodes;
  ClutterPathNode nodes[MAX_NODES];

  gboolean nodes_different;
  guint nodes_found;
};

static const char path_desc[] =
  "M 21 22 "
  "L 25 26 "
  "C 29 30 31 32 33 34 "
  "m 23 24 "
  "l 27 28 "
  "c 35 36 37 38 39 40 "
  "z";
static const ClutterPathNode path_nodes[] =
  { { CLUTTER_PATH_MOVE_TO,      { { 21, 22 }, { 0,  0 },  { 0,  0 } } },
    { CLUTTER_PATH_LINE_TO,      { { 25, 26 }, { 0,  0 },  { 0,  0 } } },
    { CLUTTER_PATH_CURVE_TO,     { { 29, 30 }, { 31, 32 }, { 33, 34 } } },
    { CLUTTER_PATH_REL_MOVE_TO,  { { 23, 24 }, { 0,  0 },  { 0,  0 } } },
    { CLUTTER_PATH_REL_LINE_TO,  { { 27, 28 }, { 0,  0 },  { 0,  0 } } },
    { CLUTTER_PATH_REL_CURVE_TO, { { 35, 36 }, { 37, 38 }, { 39, 40 } } },
    { CLUTTER_PATH_CLOSE,        { { 0,  0 },  { 0,  0 },  { 0,  0 } } } };

static gboolean
path_test_add_move_to (CallbackData *data)
{
  ClutterPathNode node = { 0, };

  node.type = CLUTTER_PATH_MOVE_TO;
  node.points[0].x = 1;
  node.points[0].y = 2;

  clutter_path_add_move_to (data->path, node.points[0].x, node.points[0].y);

  data->nodes[data->n_nodes++] = node;

  return TRUE;
}

static gboolean
path_test_add_line_to (CallbackData *data)
{
  ClutterPathNode node = { 0, };

  node.type = CLUTTER_PATH_LINE_TO;
  node.points[0].x = 3;
  node.points[0].y = 4;

  clutter_path_add_line_to (data->path, node.points[0].x, node.points[0].y);

  data->nodes[data->n_nodes++] = node;

  return TRUE;
}

static gboolean
path_test_add_curve_to (CallbackData *data)
{
  ClutterPathNode node = { 0, };

  node.type = CLUTTER_PATH_CURVE_TO;
  node.points[0].x = 5;
  node.points[0].y = 6;
  node.points[1].x = 7;
  node.points[1].y = 8;
  node.points[2].x = 9;
  node.points[2].y = 10;

  clutter_path_add_curve_to (data->path,
                             node.points[0].x, node.points[0].y,
                             node.points[1].x, node.points[1].y,
                             node.points[2].x, node.points[2].y);

  data->nodes[data->n_nodes++] = node;

  return TRUE;
}

static gboolean
path_test_add_close (CallbackData *data)
{
  ClutterPathNode node = { 0, };

  node.type = CLUTTER_PATH_CLOSE;

  clutter_path_add_close (data->path);

  data->nodes[data->n_nodes++] = node;

  return TRUE;
}

static gboolean
path_test_add_rel_move_to (CallbackData *data)
{
  ClutterPathNode node = { 0, };

  node.type = CLUTTER_PATH_REL_MOVE_TO;
  node.points[0].x = 11;
  node.points[0].y = 12;

  clutter_path_add_rel_move_to (data->path, node.points[0].x, node.points[0].y);

  data->nodes[data->n_nodes++] = node;

  return TRUE;
}

static gboolean
path_test_add_rel_line_to (CallbackData *data)
{
  ClutterPathNode node = { 0, };

  node.type = CLUTTER_PATH_REL_LINE_TO;
  node.points[0].x = 13;
  node.points[0].y = 14;

  clutter_path_add_rel_line_to (data->path, node.points[0].x, node.points[0].y);

  data->nodes[data->n_nodes++] = node;

  return TRUE;
}

static gboolean
path_test_add_rel_curve_to (CallbackData *data)
{
  ClutterPathNode node = { 0, };

  node.type = CLUTTER_PATH_REL_CURVE_TO;
  node.points[0].x = 15;
  node.points[0].y = 16;
  node.points[1].x = 17;
  node.points[1].y = 18;
  node.points[2].x = 19;
  node.points[2].y = 20;

  clutter_path_add_rel_curve_to (data->path,
                                 node.points[0].x, node.points[0].y,
                                 node.points[1].x, node.points[1].y,
                                 node.points[2].x, node.points[2].y);

  data->nodes[data->n_nodes++] = node;

  return TRUE;
}

static gboolean
path_test_add_string (CallbackData *data)
{
  int i;

  for (i = 0; i < G_N_ELEMENTS (path_nodes); i++)
    data->nodes[data->n_nodes++] = path_nodes[i];

  clutter_path_add_string (data->path, path_desc);

  return TRUE;
}

static gboolean
path_test_add_node_by_struct (CallbackData *data)
{
  int i;

  for (i = 0; i < G_N_ELEMENTS (path_nodes); i++)
    {
      data->nodes[data->n_nodes++] = path_nodes[i];
      clutter_path_add_node (data->path, path_nodes + i);
    }

  return TRUE;
}

static gboolean
path_test_get_n_nodes (CallbackData *data)
{
  return clutter_path_get_n_nodes (data->path) == data->n_nodes;
}

static gboolean
path_test_get_node (CallbackData *data)
{
  int i;

  data->nodes_found = 0;
  data->nodes_different = FALSE;

  for (i = 0; i < data->n_nodes; i++)
    {
      ClutterPathNode node;

      clutter_path_get_node (data->path, i, &node);

      compare_node (&node, data);
    }

  return !data->nodes_different;
}

static gboolean
path_test_get_nodes (CallbackData *data)
{
  GSList *list, *node;

  data->nodes_found = 0;
  data->nodes_different = FALSE;

  list = clutter_path_get_nodes (data->path);

  for (node = list; node; node = node->next)
    compare_node (node->data, data);

  g_slist_free (list);

  return !data->nodes_different && data->nodes_found == data->n_nodes;
}

static gboolean
path_test_insert_beginning (CallbackData *data)
{
  ClutterPathNode node;

  node.type = CLUTTER_PATH_LINE_TO;
  node.points[0].x = 41;
  node.points[0].y = 42;

  memmove (data->nodes + 1, data->nodes,
           data->n_nodes++ * sizeof (ClutterPathNode));
  data->nodes[0] = node;

  clutter_path_insert_node (data->path, 0, &node);

  return TRUE;
}

static gboolean
path_test_insert_end (CallbackData *data)
{
  ClutterPathNode node;

  node.type = CLUTTER_PATH_LINE_TO;
  node.points[0].x = 43;
  node.points[0].y = 44;

  data->nodes[data->n_nodes++] = node;

  clutter_path_insert_node (data->path, -1, &node);

  return TRUE;
}

static gboolean
path_test_insert_middle (CallbackData *data)
{
  ClutterPathNode node;
  int pos = data->n_nodes / 2;

  node.type = CLUTTER_PATH_LINE_TO;
  node.points[0].x = 45;
  node.points[0].y = 46;

  memmove (data->nodes + pos + 1, data->nodes + pos,
           (data->n_nodes - pos) * sizeof (ClutterPathNode));
  data->nodes[pos] = node;
  data->n_nodes++;

  clutter_path_insert_node (data->path, pos, &node);

  return TRUE;
}

static gboolean
path_test_clear (CallbackData *data)
{
  clutter_path_clear (data->path);

  data->n_nodes = 0;

  return TRUE;
}

static gboolean
path_test_clear_insert (CallbackData *data)
{
  return path_test_clear (data) && path_test_insert_middle (data);
}

static gboolean
path_test_remove_beginning (CallbackData *data)
{
  memmove (data->nodes, data->nodes + 1,
           --data->n_nodes * sizeof (ClutterPathNode));

  clutter_path_remove_node (data->path, 0);

  return TRUE;
}

static gboolean
path_test_remove_end (CallbackData *data)
{
  clutter_path_remove_node (data->path, --data->n_nodes);

  return TRUE;
}

static gboolean
path_test_remove_middle (CallbackData *data)
{
  int pos = data->n_nodes / 2;

  memmove (data->nodes + pos, data->nodes + pos + 1,
           (--data->n_nodes - pos) * sizeof (ClutterPathNode));

  clutter_path_remove_node (data->path, pos);

  return TRUE;
}

static gboolean
path_test_remove_only (CallbackData *data)
{
  return path_test_clear (data)
    && path_test_add_line_to (data)
    && path_test_remove_beginning (data);
}

static gboolean
path_test_replace (CallbackData *data)
{
  ClutterPathNode node;
  int pos = data->n_nodes / 2;

  node.type = CLUTTER_PATH_LINE_TO;
  node.points[0].x = 47;
  node.points[0].y = 48;

  data->nodes[pos] = node;

  clutter_path_replace_node (data->path, pos, &node);

  return TRUE;
}

static gboolean
path_test_set_description (CallbackData *data)
{
  data->n_nodes = G_N_ELEMENTS (path_nodes);
  memcpy (data->nodes, path_nodes, sizeof (path_nodes));

  return clutter_path_set_description (data->path, path_desc);
}

static gboolean
path_test_get_description (CallbackData *data)
{
  char *desc1, *desc2;
  gboolean ret = TRUE;

  desc1 = clutter_path_get_description (data->path);
  clutter_path_clear (data->path);
  if (!clutter_path_set_description (data->path, desc1))
    ret = FALSE;
  desc2 = clutter_path_get_description (data->path);

  if (strcmp (desc1, desc2))
    ret = FALSE;

  g_free (desc1);
  g_free (desc2);

  return ret;
}

static gboolean
path_test_convert_to_cairo_path (CallbackData *data)
{
  cairo_surface_t *surface;
  cairo_t *cr;
  cairo_path_t *cpath;
  guint i, j;
  ClutterKnot path_start = { 0, 0 }, last_point = { 0, 0 };

  /* Create a temporary image surface and context to hold the cairo
     path */
  surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 10, 10);
  cr = cairo_create (surface);

  /* Convert to a cairo path */
  clutter_path_to_cairo_path (data->path, cr);

  /* Get a copy of the cairo path data */
  cpath = cairo_copy_path (cr);

  /* Convert back to a clutter path */
  clutter_path_clear (data->path);
  clutter_path_add_cairo_path (data->path, cpath);

  /* The relative nodes will have been converted to absolute so we
     need to reflect this in the node array for comparison */
  for (i = 0; i < data->n_nodes; i++)
    {
      switch (data->nodes[i].type)
        {
        case CLUTTER_PATH_MOVE_TO:
          path_start = last_point = data->nodes[i].points[0];
          break;

        case CLUTTER_PATH_LINE_TO:
          last_point = data->nodes[i].points[0];
          break;

        case CLUTTER_PATH_CURVE_TO:
          last_point = data->nodes[i].points[2];
          break;

        case CLUTTER_PATH_REL_MOVE_TO:
          last_point.x += data->nodes[i].points[0].x;
          last_point.y += data->nodes[i].points[0].y;
          data->nodes[i].points[0] = last_point;
          data->nodes[i].type = CLUTTER_PATH_MOVE_TO;
          path_start = last_point;
          break;

        case CLUTTER_PATH_REL_LINE_TO:
          last_point.x += data->nodes[i].points[0].x;
          last_point.y += data->nodes[i].points[0].y;
          data->nodes[i].points[0] = last_point;
          data->nodes[i].type = CLUTTER_PATH_LINE_TO;
          break;

        case CLUTTER_PATH_REL_CURVE_TO:
          for (j = 0; j < 3; j++)
            {
              data->nodes[i].points[j].x += last_point.x;
              data->nodes[i].points[j].y += last_point.y;
            }
          last_point = data->nodes[i].points[2];
          data->nodes[i].type = CLUTTER_PATH_CURVE_TO;
          break;

        case CLUTTER_PATH_CLOSE:
          last_point = path_start;

          /* Cairo always adds a move to after every close so we need
             to insert one here */
          memmove (data->nodes + i + 2, data->nodes + i + 1,
                   (data->n_nodes - i - 1) * sizeof (ClutterPathNode));
          data->nodes[i + 1].type = CLUTTER_PATH_MOVE_TO;
          data->nodes[i + 1].points[0] = last_point;
          data->n_nodes++;
          break;
        }
    }

  /* Free the cairo resources */
  cairo_path_destroy (cpath);
  cairo_destroy (cr);
  cairo_surface_destroy (surface);

  return TRUE;
}

static gboolean
float_fuzzy_equals (float fa, float fb)
{
  return fabs (fa - fb) <= FLOAT_FUZZ_AMOUNT;
}

static void
set_triangle_path (CallbackData *data)
{
  /* Triangular shaped path hitting (0,0), (64,64) and (128,0) in four
     parts. The two curves are actually straight lines */
  static const ClutterPathNode nodes[] =
    { { CLUTTER_PATH_MOVE_TO,      { { 0, 0 } } },
      { CLUTTER_PATH_LINE_TO,      { { 32, 32 } } },
      { CLUTTER_PATH_CURVE_TO,     { { 40, 40 }, { 56, 56 }, { 64, 64 } } },
      { CLUTTER_PATH_REL_CURVE_TO, { { 8, -8 }, { 24, -24 }, { 32, -32 } } },
      { CLUTTER_PATH_REL_LINE_TO,  { { 32, -32 } } } };
  gint i;

  clutter_path_clear (data->path);

  for (i = 0; i < G_N_ELEMENTS (nodes); i++)
    clutter_path_add_node (data->path, nodes + i);

  memcpy (data->nodes, nodes, sizeof (nodes));
  data->n_nodes = G_N_ELEMENTS (nodes);
}

static gboolean
path_test_get_position (CallbackData *data)
{
  static const float values[] = { 0.125f, 16.0f, 16.0f,
                                  0.375f, 48.0f, 48.0f,
                                  0.625f, 80.0f, 48.0f,
                                  0.875f, 112.0f, 16.0f };
  gint i;

  set_triangle_path (data);

  for (i = 0; i < G_N_ELEMENTS (values); i += 3)
    {
      ClutterKnot pos;

      clutter_path_get_position (data->path,
                                 values[i],
                                 &pos);

      if (!float_fuzzy_equals (values[i + 1], pos.x)
          || !float_fuzzy_equals (values[i + 2], pos.y))
        return FALSE;
    }

  return TRUE;
}

static gboolean
path_test_get_length (CallbackData *data)
{
  const float actual_length /* sqrt(64**2 + 64**2) * 2 */ = 181.019336f;
  guint approx_length;

  set_triangle_path (data);

  g_object_get (data->path, "length", &approx_length, NULL);

  /* Allow 15% margin of error */
  return fabs (approx_length - actual_length) / actual_length <= 0.15f;
}

static gboolean
path_test_boxed_type (CallbackData *data)
{
  gboolean ret = TRUE;
  GSList *nodes, *l;
  GValue value;

  nodes = clutter_path_get_nodes (data->path);

  memset (&value, 0, sizeof (value));

  for (l = nodes; l; l = l->next)
    {
      g_value_init (&value, CLUTTER_TYPE_PATH_NODE);

      g_value_set_boxed (&value, l->data);

      if (!clutter_path_node_equal (l->data,
                                    g_value_get_boxed (&value)))
        ret = FALSE;

      g_value_unset (&value);
    }

  g_slist_free (nodes);

  return ret;
}

static const struct
{
  const char *desc;
  PathTestFunc func;
}
path_tests[] =
  {
    { "Add line to", path_test_add_line_to },
    { "Add move to", path_test_add_move_to },
    { "Add curve to", path_test_add_curve_to },
    { "Add close", path_test_add_close },
    { "Add relative line to", path_test_add_rel_line_to },
    { "Add relative move to", path_test_add_rel_move_to },
    { "Add relative curve to", path_test_add_rel_curve_to },
    { "Add string", path_test_add_string },
    { "Add node by struct", path_test_add_node_by_struct },
    { "Get number of nodes", path_test_get_n_nodes },
    { "Get a node", path_test_get_node },
    { "Get all nodes", path_test_get_nodes },
    { "Insert at beginning", path_test_insert_beginning },
    { "Insert at end", path_test_insert_end },
    { "Insert at middle", path_test_insert_middle },
    { "Add after insert", path_test_add_line_to },
    { "Clear then insert", path_test_clear_insert },
    { "Add string again", path_test_add_string },
    { "Remove from beginning", path_test_remove_beginning },
    { "Remove from end", path_test_remove_end },
    { "Remove from middle", path_test_remove_middle },
    { "Add after remove", path_test_add_line_to },
    { "Remove only node", path_test_remove_only },
    { "Add after remove again", path_test_add_line_to },
    { "Replace a node", path_test_replace },
    { "Set description", path_test_set_description },
    { "Get description", path_test_get_description },
    { "Convert to cairo path and back", path_test_convert_to_cairo_path },
    { "Clear", path_test_clear },
    { "Get position", path_test_get_position },
    { "Check node boxed type", path_test_boxed_type },
    { "Get length", path_test_get_length }
  };

static void
compare_node (const ClutterPathNode *node, gpointer data_p)
{
  CallbackData *data = data_p;

  if (data->nodes_found >= data->n_nodes)
    data->nodes_different = TRUE;
  else
    {
      guint n_points = 0, i;
      const ClutterPathNode *onode = data->nodes + data->nodes_found;

      if (node->type != onode->type)
        data->nodes_different = TRUE;

      switch (node->type & ~CLUTTER_PATH_RELATIVE)
        {
        case CLUTTER_PATH_MOVE_TO: n_points = 1; break;
        case CLUTTER_PATH_LINE_TO: n_points = 1; break;
        case CLUTTER_PATH_CURVE_TO: n_points = 3; break;
        case CLUTTER_PATH_CLOSE: n_points = 0; break;

        default:
          data->nodes_different = TRUE;
          break;
        }

      for (i = 0; i < n_points; i++)
        if (node->points[i].x != onode->points[i].x
            || node->points[i].y != onode->points[i].y)
          {
            data->nodes_different = TRUE;
            break;
          }
    }

  data->nodes_found++;
}

static gboolean
compare_nodes (CallbackData *data)
{
  data->nodes_different = FALSE;
  data->nodes_found = 0;

  clutter_path_foreach (data->path, compare_node, data);

  return !data->nodes_different && data->nodes_found == data->n_nodes;
}

void
test_path (TestConformSimpleFixture *fixture,
           gconstpointer _data)
{
  CallbackData data;
  gint i;

  memset (&data, 0, sizeof (data));

  data.path = clutter_path_new ();

  for (i = 0; i < G_N_ELEMENTS (path_tests); i++)
    {
      gboolean succeeded;

      if (g_test_verbose ())
        g_print ("%s... ", path_tests[i].desc);

      succeeded = path_tests[i].func (&data) && compare_nodes (&data);

      if (g_test_verbose ())
        g_print ("%s\n", succeeded ? "ok" : "FAIL");

      g_assert (succeeded);
    }

  g_object_unref (data.path);
}