import assert from "../assert";
import { quadBezierSegmentAA, quadBezierSegment } from "./quadratic";
import { line, lineAA } from "../line";
import { setPixelAlphaFn, setPixelFn } from "../types";

/**
 * Plot limited cubic Bezier segment
 * @param  {number} x0
 * @param  {number} y0
 * @param  {number} x1
 * @param  {number} y1
 * @param  {number} x2
 * @param  {number} y2
 * @param  {number} x3
 * @param  {number} y3
 * @param  {setPixel} setPixel
 */
export function cubicBezierSegment(
  x0: number,
  y0: number,
  x1: number,
  y1: number,
  x2: number,
  y2: number,
  x3: number,
  y3: number,
  setPixel: setPixelFn
) {
  var f: number,
    fx: number,
    fy: number,
    leg = 1;
  let sx = x0 < x3 ? 1 : -1;
  let sy = y0 < y3 ? 1 : -1; /* step direction */
  let xc = -Math.abs(x0 + x1 - x2 - x3);
  let xa = xc - 4 * sx * (x1 - x2);
  let xb = sx * (x0 - x1 - x2 + x3);
  let yc = -Math.abs(y0 + y1 - y2 - y3);
  let ya = yc - 4 * sy * (y1 - y2);
  let yb = sy * (y0 - y1 - y2 + y3);
  let ab: number,
    ac: number,
    bc: number,
    cb: number,
    xx: number,
    xy: number,
    yy: number,
    dx: number,
    dy: number,
    ex: number,
    pxy: number,
    EP = 0.01;
  /* check for curve restrains */
  /* slope P0-P1 == P2-P3    and  (P0-P3 == P1-P2      or   no slope change) */
  assert(
    (x1 - x0) * (x2 - x3) < EP &&
      ((x3 - x0) * (x1 - x2) < EP || xb * xb < xa * xc + EP),
    "slope change"
  );
  assert(
    (y1 - y0) * (y2 - y3) < EP &&
      ((y3 - y0) * (y1 - y2) < EP || yb * yb < ya * yc + EP),
    "slope change"
  );

  if (xa == 0 && ya == 0) {
    /* quadratic Bezier */
    sx = Math.floor((3 * x1 - x0 + 1) / 2);
    sy = Math.floor((3 * y1 - y0 + 1) / 2); /* new midpoint */
    return quadBezierSegment(x0, y0, sx, sy, x3, y3, setPixel);
  }
  x1 = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0) + 1; /* line lengths */
  x2 = (x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3) + 1;
  do {
    /* loop over both ends */
    ab = xa * yb - xb * ya;
    ac = xa * yc - xc * ya;
    bc = xb * yc - xc * yb;
    ex =
      ab * (ab + ac - 3 * bc) +
      ac * ac; /* P0 part of self-intersection loop? */
    f = ex > 0 ? 1 : Math.sqrt(1 + 1024 / x1); /* calculate resolution */
    ab *= f;
    ac *= f;
    bc *= f;
    ex *= f * f; /* increase resolution */
    xy = (9 * (ab + ac + bc)) / 8;
    cb = 8 * (xa - ya); /* init differences of 1st degree */
    dx =
      (27 * (8 * ab * (yb * yb - ya * yc) + ex * (ya + 2 * yb + yc))) / 64 -
      ya * ya * (xy - ya);
    dy =
      (27 * (8 * ab * (xb * xb - xa * xc) - ex * (xa + 2 * xb + xc))) / 64 -
      xa * xa * (xy + xa);
    /* init differences of 2nd degree */
    xx =
      (3 *
        (3 * ab * (3 * yb * yb - ya * ya - 2 * ya * yc) -
          ya * (3 * ac * (ya + yb) + ya * cb))) /
      4;
    yy =
      (3 *
        (3 * ab * (3 * xb * xb - xa * xa - 2 * xa * xc) -
          xa * (3 * ac * (xa + xb) + xa * cb))) /
      4;
    xy = xa * ya * (6 * ab + 6 * ac - 3 * bc + cb);
    ac = ya * ya;
    cb = xa * xa;
    xy =
      (3 * (xy + 9 * f * (cb * yb * yc - xb * xc * ac) - 18 * xb * yb * ab)) /
      8;

    if (ex < 0) {
      /* negate values if inside self-intersection loop */
      dx = -dx;
      dy = -dy;
      xx = -xx;
      yy = -yy;
      xy = -xy;
      ac = -ac;
      cb = -cb;
    } /* init differences of 3rd degree */
    ab = 6 * ya * ac;
    ac = -6 * xa * ac;
    bc = 6 * ya * cb;
    cb = -6 * xa * cb;
    dx += xy;
    ex = dx + dy;
    dy += xy; /* error of 1st step */

    outer: for (pxy = xy, fx = fy = f; x0 != x3 && y0 != y3; ) {
      setPixel(x0, y0); /* plot curve */
      do {
        /* move sub-steps of one pixel */
        if (dx > pxy || dy < pxy) {
          break outer;
        } /* confuMath.sing values */
        y1 = 2 * ex - dy; /* save value for test of y step */
        if (2 * ex >= dx) {
          /* x sub-step */
          fx--;
          ex += dx += xx;
          dy += xy += ac;
          yy += bc;
          xx += ab;
        }
        if (y1 <= 0) {
          /* y sub-step */
          fy--;
          ex += dy += yy;
          dx += xy += bc;
          xx += ac;
          yy += cb;
        }
      } while (fx > 0 && fy > 0); /* pixel complete? */
      if (2 * fx <= f) {
        x0 += sx;
        fx += f;
      } /* x step */
      if (2 * fy <= f) {
        y0 += sy;
        fy += f;
      } /* y step */
      if (pxy == xy && dx < 0 && dy > 0) pxy = EP; /* pixel ahead valid */
    }
    //exit:
    xx = x0;
    x0 = x3;
    x3 = xx;
    sx = -sx;
    xb = -xb; /* swap legs */

    yy = y0;
    y0 = y3;
    y3 = yy;
    sy = -sy;
    yb = -yb;
    x1 = x2;
  } while (leg--); /* try other end */
  line(
    x0,
    y0,
    x3,
    y3,
    setPixel
  ); /* remaining part in case of cusp or crunode */
}

