/*
 * Copyright © 2020 Benjamin Otte
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: Benjamin Otte <otte@gnome.org>
 */

#include "config.h"

#include "gskpathprivate.h"

#include "gskcurveprivate.h"
#include "gskpathbuilder.h"
#include "gskpathpoint.h"
#include "gskcontourprivate.h"

static void
skip_whitespace (const char **p)
{
  while (g_ascii_isspace (**p))
    (*p)++;
}

static void
skip_optional_comma (const char **p)
{
  skip_whitespace (p);
  if (**p == ',')
    (*p)++;
}

static gboolean
parse_number (const char **p,
              double      *c)
{
  char *e;
  *c = g_ascii_strtod (*p, &e);
  if (e == *p)
    return FALSE;
  *p = e;
  skip_optional_comma (p);
  return TRUE;
}

static gboolean
parse_coordinate (const char **p,
                  double      *c)
{
  return parse_number (p, c);
}

static gboolean
parse_coordinate_pair (const char **p,
                       double      *x,
                       double      *y)
{
  double xx, yy;
  const char *o = *p;

  if (!parse_coordinate (p, &xx))
    {
      *p = o;
      return FALSE;
    }
  if (!parse_coordinate (p, &yy))
    {
      *p = o;
      return FALSE;
    }

  *x = xx;
  *y = yy;

  return TRUE;
}

static gboolean
parse_nonnegative_number (const char **p,
                          double      *x)
{
  const char *o = *p;
  double n;

  if (!parse_number (p, &n))
    return FALSE;

  if (n < 0)
    {
      *p = o;
      return FALSE;
    }

  *x = n;

  return TRUE;
}

/* This fixes a flaw in our use of strchr() below:
 *
 * If p already points at the end of the string,
 * we misinterpret strchr ("xyz", *p) returning
 * non-NULL to mean that we can increment p.
 *
 * But strchr() will return a pointer to the
 * final NUL byte in this case, and we walk off
 * the end of the string. Oops
 */
static inline char *
_strchr (const char *str,
           int         c)
{
  if (c == 0)
    return NULL;
  else
    return strchr (str, c);
}

static gboolean
parse_flag (const char **p,
            gboolean    *f)
{
  skip_whitespace (p);
  if (_strchr ("01", **p))
    {
      *f = **p == '1';
      (*p)++;
      skip_optional_comma (p);
      return TRUE;
    }

  return FALSE;
}

static gboolean
parse_command (const char **p,
               char        *cmd)
{
  char *s;
  const char *allowed;

  if (*cmd == 'X')
    allowed = "mM";
  else
    allowed = "mMhHvVzZlLcCsStTqQaAoO";

  skip_whitespace (p);
  s = _strchr (allowed, **p);
  if (s)
    {
      *cmd = *s;
      (*p)++;
      return TRUE;
    }
  return FALSE;
}

static gboolean
parse_string (const char **p,
              const char  *s)
{ 
  int len = strlen (s);
  if (strncmp (*p, s, len) != 0)
    return FALSE;
  (*p) += len;
  return TRUE;
}

#define NEAR(x, y) (fabs ((x) - (y)) < 0.001)

static gboolean
is_rect (double x0, double y0,
         double x1, double y1,
         double x2, double y2,
         double x3, double y3)
{
  return NEAR (x0, x3) && NEAR (x1, x2) &&
         NEAR (y0, y1) && NEAR (y2, y3) &&
         x0 < x1 && y1 < y2;
}

static gboolean
is_line (double x0, double y0,
         double x1, double y1,
         double x2, double y2,
         double x3, double y3)
{
  if (NEAR (y0, y3))
    return x0 <= x1 && x1 <= x2 && x2 <= x3 &&
           NEAR (y0, y1) && NEAR (y0, y2) && NEAR (y0, y3);
  else
    return y0 <= y1 && y1 <= y2 && y2 <= y3 &&
           NEAR (x0, x1) && NEAR (x0, x2) && NEAR (x0, x3);
}

static gboolean
parse_rectangle (const char **p,
                 double      *x,
                 double      *y,
                 double      *w,
                 double      *h)
{
  const char *o = *p;
  double w2;

  if (parse_coordinate_pair (p, x, y) &&
      parse_string (p, "h") &&
      parse_coordinate (p, w) &&
      parse_string (p, "v") &&
      parse_coordinate (p, h) &&
      parse_string (p, "h") &&
      parse_coordinate (p, &w2) &&
      parse_string (p, "z") &&
      w2 == -*w && *w >= 0 && *h >= 0)
    {
      skip_whitespace (p);

      return TRUE;
    }

  *p = o;
  return FALSE;
}

