#include <gtk/gtk.h>
#include "gsk/gskcurveprivate.h"

static void
init_random_point (graphene_point_t *p)
{
  p->x = g_test_rand_double_range (0, 1000);
  p->y = g_test_rand_double_range (0, 1000);
}

static void
init_random_curve_with_op (GskCurve         *curve,
                           GskPathOperation  min_op,
                           GskPathOperation  max_op)
{
  switch (g_test_rand_int_range (min_op, max_op + 1))
    {
    case GSK_PATH_LINE:
      {
        GskAlignedPoint p[2];

        init_random_point (&p[0].pt);
        init_random_point (&p[1].pt);
        gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_LINE, p));
      }
      break;

    case GSK_PATH_QUAD:
      {
        GskAlignedPoint p[3];

        init_random_point (&p[0].pt);
        init_random_point (&p[1].pt);
        init_random_point (&p[2].pt);
        gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_QUAD, p));
      }
    break;

    case GSK_PATH_CUBIC:
      {
        GskAlignedPoint p[4];

        init_random_point (&p[0].pt);
        init_random_point (&p[1].pt);
        init_random_point (&p[2].pt);
        init_random_point (&p[3].pt);
        gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_CUBIC, p));
      }
    break;

    case GSK_PATH_CONIC:
      {
        GskAlignedPoint p[4];

        init_random_point (&p[0].pt);
        init_random_point (&p[1].pt);
        p[2].pt.x = g_test_rand_double_range (0.2, 20);
        p[2].pt.y = 0.f;
        init_random_point (&p[3].pt);
        gsk_curve_init (curve, gsk_pathop_encode (GSK_PATH_CONIC, p));
      }
    break;

    default:
      g_assert_not_reached ();
    }
}

static void
init_random_curve (GskCurve *curve)
{
  init_random_curve_with_op (curve, GSK_PATH_LINE, GSK_PATH_CONIC);
}

static void
test_curve_tangents (void)
{
  for (int i = 0; i < 100; i++)
    {
      GskCurve c;
      graphene_vec2_t vec, exact;

      init_random_curve (&c);

      gsk_curve_get_tangent (&c, 0, &vec);
      g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&vec), 1.0f, 0.00001);
      gsk_curve_get_start_tangent (&c, &exact);
      g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&exact), 1.0f, 0.00001);
      g_assert_true (graphene_vec2_near (&vec, &exact, 0.05));

      gsk_curve_get_tangent (&c, 1, &vec);
      g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&vec), 1.0f, 0.00001);
      gsk_curve_get_end_tangent (&c, &exact);
      g_assert_cmpfloat_with_epsilon (graphene_vec2_length (&exact), 1.0f, 0.00001);
      g_assert_true (graphene_vec2_near (&vec, &exact, 0.05));
    }
}

static void
test_curve_points (void)
{
  for (int i = 0; i < 100; i++)
    {
      GskCurve c;
      graphene_point_t p;

      init_random_curve (&c);

      /* We could assert equality here because evaluating the polynomials with 0
       * has no effect on accuracy, but for arcs, we use trigonometric functions,
       * so allow a small error.
       */
      gsk_curve_get_point (&c, 0, &p);
      g_assert_true (graphene_point_near (gsk_curve_get_start_point (&c), &p, 0.001));

      /* But here we evaluate the polynomials with 1 which gives the highest possible
       * accuracy error. So we'll just be generous here.
       */
      gsk_curve_get_point (&c, 1, &p);
      g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c), &p, 0.05));
    }
}

/* at this point the subdivision stops and the decomposer
 * violates tolerance rules
 */
#define MIN_PROGRESS (1/1024.f)

typedef struct
{
  graphene_point_t p;
  float t;
} PointOnLine;

static gboolean
add_line_to_array (const graphene_point_t *from,
                   const graphene_point_t *to,
                   float                   from_progress,
                   float                   to_progress,
                   GskCurveLineReason      reason,
                   gpointer                user_data)
{
  GArray *array = user_data;
  PointOnLine *last = &g_array_index (array, PointOnLine, array->len - 1);

  g_assert_true (array->len > 0);
  g_assert_cmpfloat (from_progress, >=, 0.0f);
  g_assert_cmpfloat (from_progress, <, to_progress);
  g_assert_cmpfloat (to_progress, <=, 1.0f);

  g_assert_true (graphene_point_equal (&last->p, from));
  g_assert_cmpfloat (last->t, ==, from_progress);

  g_array_append_vals (array, (PointOnLine[1]) { { *to, to_progress } }, 1);

  return TRUE;
}

