import PlaceableObject from "./placeable-object.mjs";
import RegionMesh from "./regions/mesh.mjs";
import RegionGeometry from "./regions/geometry.mjs";
import RegionShapeControls from "./regions/shape-controls.mjs";
import RegionDocument from "@client/documents/region.mjs";
import HighlightRegionShader from "../rendering/shaders/region/highlight.mjs";
import {CLIPPER_SCALING_FACTOR} from "@common/constants.mjs";
import ShapeObjectMixin from "./mixins/shapes.mjs";
import {PolygonTree} from "@client/data/polygon-tree.mjs";

/**
 * @import {BaseShapeData} from "@common/data/_module.mjs";
 * @import {GridOffset2D} from "@common/grid/_types.mjs";
 * @import {DeepReadonly, ElevatedPoint, Point} from "@common/_types.mjs";
 * @import {RegionAnimationState} from "./_types.mjs";
 * @import {TokenDocument} from "@client/documents/_module.mjs";
 */

/**
 * A Region is an implementation of PlaceableObject which represents a Region document
 * within a viewed Scene on the game canvas.
 * @category Canvas
 * @see {@link foundry.documents.RegionDocument}
 * @see {@link foundry.canvas.layers.RegionLayer}
 */
export default class Region extends ShapeObjectMixin(PlaceableObject) {

  /** @override */
  static embeddedName = "Region";

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

  /** @override */
  static RENDER_FLAGS = {
    redraw: {propagate: ["refresh"]},
    refresh: {propagate: ["refreshState", "refreshShapes"], alias: true},
    refreshState: {propagate: ["refreshVisibility"]},
    refreshVisibility: {},
    refreshShapes: {propagate: ["refreshGeometry"]},
    refreshGeometry: {propagate: ["refreshBorder", "refreshMeasurements"]},
    refreshBorder: {},
    refreshMeasurements: {}
  };

  /* -------------------------------------------- */
  /*  Properties                                  */
  /* -------------------------------------------- */

  /**
   * The geometry of this Region.
   *
   * The value of this property must not be mutated.
   * @type {RegionGeometry}
   */
  get geometry() {
    return this.#geometry;
  }

  #geometry = new RegionGeometry(this);

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

  /** @override */
  get bounds() {
    return this.animationState.bounds.clone(); // PlaceableObject#bounds always returns a new instance
  }

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

  /** @override */
  get center() {
    return this.animationState.bounds.center;
  }

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

  /** @override */
  get isVisible() {
    if ( this.isPreview ) return true;
    if ( this.hasPreview ) return false;
    if ( this.controlled ) return true;
    if ( this.document.hidden && !game.user.isGM ) return false;
    if ( this.isFilteredOut ) return false;
    const isObserver = this.document.testUserPermission(game.user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER);
    const V = CONST.REGION_VISIBILITY;
    if ( this.layer.active ) {
      if ( this.document.locked && (this.document.visibility === V.LAYER_UNLOCKED) ) return false;
      if ( isObserver ) return true;
    }
    switch ( this.document.visibility ) {
      case V.GAMEMASTER: return game.user.isGM;
      case V.OBSERVER: return isObserver;
      case V.ALWAYS: return true;
      default: return false;
    }
  }

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

  /** @override */
  get isInteractable() {
    if ( this.isPreview ) return false;
    if ( this.layer._placementContext ) return false;
    if ( !this.layer.active ) return false;
    if ( !ui.controls.tool?.interaction ) return false;
    if ( this.isAnimating ) return false;
    if ( this.document.locked ) return false;
    if ( !this.document.testUserPermission(game.user, "OBSERVER") ) return false;
    return true;
  }

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

  /**
   * The hitbox of this Region.
   * @type {PIXI.Container}
   */
  #hitbox;

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

  /**
   * The highlight of this Region.
   * @type {RegionMesh}
   */
  #highlight;

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

  /**
   * The border of this Region.
   * @type {PIXI.Graphics}
   */
  #border;

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

