/*************************************************************************
 * Copyright (c) 2011 AT&T Intellectual Property
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html
 *
 * Contributors: Details at https://graphviz.org
 *************************************************************************/

#include "config.h"

#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <util/agxbuf.h>
#include <util/alloc.h>
#include <util/gv_ctype.h>
#include <util/prisize_t.h>
#include <util/unreachable.h>
#include <xdot/xdot.h>

/* the parse functions should return NULL on error */
static char *parseReal(char *s, double *fp) {
  char *p;
  double d;

  d = strtod(s, &p);
  if (p == s)
    return 0;

  *fp = d;
  return p;
}

static char *parseInt(char *s, int *ip) {
  char *endp;

  *ip = (int)strtol(s, &endp, 10);
  if (s == endp)
    return 0;
  else
    return endp;
}

static char *parseUInt(char *s, unsigned int *ip) {
  char *endp;

  *ip = (unsigned int)strtoul(s, &endp, 10);
  if (s == endp)
    return 0;
  else
    return endp;
}

static char *parseRect(char *s, xdot_rect *rp) {
  char *endp;

  rp->x = strtod(s, &endp);
  if (s == endp)
    return 0;
  else
    s = endp;

  rp->y = strtod(s, &endp);
  if (s == endp)
    return 0;
  else
    s = endp;

  rp->w = strtod(s, &endp);
  if (s == endp)
    return 0;
  else
    s = endp;

  rp->h = strtod(s, &endp);
  if (s == endp)
    return 0;
  else
    s = endp;

  return s;
}

static char *parsePolyline(char *s, xdot_polyline *pp) {
  unsigned i;
  xdot_point *pts;
  xdot_point *ps;
  char *endp;

  s = parseUInt(s, &i);
  if (!s)
    return NULL;
  pts = ps = gv_calloc(i, sizeof(ps[0]));
  pp->cnt = i;
  for (i = 0; i < pp->cnt; i++) {
    ps->x = strtod(s, &endp);
    if (s == endp) {
      free(pts);
      return NULL;
    } else {
      s = endp;
    }
    ps->y = strtod(s, &endp);
    if (s == endp) {
      free(pts);
      return NULL;
    } else {
      s = endp;
    }
    ps->z = 0;
    ps++;
  }
  pp->pts = pts;
  return s;
}

static char *parseString(char *s, char **sp) {
  int i;
  s = parseInt(s, &i);
  if (!s || i <= 0)
    return 0;
  while (*s && *s != '-')
    s++;
  if (*s) {
    s++;
  } else {
    return 0;
  }

  // The leading number we just read indicates the count of originating
  // characters in the upcoming string. But the string may contain \-escaped
  // characters. So the count alone does not tell us how many bytes we need to
  // now read.
  agxbuf c = {0};
  int j = 0;
  for (int accounted = 0; accounted < i; ++j) {
    if (s[j] == '\0') {
      agxbfree(&c);
      return 0;
    }
    agxbputc(&c, s[j]);
    // only count this character if it was not an escape prefix
    if (s[j] != '\\' || (j > 0 && s[j - 1] == '\\')) {
      ++accounted;
    }
  }

  *sp = agxbdisown(&c);
  return s + j;
}

static char *parseAlign(char *s, xdot_align *ap) {
  int i;
  s = parseInt(s, &i);

  if (i < 0)
    *ap = xd_left;
  else if (i > 0)
    *ap = xd_right;
  else
    *ap = xd_center;
  return s;
}

#define CHK(s)                                                                 \
  if (!s) {                                                                    \
    *error = 1;                                                                \
    return 0;                                                                  \
  }

