/**
 * Determine the center of the circle.
 * Trivial, but used to match center method for other shapes.
 * @type {PIXI.Point}
 */
Object.defineProperty(PIXI.Circle.prototype, "center", { get: function() {
  return new PIXI.Point(this.x, this.y);
}});

/* -------------------------------------------- */

/**
 * Determine if a point is on or nearly on this circle.
 * @param {Point} point       Point to test
 * @param {number} epsilon    Tolerated margin of error
 * @returns {boolean}         Is the point on the circle within the allowed tolerance?
 */
PIXI.Circle.prototype.pointIsOn = function(point, epsilon = 1e-08) {
  const dist2 = Math.pow(point.x - this.x, 2) + Math.pow(point.y - this.y, 2);
  const r2 = Math.pow(this.radius, 2);
  return dist2.almostEqual(r2, epsilon);
};

/* -------------------------------------------- */

/**
 * Get all intersection points on this circle for a segment A|B
 * Intersections are sorted from A to B.
 * @param {Point} a             The first endpoint on segment A|B
 * @param {Point} b             The second endpoint on segment A|B
 * @returns {Point[]}           Points where the segment A|B intersects the circle
 */
PIXI.Circle.prototype.segmentIntersections = function(a, b) {
  const ixs = foundry.utils.lineCircleIntersection(a, b, this, this.radius);
  return ixs.intersections;
};

/* -------------------------------------------- */

/**
 * Calculate an x,y point on this circle's circumference given an angle
 * 0: due east
 * π / 2: due south
 * π or -π: due west
 * -π/2: due north
 * @param {number} angle      Angle of the point, in radians
 * @returns {Point}           The point on the circle at the given angle
 */
PIXI.Circle.prototype.pointAtAngle = function(angle) {
  return {
    x: this.x + (this.radius * Math.cos(angle)),
    y: this.y + (this.radius * Math.sin(angle))
  };
};

/* -------------------------------------------- */

/**
 * Get all the points for a polygon approximation of this circle between two points.
 * The two points can be anywhere in 2d space. The intersection of this circle with the line from this circle center
 * to the point will be used as the start or end point, respectively.
 * This is used to draw the portion of the circle (the arc) between two intersection points on this circle.
 * @param {Point} a             Point in 2d space representing the start point
 * @param {Point} b             Point in 2d space representing the end point
 * @param {object} [options]    Options passed on to the pointsForArc method
 * @returns { Point[]}          An array of points arranged clockwise from start to end
 */
PIXI.Circle.prototype.pointsBetween = function(a, b, options) {
  const fromAngle = Math.atan2(a.y - this.y, a.x - this.x);
  const toAngle = Math.atan2(b.y - this.y, b.x - this.x);
  return this.pointsForArc(fromAngle, toAngle, { includeEndpoints: false, ...options });
};

/* -------------------------------------------- */

/**
 * Get the points that would approximate a circular arc along this circle, given a starting and ending angle.
 * Points returned are clockwise. If from and to are the same, a full circle will be returned.
 * @param {number} fromAngle     Starting angle, in radians. π is due north, π/2 is due east
 * @param {number} toAngle       Ending angle, in radians
 * @param {object} [options]     Options which affect how the circle is converted
 * @param {number} [options.density]           The number of points which defines the density of approximation
 * @param {boolean} [options.includeEndpoints]  Whether to include points at the circle where the arc starts and ends
 * @returns {Point[]}             An array of points along the requested arc
 */