static gboolean
parse_circle (const char **p,
              double      *cx,
              double      *cy,
              double      *r)
{
  const char *o = *p;
  double x0, y0, x1, y1, x2, y2, x3, y3;
  double x4, y4, x5, y5, x6, y6, x7, y7;
  double x8, y8, w0, w1, w2, w3;
  double rr;

  if (parse_coordinate_pair (p, &x0, &y0) &&
      parse_string (p, "o") &&
      parse_coordinate_pair (p, &x1, &y1) &&
      parse_coordinate_pair (p, &x2, &y2) &&
      parse_nonnegative_number (p, &w0) &&
      parse_string (p, "o") &&
      parse_coordinate_pair (p, &x3, &y3) &&
      parse_coordinate_pair (p, &x4, &y4) &&
      parse_nonnegative_number (p, &w1) &&
      parse_string (p, "o") &&
      parse_coordinate_pair (p, &x5, &y5) &&
      parse_coordinate_pair (p, &x6, &y6) &&
      parse_nonnegative_number (p, &w2) &&
      parse_string (p, "o") &&
      parse_coordinate_pair (p, &x7, &y7) &&
      parse_coordinate_pair (p, &x8, &y8) &&
      parse_nonnegative_number (p, &w3) &&
      parse_string (p, "z"))
    {
      rr = y1;

      if (x1 == 0   && y1 == rr  &&
          x2 == -rr && y2 == rr  &&
          x3 == -rr && y3 == 0   &&
          x4 == -rr && y4 == -rr &&
          x5 == 0   && y5 == -rr &&
          x6 == rr  && y6 == -rr &&
          x7 == rr  && y7 == 0   &&
          x8 == rr  && y8 == rr &&
          NEAR (w0, M_SQRT1_2) && NEAR (w1, M_SQRT1_2) &&
          NEAR (w2, M_SQRT1_2) && NEAR (w3, M_SQRT1_2))
        {
          *cx = x0 - rr;
          *cy = y0;
          *r = rr;

          skip_whitespace (p);

          return TRUE;
        }
    }

  *p = o;
  return FALSE;
}

static gboolean
parse_rounded_rect (const char     **p,
                    GskRoundedRect  *rr)
{
  const char *o = *p;
  double x0, y0, x1, y1, x2, y2, x3, y3;
  double x4, y4, x5, y5, x6, y6, x7, y7;
  double x8, y8, x9, y9, x10, y10, x11, y11;
  double x12, y12, w0, w1, w2, w3;

  if (parse_coordinate_pair (p, &x0, &y0) &&
      parse_string (p, "L") &&
      parse_coordinate_pair (p, &x1, &y1) &&
      parse_string (p, "O") &&
      parse_coordinate_pair (p, &x2, &y2) &&
      parse_coordinate_pair (p, &x3, &y3) &&
      parse_nonnegative_number (p, &w0) &&
      parse_string (p, "L") &&
      parse_coordinate_pair (p, &x4, &y4) &&
      parse_string (p, "O") &&
      parse_coordinate_pair (p, &x5, &y5) &&
      parse_coordinate_pair (p, &x6, &y6) &&
      parse_nonnegative_number (p, &w1) &&
      parse_string (p, "L") &&
      parse_coordinate_pair (p, &x7, &y7) &&
      parse_string (p, "O") &&
      parse_coordinate_pair (p, &x8, &y8) &&
      parse_coordinate_pair (p, &x9, &y9) &&
      parse_nonnegative_number (p, &w2) &&
      parse_string (p, "L") &&
      parse_coordinate_pair (p, &x10, &y10) &&
      parse_string (p, "O") &&
      parse_coordinate_pair (p, &x11, &y11) &&
      parse_coordinate_pair (p, &x12, &y12) &&
      parse_nonnegative_number (p, &w3) &&
      parse_string (p, "Z"))
    {
      if (NEAR (x0, x12) && NEAR (y0, y12) &&
          is_rect (x11, y11, x2, y2, x5, y5, x8, y8) &&
          is_line (x11, y11, x0, y0, x1, y1, x2, y2) &&
          is_line (x2, y2, x3, y3, x4, y4, x5, y5) &&
          is_line (x8, y8, x7, y7, x6, y6, x5, y5) &&
          is_line (x11, y11, x10, y10, x9, y9, x8, y8) &&
          NEAR (w0, M_SQRT1_2) && NEAR (w1, M_SQRT1_2) &&
          NEAR (w2, M_SQRT1_2) && NEAR (w3, M_SQRT1_2))
        {
          rr->bounds = GRAPHENE_RECT_INIT (x11, y11, x5 - x11, y5 - y11);
          rr->corner[GSK_CORNER_TOP_LEFT] = GRAPHENE_SIZE_INIT (x12 - x11, y10 - y11);
          rr->corner[GSK_CORNER_TOP_RIGHT] = GRAPHENE_SIZE_INIT (x2 - x1, y3 - y2);
          rr->corner[GSK_CORNER_BOTTOM_RIGHT] = GRAPHENE_SIZE_INIT (x5 - x6, y5 - y4);
          rr->corner[GSK_CORNER_BOTTOM_LEFT] = GRAPHENE_SIZE_INIT (x7 - x8, y8 - y9);

          skip_whitespace (p);

          return TRUE;
        }
    }

  *p = o;
  return FALSE;
}