static char *parseOp(xdot_op *op, char *s, drawfunc_t ops[], int *error) {
  char *cs;
  xdot_color clr;

  *error = 0;
  while (gv_isspace(*s))
    s++;
  switch (*s++) {
  case 'E':
    op->kind = xd_filled_ellipse;
    s = parseRect(s, &op->u.ellipse);
    CHK(s);
    if (ops)
      op->drawfunc = ops[xop_ellipse];
    break;

  case 'e':
    op->kind = xd_unfilled_ellipse;
    s = parseRect(s, &op->u.ellipse);
    CHK(s);
    if (ops)
      op->drawfunc = ops[xop_ellipse];
    break;

  case 'P':
    op->kind = xd_filled_polygon;
    s = parsePolyline(s, &op->u.polygon);
    CHK(s);
    if (ops)
      op->drawfunc = ops[xop_polygon];
    break;

  case 'p':
    op->kind = xd_unfilled_polygon;
    s = parsePolyline(s, &op->u.polygon);
    CHK(s);
    if (ops)
      op->drawfunc = ops[xop_polygon];
    break;

  case 'b':
    op->kind = xd_filled_bezier;
    s = parsePolyline(s, &op->u.bezier);
    CHK(s);
    if (ops)
      op->drawfunc = ops[xop_bezier];
    break;

  case 'B':
    op->kind = xd_unfilled_bezier;
    s = parsePolyline(s, &op->u.bezier);
    CHK(s);
    if (ops)
      op->drawfunc = ops[xop_bezier];
    break;

  case 'c':
    s = parseString(s, &cs);
    CHK(s);
    cs = parseXDotColor(cs, &clr);
    CHK(cs);
    if (clr.type == xd_none) {
      op->kind = xd_pen_color;
      op->u.color = clr.u.clr;
      if (ops)
        op->drawfunc = ops[xop_pen_color];
    } else {
      op->kind = xd_grad_pen_color;
      op->u.grad_color = clr;
      if (ops)
        op->drawfunc = ops[xop_grad_color];
    }
    break;

  case 'C':
    s = parseString(s, &cs);
    CHK(s);
    cs = parseXDotColor(cs, &clr);
    CHK(cs);
    if (clr.type == xd_none) {
      op->kind = xd_fill_color;
      op->u.color = clr.u.clr;
      if (ops)
        op->drawfunc = ops[xop_fill_color];
    } else {
      op->kind = xd_grad_fill_color;
      op->u.grad_color = clr;
      if (ops)
        op->drawfunc = ops[xop_grad_color];
    }
    break;

  case 'L':
    op->kind = xd_polyline;
    s = parsePolyline(s, &op->u.polyline);
    CHK(s);
    if (ops)
      op->drawfunc = ops[xop_polyline];
    break;

  case 'T':
    op->kind = xd_text;
    s = parseReal(s, &op->u.text.x);
    CHK(s);
    s = parseReal(s, &op->u.text.y);
    CHK(s);
    s = parseAlign(s, &op->u.text.align);
    CHK(s);
    s = parseReal(s, &op->u.text.width);
    CHK(s);
    s = parseString(s, &op->u.text.text);
    CHK(s);
    if (ops)
      op->drawfunc = ops[xop_text];
    break;

  case 'F':
    op->kind = xd_font;
    s = parseReal(s, &op->u.font.size);
    CHK(s);
    s = parseString(s, &op->u.font.name);
    CHK(s);
    if (ops)
      op->drawfunc = ops[xop_font];
    break;

  case 'S':
    op->kind = xd_style;
    s = parseString(s, &op->u.style);
    CHK(s);
    if (ops)
      op->drawfunc = ops[xop_style];
    break;

  case 'I':
    op->kind = xd_image;
    s = parseRect(s, &op->u.image.pos);
    CHK(s);
    s = parseString(s, &op->u.image.name);
    CHK(s);
    if (ops)
      op->drawfunc = ops[xop_image];
    break;

  case 't':
    op->kind = xd_fontchar;
    s = parseUInt(s, &op->u.fontchar);
    CHK(s);
    if (ops)
      op->drawfunc = ops[xop_fontchar];
    break;

  case '\0':
    s = 0;
    break;

  default:
    *error = 1;
    s = 0;
    break;
  }
  return s;
}

#define XDBSIZE 100