PIXI.Circle.prototype.pointsForArc = function(fromAngle, toAngle, {density, includeEndpoints=true} = {}) {
  const pi2 = 2 * Math.PI;
  density ??= this.constructor.approximateVertexDensity(this.radius);
  const points = [];
  const delta = pi2 / density;
  if ( includeEndpoints ) points.push(this.pointAtAngle(fromAngle));

  // Determine number of points to add
  let dAngle = toAngle - fromAngle;
  while ( dAngle <= 0 ) dAngle += pi2; // Angles may not be normalized, so normalize total.
  const nPoints = Math.round(dAngle / delta);

  // Construct padding rays (clockwise)
  for ( let i = 1; i < nPoints; i++ ) points.push(this.pointAtAngle(fromAngle + (i * delta)));
  if ( includeEndpoints ) points.push(this.pointAtAngle(toAngle));
  return points;
};

/* -------------------------------------------- */

/**
 * Approximate this PIXI.Circle as a PIXI.Polygon
 * @param {object} [options]      Options forwarded on to the pointsForArc method
 * @returns {PIXI.Polygon}        The Circle expressed as a PIXI.Polygon
 */
PIXI.Circle.prototype.toPolygon = function(options) {
  const points = this.pointsForArc(0, 0, options);
  points.pop(); // Drop the repeated endpoint
  return new PIXI.Polygon(points);
};

/* -------------------------------------------- */

/**
 * The recommended vertex density for the regular polygon approximation of a circle of a given radius.
 * Small radius circles have fewer vertices. The returned value will be rounded up to the nearest integer.
 * See the formula described at:
 * https://math.stackexchange.com/questions/4132060/compute-number-of-regular-polgy-sides-to-approximate-circle-to-defined-precision
 * @param {number} radius     Circle radius
 * @param {number} [epsilon]  The maximum tolerable distance between an approximated line segment and the true radius.
 *                            A larger epsilon results in fewer points for a given radius.
 * @returns {number}          The number of points for the approximated polygon
 */
PIXI.Circle.approximateVertexDensity = function(radius, epsilon=1) {
  return Math.ceil(Math.PI / Math.sqrt(2 * (epsilon / radius)));
};

/* -------------------------------------------- */

/**
 * Intersect this PIXI.Circle with a PIXI.Polygon.
 * @param {PIXI.Polygon} polygon      A PIXI.Polygon
 * @param {object} [options]          Options which configure how the intersection is computed
 * @param {number} [options.density]              The number of points which defines the density of approximation
 * @param {number} [options.clipType]             The clipper clip type
 * @param {string} [options.weilerAtherton=true]  Use the Weiler-Atherton algorithm. Otherwise, use Clipper.
 * @returns {PIXI.Polygon}            The intersected polygon
 */
PIXI.Circle.prototype.intersectPolygon = function(polygon, {density, clipType, weilerAtherton=true, ...options}={}) {
  if ( !this.radius ) return new PIXI.Polygon([]);
  clipType ??= ClipperLib.ClipType.ctIntersection;

  // Use Weiler-Atherton for efficient intersection or union
  if ( weilerAtherton && polygon.isPositive ) {
    const res = WeilerAthertonClipper.combine(polygon, this, {clipType, density, ...options});
    if ( !res.length ) return new PIXI.Polygon([]);
    return res[0];
  }

  // Otherwise, use Clipper polygon intersection
  const approx = this.toPolygon({density});
  return polygon.intersectPolygon(approx, options);
};

/* -------------------------------------------- */

/**
 * Intersect this PIXI.Circle with an array of ClipperPoints.
 * Convert the circle to a Polygon approximation and use intersectPolygon.
 * In the future we may replace this with more specialized logic which uses the line-circle intersection formula.
 * @param {ClipperPoint[]} clipperPoints  Array of ClipperPoints generated by PIXI.Polygon.toClipperPoints()
 * @param {object} [options]              Options which configure how the intersection is computed
 * @param {number} [options.density]      The number of points which defines the density of approximation
 * @returns {PIXI.Polygon}                The intersected polygon
 */
PIXI.Circle.prototype.intersectClipper = function(clipperPoints, {density, ...options}={}) {
  if ( !this.radius ) return [];
  const approx = this.toPolygon({density});
  return approx.intersectClipper(clipperPoints, options);
};