#undef NEAR

/**
 * gsk_path_parse:
 * @string: a string
 *
 * Constructs a path from a serialized form.
 *
 * The string is expected to be in (a superset of)
 * [SVG path syntax](https://www.w3.org/TR/SVG11/paths.html#PathData),
 * as e.g. produced by [method@Gsk.Path.to_string].
 *
 * A high-level summary of the syntax:
 *
 * - `M x y` Move to `(x, y)`
 * - `L x y` Add a line from the current point to `(x, y)`
 * - `Q x1 y1 x2 y2` Add a quadratic Bézier from the current point to `(x2, y2)`, with control point `(x1, y1)`
 * - `C x1 y1 x2 y2 x3 y3` Add a cubic Bézier from the current point to `(x3, y3)`, with control points `(x1, y1)` and `(x2, y2)`
 * - `Z` Close the contour by drawing a line back to the start point
 * - `H x` Add a horizontal line from the current point to the given x value
 * - `V y` Add a vertical line from the current point to the given y value
 * - `T x2 y2` Add a quadratic Bézier, using the reflection of the previous segments' control point as control point
 * - `S x2 y2 x3 y3` Add a cubic Bézier, using the reflection of the previous segments' second control point as first control point
 * - `A rx ry r l s x y` Add an elliptical arc from the current point to `(x, y)` with radii rx and ry. See the SVG documentation for how the other parameters influence the arc.
 * - `O x1 y1 x2 y2 w` Add a rational quadratic Bézier from the current point to `(x2, y2)` with control point `(x1, y1)` and weight `w`.
 *
 * All the commands have lowercase variants that interpret coordinates
 * relative to the current point.
 *
 * The `O` command is an extension that is not supported in SVG.
 *
 * Returns: (nullable): a new `GskPath`, or `NULL` if @string could not be parsed
 *
 * Since: 4.14
 */