/**
 * Plot limited anti-aliased cubic Bezier segment
 * @param  {Number} x0
 * @param  {Number} y0
 * @param  {Number} x1
 * @param  {Number} y1
 * @param  {Number} x2
 * @param  {Number} y2
 * @param  {Number} x3
 * @param  {Number} y3
 * @param  {setPixelAA} setPixelAA
 */
export function cubicBezierSegmentAA(
  x0,
  y0,
  x1,
  y1,
  x2,
  y2,
  x3,
  y3,
  setPixelAA
) {
  let f,
    fx,
    fy,
    leg = 1;
  let sx = x0 < x3 ? 1 : -1,
    sy = y0 < y3 ? 1 : -1; /* step direction */
  let xc = -Math.abs(x0 + x1 - x2 - x3),
    xa = xc - 4 * sx * (x1 - x2),
    xb = sx * (x0 - x1 - x2 + x3);
  let yc = -Math.abs(y0 + y1 - y2 - y3),
    ya = yc - 4 * sy * (y1 - y2),
    yb = sy * (y0 - y1 - y2 + y3);
  let ab, ac, bc, ba, xx, xy, yy, dx, dy, ex, px, py, ed, ip;
  const EP = 0.01;

  /* check for curve restrains */
  /* slope P0-P1 == P2-P3     and  (P0-P3 == P1-P2      or  no slope change) */
  assert(
    (x1 - x0) * (x2 - x3) < EP &&
      ((x3 - x0) * (x1 - x2) < EP || xb * xb < xa * xc + EP)
  );
  assert(
    (y1 - y0) * (y2 - y3) < EP &&
      ((y3 - y0) * (y1 - y2) < EP || yb * yb < ya * yc + EP)
  );

  if (xa === 0 && ya === 0) {
    /* quadratic Bezier */
    sx = Math.floor((3 * x1 - x0 + 1) / 2);
    sy = Math.floor((3 * y1 - y0 + 1) / 2); /* new midpoint */
    return quadBezierSegmentAA(x0, y0, sx, sy, x3, y3, setPixelAA);
  }
  x1 = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0) + 1; /* line lengths */
  x2 = (x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3) + 1;
  do {
    /* loop over both ends */
    ab = xa * yb - xb * ya;
    ac = xa * yc - xc * ya;
    bc = xb * yc - xc * yb;
    ip = 4 * ab * bc - ac * ac; /* self intersection loop at all? */
    /* P0 part of self-intersection loop? */
    ex = ab * (ab + ac - 3 * bc) + ac * ac;
    f = ex > 0 ? 1 : Math.sqrt(1 + 1024 / x1); /* calculate resolution */
    ab *= f;
    ac *= f;
    bc *= f;
    ex *= f * f; /* increase resolution */
    xy = (9 * (ab + ac + bc)) / 8;
    ba = 8 * (xa - ya); /* init differences of 1st degree */
    dx =
      (27 * (8 * ab * (yb * yb - ya * yc) + ex * (ya + 2 * yb + yc))) / 64 -
      ya * ya * (xy - ya);
    dy =
      (27 * (8 * ab * (xb * xb - xa * xc) - ex * (xa + 2 * xb + xc))) / 64 -
      xa * xa * (xy + xa);
    /* init differences of 2nd degree */
    xx =
      (3 *
        (3 * ab * (3 * yb * yb - ya * ya - 2 * ya * yc) -
          ya * (3 * ac * (ya + yb) + ya * ba))) /
      4;
    yy =
      (3 *
        (3 * ab * (3 * xb * xb - xa * xa - 2 * xa * xc) -
          xa * (3 * ac * (xa + xb) + xa * ba))) /
      4;
    xy = xa * ya * (6 * ab + 6 * ac - 3 * bc + ba);
    ac = ya * ya;
    ba = xa * xa;
    xy =
      (3 * (xy + 9 * f * (ba * yb * yc - xb * xc * ac) - 18 * xb * yb * ab)) /
      8;

    if (ex < 0) {
      /* negate values if inside self-intersection loop */
      dx = -dx;
      dy = -dy;
      xx = -xx;
      yy = -yy;
      xy = -xy;
      ac = -ac;
      ba = -ba;
    } /* init differences of 3rd degree */
    ab = 6 * ya * ac;
    ac = -6 * xa * ac;
    bc = 6 * ya * ba;
    ba = -6 * xa * ba;
    dx += xy;
    ex = dx + dy;
    dy += xy; /* error of 1st step */

    let exit = false;
    outer: for (fx = fy = f; x0 !== x3 && y0 !== y3; ) {
      y1 = Math.min(Math.abs(xy - dx), Math.abs(dy - xy));
      /* approximate error distance */
      ed = Math.max(Math.abs(xy - dx), Math.abs(dy - xy));
      ed = f * (ed + (2 * ed * y1 * y1) / (4 * ed * ed + y1 * y1));
      y1 =
        (255 * Math.abs(ex - (f - fx + 1) * dx - (f - fy + 1) * dy + f * xy)) /
        ed;
      if (y1 < 256) setPixelAA(x0, y0, y1); /* plot curve */
      /* pixel intensity x move */
      px = Math.abs(ex - (f - fx + 1) * dx + (fy - 1) * dy);
      /* pixel intensity y move */
      py = Math.abs(ex + (fx - 1) * dx - (f - fy + 1) * dy);
      y2 = y0;
      do {
        /* move sub-steps of one pixel */
        if (ip >= -EP)
          if (dx + xx > xy || dy + yy < xy) {
            /* intersection possible? -> check.. */
            /* two x or y steps */
            exit = true;
            break outer;
          }
        y1 = 2 * ex + dx; /* save value for test of y step */
        if (2 * ex + dy > 0) {
          /* x sub-step */
          fx--;
          ex += dx += xx;
          dy += xy += ac;
          yy += bc;
          xx += ab;
        } else if (y1 > 0) {
          /* tiny nearly cusp */
          exit = true;
          break outer;
        }
        if (y1 <= 0) {
          /* y sub-step */
          fy--;
          ex += dy += yy;
          dx += xy += bc;
          xx += ac;
          yy += ba;
        }
      } while (fx > 0 && fy > 0); /* pixel complete? */
      if (2 * fy <= f) {
        /* x+ anti-aliasing pixel */
        if (py < ed) setPixelAA(x0 + sx, y0, (255 * py) / ed); /* plot curve */
        y0 += sy;
        fy += f; /* y step */
      }
      if (2 * fx <= f) {
        /* y+ anti-aliasing pixel */
        if (px < ed) setPixelAA(x0, y2 + sy, (255 * px) / ed); /* plot curve */
        x0 += sx;
        fx += f; /* x step */
      }
    }

    if (exit) {
      if (2 * ex < dy && 2 * fy <= f + 2) {
        /* round x+ approximation pixel */
        if (py < ed) setPixelAA(x0 + sx, y0, (255 * py) / ed); /* plot curve */
        y0 += sy;
      }
      if (2 * ex > dx && 2 * fx <= f + 2) {
        /* round y+ approximation pixel */
        if (px < ed) setPixelAA(x0, y2 + sy, (255 * px) / ed); /* plot curve */
        x0 += sx;
      }
      xx = x0;
      x0 = x3;
      x3 = xx;
      sx = -sx;
      xb = -xb; /* swap legs */
      yy = y0;
      y0 = y3;
      y3 = yy;
      sy = -sy;
      yb = -yb;
      x1 = x2;
    }
    break; /* finish curve by line */
  } while (leg--); /* try other end */
  /* remaining part in case of cusp or crunode */
  lineAA(x0, y0, x3, y3, setPixelAA);
}