/* parseXDotFOn:
 * Parse and append additional xops onto a given xdot object.
 * Return x.
 */
xdot *parseXDotFOn(char *s, drawfunc_t fns[], size_t sz, xdot *x) {
  xdot_op op;
  char *ops;
  size_t oldsz, bufsz;
  int error;

  if (!s)
    return x;

  if (!x) {
    x = gv_alloc(sizeof(*x));
    if (sz <= sizeof(xdot_op))
      sz = sizeof(xdot_op);

    /* cnt, freefunc, ops, flags zeroed by gv_alloc */
    x->sz = sz;
  }
  size_t initcnt = x->cnt;
  sz = x->sz;

  if (initcnt == 0) {
    bufsz = XDBSIZE;
    ops = gv_calloc(XDBSIZE, sz);
  } else {
    bufsz = initcnt + XDBSIZE;
    ops = gv_recalloc(x->ops, initcnt, bufsz, sz);
  }

  while ((s = parseOp(&op, s, fns, &error))) {
    if (x->cnt == bufsz) {
      oldsz = bufsz;
      bufsz *= 2;
      ops = gv_recalloc(ops, oldsz, bufsz, sz);
    }
    *(xdot_op *)(ops + x->cnt * sz) = op;
    x->cnt++;
  }
  if (error)
    x->flags |= XDOT_PARSE_ERROR;
  if (x->cnt) {
    x->ops = gv_recalloc(ops, bufsz, x->cnt, sz);
  } else {
    free(ops);
    free(x);
    x = NULL;
  }

  return x;
}

xdot *parseXDotF(char *s, drawfunc_t fns[], size_t sz) {
  return parseXDotFOn(s, fns, sz, NULL);
}

xdot *parseXDot(char *s) { return parseXDotF(s, 0, 0); }

typedef int (*pf)(void *, char *, ...);

static void printRect(xdot_rect *r, pf print, void *info) {
  agxbuf buf = {0};

  agxbprint(&buf, " %.02f", r->x);
  agxbuf_trim_zeros(&buf);
  print(info, "%s", agxbuse(&buf));
  agxbprint(&buf, " %.02f", r->y);
  agxbuf_trim_zeros(&buf);
  print(info, "%s", agxbuse(&buf));
  agxbprint(&buf, " %.02f", r->w);
  agxbuf_trim_zeros(&buf);
  print(info, "%s", agxbuse(&buf));
  agxbprint(&buf, " %.02f", r->h);
  agxbuf_trim_zeros(&buf);
  print(info, "%s", agxbuse(&buf));
  agxbfree(&buf);
}

static void printPolyline(xdot_polyline *p, pf print, void *info) {
  agxbuf buf = {0};

  print(info, " %" PRISIZE_T, p->cnt);
  for (size_t i = 0; i < p->cnt; i++) {
    agxbprint(&buf, " %.02f", p->pts[i].x);
    agxbuf_trim_zeros(&buf);
    print(info, "%s", agxbuse(&buf));
    agxbprint(&buf, " %.02f", p->pts[i].y);
    agxbuf_trim_zeros(&buf);
    print(info, "%s", agxbuse(&buf));
  }
  agxbfree(&buf);
}

static void printString(char *p, pf print, void *info) {
  print(info, " %" PRISIZE_T " -%s", strlen(p), p);
}

static void printFloat(double f, pf print, void *info, int space) {
  agxbuf buf = {0};

  if (space)
    agxbprint(&buf, " %.02f", f);
  else
    agxbprint(&buf, "%.02f", f);
  agxbuf_trim_zeros(&buf);
  print(info, "%s", agxbuse(&buf));
  agxbfree(&buf);
}

static void printAlign(xdot_align a, pf print, void *info) {
  switch (a) {
  case xd_left:
    print(info, " -1");
    break;
  case xd_right:
    print(info, " 1");
    break;
  case xd_center:
    print(info, " 0");
    break;
  default:
    UNREACHABLE();
  }
}

