import BaseLevel from "@common/documents/level.mjs";
import ClientDocumentMixin from "@client/documents/abstract/client-document.mjs";
import CanvasEdges from "@client/canvas/geometry/edges/edges.mjs";

/**
 * The client-side Level document which extends the common BaseTile document model.
 * @extends BaseLevel
 * @mixes ClientDocumentMixin
 * @category Documents
 *
 * @see {@link foundry.documents.Scene}: The Scene document type which contains Level documents
 */
export default class Level extends ClientDocumentMixin(BaseLevel) {

  /**
   * The integer index of the Level, assigned during Scene data preparation.
   * @type {number}
   */
  index;

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

  /**
   * Is this level currently viewed?
   * @type {boolean}
   */
  get isView() {
    return this.parent?._view === this.id;
  }

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

  /**
   * Is this level currently visible?
   * @type {boolean}
   */
  get isVisible() {
    const view = this.parent?._view;
    if ( !view ) return false;
    if ( view === this.id ) return true;
    const viewedLevel = this.parent.levels.get(view);
    return viewedLevel.visibility.levels.has(this.id);
  }

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

  /**
   * The edges of this Level.
   * @type {CanvasEdges}
   */
  get edges() {
    return this.#edges ??= new CanvasEdges(this);
  }

  #edges;

  /* -------------------------------------------- */
  /*  Document Preparation                        */
  /* -------------------------------------------- */

  /** @inheritDoc */
  prepareBaseData() {
    super.prepareBaseData();
    this.elevation.bottom ??= -Infinity;
    this.elevation.top ??= Infinity;
    this.elevation.base = Number.isFinite(this.elevation.bottom) ? this.elevation.bottom
      : Math.min(this.elevation.top, 0);
  }

  /* -------------------------------------------- */
  /*  Event Handlers                              */
  /* -------------------------------------------- */

  /** @inheritDoc */
  async _preCreate(data, options, user) {
    if ( this.parent?.firstLevel?.background && !foundry.utils.hasProperty(data, "background.color") ) {
      this.updateSource({ "background.color": this.parent.firstLevel.background.color });
    }
    return super._preCreate(data, options, user);
  }

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

  /** @inheritDoc */
  static async _onCreateOperation(documents, operation, user) {
    await super._onCreateOperation(documents, operation, user);
    operation.parent._resetEdges();
  }

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

  /** @inheritDoc */
  static async _onDeleteOperation(documents, operation, user) {
    await super._onDeleteOperation(documents, operation, user);
    const scene = operation.parent;
    for ( const level of documents ) {
      for ( const edge of level.edges.values() ) edge.removeIntersections(level.id);
    }
    // If the currently-viewed level was deleted, navigate to the initial level.
    if ( scene.isView ) {
      const deletedIds = new Set(documents.map(l => l.id));
      if ( deletedIds.has(scene._view) ) {
        const { initialLevel } = scene;
        if ( initialLevel ) await scene.view({ level: initialLevel.id });
        else scene.unview();
      } else if ( canvas.level?.visibility.levels.intersects(deletedIds) ) {
        canvas.draw();
      }
    }
  }

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

  /**
   * Clamp the given elevation (of a token with a depth) to the elevation range of this Level.
   *
   * The elevation is clamped such that the head of the token is in the range if possible, but
   * the feet are never outside of the range.
   * @param {number} elevation    The elevation (of the token)
   * @param {number} [depth=0]    The depth of the token
   * @returns {number}            The clamped elevation
   */
  clampElevation(elevation, depth=0) {

    // Same code as in RegionDocument#clampElevation
    const depthDistance = depth * (this.parent?.grid ?? foundry.documents.BaseScene.defaultGrid).distance;
    return Math.clamp(
      Math.clamp(
        elevation + depthDistance,
        this.elevation.bottom,
        this.elevation.top
      ) - depthDistance,
      this.elevation.bottom,
      this.elevation.top
    );
  }

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

  /**
   * Update the shape constraints of all Regions in this Level the current User is designated for
   * (for the given restriction types).
   * @param {Iterable<EdgeRestrictionType>} [types]    The types to update. Default: all.
   */
  updateRegionShapeConstraints(types=CONST.EDGE_RESTRICTION_TYPES) {
    if ( !this.persisted ) throw new Error("A nonpersisted Document cannot be updated.");
    types = types === CONST.EDGE_RESTRICTION_TYPES ? null : (types instanceof Set ? types : new Set(types));
    if ( types && !types.size ) return;
    for ( const region of this.parent.regions ) {
      if ( !region._source.restriction.enabled && !region._source._shapeConstraints ) continue;
      if ( !region.includedInLevel(this.id) ) continue;
      if ( types && !types.has(region._source.restriction.type) ) continue;
      this.parent._updateRegionShapeConstraints(region);
    }
  }
}
