/**
 * A mixin which decorates a DisplayObject with additional properties expected for rendering in the PrimaryCanvasGroup.
 * @category - Mixins
 * @param {typeof PIXI.DisplayObject} DisplayObject   The parent DisplayObject class being mixed
 * @returns {typeof PrimaryCanvasObject}              A DisplayObject subclass mixed with PrimaryCanvasObject features
 * @mixin
 */
function PrimaryCanvasObjectMixin(DisplayObject) {

  /**
   * A display object rendered in the PrimaryCanvasGroup.
   * @param {...*} args    The arguments passed to the base class constructor
   */
  return class PrimaryCanvasObject extends CanvasTransformMixin(DisplayObject) {
    constructor(...args) {
      super(...args);
      // Activate culling and initialize handlers
      this.cullable = true;
      this.on("added", this._onAdded);
      this.on("removed", this._onRemoved);
    }

    /**
     * An optional reference to the object that owns this PCO.
     * This property does not affect the behavior of the PCO itself.
     * @type {*}
     * @default null
     */
    object = null;

    /**
     * The entry in the quadtree.
     * @type {QuadtreeObject|null}
     */
    #quadtreeEntry = null;

    /**
     * Update the quadtree entry?
     * @type {boolean}
     */
    #quadtreeDirty = false;

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

    /**
     * The elevation of this object.
     * @type {number}
     */
    get elevation() {
      return this.#elevation;
    }

    set elevation(value) {
      if ( (typeof value !== "number") || Number.isNaN(value) ) {
        throw new Error("PrimaryCanvasObject#elevation must be a numeric value.");
      }
      if ( value === this.#elevation ) return;
      this.#elevation = value;
      if ( this.parent ) {
        this.parent.sortDirty = true;
        if ( this.shouldRenderDepth ) canvas.masks.depth._elevationDirty = true;
      }
    }

    #elevation = 0;

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

    /**
     * A key which resolves ties amongst objects at the same elevation within the same layer.
     * @type {number}
     */
    get sort() {
      return this.#sort;
    }

    set sort(value) {
      if ( (typeof value !== "number") || Number.isNaN(value) ) {
        throw new Error("PrimaryCanvasObject#sort must be a numeric value.");
      }
      if ( value === this.#sort ) return;
      this.#sort = value;
      if ( this.parent ) this.parent.sortDirty = true;
    }

    #sort = 0;

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

    /**
     * A key which resolves ties amongst objects at the same elevation of different layers.
     * @type {number}
     */
    get sortLayer() {
      return this.#sortLayer;
    }

    set sortLayer(value) {
      if ( (typeof value !== "number") || Number.isNaN(value) ) {
        throw new Error("PrimaryCanvasObject#sortLayer must be a numeric value.");
      }
      if ( value === this.#sortLayer ) return;
      this.#sortLayer = value;
      if ( this.parent ) this.parent.sortDirty = true;
    }

    #sortLayer = 0;

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

    /**
     * A key which resolves ties amongst objects at the same elevation within the same layer and same sort.
     * @type {number}
     */
    get zIndex() {
      return this._zIndex;
    }

    set zIndex(value) {
      if ( (typeof value !== "number") || Number.isNaN(value) ) {
        throw new Error("PrimaryCanvasObject#zIndex must be a numeric value.");
      }
      if ( value === this._zIndex ) return;
      this._zIndex = value;
      if ( this.parent ) this.parent.sortDirty = true;
    }

    /* -------------------------------------------- */
    /*  PIXI Events                                 */
    /* -------------------------------------------- */

    /**
     * Event fired when this display object is added to a parent.
     * @param {PIXI.Container} parent   The new parent container.
     * @protected
     */
    _onAdded(parent) {
      if ( parent !== canvas.primary ) {
        throw new Error("PrimaryCanvasObject instances may only be direct children of the PrimaryCanvasGroup");
      }
    }

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

    /**
     * Event fired when this display object is removed from its parent.
     * @param {PIXI.Container} parent   Parent from which the PCO is removed.
     * @protected
     */
    _onRemoved(parent) {
      this.#updateQuadtree(true);
    }

    /* -------------------------------------------- */
    /*  Canvas Transform & Quadtree                 */
    /* -------------------------------------------- */

    /** @inheritdoc */
    updateCanvasTransform() {
      super.updateCanvasTransform();
      this.#updateQuadtree();
      this.#updateDepth();
    }

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

    /** @inheritdoc */
    _onCanvasBoundsUpdate() {
      super._onCanvasBoundsUpdate();
      this.#quadtreeDirty = true;
    }

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

    /**
     * Update the quadtree.
     * @param {boolean} [remove=false]    Remove the quadtree entry?
     */
    #updateQuadtree(remove=false) {
      if ( !this.#quadtreeDirty && !remove ) return;
      this.#quadtreeDirty = false;
      if ( !remove && (this.canvasBounds.width > 0) && (this.canvasBounds.height > 0) ) {
        this.#quadtreeEntry ??= {r: this.canvasBounds, t: this};
        canvas.primary.quadtree.update(this.#quadtreeEntry);
      } else if ( this.#quadtreeEntry ) {
        this.#quadtreeEntry = null;
        canvas.primary.quadtree.remove(this);
      }
    }

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

    /**
     * Does this object render to the depth buffer?
     * @type {boolean}
     */
    get shouldRenderDepth() {
      return this.#shouldRenderDepth;
    }

    /** @type {boolean} */
    #shouldRenderDepth = false;

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

    /**
     * Flag the depth as dirty if necessary.
     */
    #updateDepth() {
      const shouldRenderDepth = this._shouldRenderDepth();
      if ( this.#shouldRenderDepth === shouldRenderDepth ) return;
      this.#shouldRenderDepth = shouldRenderDepth;
      canvas.masks.depth._elevationDirty = true;
    }

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

    /**
     * Does this object render to the depth buffer?
     * @returns {boolean}
     * @protected
     */
    _shouldRenderDepth() {
      return false;
    }

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

    /**
     * Render the depth of this object.
     * @param {PIXI.Renderer} renderer
     */
    renderDepthData(renderer) {}

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

    /**
     * @deprecated since v11
     * @ignore
     */
    renderOcclusion(renderer) {
      const msg = "PrimaryCanvasObject#renderOcclusion is deprecated in favor of PrimaryCanvasObject#renderDepthData";
      foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
      this.renderDepthData(renderer);
    }

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

    /**
     * @deprecated since v12
     * @ignore
     */
    get document() {
      foundry.utils.logCompatibilityWarning("PrimaryCanvasObject#document is deprecated.", {since: 12, until: 14});
      if ( !(this.object instanceof PlaceableObject) ) return null;
      return this.object.document || null;
    }

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

    /**
     * @deprecated since v12
     * @ignore
     */
    updateBounds() {
      const msg = "PrimaryCanvasObject#updateBounds is deprecated and has no effect.";
      foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14});
    }
  };
}

/**
 * A mixin which decorates a DisplayObject with additional properties for canvas transforms and bounds.
 * @category - Mixins
 * @param {typeof PIXI.Container} DisplayObject   The parent DisplayObject class being mixed
 * @returns {typeof CanvasTransformMixin}         A DisplayObject subclass mixed with CanvasTransformMixin features
 * @mixin
 */
function CanvasTransformMixin(DisplayObject) {
  return class CanvasTransformMixin extends DisplayObject {
    constructor(...args) {
      super(...args);
      this.on("added", this.#resetCanvasTransformParentID);
      this.on("removed", this.#resetCanvasTransformParentID);
    }

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

    /**
     * The transform matrix from local space to canvas space.
     * @type {PIXI.Matrix}
     */
    canvasTransform = new PIXI.Matrix();

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

    /**
     * The update ID of canvas transform matrix.
     * @type {number}
     * @internal
     */
    _canvasTransformID = -1;

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

    /**
     * The update ID of the local transform of this object.
     * @type {number}
     */
    #canvasTransformLocalID = -1;

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

    /**
     * The update ID of the canvas transform of the parent.
     * @type {number}
     */
    #canvasTransformParentID = -1;

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

    /**
     * The canvas bounds of this object.
     * @type {PIXI.Rectangle}
     */
    canvasBounds = new PIXI.Rectangle();

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

    /**
     * The canvas bounds of this object.
     * @type {PIXI.Bounds}
     * @protected
     */
    _canvasBounds = new PIXI.Bounds();

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

    /**
     * The update ID of the canvas bounds.
     * Increment to force recalculation.
     * @type {number}
     * @protected
     */
    _canvasBoundsID = 0;

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

    /**
     * Reset the parent ID of the canvas transform.
     */
    #resetCanvasTransformParentID() {
      this.#canvasTransformParentID = -1;
    }

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

    /**
     * Calculate the canvas bounds of this object.
     * @protected
     */
    _calculateCanvasBounds() {}

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

    /**
     * Recalculate the canvas transform and bounds of this object and its children, if necessary.
     */
    updateCanvasTransform() {
      this.transform.updateLocalTransform();

      // If the local transform or the parent canvas transform has changed,
      // recalculate the canvas transform of this object
      if ( (this.#canvasTransformLocalID !== this.transform._localID)
        || (this.#canvasTransformParentID !== (this.parent._canvasTransformID ?? 0)) ) {
        this.#canvasTransformLocalID = this.transform._localID;
        this.#canvasTransformParentID = this.parent._canvasTransformID ?? 0;
        this._canvasTransformID++;
        this.canvasTransform.copyFrom(this.transform.localTransform);

        // Prepend the parent canvas transform matrix (if exists)
        if ( this.parent.canvasTransform ) this.canvasTransform.prepend(this.parent.canvasTransform);
        this._canvasBoundsID++;
        this._onCanvasTransformUpdate();
      }

      // Recalculate the canvas bounds of this object if necessary
      if ( this._canvasBounds.updateID !== this._canvasBoundsID ) {
        this._canvasBounds.updateID = this._canvasBoundsID;
        this._canvasBounds.clear();
        this._calculateCanvasBounds();

        // Set the width and height of the canvas bounds rectangle to 0
        // if the bounds are empty. PIXI.Bounds#getRectangle does not
        // change the rectangle passed to it if the bounds are empty:
        // so we need to handle the empty case here.
        if ( this._canvasBounds.isEmpty() ) {
          this.canvasBounds.x = this.x;
          this.canvasBounds.y = this.y;
          this.canvasBounds.width = 0;
          this.canvasBounds.height = 0;
        }

        // Update the canvas bounds rectangle
        else this._canvasBounds.getRectangle(this.canvasBounds);
        this._onCanvasBoundsUpdate();
      }

      // Recursively update child canvas transforms
      const children = this.children;
      for ( let i = 0, n = children.length; i < n; i++ ) {
        children[i].updateCanvasTransform?.();
      }
    }

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

    /**
     * Called when the canvas transform changed.
     * @protected
     */
    _onCanvasTransformUpdate() {}

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

    /**
     * Called when the canvas bounds changed.
     * @protected
     */
    _onCanvasBoundsUpdate() {}

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

    /**
     * Is the given point in canvas space contained in this object?
     * @param {PIXI.IPointData} point    The point in canvas space.
     * @returns {boolean}
     */
    containsCanvasPoint(point) {
      return false;
    }
  };
}