/// wrapper to translate `pf` calling convention to `agxbprint`
static int pf_agxbprint(void *xb, char *fmt, ...) {
  va_list ap;
  va_start(ap, fmt);
  const int result = vagxbprint(xb, fmt, ap);
  va_end(ap);
  return result;
}

static void toGradString(agxbuf *xb, xdot_color *cp) {
  int i, n_stops;
  xdot_color_stop *stops;

  if (cp->type == xd_linear) {
    agxbputc(xb, '[');
    printFloat(cp->u.ling.x0, pf_agxbprint, xb, 0);
    printFloat(cp->u.ling.y0, pf_agxbprint, xb, 1);
    printFloat(cp->u.ling.x1, pf_agxbprint, xb, 1);
    printFloat(cp->u.ling.y1, pf_agxbprint, xb, 1);
    n_stops = cp->u.ling.n_stops;
    stops = cp->u.ling.stops;
  } else {
    agxbputc(xb, '(');
    printFloat(cp->u.ring.x0, pf_agxbprint, xb, 0);
    printFloat(cp->u.ring.y0, pf_agxbprint, xb, 1);
    printFloat(cp->u.ring.r0, pf_agxbprint, xb, 1);
    printFloat(cp->u.ring.x1, pf_agxbprint, xb, 1);
    printFloat(cp->u.ring.y1, pf_agxbprint, xb, 1);
    printFloat(cp->u.ring.r1, pf_agxbprint, xb, 1);
    n_stops = cp->u.ring.n_stops;
    stops = cp->u.ring.stops;
  }
  agxbprint(xb, " %d", n_stops);
  for (i = 0; i < n_stops; i++) {
    printFloat(stops[i].frac, pf_agxbprint, xb, 1);
    printString(stops[i].color, pf_agxbprint, xb);
  }

  if (cp->type == xd_linear)
    agxbputc(xb, ']');
  else
    agxbputc(xb, ')');
}

typedef void (*print_op)(xdot_op *op, pf print, void *info, int more);

static void printXDot_Op(xdot_op *op, pf print, void *info, int more) {
  agxbuf xb = {0};
  switch (op->kind) {
  case xd_filled_ellipse:
    print(info, "E");
    printRect(&op->u.ellipse, print, info);
    break;
  case xd_unfilled_ellipse:
    print(info, "e");
    printRect(&op->u.ellipse, print, info);
    break;
  case xd_filled_polygon:
    print(info, "P");
    printPolyline(&op->u.polygon, print, info);
    break;
  case xd_unfilled_polygon:
    print(info, "p");
    printPolyline(&op->u.polygon, print, info);
    break;
  case xd_filled_bezier:
    print(info, "b");
    printPolyline(&op->u.bezier, print, info);
    break;
  case xd_unfilled_bezier:
    print(info, "B");
    printPolyline(&op->u.bezier, print, info);
    break;
  case xd_pen_color:
    print(info, "c");
    printString(op->u.color, print, info);
    break;
  case xd_grad_pen_color:
    print(info, "c");
    toGradString(&xb, &op->u.grad_color);
    printString(agxbuse(&xb), print, info);
    break;
  case xd_fill_color:
    print(info, "C");
    printString(op->u.color, print, info);
    break;
  case xd_grad_fill_color:
    print(info, "C");
    toGradString(&xb, &op->u.grad_color);
    printString(agxbuse(&xb), print, info);
    break;
  case xd_polyline:
    print(info, "L");
    printPolyline(&op->u.polyline, print, info);
    break;
  case xd_text:
    print(info, "T %.f %.f", op->u.text.x, op->u.text.y);
    printAlign(op->u.text.align, print, info);
    print(info, " %.f", op->u.text.width);
    printString(op->u.text.text, print, info);
    break;
  case xd_font:
    print(info, "F");
    printFloat(op->u.font.size, print, info, 1);
    printString(op->u.font.name, print, info);
    break;
  case xd_fontchar:
    print(info, "t %u", op->u.fontchar);
    break;
  case xd_style:
    print(info, "S");
    printString(op->u.style, print, info);
    break;
  case xd_image:
    print(info, "I");
    printRect(&op->u.image.pos, print, info);
    printString(op->u.image.name, print, info);
    break;
  default: // invalid type; ignore
    break;
  }
  if (more)
    print(info, " ");
  agxbfree(&xb);
}

