import PlaceableObject from "@client/canvas/placeables/placeable-object.mjs";
import Document from "@common/abstract/document.mjs";

/**
 * @import {Point} from "@common/_types.mjs";
 * @import {EdgeRestrictionType, EdgeDirection, EdgeSenseType} from "@common/constants.mjs";
 * @import {LineIntersection} from "@common/utils/_types.mjs";
 * @import PolygonVertex from "./vertex.mjs";
 * @import {EdgeCreationOptions, EdgeThresholdData} from "../_types.mjs";
 */

/**
 * A data structure used to represent potential edges used by the ClockwiseSweepPolygon.
 * Edges are not polygon-specific, meaning they can be reused across many polygon instances.
 */
export default class Edge {
  /**
   * Construct an Edge by providing the following information.
   * @param {Point} a                         The first endpoint of the edge
   * @param {Point} b                         The second endpoint of the edge
   * @param {EdgeCreationOptions} [options]   Additional options which describe the edge
   */
  constructor(a, b, {id, object, direction, type, light, darkness, move, sight, sound, threshold=null, priority}={}) {
    this.a = new PIXI.Point(a.x, a.y);
    this.b = new PIXI.Point(b.x, b.y);
    this.type = type || "wall";
    if ( id ) this.id = id;
    else if ( object instanceof PlaceableObject ) this.id = `${object.sourceId}`;
    else if ( object instanceof Document ) this.id = `${object.documentName}.${object.id}`;
    this.object = object;
    this.direction = direction ?? CONST.EDGE_DIRECTIONS.BOTH;
    this.light = light ?? CONST.EDGE_SENSE_TYPES.NONE;
    this.darkness = darkness ?? CONST.EDGE_SENSE_TYPES.NONE;
    this.move = move ?? CONST.EDGE_SENSE_TYPES.NONE;
    this.sight = sight ?? CONST.EDGE_SENSE_TYPES.NONE;
    this.sound = sound ?? CONST.EDGE_SENSE_TYPES.NONE;
    this.threshold = threshold;
    this.priority = priority ?? 0;

    // Record the edge orientation arranged from top-left to bottom-right
    const isSE = b.x === a.x ? b.y > a.y : b.x > a.x;
    if ( isSE ) {
      this.nw = a;
      this.se = b;
    }
    else {
      this.nw = b;
      this.se = a;
    }
    this.bounds = new PIXI.Rectangle(this.nw.x, this.nw.y, this.se.x - this.nw.x, this.se.y - this.nw.y).normalize();
  }

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

  /**
   * The first endpoint of the edge.
   * @type {PIXI.Point}
   */
  a;

  /**
   * The second endpoint of the edge.
   * @type {PIXI.Point}
   */
  b;

  /**
   * The endpoint of the edge which is oriented towards the top-left.
   */
  nw;

  /**
   * The endpoint of the edge which is oriented towards the bottom-right.
   */
  se;

  /**
   * The rectangular bounds of the edge. Used by the quadtree.
   * @type {PIXI.Rectangle}
   */
  bounds;

  /**
   * The direction of effect for the edge.
   * @type {EdgeDirection}
   */
  direction;

  /**
   * A string used to uniquely identify this edge, if any.
   * @type {string|undefined}
   */
  id;

  /**
   * The Document/PlaceableObject the edge belongs to, if any.
   * @type {Document|PlaceableObject|undefined}
   */
  object;

  /**
   * How this edge restricts light.
   * @type {EdgeSenseType}
   */
  light;

  /**
   * How this edge restricts darkness.
   * @type {EdgeSenseType}
   */
  darkness;

  /**
   * How this edge restricts movement.
   * @type {EdgeSenseType}
   */
  move;

  /**
   * How this edge restricts sight.
   * @type {EdgeSenseType}
   */
  sight;

  /**
   * How this edge restricts sound.
   * @type {EdgeSenseType}
   */
  sound;

  /**
   * Specialized threshold data for this edge.
   * @type {EdgeThresholdData|null}
   */
  threshold;

  /**
   * Record other edges which this one intersects with by level.
   * @type {{[level: string]: {edge: Edge; intersection: LineIntersection}[]}}
   */
  intersections = {};

  /**
   * A PolygonVertex instance.
   * Used as part of ClockwiseSweepPolygon computation.
   * @type {PolygonVertex|undefined}
   */
  vertexA;

  /**
   * A PolygonVertex instance.
   * Used as part of ClockwiseSweepPolygon computation.
   * @type {PolygonVertex|undefined}
   */
  vertexB;

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

  /**
   * Is this edge limited for a particular type?
   * @param {EdgeRestrictionType} type
   * @returns {boolean}
   */
  isLimited(type) {
    return this[type] === CONST.EDGE_SENSE_TYPES.LIMITED;
  }

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

  /**
   * Create a copy of the Edge which can be safely mutated.
   * @returns {Edge}
   */
  clone() {
    const clone = new this.constructor(this.a, this.b, this);
    clone.intersections = Object.entries(this.intersections).reduce((obj, [level, intersections]) => {
      obj[level] = [...intersections];
      return obj;
    }, {});
    clone.vertexA = this.vertexA;
    clone.vertexB = this.vertexB;
    return clone;
  }

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

