import CanvasTransformMixin from "./canvas-transform-mixin.mjs";

/**
 * Primary canvas container are reserved for advanced usage.
 * They allow to group PrimarySpriteMesh in a single Container.
 * The container elevation is replacing individual sprite elevation.
 */
export default class PrimaryCanvasContainer extends CanvasTransformMixin(PIXI.Container) {
  constructor(...args) {
    super(...args);
    this.sortableChildren = true;
    this.on("added", this._onAdded);
    this.on("removed", this._onRemoved);
  }

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

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

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

  #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("PrimaryCanvasContainer#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("PrimaryCanvasContainer#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("PrimaryCanvasContainer#zIndex must be a numeric value.");
    }
    if ( value === this._zIndex ) return;
    this._zIndex = value;
    if ( this.parent ) this.parent.sortDirty = true;
  }

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

  /**
   * Is this container in the primary group?
   * @type {boolean}
   */
  get inPrimary() {
    return this._inPrimary;
  }

  /**
   * @type {boolean}
   * @protected
   */
  _inPrimary = false;

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

  /**
   * To know if this container has at least one children that should render its depth.
   * @returns {boolean}
   */
  get shouldRenderDepth() {
    return this.#shouldRenderDepth;
  }

  #shouldRenderDepth = false;

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

  /**
   * Event fired when this container is added to a parent.
   * @param {PIXI.Container} parent   The new parent container.
   * @protected
   */
  _onAdded(parent) {
    if ( parent === canvas.primary ) {
      this._onAddedPrimary();
      return;
    }
    if ( parent instanceof PrimaryCanvasContainer ) {
      if ( parent.inPrimary ) this._onAddedPrimary();
      this.elevation = parent.elevation;
      return;
    }
    throw new Error("PrimaryCanvasContainer instances may only be direct children of the PrimaryCanvasGroup or a PrimaryCanvasContainer");
  }

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

  /**
   * Called when the container is now in the primary group.
   * @protected
   */
  _onAddedPrimary() {
    this._inPrimary = true;
    for ( const child of this.children ) child._onAddedPrimary?.();
  }

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

  /**
   * Event fired when this container is removed from its parent.
   * @param {PIXI.Container} parent   Parent from which the container is removed.
   * @protected
   */
  _onRemoved(parent) {
    if ( this._inPrimary ) this._onRemovedPrimary();
  }

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

  /**
   * Called when the container is no longer in the primary group.
   * @protected
   */
  _onRemovedPrimary() {
    this._inPrimary = false;
    for ( const child of this.children ) child._onRemovedPrimary?.();
  }

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

  /**
   * Called when the elevation was changed.
   * @protected
   */
  _onElevationChange() {
    for ( const child of this.children ) {
      if ( "elevation" in child ) child.elevation = this.#elevation;
    }
  }

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

  /**
   * Does this object render to the depth buffer?
   * @returns {boolean}
   * @protected
   */
  _shouldRenderDepth() {
    return this.children.some(c => c.shouldRenderDepth);
  }

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

  /**
   * Render the depth of this object.
   * @param {PIXI.Renderer} renderer
   */
  renderDepthData(renderer) {
    if ( !this.#shouldRenderDepth || !this.visible || !this.renderable ) return;
    for ( const c of this.children ) c.renderDepthData?.(renderer);
  }

  /* -------------------------------------------- */
  /*  Canvas Transform & Sorting                  */
  /* -------------------------------------------- */

  /** @override */
  updateCanvasTransform() {
    if ( this.sortableChildren && this.sortDirty ) this.sortChildren();
    super.updateCanvasTransform();
    this.#shouldRenderDepth = this._shouldRenderDepth();
  }

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

  /** @override */
  sortChildren() {
    const children = this.children;
    for ( let i = 0, n = children.length; i < n; i++ ) children[i]._lastSortedIndex = i;
    children.sort(PrimaryCanvasContainer.#compareObjects);
    this.sortDirty = false;
  }

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

  /**
   * The sorting function used to order objects inside the Primary Canvas Container.
   * Overrides the default sorting function defined for the PIXI.Container.
   * @param {PrimaryCanvasObject|PIXI.DisplayObject} a     An object to display
   * @param {PrimaryCanvasObject|PIXI.DisplayObject} b     Some other object to display
   * @returns {number}
   */
  static #compareObjects(a, b) {
    return ((a.sortLayer || 0) - (b.sortLayer || 0))
      || ((a.sort || 0) - (b.sort || 0))
      || (a.zIndex - b.zIndex)
      || (a._lastSortedIndex - b._lastSortedIndex);
  }
}