static void jsonRect(xdot_rect *r, pf print, void *info) {
  print(info, "[%.06f,%.06f,%.06f,%.06f]", r->x, r->y, r->w, r->h);
}

static void jsonPolyline(xdot_polyline *p, pf print, void *info) {
  print(info, "[");
  for (size_t i = 0; i < p->cnt; i++) {
    print(info, "%.06f,%.06f", p->pts[i].x, p->pts[i].y);
    if (i < p->cnt - 1)
      print(info, ",");
  }
  print(info, "]");
}

static void jsonString(char *p, pf print, void *info) {
  char c;

  print(info, "\"");
  while ((c = *p++)) {
    if (c == '"')
      print(info, "\\\"");
    else if (c == '\\')
      print(info, "\\\\");
    else
      print(info, "%c", c);
  }
  print(info, "\"");
}

static void jsonXDot_Op(xdot_op *op, pf print, void *info, int more) {
  agxbuf xb = {0};
  switch (op->kind) {
  case xd_filled_ellipse:
    print(info, "{\"E\" : ");
    jsonRect(&op->u.ellipse, print, info);
    break;
  case xd_unfilled_ellipse:
    print(info, "{\"e\" : ");
    jsonRect(&op->u.ellipse, print, info);
    break;
  case xd_filled_polygon:
    print(info, "{\"P\" : ");
    jsonPolyline(&op->u.polygon, print, info);
    break;
  case xd_unfilled_polygon:
    print(info, "{\"p\" : ");
    jsonPolyline(&op->u.polygon, print, info);
    break;
  case xd_filled_bezier:
    print(info, "{\"b\" : ");
    jsonPolyline(&op->u.bezier, print, info);
    break;
  case xd_unfilled_bezier:
    print(info, "{\"B\" : ");
    jsonPolyline(&op->u.bezier, print, info);
    break;
  case xd_pen_color:
    print(info, "{\"c\" : ");
    jsonString(op->u.color, print, info);
    break;
  case xd_grad_pen_color:
    print(info, "{\"c\" : ");
    toGradString(&xb, &op->u.grad_color);
    jsonString(agxbuse(&xb), print, info);
    break;
  case xd_fill_color:
    print(info, "{\"C\" : ");
    jsonString(op->u.color, print, info);
    break;
  case xd_grad_fill_color:
    print(info, "{\"C\" : ");
    toGradString(&xb, &op->u.grad_color);
    jsonString(agxbuse(&xb), print, info);
    break;
  case xd_polyline:
    print(info, "{\"L\" :");
    jsonPolyline(&op->u.polyline, print, info);
    break;
  case xd_text:
    print(info, "{\"T\" : [ %.f, %.f,", op->u.text.x, op->u.text.y);
    printAlign(op->u.text.align, print, info);
    print(info, ", %.f,", op->u.text.width);
    jsonString(op->u.text.text, print, info);
    print(info, "]");
    break;
  case xd_font:
    print(info, "{\"F\" : [");
    op->kind = xd_font;
    printFloat(op->u.font.size, print, info, 1);
    print(info, ",");
    jsonString(op->u.font.name, print, info);
    print(info, "]");
    break;
  case xd_fontchar:
    print(info, "{\"t\" :  %u", op->u.fontchar);
    break;
  case xd_style:
    print(info, "{\"S\" : ");
    jsonString(op->u.style, print, info);
    break;
  case xd_image:
    print(info, "{\"I\" : [");
    jsonRect(&op->u.image.pos, print, info);
    print(info, ",");
    jsonString(op->u.image.name, print, info);
    print(info, "]");
    break;
  default: // invalid type; ignore
    break;
  }
  if (more)
    print(info, "},\n");
  else
    print(info, "}\n");
  agxbfree(&xb);
}