  /**
   * Get an intersection point between this Edge and another.
   * @param {Edge} other
   * @returns {LineIntersection|void}
   */
  getIntersection(other) {
    if ( this === other ) return;
    const {a: a0, b: b0} = this;
    const {a: a1, b: b1} = other;

    // Ignore edges which share an endpoint
    if ( a0.equals(a1) || a0.equals(b1) || b0.equals(a1) || b0.equals(b1) ) return;

    // Initial fast CCW test for intersection
    if ( !foundry.utils.lineSegmentIntersects(a0, b0, a1, b1) ) return;

    // Slower computation of intersection point
    const i = foundry.utils.lineLineIntersection(a0, b0, a1, b1, {t1: true});
    if ( !i ) return;  // Eliminates co-linear lines, theoretically should not be necessary but just in case
    return i;
  }

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

  /**
   * Test whether to apply a proximity threshold to this edge.
   * If the proximity threshold is met, this edge excluded from perception calculations.
   * @param {string} sourceType     Sense type for the source
   * @param {Point} sourceOrigin    The origin or position of the source on the canvas
   * @param {number} [externalRadius=0] The external radius of the source
   * @returns {boolean}             True if the edge has a threshold greater than 0 for the source type,
   *                                and the source type is within that distance.
   */
  applyThreshold(sourceType, sourceOrigin, externalRadius=0) {
    const d = this.threshold?.[sourceType];
    const t = this[sourceType];
    if ( !d || (t < CONST.EDGE_SENSE_TYPES.PROXIMITY) ) return false; // Threshold behavior does not apply
    const proximity = t === CONST.EDGE_SENSE_TYPES.PROXIMITY;
    const pt = foundry.utils.closestPointToSegment(sourceOrigin, this.a, this.b);
    const sourceDistance = Math.hypot(pt.x - sourceOrigin.x, pt.y - sourceOrigin.y);
    return proximity ? Math.max(sourceDistance - externalRadius, 0) < d : (sourceDistance + externalRadius) > d;
  }

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

  /**
   * Determine the orientation of this Edge with respect to a reference point.
   * @param {Point} point       Some reference point, relative to which orientation is determined
   * @returns {number}          An orientation in CONST.EDGE_DIRECTIONS which indicates whether the Point is left,
   *                            right, or collinear (both) with the Edge
   */
  orientPoint(point) {
    const orientation = foundry.utils.orient2dFast(this.a, this.b, point);
    if ( orientation === 0 ) return CONST.EDGE_DIRECTIONS.BOTH;
    return orientation < 0 ? CONST.EDGE_DIRECTIONS.LEFT : CONST.EDGE_DIRECTIONS.RIGHT;
  }

  /* -------------------------------------------- */
  /*  Intersection Management                     */
  /* -------------------------------------------- */

  /**
   * Identify intersections between a provided iterable of edges.
   * @param {Iterable<Edge>} edges    An iterable of edges
   * @param {string} level            The ID of the Level the edges are in
   */
  static identifyEdgeIntersections(edges, level) {

    // Sort edges by their north-west x value, breaking ties with the south-east x value
    const sorted = [];
    for ( const edge of edges ) {
      delete edge.intersections[level]; // Clear prior intersections
      sorted.push(edge);
    }
    sorted.sort((e1, e2) => (e1.nw.x - e2.nw.x) || (e1.se.x - e2.se.x));

    // Iterate over all known edges, identifying intersections
    const ln = sorted.length;
    for ( let i=0; i<ln; i++ ) {
      const e1 = sorted[i];
      for ( let j=i+1; j<ln; j++ ) {
        const e2 = sorted[j];
        if ( e2.nw.x > e1.se.x ) break; // Segment e2 is entirely right of segment e1
        e1.recordIntersections(e2, level);
      }
    }
  }

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

  /**
   * Record the intersections between two edges.
   * @param {Edge} other          Another edge to test and record
   * @param {string} level        The ID of the Level the edges are in
   */
  recordIntersections(other, level) {
    if ( other === this ) return;
    const i = this.getIntersection(other);
    if ( !i ) return;
    this.intersections[level] ??= [];
    this.intersections[level].push({edge: other, intersection: i});
    other.intersections[level] ??= [];
    other.intersections[level].push({edge: this, intersection: {x: i.x, y: i.y, t0: i.t1, t1: i.t0}});
  }

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

  /**
   * Remove intersections of this edge with all other edges.
   * @param {string} level    The ID of the Level the edges are in
   */
  removeIntersections(level) {
    const intersections = this.intersections[level];
    if ( !intersections ) return;
    delete this.intersections[level];
    for ( const {edge: other} of intersections ) {
      const otherIntersections = other.intersections[level];
      otherIntersections.findSplice(e => e.edge === this);
      if ( !otherIntersections.length ) delete other.intersections[level];
    }
  }
}