  /**
   * The geometry of covered grid spaces.
   * @type {PIXI.Geometry | null}
   */
  #coverageGeometry = null;

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

  /**
   * The covered grid space offsets keys.
   * @type {Set<number>}
   */
  #coverageOffsetKeys = new Set();

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

  /**
   * The shape controls of this Region.
   * @type {PIXI.Container}
   */
  #controls;

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

  /**
   * The animation state of this Region.
   * @type {DeepReadonly<RegionAnimationState>}
   */
  get animationState() {
    return this.#animationState;
  }

  #animationState = Object.freeze(this.#createAnimationState());

  /**
   * Is this Region currently animating?
   * @type {boolean}
   */
  get isAnimating() {
    return !!(this.#animatedShapes || this.#animatedElevation);
  }

  /** @type {BaseShapeData[]|undefined} */
  #animatedShapes;

  /** @type {{origins: Point[]; lastRotation: {angle: number; pivot: Point}}|undefined} */
  #animatedShapeData;

  /** @type {number|undefined} */
  #animatedElevation;

  /** @type {PolygonTree|undefined} */
  #animatedPolygonTree;

  /** @type {ClipperLib.PolyTree|undefined} */
  #animatedClipperPolyTree;

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

  /**
   * Create the animation state of this Region.
   * @returns {RegionAnimationState}
   */
  #createAnimationState() {
    return Object.defineProperties({}, {
      shapes: {
        get: () => this.#animatedShapes ?? this.document.shapes
      },
      elevation: {
        get: () => this.#animatedElevation ?? this.document.elevation
      },
      polygons: {
        get: () => this.#animationState.polygonTree.polygons
      },
      polygonTree: {
        get: () => {
          if ( !this.#animatedShapes ) return this.document.polygonTree;
          return this.#animatedPolygonTree ??= PolygonTree.fromClipperPolyTree(this.#animationState.clipperPolyTree);
        }
      },
      clipperPaths: {
        get: () => this.#animationState.polygonTree.clipperPaths
      },
      clipperPolyTree: {
        get: () => {
          if ( !this.#animatedShapes ) return this.document.clipperPolyTree;
          if ( this.#animatedClipperPolyTree ) return this.#animatedClipperPolyTree;
          const shapes = this.#animationState.shapes;
          const elevation = this.#animationState.elevation;
          const shapeConstraints = this.document._computeShapeConstraints({shapes, elevation});
          return this.#animatedClipperPolyTree = this.document._createClipperPolyTree(shapes, shapeConstraints);
        }
      },
      triangulation: {
        get: () => this.#animationState.polygonTree.triangulation
      },
      bounds: {
        get: () => this.#animationState.polygonTree.bounds
      },
      area: {
        get: () => this.#animationState.polygonTree.area
      },
      testPoint: {
        value: point => {
          return RegionDocument._testElevation(this.animationState.elevation, point.elevation)
            && this.#animationState.polygonTree.testPoint(point);
        }
      }
    });
  }

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

  /** @override */
  getSnappedPosition(position) {
    throw new Error("Region#getSnappedPosition is not supported: RegionDocument does not have a (x, y) position");
  }

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

  /** @override */
  _pasteObject(offset, _options) {
    const shapes = [];
    for ( let shape of this.document.shapes ) {
      shape = shape.clone({}, {parent: canvas.scene});
      shape.move({x: shape.origin.x + offset.x, y: shape.origin.y + offset.y});
      shapes.push(shape._source);
    }

    // Same logic as in PlaceableObject#_pasteObject
    let levels;
    if ( !this.document.levels.size || (this.scene !== canvas.scene) ) levels = [];
    else if ( this.document.levels.has(canvas.level.id) ) levels = [...this.document.levels];
    else levels = [canvas.level.id];
    return {shapes, levels};
  }

  /* -------------------------------------------- */
  /*  Rendering                                   */
  /* -------------------------------------------- */

  /** @inheritDoc */
  async _draw(options) {
    await super._draw(options);
    this.#hitbox = this.addChild(this.#drawHitbox());
    this.#highlight = this.layer._highlights.addChild(new RegionMesh(this, HighlightRegionShader));
    this.#highlight.eventMode = "auto";
    this.#highlight.shader.uniforms.hatchThickness = 4 * canvas.dimensions.uiScale;
    this.#highlight.alpha = 0.5;
    this.addChild(this._measurementLines);
    this.#border = this.addChild(new PIXI.Graphics());
    this.#border.eventMode = "none";
    this.#controls = this.addChild(new PIXI.Container());
    await this.#drawShapeControls();
    this.addChild(this._measurementLabels);
    this.cursor = "pointer";
  }

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

  /**
   * Draw the hitbox.
   * @returns {PIXI.Container}
   */
  #drawHitbox() {
    const point = new PIXI.Point();
    const hitbox = new PIXI.Container();
    hitbox.eventMode = "auto";
    hitbox.hitArea = {
      contains: (x, y) => {
        if ( !this.animationState.bounds.contains(x, y) ) return false;
        point.set(x, y);
        if ( this.#coverageOffsetKeys.size ) {
          const offset = canvas.grid.getOffset(point);
          const key = offset.i | (offset.j << 16);
          if ( this.#coverageOffsetKeys.has(key) ) return true;
        }
        return this.animationState.polygonTree.testPoint(point);
      }
    };
    return hitbox;
  }

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

  /**
   * Draw the shape controls.
   * @returns {Promise<void>}
   */
  async #drawShapeControls() {
    for ( const shape of this.document.shapes ) {
      const control = new RegionShapeControls(shape.clone());
      this.#controls.addChild(await control.draw());
    }
    this.renderFlags.set({refreshState: true});
  }

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

  /**
   * Re-draw the shape controls.
   * @internal
   */
  _redrawShapeControls() {
    this.#controls?.removeChildren().forEach(c => c.destroy({children: true}));
    this._partialDraw(() => this.#drawShapeControls());
  }

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

  /** @inheritDoc */
  _clear() {
    this.#highlight?.destroy({children: true});
    this.#highlight = undefined;
    super._clear();
  }

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

  /** @inheritDoc */
  _destroy(options) {
    this.#highlight?.destroy({children: true});
    super._destroy(options);
  }

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

  /** @override */
  _getMeasuredShapes() {
    if ( this.document.displayMeasurements ) return this.animationState.shapes;
    if ( this.previewType === "creation" ) {
      if ( this.layer._placementContext?.preview === this ) return this.document.shapes;
      return this.document.shapes.slice(-1);
    }
    if ( (this.previewType === "wheel") && (this.layer._mouseWheelContext?.preview === this) ) {
      return [this.document.shapes.at(this.layer._mouseWheelContext.shape._index)];
    }
    if ( (this.previewType === "controls") && this._original.mouseInteractionManager?.isDragging ) {
      return [this.document.shapes.at(this._original.mouseInteractionManager.interactionData.shape._index)];
    }
    return [];
  }

  /* -------------------------------------------- */
  /*  Incremental Refresh                         */
  /* -------------------------------------------- */

  /** @inheritDoc */
  _applyRenderFlags(flags) {
    super._applyRenderFlags(flags);
    if ( flags.refreshShapes ) this._refreshShapes();
    if ( flags.refreshGeometry ) this._refreshGeometry();
    if ( flags.refreshBorder ) this._refreshBorder();
    if ( flags.refreshMeasurements ) this._refreshMeasurements();
  }

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

  /** @inheritDoc */
  _refreshVisibility() {
    super._refreshVisibility();
    this.#highlight.visible = this.visible;
  }

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

  /** @inheritDoc */
  _refreshState() {
    super._refreshState();
    this.#highlight.zIndex = this.zIndex;
    const {hidden, locked, color} = this.document;
    this.#highlight.tint = color;
    this.#highlight.shader.uniforms.hatchEnabled = !this.controlled && !this.hover && !this.isPreview;
    const colors = CONFIG.Canvas.dispositionColors;
    const tint = this.controlled || this.isPreview ? colors.CONTROLLED : (locked ? colors.HOSTILE : colors.INACTIVE);
    this.#border.tint = tint;
    const borderOrShapeControlsVisible = this.controlled || this.hover || this.layer.highlightObjects || (this.isPreview
      && !this.layer.templateMode);
    const couldEditShapes = !this.isAnimating && this.layer.active && !!ui.controls.tool?.interaction
      && !!ui.controls.tool?.control && !locked && [null, "dragging", "wheel", "controls"].includes(this.previewType);
    if ( couldEditShapes ) {
      this.#border.visible = false;
      this.#controls.visible = borderOrShapeControlsVisible;
    } else {
      this.#border.visible = borderOrShapeControlsVisible;
      this.#controls.visible = false;
    }
    for ( const controls of this.#controls.children ) {
      controls.handles.visible = this.controlled;
      controls.editable = !locked;
      controls.tint = tint;
      controls.dashed = hidden;
    }
  }

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

  /**
   * Refresh the shapes of the Region.
   * @protected
   */
  _refreshShapes() {
    for ( const [i, control] of this.#controls.children.entries() ) {
      control.shape.updateSource(this.document.shapes[i].toObject());
      control.refresh();
    }
  }

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

  /**
   * Refresh the geometry of the Region.
   * @protected
   */
  _refreshGeometry() {
    this.#geometry?._clearBuffers();
    if ( (this.document.highlightMode !== "coverage") || canvas.grid.isGridless ) {
      this.#coverageGeometry = null;
      this.#highlight.geometry = this.#geometry;
      return;
    }
    const offsets = this._getCoveredGridSpaceOffsets();
    if ( !this.#coverageGeometry ) {
      this.#coverageGeometry = new PIXI.Geometry();
      this.#coverageGeometry.addAttribute("aVertexPosition", new PIXI.Buffer(new Float32Array(), true, false), 2);
      this.#coverageGeometry.addIndex(new PIXI.Buffer(new Uint16Array(), true, true));
    }
    this.#coverageOffsetKeys.clear();
    const n = canvas.grid.isSquare ? 4 : 6;
    const vertices = new Float32Array(offsets.length * n * 2);
    const indices = new (offsets.length > 0xFFFF ? Uint32Array : Uint16Array)(offsets.length * (n - 2) * 3);
    let i = 0;
    let j = 0;
    for ( const offset of offsets ) {
      const key = offset.i | (offset.j << 16);
      this.#coverageOffsetKeys.add(key);
      const k0 = i / 2;
      for ( const {x, y} of canvas.grid.getVertices(offset) ) {
        vertices[i++] = x;
        vertices[i++] = y;
      }
      for ( let dk = 1; dk < n - 1; dk++ ) {
        indices[j++] = k0;
        indices[j++] = k0 + dk;
        indices[j++] = k0 + dk + 1;
      }
    }
    this.#coverageGeometry.buffers[0].update(vertices);
    this.#coverageGeometry.indexBuffer.update(indices);
    this.#highlight.geometry = offsets.length ? this.#coverageGeometry : this.#geometry;
  }

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

  /**
   * Refresh the border of the Region.
   * @protected
   */
  _refreshBorder() {
    foundry.canvas.borders.drawBorder(this.#border, border => this.animationState.polygonTree.drawShape(border),
      {dashed: this.document.hidden});
  }

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

  /**
   * Get the grid space offsets that are covered by this Region.
   * @returns {GridOffset2D[]}
   * @protected
   */
  _getCoveredGridSpaceOffsets() {
    const offsets = [];
    if ( canvas.grid.isGridless ) return [];
    const dx = canvas.grid.sizeX / 2;
    const dy = canvas.grid.sizeY / 2;
    const [i0, j0, i1, j1] = canvas.grid.getOffsetRange(this.animationState.bounds.fit(canvas.dimensions.rect).pad(1));
    for ( let i = i0; i < i1; i++ ) {
      for ( let j = j0; j < j1; j++ ) {
        const offset = {i, j};
        const center = canvas.grid.getCenterPoint(offset);

        // Round the top-left as a token position would be
        center.x = Math.round(center.x - dx) + dx;
        center.y = Math.round(center.y - dy) + dy;

        // A 1x1 token is covered if is center point intersects with a tolerance of 0.75
        const covered = this.animationState.polygonTree.testPoint(center, 0.75);
        if ( covered ) offsets.push(offset);
      }
    }
    return offsets;
  }

  /* -------------------------------------------- */
  /*  Animation                                   */
  /* -------------------------------------------- */

  /**
   * Update the animation state of this Region based on the animation state.
   * @internal
   */
  _onTokenAnimationFrame() {
    const token = this.document.attachment.token;
    if ( !token ) return;
    const wasAnimating = this.isAnimating;
    const dx = token.x - token._source.x;
    const dy = token.y - token._source.y;
    const de = token.elevation - token._source.elevation;
    const dr = Math.normalizeDegrees(token.rotation - token._source.rotation);
    if ( dx || dy || dr ) {
      if ( !this.#animatedShapeData ) {
        this.#animatedShapes = [];
        this.#animatedShapeData = {origins: [], lastRotation: {angle: 0, pivot: {x: 0, y: 0}}};
        for ( const shape of this.document.shapes ) {
          this.#animatedShapes.push(shape.clone());
          this.#animatedShapeData.origins.push(shape.origin);
        }
      }
      const center = dr ? token.getCenterPoint() : undefined;
      const {origins, lastRotation} = this.#animatedShapeData;
      for ( let i = 0; i < this.#animatedShapes.length; i++ ) {
        const shape = this.#animatedShapes[i];

        // TODO optimize this further?
        if ( dx || dy ) {
          if ( lastRotation.angle ) shape.rotate(-lastRotation.angle, {pivot: lastRotation.pivot});
          const origin = origins[i];
          shape.move({x: origin.x + dx, y: origin.y + dy});
          if ( dr ) shape.rotate(dr, {pivot: center});
        } else if ( dr ) {
          if ( (center.x === lastRotation.pivot.x) && (center.y === lastRotation.pivot.y) ) {
            shape.rotate(dr - lastRotation.angle, {pivot: center});
          } else {
            shape.rotate(-lastRotation.angle, {pivot: lastRotation.pivot});
            shape.rotate(dr, {pivot: center});
          }
        }
      }
      lastRotation.angle = dr;
      lastRotation.pivot = center;
    } else {
      this.#animatedShapes = undefined;
      this.#animatedShapeData = undefined;
    }
    this.#animatedPolygonTree = undefined;
    this.#animatedClipperPolyTree = undefined;
    this.#animatedElevation = de ? {bottom: this.document.elevation.bottom + de, top: this.document.elevation.top + de,
      topInclusive: this.document.elevation.topInclusive} : undefined;
    const isAnimating = this.isAnimating;
    this.renderFlags.set({refreshState: isAnimating !== wasAnimating, refreshGeometry: true});
    this.document._handleEvent({name: CONST.REGION_EVENTS.REGION_ANIMATED, data: {}, region: this.document,
      user: game.user});
    this._onAnimationStateChange();
  }

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

  /**
   * Reset the animation state.
   */
  #resetAnimationState() {
    if ( !this.isAnimating ) return;
    this.#animatedShapes = undefined;
    this.#animatedShapeData = undefined;
    this.#animatedElevation = undefined;
    this.#animatedPolygonTree = undefined;
    this.#animatedClipperPolyTree = undefined;
    this.renderFlags.set({refreshState: true, refreshGeometry: true});
    this.document._handleEvent({name: CONST.REGION_EVENTS.REGION_ANIMATED, data: {}, region: this.document,
      user: game.user});
    this._onAnimationStateChange();
  }

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

  /**
   * Called when the animation state of the Region has changed.
   * @protected
   */
  _onAnimationStateChange() {}

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

  /** @override */
  _canHUD(user, event) {
    return false; // Regions don't have a HUD
  }

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

  /** @inheritDoc */
  _onHoverIn(event, options={}) {
    if ( options.updateLegend ) this.layer.legend._hoverRegion(this, true);
    return super._onHoverIn(event, options);
  }

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

  /** @inheritDoc */
  _onHoverOut(event, options={}) {
    if ( options.updateLegend ) this.layer.legend._hoverRegion(this, false);
    return super._onHoverOut(event, options);
  }

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

  /** @inheritDoc */
  _onControl(options) {
    super._onControl(options);
    if ( ui.controls.tool?.creation && !this.layer.templateMode ) {
      this.layer._togglePaletteVisible = false;
      if ( "togglePalette" in ui.controls.tools ) ui.controls.render({reset: true});
    }
  }

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

  /** @inheritDoc */
  _onRelease(options) {
    super._onRelease(options);
    if ( ui.controls.tool?.creation && !this.layer.templateMode && !this.layer.controlled.length ) {
      this.layer._togglePaletteVisible = true;
      if ( !("togglePalette" in ui.controls.tools) ) ui.controls.render({reset: true});
    }
  }

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

  /** @override */
  _overlapsSelection(rectangle) {
    if ( !rectangle.intersects(this.animationState.bounds) ) return false;
    const x0 = Math.round(rectangle.left * CLIPPER_SCALING_FACTOR);
    const y0 = Math.round(rectangle.top * CLIPPER_SCALING_FACTOR);
    const x1 = Math.round(rectangle.right * CLIPPER_SCALING_FACTOR);
    const y1 = Math.round(rectangle.bottom * CLIPPER_SCALING_FACTOR);
    if ( (x0 === x1) || (y0 === y1) ) return false;
    const rectanglePath = [
      new ClipperLib.IntPoint(x0, y0),
      new ClipperLib.IntPoint(x1, y0),
      new ClipperLib.IntPoint(x1, y1),
      new ClipperLib.IntPoint(x0, y1)
    ];
    const clipper = new ClipperLib.Clipper();
    const solution = [];
    clipper.Clear();
    clipper.AddPath(rectanglePath, ClipperLib.PolyType.ptSubject, true);
    clipper.AddPaths(this.animationState.clipperPaths, ClipperLib.PolyType.ptClip, true);
    clipper.Execute(ClipperLib.ClipType.ctIntersection, solution);
    return solution.length !== 0;
  }

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

  /** @inheritDoc */
  _updateDragPreviews(event) {
    super._updateDragPreviews(event);
    for ( const clone of event.interactionData.clones ) clone.document.updateShapeConstraints();
  }

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

  /** @inheritDoc */
  _onUpdate(changed, options, userId) {
    super._onUpdate(changed, options, userId);

    const shapesChanged = "shapes" in changed;
    if ( shapesChanged ) this.#animatedShapeData = undefined;

    if ( "attachment" in changed ) this.#resetAnimationState();

    const redrawShapeControls = shapesChanged && !this.document.shapes.map(s => `${s.type}.${s.base?.type ?? ""}`)
      .equals(this.#controls.children.map(c => `${c.shape.type}.${c.shape.base?.type ?? ""}`));
    if ( redrawShapeControls ) this._redrawShapeControls();

    // Incremental Refresh
    this.renderFlags.set({
      refreshState: ["color", "visibility", "hidden", "locked", "ownership"].some(k => k in changed),
      refreshShapes: shapesChanged,
      refreshGeometry: ("highlightMode" in changed) || ("_shapeConstraints" in changed),
      refreshBorder: "hidden" in changed,
      refreshMeasurements: "displayMeasurements" in changed
    });
  }

  /* -------------------------------------------- */
  /*  Deprecations and Compatibility              */
  /* -------------------------------------------- */

  /**
   * @deprecated since v13
   * @ignore
   */
  static get CLIPPER_SCALING_FACTOR() {
    foundry.utils.logCompatibilityWarning("Region.CLIPPER_SCALING_FACTOR has been deprecated in favor of CONST.CLIPPER_SCALING_FACTOR.",
      {since: 13, until: 15, once: true});
    return CLIPPER_SCALING_FACTOR;
  }

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

  /**
   * @deprecated since v13
   * @ignore
   */
  static get MOVEMENT_SEGMENT_TYPES() {
    foundry.utils.logCompatibilityWarning("Region.MOVEMENT_SEGMENT_TYPES has been deprecated in favor of CONST.REGION_MOVEMENT_SEGMENTS.",
      {since: 13, until: 15, once: true});
    return CONST.REGION_MOVEMENT_SEGMENTS;
  }

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

  /**
   * @deprecated since v13
   * @ignore
   */
  get bottom() {
    foundry.utils.logCompatibilityWarning("Region#bottom has been deprecated in favor of RegionDocument#elevation.bottom.",
      {since: 13, until: 15, once: true});
    return this.document.elevation.bottom;
  }

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

  /**
   * @deprecated since v13
   * @ignore
   */
  get top() {
    foundry.utils.logCompatibilityWarning("Region#top has been deprecated in favor of RegionDocument#elevation.top.",
      {since: 13, until: 15, once: true});
    return this.document.elevation.top;
  }

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

  /**
   * @deprecated since v13
   * @ignore
   */
  get shapes() {
    foundry.utils.logCompatibilityWarning("Region#shapes has been deprecated. Use RegionDocument#shapes instead.",
      {since: 13, until: 15, once: true});
    return this.document._regionShapes;
  }

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

  /**
   * @deprecated since v13
   * @ignore
   */
  get polygons() {
    foundry.utils.logCompatibilityWarning("Region#polygons has been deprecated in favor of RegionDocument#polygons.",
      {since: 13, until: 15, once: true});
    return this.document.polygons;
  }

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

  /**
   * @deprecated since v13
   * @ignore
   */
  get polygonTree() {
    foundry.utils.logCompatibilityWarning("Region#polygons has been deprecated in favor of RegionDocument#polygons.",
      {since: 13, until: 15, once: true});
    return this.document.polygonTree;
  }

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

  /**
   * @deprecated since v13
   * @ignore
   */
  get clipperPaths() {
    foundry.utils.logCompatibilityWarning("Region#clipperPaths has been deprecated in favor of RegionDocument#clipperPaths.",
      {since: 13, until: 15, once: true});
    return this.document.clipperPaths;
  }

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

  /**
   * @deprecated since v13
   * @ignore
   */
  get triangulation() {
    foundry.utils.logCompatibilityWarning("Region#triangulation has been deprecated in favor of RegionDocument#triangulation.",
      {since: 13, until: 15, once: true});
    return this.document.triangulation;
  }

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

  /**
   * @deprecated since v13
   * @ignore
   */
  segmentizeMovement(waypoints, samples, options) {
    const msg = "Region#segmentizeMovement has been deprecated in favor of RegionDocument#segmentizeMovementPath.";
    foundry.utils.logCompatibilityWarning(msg, {since: 13, until: 15, once: true});
    if ( options?.teleport !== undefined ) {
      waypoints = waypoints.map(waypoint => {
        waypoint = {...waypoint};
        waypoint.teleport ??= options.teleport;
        return waypoint;
      });
    }
    return this.document.segmentizeMovementPath(waypoints, samples);
  }

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

  /**
   * @deprecated since v13
   * @ignore
   */
  testPoint(point, elevation) {
    foundry.utils.logCompatibilityWarning("Region#testPoint(point: Point, elevation?: number) has been deprecated "
      + "in favor of RegionDocument#testPoint(point: ElevatedPoint).", {since: 13, until: 15, once: true});
    return this.document.testPoint({...point, elevation: elevation ?? this.document.elevation.bottom});
  }
}