static void _printXDot(xdot *x, pf print, void *info, print_op ofn) {
  xdot_op *op;
  char *base = (char *)x->ops;
  for (size_t i = 0; i < x->cnt; i++) {
    op = (xdot_op *)(base + i * x->sz);
    ofn(op, print, info, i < x->cnt - 1);
  }
}

char *sprintXDot(xdot *x) {
  agxbuf xb = {0};
  _printXDot(x, pf_agxbprint, &xb, printXDot_Op);
  return agxbdisown(&xb);
}

/// wrapper to translate `pf` calling convention to `fprintf`
static int pf_fprintf(void *stream, char *format, ...) {
  va_list ap;
  va_start(ap, format);
  const int r = vfprintf(stream, format, ap);
  va_end(ap);
  return r;
}

void fprintXDot(FILE *fp, xdot *x) {
  _printXDot(x, pf_fprintf, fp, printXDot_Op);
}

void jsonXDot(FILE *fp, xdot *x) {
  fputs("[\n", fp);
  _printXDot(x, pf_fprintf, fp, jsonXDot_Op);
  fputs("]\n", fp);
}

static void freeXOpData(xdot_op *x) {
  switch (x->kind) {
  case xd_filled_polygon:
  case xd_unfilled_polygon:
    free(x->u.polyline.pts);
    break;
  case xd_filled_bezier:
  case xd_unfilled_bezier:
    free(x->u.polyline.pts);
    break;
  case xd_polyline:
    free(x->u.polyline.pts);
    break;
  case xd_text:
    free(x->u.text.text);
    break;
  case xd_fill_color:
  case xd_pen_color:
    free(x->u.color);
    break;
  case xd_grad_fill_color:
  case xd_grad_pen_color:
    freeXDotColor(&x->u.grad_color);
    break;
  case xd_font:
    free(x->u.font.name);
    break;
  case xd_style:
    free(x->u.style);
    break;
  case xd_image:
    free(x->u.image.name);
    break;
  default:
    break;
  }
}

void freeXDot(xdot *x) {
  if (!x)
    return;
  freefunc_t ff = x->freefunc;
  char *const base = (char *)x->ops;
  for (size_t i = 0; i < x->cnt; i++) {
    void *const op = base + i * x->sz;
    if (ff)
      ff(op);
    freeXOpData(op);
  }
  free(base);
  free(x);
}

int statXDot(xdot *x, xdot_stats *sp) {
  xdot_op *op;
  char *base;

  if (!x || !sp)
    return 1;
  *sp = (xdot_stats){0};
  sp->cnt = x->cnt;
  base = (char *)x->ops;
  for (size_t i = 0; i < x->cnt; i++) {
    op = (xdot_op *)(base + i * x->sz);
    switch (op->kind) {
    case xd_filled_ellipse:
    case xd_unfilled_ellipse:
      sp->n_ellipse++;
      break;
    case xd_filled_polygon:
    case xd_unfilled_polygon:
      sp->n_polygon++;
      sp->n_polygon_pts += op->u.polygon.cnt;
      break;
    case xd_filled_bezier:
    case xd_unfilled_bezier:
      sp->n_bezier++;
      sp->n_bezier_pts += op->u.bezier.cnt;
      break;
    case xd_polyline:
      sp->n_polyline++;
      sp->n_polyline_pts += op->u.polyline.cnt;
      break;
    case xd_text:
      sp->n_text++;
      break;
    case xd_image:
      sp->n_image++;
      break;
    case xd_fill_color:
    case xd_pen_color:
      sp->n_color++;
      break;
    case xd_grad_fill_color:
    case xd_grad_pen_color:
      sp->n_gradcolor++;
      break;
    case xd_font:
      sp->n_font++;
      break;
    case xd_fontchar:
      sp->n_fontchar++;
      break;
    case xd_style:
      sp->n_style++;
      break;
    default:
      break;
    }
  }

  return 0;
}

