import BaseWall from "@common/documents/wall.mjs";
import CanvasDocumentMixin from "./abstract/canvas-document.mjs";
import Edge from "@client/canvas/geometry/edges/edge.mjs";

/**
 * @import {WallCategory} from "./_types.mjs";
 * @import {EdgeRestrictionType, EdgeSenseType} from "@common/constants.mjs";
 * @import {WallData} from "@common/documents/_types.mjs";
 * @import Level from "./level.mjs";
 */

/**
 * The client-side Wall document which extends the common BaseWall document model.
 * @extends BaseWall
 * @mixes ClientDocumentMixin
 * @category Documents
 *
 * @see {@link foundry.documents.Scene}: The Scene document type which contains Wall documents
 * @see {@link foundry.applications.sheets.WallConfig}: The Wall configuration application
 */
export default class WallDocument extends CanvasDocumentMixin(BaseWall) {

  /**
   * A Wall cannot be hidden in the same sense as other canvas Documents, though Walls in general (excluding non-secret
   * doors) are not visible to players.
   * @readonly
   */
  hidden = false;

  /**
   * A Wall cannot be locked.
   * @readonly
   */
  locked = false;

  /**
   * The Edge instance which represents this Wall.
   * The Edge is re-created when data for the Wall changes.
   * @type {Edge|null}
   */
  get edge() {
    return this.#edge;
  }

  /** @type {Edge|null} */
  #edge = null;

  /**
   * The darkness edge sense type, which is the same value as {@link WallData#light}.
   * @type {EdgeSenseType}
   */
  get darkness() {
    return this.light;
  }

  /**
   * Whether this Document represents a door.
   * @type {boolean}
   */
  get isDoor() {
    return this.door > CONST.WALL_DOOR_TYPES.NONE;
  }

  /**
   * Whether this Document represents an open door.
   * @type {boolean}
   */
  get isOpen() {
    return this.isDoor && (this.ds === CONST.WALL_DOOR_STATES.OPEN);
  }

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

  /** @override */
  prepareBaseData() {
    this.threshold.darkness = this.threshold.light;
  }

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

  /**
   * Broadly classify a wall into one of several categories, based on its properties.
   * @returns {WallCategory}
   */
  getWallCategory() {
    const {NONE, LIMITED, PROXIMITY, DISTANCE} = CONST.EDGE_SENSE_TYPES;
    const {DOOR, SECRET} = CONST.WALL_DOOR_TYPES;
    if ( this.sight === NONE ) {
      if ( this.light + this.sound + this.move === NONE ) return "blank";
      return "invisible";
    }
    if ( this.sight === LIMITED ) return "terrain";
    if ( (this.sight === DISTANCE) || (this.sight === PROXIMITY) ) return "window";
    if ( this.move === NONE ) return "ethereal";
    if ( this.door === DOOR ) return "door";
    if ( this.door === SECRET ) return "secret";
    return "normal";
  }

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