static void
test_curve_decompose (void)
{
  static const float tolerance = 0.5;

  for (int i = 0; i < 100; i++)
    {
      GArray *array;
      GskCurve c;

      init_random_curve (&c);

      array = g_array_new (FALSE, FALSE, sizeof (PointOnLine));
      g_array_append_vals (array, (PointOnLine[1]) { { *gsk_curve_get_start_point (&c), 0.f } }, 1);

      g_assert_true (gsk_curve_decompose (&c, tolerance, add_line_to_array, array));

      g_assert_cmpint (array->len, >=, 2); /* We at least got a line to the end */
      g_assert_cmpfloat (g_array_index (array, PointOnLine, array->len - 1).t, ==, 1.0);

      for (int j = 0; j < array->len; j++)
        {
          PointOnLine *pol = &g_array_index (array, PointOnLine, j);
          graphene_point_t p;

          /* Check that the points we got are actually on the line */
          gsk_curve_get_point (&c, pol->t, &p);
          g_assert_true (graphene_point_near (&pol->p, &p, 0.05));

          /* Check that the mid point is not further than the tolerance */
          if (j > 0)
            {
              PointOnLine *last = &g_array_index (array, PointOnLine, j - 1);
              graphene_point_t mid;

              if (pol->t - last->t > MIN_PROGRESS)
                {
                  graphene_point_interpolate (&last->p, &pol->p, 0.5, &mid);
                  gsk_curve_get_point (&c, (pol->t + last->t) / 2, &p);
                  /* The decomposer does this cheaper Manhattan distance test,
                   * so graphene_point_near() does not work */
                  g_assert_cmpfloat (fabs (mid.x - p.x), <=, tolerance + 0.0002);
                  g_assert_cmpfloat (fabs (mid.y - p.y), <=, tolerance + 0.0002);
                }
            }
        }

      g_array_unref (array);
    }
}

static gboolean
add_curve_to_array (GskPathOperation        op,
                    const graphene_point_t *pts,
                    gsize                   n_pts,
                    float                   weight,
                    gpointer                user_data)
{
  GArray *array = user_data;
  GskCurve c;

  gsk_curve_init_foreach (&c, op, pts, n_pts, weight);
  g_array_append_val (array, c);

  return TRUE;
}

static void
test_curve_decompose_into (GskPathForeachFlags flags)
{
  for (int i = 0; i < 100; i++)
    {
      GskCurve c;
      GskPathBuilder *builder;
      const graphene_point_t *s;
      GskPath *path;
      GArray *array;

      init_random_curve (&c);

      builder = gsk_path_builder_new ();

      s = gsk_curve_get_start_point (&c);
      gsk_path_builder_move_to (builder, s->x, s->y);
      gsk_curve_builder_to (&c, builder);
      path = gsk_path_builder_free_to_path (builder);

      array = g_array_new (FALSE, FALSE, sizeof (GskCurve));

      g_assert_true (gsk_curve_decompose_curve (&c, flags, 0.1, add_curve_to_array, array));

      g_assert_cmpint (array->len, >=, 1);

      for (int j = 0; j < array->len; j++)
        {
          GskCurve *c2 = &g_array_index (array, GskCurve, j);

          switch (c2->op)
            {
            case GSK_PATH_MOVE:
            case GSK_PATH_CLOSE:
            case GSK_PATH_LINE:
              break;
            case GSK_PATH_QUAD:
              g_assert_true (flags & GSK_PATH_FOREACH_ALLOW_QUAD);
              break;
            case GSK_PATH_CUBIC:
              g_assert_true (flags & GSK_PATH_FOREACH_ALLOW_CUBIC);
              break;
            case GSK_PATH_CONIC:
              g_assert_true (flags & GSK_PATH_FOREACH_ALLOW_CONIC);
              break;
            default:
              g_assert_not_reached ();
            }
        }

      g_array_unref (array);
      gsk_path_unref (path);
    }
}

static void
test_curve_decompose_into_line (void)
{
  test_curve_decompose_into (0);
}

static void
test_curve_decompose_into_quad (void)
{
  test_curve_decompose_into (GSK_PATH_FOREACH_ALLOW_QUAD);
}