function cubicBezierGeneral(
  x0: number,
  y0: number,
  x1: number,
  y1: number,
  x2: number,
  y2: number,
  x3: number,
  y3: number,
  segment: typeof cubicBezierSegment | typeof cubicBezierSegmentAA,
  setPixel: setPixelFn | setPixelAlphaFn
) {
  let n = 0;
  let i = 0;
  let xc = x0 + x1 - x2 - x3;
  let xa = xc - 4 * (x1 - x2);
  let xb = x0 - x1 - x2 + x3;
  let xd = xb + 4 * (x1 + x2);
  let yc = y0 + y1 - y2 - y3;
  let ya = yc - 4 * (y1 - y2);
  let yb = y0 - y1 - y2 + y3;
  let yd = yb + 4 * (y1 + y2);
  var fx0 = x0,
    fx1: number,
    fx2: number,
    fx3: number,
    fy0 = y0,
    fy1: number,
    fy2: number,
    fy3: number;
  let t1 = xb * xb - xa * xc;
  let t2: number;
  const t = [0, 0, 0, 0, 0];
  /* sub-divide curve at gradient sign changes */
  if (xa == 0) {
    /* horizontal */
    if (Math.abs(xc) < 2 * Math.abs(xb))
      t[n++] = xc / (2 * xb); /* one change */
  } else if (t1 > 0.0) {
    /* two changes */
    t2 = Math.sqrt(t1);
    t1 = (xb - t2) / xa;
    if (Math.abs(t1) < 1) t[n++] = t1;
    t1 = (xb + t2) / xa;
    if (Math.abs(t1) < 1) t[n++] = t1;
  }
  t1 = yb * yb - ya * yc;
  if (ya == 0) {
    /* vertical */
    if (Math.abs(yc) < 2 * Math.abs(yb))
      t[n++] = yc / (2 * yb); /* one change */
  } else if (t1 > 0.0) {
    /* two changes */
    t2 = Math.sqrt(t1);
    t1 = (yb - t2) / ya;
    if (Math.abs(t1) < 1) t[n++] = t1;
    t1 = (yb + t2) / ya;
    if (Math.abs(t1) < 1) t[n++] = t1;
  }
  for (i = 1; i < n; i++ /* bubble sort of 4 points */)
    if ((t1 = t[i - 1]) > t[i]) {
      t[i - 1] = t[i];
      t[i] = t1;
      i = 0;
    }

  t1 = -1;
  t[n] = 1; /* begin / end point */
  for (i = 0; i <= n; i++) {
    /* plot each segment separately */
    t2 = t[i]; /* sub-divide at t[i-1], t[i] */
    fx1 =
      (t1 * (t1 * xb - 2 * xc) - t2 * (t1 * (t1 * xa - 2 * xb) + xc) + xd) / 8 -
      fx0;
    fy1 =
      (t1 * (t1 * yb - 2 * yc) - t2 * (t1 * (t1 * ya - 2 * yb) + yc) + yd) / 8 -
      fy0;
    fx2 =
      (t2 * (t2 * xb - 2 * xc) - t1 * (t2 * (t2 * xa - 2 * xb) + xc) + xd) / 8 -
      fx0;
    fy2 =
      (t2 * (t2 * yb - 2 * yc) - t1 * (t2 * (t2 * ya - 2 * yb) + yc) + yd) / 8 -
      fy0;
    fx0 -= fx3 = (t2 * (t2 * (3 * xb - t2 * xa) - 3 * xc) + xd) / 8;
    fy0 -= fy3 = (t2 * (t2 * (3 * yb - t2 * ya) - 3 * yc) + yd) / 8;
    x3 = Math.floor(fx3 + 0.5);
    y3 = Math.floor(fy3 + 0.5); /* scale bounds to int */
    if (fx0 != 0.0) {
      fx1 *= fx0 = (x0 - x3) / fx0;
      fx2 *= fx0;
    }
    if (fy0 != 0.0) {
      fy1 *= fy0 = (y0 - y3) / fy0;
      fy2 *= fy0;
    }
    if (x0 != x3 || y0 != y3)
      /* @ts-expect-error segment t1 - t2 */
      segment(x0, y0, x0 + fx1, y0 + fy1, x0 + fx2, y0 + fy2, x3, y3, setPixel);
    x0 = x3;
    y0 = y3;
    fx0 = fx3;
    fy0 = fy3;
    t1 = t2;
  }
}