GskPath *
gsk_path_parse (const char *string)
{
  GskPathBuilder *builder;
  double x, y; /* current point */
  double prev_x1, prev_y1;
  double path_x, path_y; /* start point of the current subpath */
  const char *p;
  char cmd;
  char prev_cmd;
  gboolean after_comma;
  gboolean repeat;

  builder = gsk_path_builder_new ();

  cmd = 'X';
  path_x = path_y = 0;
  x = y = 0;
  prev_x1 = prev_y1 = 0;
  after_comma = FALSE;

  p = string;
  while (*p)
    {
      prev_cmd = cmd;
      repeat = !parse_command (&p, &cmd);

      if (after_comma && !repeat)
        goto error;

      switch (cmd)
        {
        case 'X':
          goto error;

        case 'Z':
        case 'z':
          if (repeat)
            goto error;
          else
            {
              gsk_path_builder_close (builder);
              x = path_x;
              y = path_y;
            }
          break;

        case 'M':
        case 'm':
          {
            double x1, y1, w, h, r;
            GskRoundedRect rr;

            /* Look for special contours */
            if (parse_rectangle (&p, &x1, &y1, &w, &h))
              {
                if (cmd == 'm')
                  {
                    x1 += x;
                    y1 += y;
                  }
                gsk_path_builder_add_rect (builder, &GRAPHENE_RECT_INIT (x1, y1, w, h));
                path_x = x1;
                path_y = y1;

                cmd = 'z';
                x = x1;
                y = y1;
              }
            else if (parse_circle (&p, &x1, &y1, &r))
              {
                if (cmd == 'm')
                  {
                    x1 += x;
                    y1 += y;
                  }
                gsk_path_builder_add_circle (builder, &GRAPHENE_POINT_INIT (x1, y1), r);
                path_x = x1 + r;
                path_y = y1;

                cmd = 'z';
                x = x1 + r;
                y = y1;
              }
            else if (parse_rounded_rect (&p, &rr))
              {
                if (cmd == 'm')
                  {
                    rr.bounds.origin.x += x;
                    rr.bounds.origin.y += y;
                  }
                gsk_path_builder_add_rounded_rect (builder, &rr);

                path_x = rr.bounds.origin.x + rr.corner[GSK_CORNER_TOP_LEFT].width;
                path_y = rr.bounds.origin.y;

                cmd = 'Z';
                x = rr.bounds.origin.x + rr.corner[GSK_CORNER_TOP_LEFT].width;
                y = rr.bounds.origin.y;
              }
            else if (parse_coordinate_pair (&p, &x1, &y1))
              {
                if (cmd == 'm')
                  {
                    x1 += x;
                    y1 += y;
                  }

                if (repeat)
                  gsk_path_builder_line_to (builder, x1, y1);
                else
                  {
                    gsk_path_builder_move_to (builder, x1, y1);
                    path_x = x1;
                    path_y = y1;
                  }

                x = x1;
                y = y1;
              }
            else
              goto error;
          }
          break;

        case 'L':
        case 'l':
          {
            double x1, y1;

            if (parse_coordinate_pair (&p, &x1, &y1))
              {
                if (cmd == 'l')
                  {
                    x1 += x;
                    y1 += y;
                  }

                if (_strchr ("zZ", prev_cmd))
                  {
                    gsk_path_builder_move_to (builder, x, y);
                    path_x = x;
                    path_y = y;
                  }
                gsk_path_builder_line_to (builder, x1, y1);
                x = x1;
                y = y1;
              }
            else
              goto error;
          }
          break;

        case 'H':
        case 'h':
          {
            double x1;

            if (parse_coordinate (&p, &x1))
              {
                if (cmd == 'h')
                  x1 += x;
                if (_strchr ("zZ", prev_cmd))
                  {
                    gsk_path_builder_move_to (builder, x, y);
                    path_x = x;
                    path_y = y;
                  }
                gsk_path_builder_line_to (builder, x1, y);
                x = x1;
              }
            else
              goto error;
          }
          break;

        case 'V':
        case 'v':
          {
            double y1;

            if (parse_coordinate (&p, &y1))
              {
                if (cmd == 'v')
                  y1 += y;
                if (_strchr ("zZ", prev_cmd))
                  {
                    gsk_path_builder_move_to (builder, x, y);
                    path_x = x;
                    path_y = y;
                  }
                gsk_path_builder_line_to (builder, x, y1);
                y = y1;
              }
            else
              goto error;
          }
          break;

        case 'C':
        case 'c':
          {
            double x0, y0, x1, y1, x2, y2;

            if (parse_coordinate_pair (&p, &x0, &y0) &&
                parse_coordinate_pair (&p, &x1, &y1) &&
                parse_coordinate_pair (&p, &x2, &y2))
              {
                if (cmd == 'c')
                  {
                    x0 += x;
                    y0 += y;
                    x1 += x;
                    y1 += y;
                    x2 += x;
                    y2 += y;
                  }
                if (_strchr ("zZ", prev_cmd))
                  {
                    gsk_path_builder_move_to (builder, x, y);
                    path_x = x;
                    path_y = y;
                  }
                gsk_path_builder_cubic_to (builder, x0, y0, x1, y1, x2, y2);
                prev_x1 = x1;
                prev_y1 = y1;
                x = x2;
                y = y2;
              }
            else
              goto error;
          }
          break;

        case 'S':
        case 's':
          {
            double x0, y0, x1, y1, x2, y2;

            if (parse_coordinate_pair (&p, &x1, &y1) &&
                parse_coordinate_pair (&p, &x2, &y2))
              {
                if (cmd == 's')
                  {
                    x1 += x;
                    y1 += y;
                    x2 += x;
                    y2 += y;
                  }
                if (_strchr ("CcSs", prev_cmd))
                  {
                    x0 = 2 * x - prev_x1;
                    y0 = 2 * y - prev_y1;
                  }
                else
                  {
                    x0 = x;
                    y0 = y;
                  }
                if (_strchr ("zZ", prev_cmd))
                  {
                    gsk_path_builder_move_to (builder, x, y);
                    path_x = x;
                    path_y = y;
                  }
                gsk_path_builder_cubic_to (builder, x0, y0, x1, y1, x2, y2);
                prev_x1 = x1;
                prev_y1 = y1;
                x = x2;
                y = y2;
              }
            else
              goto error;
          }
          break;

        case 'Q':
        case 'q':
          {
            double x1, y1, x2, y2;

            if (parse_coordinate_pair (&p, &x1, &y1) &&
                parse_coordinate_pair (&p, &x2, &y2))
              {
                if (cmd == 'q')
                  {
                    x1 += x;
                    y1 += y;
                    x2 += x;
                    y2 += y;
                  }
                if (_strchr ("zZ", prev_cmd))
                  {
                    gsk_path_builder_move_to (builder, x, y);
                    path_x = x;
                    path_y = y;
                  }
                gsk_path_builder_quad_to (builder, x1, y1, x2, y2);
                prev_x1 = x1;
                prev_y1 = y1;
                x = x2;
                y = y2;
              }
            else
              goto error;
          }
          break;

        case 'T':
        case 't':
          {
            double x1, y1, x2, y2;

            if (parse_coordinate_pair (&p, &x2, &y2))
              {
                if (cmd == 't')
                  {
                    x2 += x;
                    y2 += y;
                  }
                if (_strchr ("QqTt", prev_cmd))
                  {
                    x1 = 2 * x - prev_x1;
                    y1 = 2 * y - prev_y1;
                  }
                else
                  {
                    x1 = x;
                    y1 = y;
                  }
                if (_strchr ("zZ", prev_cmd))
                  {
                    gsk_path_builder_move_to (builder, x, y);
                    path_x = x;
                    path_y = y;
                  }
                gsk_path_builder_quad_to (builder, x1, y1, x2, y2);
                prev_x1 = x1;
                prev_y1 = y1;
                x = x2;
                y = y2;
              }
            else
              goto error;
          }
          break;

        case 'O':
        case 'o':
          {
            double x1, y1, x2, y2, weight;

            if (parse_coordinate_pair (&p, &x1, &y1) &&
                parse_coordinate_pair (&p, &x2, &y2) &&
                parse_nonnegative_number (&p, &weight))
              {
                if (cmd == 'o')
                  {
                    x1 += x;
                    y1 += y;
                    x2 += x;
                    y2 += y;
                  }
                if (_strchr ("zZ", prev_cmd))
                  {
                    gsk_path_builder_move_to (builder, x, y);
                    path_x = x;
                    path_y = y;
                  }
                gsk_path_builder_conic_to (builder, x1, y1, x2, y2, weight);
                x = x2;
                y = y2;
              }
            else
              goto error;
          }
          break;

        case 'A':
        case 'a':
          {
            double rx, ry;
            double x_axis_rotation;
            int large_arc, sweep;
            double x1, y1;

            if (parse_nonnegative_number (&p, &rx) &&
                parse_nonnegative_number (&p, &ry) &&
                parse_number (&p, &x_axis_rotation) &&
                parse_flag (&p, &large_arc) &&
                parse_flag (&p, &sweep) &&
                parse_coordinate_pair (&p, &x1, &y1))
              {
                if (cmd == 'a')
                  {
                    x1 += x;
                    y1 += y;
                  }

                if (_strchr ("zZ", prev_cmd))
                  {
                    gsk_path_builder_move_to (builder, x, y);
                    path_x = x;
                    path_y = y;
                  }
                gsk_path_builder_svg_arc_to (builder,
                                             rx, ry, x_axis_rotation,
                                             large_arc, sweep,
                                             x1, y1);
                x = x1;
                y = y1;
              }
            else
              goto error;
          }
          break;

        default:
          goto error;
        }

      after_comma = (p > string) && p[-1] == ',';
    }

  if (after_comma)
    goto error;

  return gsk_path_builder_free_to_path (builder);

error:
  //g_warning ("Can't parse string '%s' as GskPath, error at %ld", string, p - string);
  gsk_path_builder_unref (builder);

  return NULL;
}

/* vim:set foldmethod=marker: */