static void
test_curve_decompose_into_cubic (void)
{
  test_curve_decompose_into (GSK_PATH_FOREACH_ALLOW_CUBIC);
}

/* Some sanity checks for splitting curves. */
static void
test_curve_split (void)
{
  for (int i = 0; i < 20; i++)
    {
      GskCurve c;

      init_random_curve (&c);

      for (int j = 0; j < 20; j++)
        {
          GskCurve c1, c2;
          graphene_point_t p;
          graphene_vec2_t t, t1, t2;
          float split;

          split = g_test_rand_double_range (0.1, 0.9);

          gsk_curve_split (&c, split, &c1, &c2);

          g_assert_true (c1.op == c.op);
          g_assert_true (c2.op == c.op);

          g_assert_true (graphene_point_near (gsk_curve_get_start_point (&c),
                                              gsk_curve_get_start_point (&c1), 0.005));
          g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c1),
                                              gsk_curve_get_start_point (&c2), 0.005));
          g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c),
                                              gsk_curve_get_end_point (&c2), 0.005));
          gsk_curve_get_point (&c, split, &p);
          gsk_curve_get_tangent (&c, split, &t);
          g_assert_true (graphene_point_near (gsk_curve_get_end_point (&c1), &p, 0.005));
          g_assert_true (graphene_point_near (gsk_curve_get_start_point (&c2), &p, 0.005));

          gsk_curve_get_start_tangent (&c, &t1);
          gsk_curve_get_start_tangent (&c1, &t2);
          g_assert_true (graphene_vec2_near (&t1, &t2, 0.005));
          gsk_curve_get_end_tangent (&c1, &t1);
          gsk_curve_get_start_tangent (&c2, &t2);
          g_assert_true (graphene_vec2_near (&t1, &t2, 0.005));
          g_assert_true (graphene_vec2_near (&t, &t1, 0.005));
          g_assert_true (graphene_vec2_near (&t, &t2, 0.005));
          gsk_curve_get_end_tangent (&c, &t1);
          gsk_curve_get_end_tangent (&c2, &t2);
          g_assert_true (graphene_vec2_near (&t1, &t2, 0.005));

#if 0
          /* hard to guarantee this for totally random random curves */
          g_assert_cmpfloat_with_epsilon (gsk_curve_get_length (&c),
                                          gsk_curve_get_length (&c1) + gsk_curve_get_length (&c2),
                                          1);
#endif
        }
    }
}

static void
test_curve_derivative (void)
{
  GskCurve c;
  float t;
  graphene_vec2_t t1, t2;
  graphene_point_t p;

  for (int i = 0; i < 100; i++)
    {
      init_random_curve (&c);

      for (int j = 0; j < 100; j++)
        {
          t = g_test_rand_double_range (0, 1);
          gsk_curve_get_derivative_at (&c, t, &p);
          gsk_curve_get_tangent (&c, t, &t1);
          graphene_vec2_init (&t2, p.x, p.y);
          graphene_vec2_normalize (&t2, &t2);
          g_assert_true (graphene_vec2_near (&t1, &t2, 0.1));
        }
    }
}

static void
test_curve_length (void)
{
  GskCurve c;
  float l, l0;

  for (int i = 0; i < 1000; i++)
    {
      init_random_curve (&c);

      l = gsk_curve_get_length (&c);
      l0 = graphene_point_distance (gsk_curve_get_start_point (&c),
                                    gsk_curve_get_end_point (&c),
                                    NULL, NULL);
      g_assert_true (l >= l0 - 0.001);
      if (c.op == GSK_PATH_LINE)
        g_assert_cmpfloat_with_epsilon (l, l0, 0.001);
    }
}

int
main (int argc, char *argv[])
{
  (g_test_init) (&argc, &argv, NULL);

  g_test_add_func ("/curve/points", test_curve_points);
  g_test_add_func ("/curve/tangents", test_curve_tangents);
  g_test_add_func ("/curve/decompose", test_curve_decompose);
  g_test_add_func ("/curve/decompose-line", test_curve_decompose_into_line);
  g_test_add_func ("/curve/decompose-quad", test_curve_decompose_into_quad);
  g_test_add_func ("/curve/decompose-cubic", test_curve_decompose_into_cubic);
  g_test_add_func ("/curve/split", test_curve_split);
  g_test_add_func ("/curve/derivative", test_curve_derivative);
  g_test_add_func ("/curve/length", test_curve_length);

  return g_test_run ();
}
