/**
 * Test whether the polygon is has a positive signed area.
 * Using a y-down axis orientation, this means that the polygon is "clockwise".
 * @type {boolean}
 */
Object.defineProperties(PIXI.Polygon.prototype, {
  isPositive: {
    get: function() {
      if ( this._isPositive !== undefined ) return this._isPositive;
      if ( this.points.length < 6 ) return undefined;
      return this._isPositive = this.signedArea() > 0;
    }
  },
  _isPositive: {value: undefined, writable: true, enumerable: false}
});

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

/**
 * Clear the cached signed orientation.
 */
PIXI.Polygon.prototype.clearCache = function() {
  this._isPositive = undefined;
};

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

/**
 * Compute the signed area of polygon using an approach similar to ClipperLib.Clipper.Area.
 * The math behind this is based on the Shoelace formula. https://en.wikipedia.org/wiki/Shoelace_formula.
 * The area is positive if the orientation of the polygon is positive.
 * @returns {number}              The signed area of the polygon
 */
PIXI.Polygon.prototype.signedArea = function() {
  const points = this.points;
  const ln = points.length;
  if ( ln < 6 ) return 0;

  // Compute area
  let area = 0;
  let x1 = points[ln - 2];
  let y1 = points[ln - 1];
  for ( let i = 0; i < ln; i += 2 ) {
    const x2 = points[i];
    const y2 = points[i + 1];
    area += (x2 - x1) * (y2 + y1);
    x1 = x2;
    y1 = y2;
  }

  // Negate the area because in Foundry canvas, y-axis is reversed
  // See https://sourceforge.net/p/jsclipper/wiki/documentation/#clipperlibclipperorientation
  // The 1/2 comes from the Shoelace formula
  return area * -0.5;
};

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

/**
 * Reverse the order of the polygon points in-place, replacing the points array into the polygon.
 * Note: references to the old points array will not be affected.
 * @returns {PIXI.Polygon}      This polygon with its orientation reversed
 */
PIXI.Polygon.prototype.reverseOrientation = function() {
  const reversed_pts = [];
  const pts = this.points;
  const ln = pts.length - 2;
  for ( let i = ln; i >= 0; i -= 2 ) reversed_pts.push(pts[i], pts[i + 1]);
  this.points = reversed_pts;
  if ( this._isPositive !== undefined ) this._isPositive = !this._isPositive;
  return this;
};

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

/**
 * Add a de-duplicated point to the Polygon.
 * @param {Point} point         The point to add to the Polygon
 * @returns {PIXI.Polygon}      A reference to the polygon for method chaining
 */
PIXI.Polygon.prototype.addPoint = function({x, y}={}) {
  const l = this.points.length;
  if ( (x === this.points[l-2]) && (y === this.points[l-1]) ) return this;
  this.points.push(x, y);
  this.clearCache();
  return this;
};

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

/**
 * Return the bounding box for a PIXI.Polygon.
 * The bounding rectangle is normalized such that the width and height are non-negative.
 * @returns {PIXI.Rectangle}    The bounding PIXI.Rectangle
 */
PIXI.Polygon.prototype.getBounds = function() {
  if ( this.points.length < 2 ) return new PIXI.Rectangle(0, 0, 0, 0);
  let maxX; let maxY;
  let minX = maxX = this.points[0];
  let minY = maxY = this.points[1];
  for ( let i=3; i<this.points.length; i+=2 ) {
    const x = this.points[i-1];
    const y = this.points[i];
    if ( x < minX ) minX = x;
    else if ( x > maxX ) maxX = x;
    if ( y < minY ) minY = y;
    else if ( y > maxY ) maxY = y;
  }
  return new PIXI.Rectangle(minX, minY, maxX - minX, maxY - minY);
};

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

/**
 * @typedef {Object} ClipperPoint
 * @property {number} X
 * @property {number} Y
 */

/**
 * Construct a PIXI.Polygon instance from an array of clipper points [{X,Y}, ...].
 * @param {ClipperPoint[]} points                 An array of points returned by clipper
 * @param {object} [options]                      Options which affect how canvas points are generated
 * @param {number} [options.scalingFactor=1]        A scaling factor used to preserve floating point precision
 * @returns {PIXI.Polygon}                        The resulting PIXI.Polygon
 */