/**
 * plot any cubic Bezier curve
 * @param  {number} x0
 * @param  {number} y0
 * @param  {number} x1
 * @param  {number} y1
 * @param  {number} x2
 * @param  {number} y2
 * @param  {number} x3
 * @param  {number} y3
 * @param  {setPixel} setPixel
 */
export function cubicBezier(
  x0: number,
  y0: number,
  x1: number,
  y1: number,
  x2: number,
  y2: number,
  x3: number,
  y3: number,
  setPixel
) {
  cubicBezierGeneral(
    x0,
    y0,
    x1,
    y1,
    x2,
    y2,
    x3,
    y3,
    cubicBezierSegment,
    setPixel
  );
}

/**
 * plot any cubic Bezier curve
 * @param  {number} x0
 * @param  {number} y0
 * @param  {number} x1
 * @param  {number} y1
 * @param  {number} x2
 * @param  {number} y2
 * @param  {number} x3
 * @param  {number} y3
 * @param  {setPixelAlpha} setPixelAA
 */
export function cubicBezierAA(
  x0: number,
  y0: number,
  x1: number,
  y1: number,
  x2: number,
  y2: number,
  x3: number,
  y3: number,
  setPixelAA: setPixelAlphaFn
) {
  cubicBezierGeneral(
    x0,
    y0,
    x1,
    y1,
    x2,
    y2,
    x3,
    y3,
    cubicBezierSegmentAA,
    setPixelAA
  );
}