#define CHK1(s)                                                                \
  if (!s) {                                                                    \
    free(stops);                                                               \
    return NULL;                                                               \
  }

/* radGradient:
 * Parse radial gradient spec
 * Return NULL on failure.
 */
static char *radGradient(char *cp, xdot_color *clr) {
  char *s = cp;
  int i;
  double d;
  xdot_color_stop *stops = NULL;

  clr->type = xd_radial;
  s = parseReal(s, &clr->u.ring.x0);
  CHK1(s);
  s = parseReal(s, &clr->u.ring.y0);
  CHK1(s);
  s = parseReal(s, &clr->u.ring.r0);
  CHK1(s);
  s = parseReal(s, &clr->u.ring.x1);
  CHK1(s);
  s = parseReal(s, &clr->u.ring.y1);
  CHK1(s);
  s = parseReal(s, &clr->u.ring.r1);
  CHK1(s);
  s = parseInt(s, &clr->u.ring.n_stops);
  CHK1(s);

  stops = gv_calloc(clr->u.ring.n_stops, sizeof(stops[0]));
  for (i = 0; i < clr->u.ring.n_stops; i++) {
    s = parseReal(s, &d);
    CHK1(s);
    stops[i].frac = d;
    s = parseString(s, &stops[i].color);
    CHK1(s);
  }
  clr->u.ring.stops = stops;

  return cp;
}

/* linGradient:
 * Parse linear gradient spec
 * Return NULL on failure.
 */
static char *linGradient(char *cp, xdot_color *clr) {
  char *s = cp;
  int i;
  double d;
  xdot_color_stop *stops = NULL;

  clr->type = xd_linear;
  s = parseReal(s, &clr->u.ling.x0);
  CHK1(s);
  s = parseReal(s, &clr->u.ling.y0);
  CHK1(s);
  s = parseReal(s, &clr->u.ling.x1);
  CHK1(s);
  s = parseReal(s, &clr->u.ling.y1);
  CHK1(s);
  s = parseInt(s, &clr->u.ling.n_stops);
  CHK1(s);

  stops = gv_calloc(clr->u.ling.n_stops, sizeof(stops[0]));
  for (i = 0; i < clr->u.ling.n_stops; i++) {
    s = parseReal(s, &d);
    CHK1(s);
    stops[i].frac = d;
    s = parseString(s, &stops[i].color);
    CHK1(s);
  }
  clr->u.ling.stops = stops;

  return cp;
}

/* parseXDotColor:
 * Parse xdot color spec: ordinary or gradient
 * The result is stored in clr.
 * Return NULL on failure.
 */
char *parseXDotColor(char *cp, xdot_color *clr) {
  char c = *cp;

  switch (c) {
  case '[':
    return linGradient(cp + 1, clr);
  case '(':
    return radGradient(cp + 1, clr);
  case '#':
  case '/':
    clr->type = xd_none;
    clr->u.clr = cp;
    return cp;
  default:
    if (gv_isalnum(c)) {
      clr->type = xd_none;
      clr->u.clr = cp;
      return cp;
    }
    return NULL;
  }
}

void freeXDotColor(xdot_color *cp) {
  int i;

  if (cp->type == xd_linear) {
    for (i = 0; i < cp->u.ling.n_stops; i++) {
      free(cp->u.ling.stops[i].color);
    }
    free(cp->u.ling.stops);
  } else if (cp->type == xd_radial) {
    for (i = 0; i < cp->u.ring.n_stops; i++) {
      free(cp->u.ring.stops[i].color);
    }
    free(cp->u.ring.stops);
  }
}

/**
 * @dir lib/xdot
 * @brief parsing and deparsing of xdot operations, API xdot.h
 *
 * [man 3 xdot](https://graphviz.org/pdf/xdot.3.pdf)
 *
 */