PIXI.Polygon.fromClipperPoints = function(points, {scalingFactor=1}={}) {
  const polygonPoints = [];
  for ( const point of points ) {
    polygonPoints.push(point.X / scalingFactor, point.Y / scalingFactor);
  }
  return new PIXI.Polygon(polygonPoints);
};

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

/**
 * Convert a PIXI.Polygon into an array of clipper points [{X,Y}, ...].
 * Note that clipper points must be rounded to integers.
 * In order to preserve some amount of floating point precision, an optional scaling factor may be provided.
 * @param {object} [options]                  Options which affect how clipper points are generated
 * @param {number} [options.scalingFactor=1]    A scaling factor used to preserve floating point precision
 * @returns {ClipperPoint[]}                  An array of points to be used by clipper
 */
PIXI.Polygon.prototype.toClipperPoints = function({scalingFactor=1}={}) {
  const points = [];
  for ( let i = 1; i < this.points.length; i += 2 ) {
    points.push({
      X: Math.round(this.points[i-1] * scalingFactor),
      Y: Math.round(this.points[i] * scalingFactor)
    });
  }
  return points;
};

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

/**
 * Determine whether the PIXI.Polygon is closed, defined by having the same starting and ending point.
 * @type {boolean}
 */
Object.defineProperty(PIXI.Polygon.prototype, "isClosed", {
  get: function() {
    const ln = this.points.length;
    if ( ln < 4 ) return false;
    return (this.points[0] === this.points[ln-2]) && (this.points[1] === this.points[ln-1]);
  },
  enumerable: false
});

/* -------------------------------------------- */
/*  Intersection Methods                        */
/* -------------------------------------------- */

/**
 * Intersect this PIXI.Polygon with another PIXI.Polygon using the clipper library.
 * @param {PIXI.Polygon} other        Another PIXI.Polygon
 * @param {object} [options]          Options which configure how the intersection is computed
 * @param {number} [options.clipType]       The clipper clip type
 * @param {number} [options.scalingFactor]  A scaling factor passed to Polygon#toClipperPoints to preserve precision
 * @returns {PIXI.Polygon}       The intersected polygon
 */
PIXI.Polygon.prototype.intersectPolygon = function(other, {clipType, scalingFactor}={}) {
  const otherPts = other.toClipperPoints({scalingFactor});
  const solution = this.intersectClipper(otherPts, {clipType, scalingFactor});
  return PIXI.Polygon.fromClipperPoints(solution.length ? solution[0] : [], {scalingFactor});
};

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

/**
 * Intersect this PIXI.Polygon with an array of ClipperPoints.
 * @param {ClipperPoint[]} clipperPoints    Array of clipper points generated by PIXI.Polygon.toClipperPoints()
 * @param {object} [options]                Options which configure how the intersection is computed
 * @param {number} [options.clipType]         The clipper clip type
 * @param {number} [options.scalingFactor]    A scaling factor passed to Polygon#toClipperPoints to preserve precision
 * @returns {ClipperPoint[]}                The resulting ClipperPaths
 */
PIXI.Polygon.prototype.intersectClipper = function(clipperPoints, {clipType, scalingFactor} = {}) {
  clipType ??= ClipperLib.ClipType.ctIntersection;
  const c = new ClipperLib.Clipper();
  c.AddPath(this.toClipperPoints({scalingFactor}), ClipperLib.PolyType.ptSubject, true);
  c.AddPath(clipperPoints, ClipperLib.PolyType.ptClip, true);
  const solution = new ClipperLib.Paths();
  c.Execute(clipType, solution);
  return solution;
};

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

/**
 * Intersect this PIXI.Polygon with a PIXI.Circle.
 * For now, 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 {PIXI.Circle} circle        A PIXI.Circle
 * @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.Polygon.prototype.intersectCircle = function(circle, options) {
  return circle.intersectPolygon(this, options);
};

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

/**
 * Intersect this PIXI.Polygon with a PIXI.Rectangle.
 * For now, convert the rectangle to a Polygon and use intersectPolygon.
 * In the future we may replace this with more specialized logic which uses the line-line intersection formula.
 * @param {PIXI.Rectangle} rect       A PIXI.Rectangle
 * @param {object} [options]          Options which configure how the intersection is computed
 * @returns {PIXI.Polygon}            The intersected polygon
 */
PIXI.Polygon.prototype.intersectRectangle = function(rect, options) {
  return rect.intersectPolygon(this, options);
};
