// SPDX-License-Identifier: CC0
/** @file
 * Use Canvas to render a mesh gradient, passing the rendering to an image via a data stream.
 *//*
 * Authors:
 *  - Tavmjong Bah 2018
 *  - Valentin Ionita (2019)
 * License: CC0 / Public Domain
 */

(function () {
  // Name spaces -----------------------------------
  const svgNS = 'http://www.w3.org/2000/svg';
  const xlinkNS = 'http://www.w3.org/1999/xlink';
  const xhtmlNS = 'http://www.w3.org/1999/xhtml';
  /*
   * Maximum threshold for Bezier step size
   * Larger values leave holes, smaller take longer to render.
   */
  const maxBezierStep = 2.0;

  // Test if mesh gradients are supported.
  if (document.createElementNS(svgNS, 'meshgradient').x) {
    return;
  }

  /*
   * Utility functions -----------------------------
   */
  // Split Bezier using de Casteljau's method.
  const splitBezier = (p0, p1, p2, p3) => {
    let tmp = new Point((p1.x + p2.x) * 0.5, (p1.y + p2.y) * 0.5);
    let p01 = new Point((p0.x + p1.x) * 0.5, (p0.y + p1.y) * 0.5);
    let p12 = new Point((p2.x + p3.x) * 0.5, (p2.y + p3.y) * 0.5);
    let p02 = new Point((tmp.x + p01.x) * 0.5, (tmp.y + p01.y) * 0.5);
    let p11 = new Point((tmp.x + p12.x) * 0.5, (tmp.y + p12.y) * 0.5);
    let p03 = new Point((p02.x + p11.x) * 0.5, (p02.y + p11.y) * 0.5);

    return ([
      [p0, p01, p02, p03],
      [p03, p11, p12, p3]
    ]);
  };

  // See Cairo: cairo-mesh-pattern-rasterizer.c
  const bezierStepsSquared = (points) => {
    let tmp0 = points[0].distSquared(points[1]);
    let tmp1 = points[2].distSquared(points[3]);
    let tmp2 = points[0].distSquared(points[2]) * 0.25;
    let tmp3 = points[1].distSquared(points[3]) * 0.25;

    let max1 = tmp0 > tmp1 ? tmp0 : tmp1;

    let max2 = tmp2 > tmp3 ? tmp2 : tmp3;

    let max = max1 > max2 ? max1 : max2;

    return max * 18;
  };

  // Euclidean distance
  const distance = (p0, p1) => Math.sqrt(p0.distSquared(p1));

  // Weighted average to find Bezier points for linear sides.
  const wAvg = (p0, p1) => p0.scale(2.0 / 3.0).add(p1.scale(1.0 / 3.0));

  // Browsers return a string rather than a transform list for gradientTransform!
  const parseTransform = (t) => {
    let affine = new Affine();
    let trans, scale, radian, tan, skewx, skewy, rotate;
    let transforms = t.match(/(\w+\(\s*[^)]+\))+/g);

    transforms.forEach((i) => {
      let c = i.match(/[\w.-]+/g);
      let type = c.shift();

      switch (type) {
        case 'translate':
          if (c.length === 2) {
            trans = new Affine(1, 0, 0, 1, c[0], c[1]);
          } else {
            console.error('mesh.js: translate does not have 2 arguments!');
            trans = new Affine(1, 0, 0, 1, 0, 0);
          }
          affine = affine.append(trans);
          break;

        case 'scale':
          if (c.length === 1) {
            scale = new Affine(c[0], 0, 0, c[0], 0, 0);
          } else if (c.length === 2) {
            scale = new Affine(c[0], 0, 0, c[1], 0, 0);
          } else {
            console.error('mesh.js: scale does not have 1 or 2 arguments!');
            scale = new Affine(1, 0, 0, 1, 0, 0);
          }
          affine = affine.append(scale);
          break;

        case 'rotate':
          if (c.length === 3) {
            trans = new Affine(1, 0, 0, 1, c[1], c[2]);
            affine = affine.append(trans);
          }
          if (c[0]) {
            radian = c[0] * Math.PI / 180.0;
            let cos = Math.cos(radian);
            let sin = Math.sin(radian);
            if (Math.abs(cos) < 1e-16) { // I hate rounding errors...
              cos = 0;
            }
            if (Math.abs(sin) < 1e-16) { // I hate rounding errors...
              sin = 0;
            }
            rotate = new Affine(cos, sin, -sin, cos, 0, 0);
            affine = affine.append(rotate);
          } else {
            console.error('math.js: No argument to rotate transform!');
          }
          if (c.length === 3) {
            trans = new Affine(1, 0, 0, 1, -c[1], -c[2]);
            affine = affine.append(trans);
          }
          break;

        case 'skewX':
          if (c[0]) {
            radian = c[0] * Math.PI / 180.0;
            tan = Math.tan(radian);
            skewx = new Affine(1, 0, tan, 1, 0, 0);
            affine = affine.append(skewx);
          } else {
            console.error('math.js: No argument to skewX transform!');
          }
          break;

        case 'skewY':
          if (c[0]) {
            radian = c[0] * Math.PI / 180.0;
            tan = Math.tan(radian);
            skewy = new Affine(1, tan, 0, 1, 0, 0);
            affine = affine.append(skewy);
          } else {
            console.error('math.js: No argument to skewY transform!');
          }
          break;

        case 'matrix':
          if (c.length === 6) {
            affine = affine.append(new Affine(...c));
          } else {
            console.error('math.js: Incorrect number of arguments for matrix!');
          }
          break;

        default:
          console.error('mesh.js: Unhandled transform type: ' + type);
          break;
      }
    });

    return affine;
  };

  const parsePoints = (s) => {
    let points = [];
    let values = s.split(/[ ,]+/);
    for (let i = 0, imax = values.length - 1; i < imax; i += 2) {
      points.push(new Point(parseFloat(values[i]), parseFloat(values[i + 1])));
    }
    return points;
  };

  // Set multiple attributes to an element
  const setAttributes = (el, attrs) => {
    for (let key in attrs) {
      el.setAttribute(key, attrs[key]);
    }
  };

  // Find the slope of point p_k by the values in p_k-1 and p_k+1
  const finiteDifferences = (c0, c1, c2, d01, d12) => {
    let slope = [0, 0, 0, 0];
    let slow, shigh;

    for (let k = 0; k < 3; ++k) {
      if ((c1[k] < c0[k] && c1[k] < c2[k]) || (c0[k] < c1[k] && c2[k] < c1[k])) {
        slope[k] = 0;
      } else {
        slope[k] = 0.5 * ((c1[k] - c0[k]) / d01 + (c2[k] - c1[k]) / d12);
        slow = Math.abs(3.0 * (c1[k] - c0[k]) / d01);
        shigh = Math.abs(3.0 * (c2[k] - c1[k]) / d12);

        if (slope[k] > slow) {
          slope[k] = slow;
        } else if (slope[k] > shigh) {
          slope[k] = shigh;
        }
      }
    }

    return slope;
  };

  // Coefficient matrix used for solving linear system
  const A = [
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [-3, 3, 0, 0, -2, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [2, -2, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, -3, 3, 0, 0, -2, -1, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 2, -2, 0, 0, 1, 1, 0, 0],
    [-3, 0, 3, 0, 0, 0, 0, 0, -2, 0, -1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, -3, 0, 3, 0, 0, 0, 0, 0, -2, 0, -1, 0],
    [9, -9, -9, 9, 6, 3, -6, -3, 6, -6, 3, -3, 4, 2, 2, 1],
    [-6, 6, 6, -6, -3, -3, 3, 3, -4, 4, -2, 2, -2, -2, -1, -1],
    [2, 0, -2, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 2, 0, -2, 0, 0, 0, 0, 0, 1, 0, 1, 0],
    [-6, 6, 6, -6, -4, -2, 4, 2, -3, 3, -3, 3, -2, -1, -2, -1],
    [4, -4, -4, 4, 2, 2, -2, -2, 2, -2, 2, -2, 1, 1, 1, 1]
  ];

  // Solve the linear system for bicubic interpolation
  const solveLinearSystem = (v) => {
    let alpha = [];

    for (let i = 0; i < 16; ++i) {
      alpha[i] = 0;
      for (let j = 0; j < 16; ++j) {
        alpha[i] += A[i][j] * v[j];
      }
    }

    return alpha;
  };

  // Evaluate the interpolation parameters at (y, x)
  const evaluateSolution = (alpha, x, y) => {
    const xx = x * x;
    const yy = y * y;
    const xxx = x * x * x;
    const yyy = y * y * y;

    let result =
      alpha[0] +
      alpha[1] * x +
      alpha[2] * xx +
      alpha[3] * xxx +
      alpha[4] * y +
      alpha[5] * y * x +
      alpha[6] * y * xx +
      alpha[7] * y * xxx +
      alpha[8] * yy +
      alpha[9] * yy * x +
      alpha[10] * yy * xx +
      alpha[11] * yy * xxx +
      alpha[12] * yyy +
      alpha[13] * yyy * x +
      alpha[14] * yyy * xx +
      alpha[15] * yyy * xxx;

    return result;
  };

  // Split a patch into 8x8 smaller patches
  const splitPatch = (patch) => {
    let yPatches = [];
    let xPatches = [];
    let patches = [];

    // Horizontal splitting
    for (let i = 0; i < 4; ++i) {
      yPatches[i] = [];
      yPatches[i][0] = splitBezier(
        patch[0][i], patch[1][i],
        patch[2][i], patch[3][i]
      );

      yPatches[i][1] = [];
      yPatches[i][1].push(...splitBezier(...yPatches[i][0][0]));
      yPatches[i][1].push(...splitBezier(...yPatches[i][0][1]));

      yPatches[i][2] = [];
      yPatches[i][2].push(...splitBezier(...yPatches[i][1][0]));
      yPatches[i][2].push(...splitBezier(...yPatches[i][1][1]));
      yPatches[i][2].push(...splitBezier(...yPatches[i][1][2]));
      yPatches[i][2].push(...splitBezier(...yPatches[i][1][3]));
    }

    // Vertical splitting
    for (let i = 0; i < 8; ++i) {
      xPatches[i] = [];

      for (let j = 0; j < 4; ++j) {
        xPatches[i][j] = [];
        xPatches[i][j][0] = splitBezier(
          yPatches[0][2][i][j], yPatches[1][2][i][j],
          yPatches[2][2][i][j], yPatches[3][2][i][j]
        );

        xPatches[i][j][1] = [];
        xPatches[i][j][1].push(...splitBezier(...xPatches[i][j][0][0]));
        xPatches[i][j][1].push(...splitBezier(...xPatches[i][j][0][1]));

        xPatches[i][j][2] = [];
        xPatches[i][j][2].push(...splitBezier(...xPatches[i][j][1][0]));
        xPatches[i][j][2].push(...splitBezier(...xPatches[i][j][1][1]));
        xPatches[i][j][2].push(...splitBezier(...xPatches[i][j][1][2]));
        xPatches[i][j][2].push(...splitBezier(...xPatches[i][j][1][3]));
      }
    }

    for (let i = 0; i < 8; ++i) {
      patches[i] = [];

      for (let j = 0; j < 8; ++j) {
        patches[i][j] = [];

        patches[i][j][0] = xPatches[i][0][2][j];
        patches[i][j][1] = xPatches[i][1][2][j];
        patches[i][j][2] = xPatches[i][2][2][j];
        patches[i][j][3] = xPatches[i][3][2][j];
      }
    }

    return patches;
  };

  // Point class -----------------------------------
  class Point {
    constructor (x, y) {
      this.x = x || 0;
      this.y = y || 0;
    }

    toString () {
      return `(x=${this.x}, y=${this.y})`;
    }

    clone () {
      return new Point(this.x, this.y);
    }

    add (v) {
      return new Point(this.x + v.x, this.y + v.y);
    }

    scale (v) {
      if (v.x === undefined) {
        return new Point(this.x * v, this.y * v);
      }
      return new Point(this.x * v.x, this.y * v.y);
    }

    distSquared (v) {
      let x = this.x - v.x;
      let y = this.y - v.y;
      return (x * x + y * y);
    }

    // Transform by affine
    transform (affine) {
      let x = this.x * affine.a + this.y * affine.c + affine.e;
      let y = this.x * affine.b + this.y * affine.d + affine.f;
      return new Point(x, y);
    }
  }

  /*
   * Affine class -------------------------------------
   *
   * As defined in the SVG spec
   * | a  c  e |
   * | b  d  f |
   * | 0  0  1 |
   *
   */

  class Affine {
    constructor (a, b, c, d, e, f) {
      if (a === undefined) {
        this.a = 1;
        this.b = 0;
        this.c = 0;
        this.d = 1;
        this.e = 0;
        this.f = 0;
      } else {
        this.a = a;
        this.b = b;
        this.c = c;
        this.d = d;
        this.e = e;
        this.f = f;
      }
    }

    toString () {
      return `affine: ${this.a} ${this.c} ${this.e} \n\
       ${this.b} ${this.d} ${this.f}`;
    }

    append (v) {
      if (!(v instanceof Affine)) {
        console.error(`mesh.js: argument to Affine.append is not affine!`);
      }
      let a = this.a * v.a + this.c * v.b;
      let b = this.b * v.a + this.d * v.b;
      let c = this.a * v.c + this.c * v.d;
      let d = this.b * v.c + this.d * v.d;
      let e = this.a * v.e + this.c * v.f + this.e;
      let f = this.b * v.e + this.d * v.f + this.f;
      return new Affine(a, b, c, d, e, f);
    }
  }

  // Curve class --------------------------------------
  class Curve {
    constructor (nodes, colors) {
      this.nodes = nodes; // 4 Bezier points
      this.colors = colors; // 2 x 4 colors (two ends x R+G+B+A)
    }

    /*
     * Paint a Bezier curve
     * w is canvas.width
     * h is canvas.height
     */
    paintCurve (v, w) {
      // If inside, see if we need to split
      if (bezierStepsSquared(this.nodes) > maxBezierStep) {
        const beziers = splitBezier(...this.nodes);
        // ([start][end])
        let colors0 = [[], []];
        let colors1 = [[], []];

        /*
         * Linear horizontal interpolation of the middle value for every
         * patch exceeding thereshold
         */
        for (let i = 0; i < 4; ++i) {
          colors0[0][i] = this.colors[0][i];
          colors0[1][i] = (this.colors[0][i] + this.colors[1][i]) / 2;
          colors1[0][i] = colors0[1][i];
          colors1[1][i] = this.colors[1][i];
        }
        let curve0 = new Curve(beziers[0], colors0);
        let curve1 = new Curve(beziers[1], colors1);
        curve0.paintCurve(v, w);
        curve1.paintCurve(v, w);
      } else {
        // Directly write data
        let x = Math.round(this.nodes[0].x);
        if (x >= 0 && x < w) {
          let index = (~~this.nodes[0].y * w + x) * 4;
          v[index] = Math.round(this.colors[0][0]);
          v[index + 1] = Math.round(this.colors[0][1]);
          v[index + 2] = Math.round(this.colors[0][2]);
          v[index + 3] = Math.round(this.colors[0][3]); // Alpha
        }
      }
    }
  }

  // Patch class -------------------------------------
  class Patch {
    constructor (nodes, colors) {
      this.nodes = nodes; // 4x4 array of points
      this.colors = colors; // 2x2x4 colors (four corners x R+G+B+A)
    }

    // Split patch horizontally into two patches.
    split () {
      let nodes0 = [[], [], [], []];
      let nodes1 = [[], [], [], []];
      let colors0 = [
        [[], []],
        [[], []]
      ];
      let colors1 = [
        [[], []],
        [[], []]
      ];

      for (let i = 0; i < 4; ++i) {
        const beziers = splitBezier(
          this.nodes[0][i], this.nodes[1][i],
          this.nodes[2][i], this.nodes[3][i]
        );

        nodes0[0][i] = beziers[0][0];
        nodes0[1][i] = beziers[0][1];
        nodes0[2][i] = beziers[0][2];
        nodes0[3][i] = beziers[0][3];
        nodes1[0][i] = beziers[1][0];
        nodes1[1][i] = beziers[1][1];
        nodes1[2][i] = beziers[1][2];
        nodes1[3][i] = beziers[1][3];
      }

      /*
       * Linear vertical interpolation of the middle value for every
       * patch exceeding thereshold
       */
      for (let i = 0; i < 4; ++i) {
        colors0[0][0][i] = this.colors[0][0][i];
        colors0[0][1][i] = this.colors[0][1][i];
        colors0[1][0][i] = (this.colors[0][0][i] + this.colors[1][0][i]) / 2;
        colors0[1][1][i] = (this.colors[0][1][i] + this.colors[1][1][i]) / 2;
        colors1[0][0][i] = colors0[1][0][i];
        colors1[0][1][i] = colors0[1][1][i];
        colors1[1][0][i] = this.colors[1][0][i];
        colors1[1][1][i] = this.colors[1][1][i];
      }

      return ([new Patch(nodes0, colors0), new Patch(nodes1, colors1)]);
    }

    paint (v, w) {
      // Check if we need to split
      let larger = false;
      let step;
      for (let i = 0; i < 4; ++i) {
        step = bezierStepsSquared([
          this.nodes[0][i], this.nodes[1][i],
          this.nodes[2][i], this.nodes[3][i]
        ]);

        if (step > maxBezierStep) {
          larger = true;
          break;
        }
      }

      if (larger) {
        let patches = this.split();
        patches[0].paint(v, w);
        patches[1].paint(v, w);
      } else {
        /*
         * Paint a Bezier curve using just the top of the patch. If
         * the patch is thin enough this should work. We leave this
         * function here in case we want to do something more fancy.
         */
        let curve = new Curve([...this.nodes[0]], [...this.colors[0]]);
        curve.paintCurve(v, w);
      }
    }
  }

  // Mesh class ---------------------------------------
  class Mesh {
    constructor (mesh) {
      this.readMesh(mesh);
      this.type = mesh.getAttribute('type') || 'bilinear';
    }

    // Function to parse an SVG mesh and set the nodes (points) and colors
    readMesh (mesh) {
      let nodes = [[]];
      let colors = [[]];

      let x = Number(mesh.getAttribute('x'));
      let y = Number(mesh.getAttribute('y'));
      nodes[0][0] = new Point(x, y);

      let rows = mesh.children;
      for (let i = 0, imax = rows.length; i < imax; ++i) {
        // Need to validate if meshrow...
        nodes[3 * i + 1] = []; // Need three extra rows for each meshrow.
        nodes[3 * i + 2] = [];
        nodes[3 * i + 3] = [];
        colors[i + 1] = []; // Need one more row than number of meshrows.

        let patches = rows[i].children;
        for (let j = 0, jmax = patches.length; j < jmax; ++j) {
          let stops = patches[j].children;
          for (let k = 0, kmax = stops.length; k < kmax; ++k) {
            let l = k;
            if (i !== 0) {
              ++l; // There is no top if row isn't first row.
            }
            let path = stops[k].getAttribute('path');
            let parts;

            let type = 'l'; // We need to still find mid-points even if no path.
            if (path != null) {
              parts = path.match(/\s*([lLcC])\s*(.*)/);
              type = parts[1];
            }
            let stopNodes = parsePoints(parts[2]);

            switch (type) {
              case 'l':
                if (l === 0) { // Top
                  nodes[3 * i][3 * j + 3] = stopNodes[0].add(nodes[3 * i][3 * j]);
                  nodes[3 * i][3 * j + 1] = wAvg(nodes[3 * i][3 * j], nodes[3 * i][3 * j + 3]);
                  nodes[3 * i][3 * j + 2] = wAvg(nodes[3 * i][3 * j + 3], nodes[3 * i][3 * j]);
                } else if (l === 1) { // Right
                  nodes[3 * i + 3][3 * j + 3] = stopNodes[0].add(nodes[3 * i][3 * j + 3]);
                  nodes[3 * i + 1][3 * j + 3] = wAvg(nodes[3 * i][3 * j + 3], nodes[3 * i + 3][3 * j + 3]);
                  nodes[3 * i + 2][3 * j + 3] = wAvg(nodes[3 * i + 3][3 * j + 3], nodes[3 * i][3 * j + 3]);
                } else if (l === 2) { // Bottom
                  if (j === 0) {
                    nodes[3 * i + 3][3 * j + 0] = stopNodes[0].add(nodes[3 * i + 3][3 * j + 3]);
                  }
                  nodes[3 * i + 3][3 * j + 1] = wAvg(nodes[3 * i + 3][3 * j], nodes[3 * i + 3][3 * j + 3]);
                  nodes[3 * i + 3][3 * j + 2] = wAvg(nodes[3 * i + 3][3 * j + 3], nodes[3 * i + 3][3 * j]);
                } else { // Left
                  nodes[3 * i + 1][3 * j] = wAvg(nodes[3 * i][3 * j], nodes[3 * i + 3][3 * j]);
                  nodes[3 * i + 2][3 * j] = wAvg(nodes[3 * i + 3][3 * j], nodes[3 * i][3 * j]);
                }
                break;
              case 'L':
                if (l === 0) { // Top
                  nodes[3 * i][3 * j + 3] = stopNodes[0];
                  nodes[3 * i][3 * j + 1] = wAvg(nodes[3 * i][3 * j], nodes[3 * i][3 * j + 3]);
                  nodes[3 * i][3 * j + 2] = wAvg(nodes[3 * i][3 * j + 3], nodes[3 * i][3 * j]);
                } else if (l === 1) { // Right
                  nodes[3 * i + 3][3 * j + 3] = stopNodes[0];
                  nodes[3 * i + 1][3 * j + 3] = wAvg(nodes[3 * i][3 * j + 3], nodes[3 * i + 3][3 * j + 3]);
                  nodes[3 * i + 2][3 * j + 3] = wAvg(nodes[3 * i + 3][3 * j + 3], nodes[3 * i][3 * j + 3]);
                } else if (l === 2) { // Bottom
                  if (j === 0) {
                    nodes[3 * i + 3][3 * j + 0] = stopNodes[0];
                  }
                  nodes[3 * i + 3][3 * j + 1] = wAvg(nodes[3 * i + 3][3 * j], nodes[3 * i + 3][3 * j + 3]);
                  nodes[3 * i + 3][3 * j + 2] = wAvg(nodes[3 * i + 3][3 * j + 3], nodes[3 * i + 3][3 * j]);
                } else { // Left
                  nodes[3 * i + 1][3 * j] = wAvg(nodes[3 * i][3 * j], nodes[3 * i + 3][3 * j]);
                  nodes[3 * i + 2][3 * j] = wAvg(nodes[3 * i + 3][3 * j], nodes[3 * i][3 * j]);
                }
                break;
              case 'c':
                if (l === 0) { // Top
                  nodes[3 * i][3 * j + 1] = stopNodes[0].add(nodes[3 * i][3 * j]);
                  nodes[3 * i][3 * j + 2] = stopNodes[1].add(nodes[3 * i][3 * j]);
                  nodes[3 * i][3 * j + 3] = stopNodes[2].add(nodes[3 * i][3 * j]);
                } else if (l === 1) { // Right
                  nodes[3 * i + 1][3 * j + 3] = stopNodes[0].add(nodes[3 * i][3 * j + 3]);
                  nodes[3 * i + 2][3 * j + 3] = stopNodes[1].add(nodes[3 * i][3 * j + 3]);
                  nodes[3 * i + 3][3 * j + 3] = stopNodes[2].add(nodes[3 * i][3 * j + 3]);
                } else if (l === 2) { // Bottom
                  nodes[3 * i + 3][3 * j + 2] = stopNodes[0].add(nodes[3 * i + 3][3 * j + 3]);
                  nodes[3 * i + 3][3 * j + 1] = stopNodes[1].add(nodes[3 * i + 3][3 * j + 3]);
                  if (j === 0) {
                    nodes[3 * i + 3][3 * j + 0] = stopNodes[2].add(nodes[3 * i + 3][3 * j + 3]);
                  }
                } else { // Left
                  nodes[3 * i + 2][3 * j] = stopNodes[0].add(nodes[3 * i + 3][3 * j]);
                  nodes[3 * i + 1][3 * j] = stopNodes[1].add(nodes[3 * i + 3][3 * j]);
                }
                break;
              case 'C':
                if (l === 0) { // Top
                  nodes[3 * i][3 * j + 1] = stopNodes[0];
                  nodes[3 * i][3 * j + 2] = stopNodes[1];
                  nodes[3 * i][3 * j + 3] = stopNodes[2];
                } else if (l === 1) { // Right
                  nodes[3 * i + 1][3 * j + 3] = stopNodes[0];
                  nodes[3 * i + 2][3 * j + 3] = stopNodes[1];
                  nodes[3 * i + 3][3 * j + 3] = stopNodes[2];
                } else if (l === 2) { // Bottom
                  nodes[3 * i + 3][3 * j + 2] = stopNodes[0];
                  nodes[3 * i + 3][3 * j + 1] = stopNodes[1];
                  if (j === 0) {
                    nodes[3 * i + 3][3 * j + 0] = stopNodes[2];
                  }
                } else { // Left
                  nodes[3 * i + 2][3 * j] = stopNodes[0];
                  nodes[3 * i + 1][3 * j] = stopNodes[1];
                }
                break;
              default:
                console.error('mesh.js: ' + type + ' invalid path type.');
            }

            if ((i === 0 && j === 0) || k > 0) {
              let colorRaw = window.getComputedStyle(stops[k]).stopColor
                .match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
              let alphaRaw = window.getComputedStyle(stops[k]).stopOpacity;
              let alpha = 255;
              if (alphaRaw) {
                alpha = Math.floor(alphaRaw * 255);
              }

              if (colorRaw) {
                if (l === 0) { // upper left corner
                  colors[i][j] = [];
                  colors[i][j][0] = Math.floor(colorRaw[1]);
                  colors[i][j][1] = Math.floor(colorRaw[2]);
                  colors[i][j][2] = Math.floor(colorRaw[3]);
                  colors[i][j][3] = alpha; // Alpha
                } else if (l === 1) { // upper right corner
                  colors[i][j + 1] = [];
                  colors[i][j + 1][0] = Math.floor(colorRaw[1]);
                  colors[i][j + 1][1] = Math.floor(colorRaw[2]);
                  colors[i][j + 1][2] = Math.floor(colorRaw[3]);
                  colors[i][j + 1][3] = alpha; // Alpha
                } else if (l === 2) { // lower right corner
                  colors[i + 1][j + 1] = [];
                  colors[i + 1][j + 1][0] = Math.floor(colorRaw[1]);
                  colors[i + 1][j + 1][1] = Math.floor(colorRaw[2]);
                  colors[i + 1][j + 1][2] = Math.floor(colorRaw[3]);
                  colors[i + 1][j + 1][3] = alpha; // Alpha
                } else if (l === 3) { // lower left corner
                  colors[i + 1][j] = [];
                  colors[i + 1][j][0] = Math.floor(colorRaw[1]);
                  colors[i + 1][j][1] = Math.floor(colorRaw[2]);
                  colors[i + 1][j][2] = Math.floor(colorRaw[3]);
                  colors[i + 1][j][3] = alpha; // Alpha
                }
              }
            }
          }

          // SVG doesn't use tensor points but we need them for rendering.
          nodes[3 * i + 1][3 * j + 1] = new Point();
          nodes[3 * i + 1][3 * j + 2] = new Point();
          nodes[3 * i + 2][3 * j + 1] = new Point();
          nodes[3 * i + 2][3 * j + 2] = new Point();

          nodes[3 * i + 1][3 * j + 1].x =
            (-4.0 * nodes[3 * i][3 * j].x +
              6.0 * (nodes[3 * i][3 * j + 1].x + nodes[3 * i + 1][3 * j].x) +
              -2.0 * (nodes[3 * i][3 * j + 3].x + nodes[3 * i + 3][3 * j].x) +
              3.0 * (nodes[3 * i + 3][3 * j + 1].x + nodes[3 * i + 1][3 * j + 3].x) +
              -1.0 * nodes[3 * i + 3][3 * j + 3].x) / 9.0;
          nodes[3 * i + 1][3 * j + 2].x =
            (-4.0 * nodes[3 * i][3 * j + 3].x +
              6.0 * (nodes[3 * i][3 * j + 2].x + nodes[3 * i + 1][3 * j + 3].x) +
              -2.0 * (nodes[3 * i][3 * j].x + nodes[3 * i + 3][3 * j + 3].x) +
              3.0 * (nodes[3 * i + 3][3 * j + 2].x + nodes[3 * i + 1][3 * j].x) +
              -1.0 * nodes[3 * i + 3][3 * j].x) / 9.0;
          nodes[3 * i + 2][3 * j + 1].x =
            (-4.0 * nodes[3 * i + 3][3 * j].x +
              6.0 * (nodes[3 * i + 3][3 * j + 1].x + nodes[3 * i + 2][3 * j].x) +
              -2.0 * (nodes[3 * i + 3][3 * j + 3].x + nodes[3 * i][3 * j].x) +
              3.0 * (nodes[3 * i][3 * j + 1].x + nodes[3 * i + 2][3 * j + 3].x) +
              -1.0 * nodes[3 * i][3 * j + 3].x) / 9.0;
          nodes[3 * i + 2][3 * j + 2].x =
            (-4.0 * nodes[3 * i + 3][3 * j + 3].x +
              6.0 * (nodes[3 * i + 3][3 * j + 2].x + nodes[3 * i + 2][3 * j + 3].x) +
              -2.0 * (nodes[3 * i + 3][3 * j].x + nodes[3 * i][3 * j + 3].x) +
              3.0 * (nodes[3 * i][3 * j + 2].x + nodes[3 * i + 2][3 * j].x) +
              -1.0 * nodes[3 * i][3 * j].x) / 9.0;

          nodes[3 * i + 1][3 * j + 1].y =
            (-4.0 * nodes[3 * i][3 * j].y +
              6.0 * (nodes[3 * i][3 * j + 1].y + nodes[3 * i + 1][3 * j].y) +
              -2.0 * (nodes[3 * i][3 * j + 3].y + nodes[3 * i + 3][3 * j].y) +
              3.0 * (nodes[3 * i + 3][3 * j + 1].y + nodes[3 * i + 1][3 * j + 3].y) +
              -1.0 * nodes[3 * i + 3][3 * j + 3].y) / 9.0;
          nodes[3 * i + 1][3 * j + 2].y =
            (-4.0 * nodes[3 * i][3 * j + 3].y +
              6.0 * (nodes[3 * i][3 * j + 2].y + nodes[3 * i + 1][3 * j + 3].y) +
              -2.0 * (nodes[3 * i][3 * j].y + nodes[3 * i + 3][3 * j + 3].y) +
              3.0 * (nodes[3 * i + 3][3 * j + 2].y + nodes[3 * i + 1][3 * j].y) +
              -1.0 * nodes[3 * i + 3][3 * j].y) / 9.0;
          nodes[3 * i + 2][3 * j + 1].y =
            (-4.0 * nodes[3 * i + 3][3 * j].y +
              6.0 * (nodes[3 * i + 3][3 * j + 1].y + nodes[3 * i + 2][3 * j].y) +
              -2.0 * (nodes[3 * i + 3][3 * j + 3].y + nodes[3 * i][3 * j].y) +
              3.0 * (nodes[3 * i][3 * j + 1].y + nodes[3 * i + 2][3 * j + 3].y) +
              -1.0 * nodes[3 * i][3 * j + 3].y) / 9.0;
          nodes[3 * i + 2][3 * j + 2].y =
            (-4.0 * nodes[3 * i + 3][3 * j + 3].y +
              6.0 * (nodes[3 * i + 3][3 * j + 2].y + nodes[3 * i + 2][3 * j + 3].y) +
              -2.0 * (nodes[3 * i + 3][3 * j].y + nodes[3 * i][3 * j + 3].y) +
              3.0 * (nodes[3 * i][3 * j + 2].y + nodes[3 * i + 2][3 * j].y) +
              -1.0 * nodes[3 * i][3 * j].y) / 9.0;
        }
      }

      this.nodes = nodes; // (m*3+1) x (n*3+1) points
      this.colors = colors; // (m+1) x (n+1) x 4  colors (R+G+B+A)
    }

    // Extracts out each patch and then paints it
    paintMesh (v, w) {
      let imax = (this.nodes.length - 1) / 3;
      let jmax = (this.nodes[0].length - 1) / 3;

      if (this.type === 'bilinear' || imax < 2 || jmax < 2) {
        let patch;

        for (let i = 0; i < imax; ++i) {
          for (let j = 0; j < jmax; ++j) {
            let sliceNodes = [];
            for (let k = i * 3, kmax = (i * 3) + 4; k < kmax; ++k) {
              sliceNodes.push(this.nodes[k].slice(j * 3, (j * 3) + 4));
            }

            let sliceColors = [];
            sliceColors.push(this.colors[i].slice(j, j + 2));
            sliceColors.push(this.colors[i + 1].slice(j, j + 2));

            patch = new Patch(sliceNodes, sliceColors);
            patch.paint(v, w);
          }
        }
      } else {
        // Reference:
        // https://en.wikipedia.org/wiki/Bicubic_interpolation#Computation
        let d01, d12, patch, sliceNodes, nodes, f, alpha;
        const ilast = imax;
        const jlast = jmax;
        imax++;
        jmax++;

        /*
         * d = the interpolation data
         * d[i][j] = a node record (Point, color_array, color_dx, color_dy)
         * d[i][j][0] : Point
         * d[i][j][1] : [RGBA]
         * d[i][j][2] = dx [RGBA]
         * d[i][j][3] = dy [RGBA]
         * d[i][j][][k] : color channel k
         */
        let d = new Array(imax);

        // Setting the node and the colors
        for (let i = 0; i < imax; ++i) {
          d[i] = new Array(jmax);
          for (let j = 0; j < jmax; ++j) {
            d[i][j] = [];
            d[i][j][0] = this.nodes[3 * i][3 * j];
            d[i][j][1] = this.colors[i][j];
          }
        }

        // Calculate the inner derivatives
        for (let i = 0; i < imax; ++i) {
          for (let j = 0; j < jmax; ++j) {
            // dx
            if (i !== 0 && i !== ilast) {
              d01 = distance(d[i - 1][j][0], d[i][j][0]);
              d12 = distance(d[i + 1][j][0], d[i][j][0]);
              d[i][j][2] = finiteDifferences(d[i - 1][j][1], d[i][j][1],
                d[i + 1][j][1], d01, d12);
            }

            // dy
            if (j !== 0 && j !== jlast) {
              d01 = distance(d[i][j - 1][0], d[i][j][0]);
              d12 = distance(d[i][j + 1][0], d[i][j][0]);
              d[i][j][3] = finiteDifferences(d[i][j - 1][1], d[i][j][1],
                d[i][j + 1][1], d01, d12);
            }

            // dxy is, by standard, set to 0
          }
        }

        /*
         * Calculate the exterior derivatives
         * We fit the exterior derivatives onto parabolas generated by
         * the point and the interior derivatives.
         */
        for (let j = 0; j < jmax; ++j) {
          d[0][j][2] = [];
          d[ilast][j][2] = [];

          for (let k = 0; k < 4; ++k) {
            d01 = distance(d[1][j][0], d[0][j][0]);
            d12 = distance(d[ilast][j][0], d[ilast - 1][j][0]);

            if (d01 > 0) {
              d[0][j][2][k] = 2.0 * (d[1][j][1][k] - d[0][j][1][k]) / d01 -
                d[1][j][2][k];
            } else {
              console.log(`0 was 0! (j: ${j}, k: ${k})`);
              d[0][j][2][k] = 0;
            }

            if (d12 > 0) {
              d[ilast][j][2][k] = 2.0 * (d[ilast][j][1][k] - d[ilast - 1][j][1][k]) /
                d12 - d[ilast - 1][j][2][k];
            } else {
              console.log(`last was 0! (j: ${j}, k: ${k})`);
              d[ilast][j][2][k] = 0;
            }
          }
        }

        for (let i = 0; i < imax; ++i) {
          d[i][0][3] = [];
          d[i][jlast][3] = [];

          for (let k = 0; k < 4; ++k) {
            d01 = distance(d[i][1][0], d[i][0][0]);
            d12 = distance(d[i][jlast][0], d[i][jlast - 1][0]);

            if (d01 > 0) {
              d[i][0][3][k] = 2.0 * (d[i][1][1][k] - d[i][0][1][k]) / d01 -
                d[i][1][3][k];
            } else {
              console.log(`0 was 0! (i: ${i}, k: ${k})`);
              d[i][0][3][k] = 0;
            }

            if (d12 > 0) {
              d[i][jlast][3][k] = 2.0 * (d[i][jlast][1][k] - d[i][jlast - 1][1][k]) /
                d12 - d[i][jlast - 1][3][k];
            } else {
              console.log(`last was 0! (i: ${i}, k: ${k})`);
              d[i][jlast][3][k] = 0;
            }
          }
        }

        // Fill patches
        for (let i = 0; i < ilast; ++i) {
          for (let j = 0; j < jlast; ++j) {
            let dLeft = distance(d[i][j][0], d[i + 1][j][0]);
            let dRight = distance(d[i][j + 1][0], d[i + 1][j + 1][0]);
            let dTop = distance(d[i][j][0], d[i][j + 1][0]);
            let dBottom = distance(d[i + 1][j][0], d[i + 1][j + 1][0]);
            let r = [[], [], [], []];

            for (let k = 0; k < 4; ++k) {
              f = [];

              f[0] = d[i][j][1][k];
              f[1] = d[i + 1][j][1][k];
              f[2] = d[i][j + 1][1][k];
              f[3] = d[i + 1][j + 1][1][k];
              f[4] = d[i][j][2][k] * dLeft;
              f[5] = d[i + 1][j][2][k] * dLeft;
              f[6] = d[i][j + 1][2][k] * dRight;
              f[7] = d[i + 1][j + 1][2][k] * dRight;
              f[8] = d[i][j][3][k] * dTop;
              f[9] = d[i + 1][j][3][k] * dBottom;
              f[10] = d[i][j + 1][3][k] * dTop;
              f[11] = d[i + 1][j + 1][3][k] * dBottom;
              f[12] = 0; // dxy
              f[13] = 0; // dxy
              f[14] = 0; // dxy
              f[15] = 0; // dxy

              // get alpha values
              alpha = solveLinearSystem(f);

              for (let l = 0; l < 9; ++l) {
                r[k][l] = [];

                for (let m = 0; m < 9; ++m) {
                  // evaluation
                  r[k][l][m] = evaluateSolution(alpha, l / 8, m / 8);

                  if (r[k][l][m] > 255) {
                    r[k][l][m] = 255;
                  } else if (r[k][l][m] < 0.0) {
                    r[k][l][m] = 0.0;
                  }
                }
              }
            }

            // split the bezier patch into 8x8 patches
            sliceNodes = [];
            for (let k = i * 3, kmax = (i * 3) + 4; k < kmax; ++k) {
              sliceNodes.push(this.nodes[k].slice(j * 3, (j * 3) + 4));
            }

            nodes = splitPatch(sliceNodes);

            // Create patches and paint the bilinearliy
            for (let l = 0; l < 8; ++l) {
              for (let m = 0; m < 8; ++m) {
                patch = new Patch(
                  nodes[l][m],
                  [[
                    [r[0][l][m], r[1][l][m], r[2][l][m], r[3][l][m]],
                    [r[0][l][m + 1], r[1][l][m + 1], r[2][l][m + 1], r[3][l][m + 1]]
                  ], [
                    [r[0][l + 1][m], r[1][l + 1][m], r[2][l + 1][m], r[3][l + 1][m]],
                    [r[0][l + 1][m + 1], r[1][l + 1][m + 1], r[2][l + 1][m + 1], r[3][l + 1][m + 1]]
                  ]]
                );

                patch.paint(v, w);
              }
            }
          }
        }
      }
    }

    // Transforms mesh into coordinate space of canvas (t is either Point or Affine).
    transform (t) {
      if (t instanceof Point) {
        for (let i = 0, imax = this.nodes.length; i < imax; ++i) {
          for (let j = 0, jmax = this.nodes[0].length; j < jmax; ++j) {
            this.nodes[i][j] = this.nodes[i][j].add(t);
          }
        }
      } else if (t instanceof Affine) {
        for (let i = 0, imax = this.nodes.length; i < imax; ++i) {
          for (let j = 0, jmax = this.nodes[0].length; j < jmax; ++j) {
            this.nodes[i][j] = this.nodes[i][j].transform(t);
          }
        }
      }
    }

    // Scale mesh into coordinate space of canvas (t is a Point).
    scale (t) {
      for (let i = 0, imax = this.nodes.length; i < imax; ++i) {
        for (let j = 0, jmax = this.nodes[0].length; j < jmax; ++j) {
          this.nodes[i][j] = this.nodes[i][j].scale(t);
        }
      }
    }
  }

  // Start of document processing ---------------------
  const shapes = document.querySelectorAll('rect,circle,ellipse,path,text');

  shapes.forEach((shape, i) => {
    // Get id. If no id, create one.
    let shapeId = shape.getAttribute('id');
    if (!shapeId) {
      shapeId = 'patchjs_shape' + i;
      shape.setAttribute('id', shapeId);
    }

    const fillURL = shape.style.fill.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/);
    const strokeURL = shape.style.stroke.match(/^url\(\s*"?\s*#([^\s"]+)"?\s*\)/);

    if (fillURL && fillURL[1]) {
      const mesh = document.getElementById(fillURL[1]);

      if (mesh && mesh.nodeName === 'meshgradient') {
        const bbox = shape.getBBox();

        // Create temporary canvas
        let myCanvas = document.createElementNS(xhtmlNS, 'canvas');
        setAttributes(myCanvas, {
          'width': bbox.width,
          'height': bbox.height
        });

        const myContext = myCanvas.getContext('2d');
        let myCanvasImage = myContext.createImageData(bbox.width, bbox.height);

        // Draw a mesh
        const myMesh = new Mesh(mesh);

        // Adjust for bounding box if necessary.
        if (mesh.getAttribute('gradientUnits') === 'objectBoundingBox') {
          myMesh.scale(new Point(bbox.width, bbox.height));
        }

        // Apply gradient transform.
        const gradientTransform = mesh.getAttribute('gradientTransform');
        if (gradientTransform != null) {
          myMesh.transform(parseTransform(gradientTransform));
        }

        // Position to Canvas coordinate.
        if (mesh.getAttribute('gradientUnits') === 'userSpaceOnUse') {
          myMesh.transform(new Point(-bbox.x, -bbox.y));
        }

        // Paint
        myMesh.paintMesh(myCanvasImage.data, myCanvas.width);
        myContext.putImageData(myCanvasImage, 0, 0);

        // Create image element of correct size
        const myImage = document.createElementNS(svgNS, 'image');
        setAttributes(myImage, {
          'width': bbox.width,
          'height': bbox.height,
          'x': bbox.x,
          'y': bbox.y
        });

        // Set image to data url
        let myPNG = myCanvas.toDataURL();
        myImage.setAttributeNS(xlinkNS, 'xlink:href', myPNG);

        // Insert image into document
        shape.parentNode.insertBefore(myImage, shape);
        shape.style.fill = 'none';

        // Create clip referencing shape and insert into document
        const use = document.createElementNS(svgNS, 'use');
        use.setAttributeNS(xlinkNS, 'xlink:href', '#' + shapeId);

        const clipId = 'patchjs_clip' + i;
        const clip = document.createElementNS(svgNS, 'clipPath');
        clip.setAttribute('id', clipId);
        clip.appendChild(use);
        shape.parentElement.insertBefore(clip, shape);
        myImage.setAttribute('clip-path', 'url(#' + clipId + ')');

        // Force the Garbage Collector to free the space
        myCanvasImage = null;
        myCanvas = null;
        myPNG = null;
      }
    }

    if (strokeURL && strokeURL[1]) {
      const mesh = document.getElementById(strokeURL[1]);

      if (mesh && mesh.nodeName === 'meshgradient') {
        const strokeWidth = parseFloat(shape.style.strokeWidth.slice(0, -2));
        const strokeMiterlimit = parseFloat(shape.style.strokeMiterlimit) ||
          parseFloat(shape.getAttribute('stroke-miterlimit')) || 1;
        const phase = strokeWidth * strokeMiterlimit;

        const bbox = shape.getBBox();
        const boxWidth = Math.trunc(bbox.width + phase);
        const boxHeight = Math.trunc(bbox.height + phase);
        const boxX = Math.trunc(bbox.x - phase / 2);
        const boxY = Math.trunc(bbox.y - phase / 2);

        // Create temporary canvas
        let myCanvas = document.createElementNS(xhtmlNS, 'canvas');
        setAttributes(myCanvas, {
          'width': boxWidth,
          'height': boxHeight
        });

        const myContext = myCanvas.getContext('2d');
        let myCanvasImage = myContext.createImageData(boxWidth, boxHeight);

        // Draw a mesh
        const myMesh = new Mesh(mesh);

        // Adjust for bounding box if necessary.
        if (mesh.getAttribute('gradientUnits') === 'objectBoundingBox') {
          myMesh.scale(new Point(boxWidth, boxHeight));
        }

        // Apply gradient transform.
        const gradientTransform = mesh.getAttribute('gradientTransform');
        if (gradientTransform != null) {
          myMesh.transform(parseTransform(gradientTransform));
        }

        // Position to Canvas coordinate.
        if (mesh.getAttribute('gradientUnits') === 'userSpaceOnUse') {
          myMesh.transform(new Point(-boxX, -boxY));
        }

        // Paint
        myMesh.paintMesh(myCanvasImage.data, myCanvas.width);
        myContext.putImageData(myCanvasImage, 0, 0);

        // Create image element of correct size
        const myImage = document.createElementNS(svgNS, 'image');
        setAttributes(myImage, {
          'width': boxWidth,
          'height': boxHeight,
          'x': 0,
          'y': 0
        });

        // Set image to data url
        let myPNG = myCanvas.toDataURL();
        myImage.setAttributeNS(xlinkNS, 'xlink:href', myPNG);

        // Create pattern to hold the stroke image
        const patternId = 'pattern_clip' + i;
        const myPattern = document.createElementNS(svgNS, 'pattern');
        setAttributes(myPattern, {
          'id': patternId,
          'patternUnits': 'userSpaceOnUse',
          'width': boxWidth,
          'height': boxHeight,
          'x': boxX,
          'y': boxY
        });
        myPattern.appendChild(myImage);

        // Insert image into document
        mesh.parentNode.appendChild(myPattern);
        shape.style.stroke = 'url(#' + patternId + ')';

        // Force the Garbage Collector to free the space
        myCanvasImage = null;
        myCanvas = null;
        myPNG = null;
      }
    }
  });
})();