  /**
   * Initialize the edge which represents this Wall document.
   * @param {object} [options]                                 Options which modify how the edge is initialized
   * @param {boolean} [options.deleted]                        Delete the edge of this Wall document?
   */
  initializeEdge({deleted=false}={}) {
    if ( !this.parent?.initializedEdges || (deleted && !this.#edge) ) return;
    const priorEdge = this.#edge;
    const newEdge = this.#edge = deleted ? null : this.#createEdge();
    const addedLevels = [];
    const removedLevels = [];
    const changedLevels = [];
    for ( const level of this.parent.levels ) {
      const hadPriorEdge = (priorEdge !== null) && level.edges.delete(priorEdge.id);
      if ( newEdge && this.includedInLevel(level) ) {
        level.edges.set(newEdge.id, newEdge);
        if ( hadPriorEdge ) changedLevels.push(level);
        else addedLevels.push(level);
      }
      else if ( hadPriorEdge ) removedLevels.push(level);
    }
    if ( addedLevels.length ) {
      const changedTypes = new Set();
      for ( const type of CONST.EDGE_RESTRICTION_TYPES ) {
        if ( newEdge[type] ) changedTypes.add(type);
      }
      for ( const level of addedLevels ) this._onEdgeChange(level, newEdge, null, changedTypes);
    }
    if ( removedLevels.length ) {
      const changedTypes = new Set();
      for ( const type of CONST.EDGE_RESTRICTION_TYPES ) {
        if ( priorEdge[type] ) changedTypes.add(type);
      }
      for ( const level of removedLevels ) this._onEdgeChange(level, null, priorEdge, changedTypes);
    }
    if ( changedLevels.length ) {
      const changedTypes = new Set();
      const coordinatesChanged = !newEdge.a.equals(priorEdge.a) || !newEdge.b.equals(priorEdge.b)
        || (newEdge.direction !== priorEdge.direction);
      const attenuationChanged = !newEdge.threshold?.attenuation !== !priorEdge.threshold?.attenuation;
      for ( const type of CONST.EDGE_RESTRICTION_TYPES ) {
        if ( (newEdge[type] !== priorEdge[type]) || (newEdge[type] && coordinatesChanged)
         || ((newEdge[type] >= CONST.EDGE_SENSE_TYPES.PROXIMITY) && (attenuationChanged
           || (newEdge.threshold?.[type] ?? 0) !== (priorEdge.threshold?.[type] ?? 0))) ) {
          changedTypes.add(type);
        }
      }
      for ( const level of changedLevels ) this._onEdgeChange(level, newEdge, priorEdge, changedTypes);
    }
  }

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

  /**
   * Create an Edge from the Wall placeable.
   * @returns {Edge}
   */
  #createEdge() {
    const {c, threshold, dir: direction} = this;
    let {light, darkness, sight, sound, move} = this;
    if ( this.isOpen ) light = darkness = sight = sound = move = CONST.EDGE_SENSE_TYPES.NONE;
    const dpx = this.parent.dimensions.distancePixels;
    return new Edge({x: c[0], y: c[1]}, {x: c[2], y: c[3]}, {
      id: `wall.${this.id}`,
      object: this,
      type: "wall",
      direction,
      light,
      darkness,
      sight,
      sound,
      move,
      threshold: {
        light: threshold.light * dpx,
        darkness: threshold.darkness * dpx,
        sight: threshold.sight * dpx,
        sound: threshold.sound * dpx,
        attenuation: threshold.attenuation
      }
    });
  }

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

  /**
   * Handle changes to the edge of this Wall document in a given level.
   * @param {Level} level                                    The level the edge is changed in
   * @param {Edge|null} newEdge                              The new edge in the level, unless removed from level
   * @param {Edge|null} priorEdge                            The prior edge in the level, unless added to level
   * @param {ReadonlySet<EdgeRestrictionType>} changedTypes  The affected edge restriction types
   * @protected
   */
  _onEdgeChange(level, newEdge, priorEdge, changedTypes) {
    if ( !changedTypes.size ) return;

    // Re-initialize vision sources and re-calculate planned movement paths
    if ( level.isView ) {
      if ( changedTypes.has("sight") ) canvas.perception.update({initializeVision: true});
      if ( changedTypes.has("move") ) canvas.tokens.recalculatePlannedMovementPaths();
    }

    // Refresh visibility and re-initialize light, darkness, and sound sources
    if ( level.isVisible ) {
      canvas.perception.update({
        refreshVision: changedTypes.size !== 0,
        initializeLighting: changedTypes.has("light") || changedTypes.has("darkness"),
        initializeSounds: changedTypes.has("sound")
      });
    }

    // Update region shape constraints
    level.updateRegionShapeConstraints(changedTypes);
  }

  /* -------------------------------------------- */
  /*  Event Listeners and Handlers                */
  /* -------------------------------------------- */

  /** @inheritDoc */
  _onCreate(data, options, userId) {
    super._onCreate(data, options, userId);
    this.initializeEdge();
  }

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

  /** @inheritDoc */
  _onUpdate(changed, options, userId) {
    super._onUpdate(changed, options, userId);
    if ( !this.parent.initializedEdges ) return;
    const edgeChanged = ["c", "levels", "light", "move", "sight", "sound", "dir", "door", "ds", "threshold"].some(k => k in changed);
    if ( edgeChanged ) this.initializeEdge();
  }

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

  /** @inheritDoc */
  _onDelete(options, userId) {
    super._onDelete(options, userId);
    this.initializeEdge({deleted: true});
  }
}
