function fitCurve(points, maxError, progressCallback) {
  if (!Array.isArray(points)) {
    throw new TypeError("First argument should be an array");
  }
  points.forEach(point => {
    if (!Array.isArray(point) || point.some(item => typeof item !== 'number') || point.length !== points[0].length) {
      throw Error("Each point should be an array of numbers. Each point should have the same amount of numbers.");
    }
  });
  points = points.filter((point, i) => i === 0 || !point.every((val, j) => val === points[i - 1][j]));
  if (points.length < 2) {
    return [];
  }
  const len = points.length;
  const leftTangent = createTangent(points[1], points[0]);
  const rightTangent = createTangent(points[len - 2], points[len - 1]);
  return fitCubic(points, leftTangent, rightTangent, maxError, progressCallback);
}

function createTangent(pointA, pointB) {
  return maths.normalize(maths.subtract(pointA, pointB));
}

function fitCubic(points, leftTangent, rightTangent, error, progressCallback) {
  const MaxIterations = 20;
  var bezCurve, u, uPrime, maxError, prevErr, splitPoint, prevSplit, centerVector, toCenterTangent, fromCenterTangent, beziers, dist, i;
  if (points.length === 2) {
    dist = maths.vectorLen(maths.subtract(points[0], points[1])) / 3.0;
    bezCurve = [points[0], maths.addArrays(points[0], maths.mulItems(leftTangent, dist)), maths.addArrays(points[1], maths.mulItems(rightTangent, dist)), points[1]];
    return [bezCurve];
  }
  u = chordLengthParameterize(points);
  [bezCurve, maxError, splitPoint] = generateAndReport(points, u, u, leftTangent, rightTangent, progressCallback);
  if (maxError === 0 || maxError < error) {
    return [bezCurve];
  }
  if (maxError < error * error) {
    uPrime = u;
    prevErr = maxError;
    prevSplit = splitPoint;
    for (i = 0; i < MaxIterations; i++) {
      uPrime = reparameterize(bezCurve, points, uPrime);
      [bezCurve, maxError, splitPoint] = generateAndReport(points, u, uPrime, leftTangent, rightTangent, progressCallback);
      if (maxError < error) {
        return [bezCurve];
      } else if (splitPoint === prevSplit) {
        let errChange = maxError / prevErr;
        if (errChange > .9999 && errChange < 1.0001) {
          break;
        }
      }
      prevErr = maxError;
      prevSplit = splitPoint;
    }
  }
  beziers = [];
  centerVector = maths.subtract(points[splitPoint - 1], points[splitPoint + 1]);
  if (centerVector.every(val => val === 0)) {
    centerVector = maths.subtract(points[splitPoint - 1], points[splitPoint]);
    [centerVector[0], centerVector[1]] = [-centerVector[1], centerVector[0]];
  }
  toCenterTangent = maths.normalize(centerVector);
  fromCenterTangent = maths.mulItems(toCenterTangent, -1);
  beziers = beziers.concat(fitCubic(points.slice(0, splitPoint + 1), leftTangent, toCenterTangent, error, progressCallback));
  beziers = beziers.concat(fitCubic(points.slice(splitPoint), fromCenterTangent, rightTangent, error, progressCallback));
  return beziers;
}

class maths {
  static mulItems(items, multiplier) {
    return items.map(x => x * multiplier);
  }
  static mulMatrix(m1, m2) {
    return m1.reduce((sum, x1, i) => sum + x1 * m2[i], 0);
  }
  static subtract(arr1, arr2) {
    return arr1.map((x1, i) => x1 - arr2[i]);
  }
  static addArrays(arr1, arr2) {
    return arr1.map((x1, i) => x1 + arr2[i]);
  }
  static vectorLen(v) {
    return Math.hypot(...v);
  }
  static divItems(items, divisor) {
    return items.map(x => x / divisor);
  }
  static normalize(v) {
    return this.divItems(v, this.vectorLen(v));
  }
}

export { fitCurve }