import PrimaryCanvasObjectMixin from "./primary-canvas-object.mjs";
import CanvasAnimation from "../animation/canvas-animation.mjs";

/**
 * A mixin which decorates a DisplayObject with depth and/or occlusion properties.
 * @category Mixins
 * @param {typeof PIXI.DisplayObject} DisplayObject   The parent DisplayObject class being mixed
 */
export default function PrimaryOccludableObjectMixin(DisplayObject) {
  class PrimaryOccludableObject extends PrimaryCanvasObjectMixin(DisplayObject) {

    /**
     * Restrictions options packed into a single value with bitwise logic.
     * @type {foundry.utils.BitMask}
     */
    #restrictionState = new foundry.utils.BitMask({
      light: false,
      weather: false
    });

    /**
     * Is this occludable object hidden for Gamemaster visibility only?
     * @type {boolean}
     */
    hidden = false;

    /**
     * A flag which tracks whether the primary canvas object is currently in an occluded state.
     * @type {boolean}
     */
    occluded = false;

    /**
     * The occlusion mode of this occludable object (a union of {@link CONST.OCCLUSION_MODES})
     * @type {number}
     */
    occlusionMode = CONST.OCCLUSION_MODES.NONE;

    /**
     * Do surfaces at the same elevation as this object apply occlusion to this object?
     * This property only applies to SURFACE occlusion.
     * @type {boolean}
     * @internal
     */
    _occludedBySameElevationSurfaces = true;

    /**
     * The unoccluded alpha of this object.
     * @type {number}
     */
    unoccludedAlpha = 1;

    /**
     * The occlusion alpha of this object.
     * @type {number}
     */
    occludedAlpha = 0;

    /**
     * Fade this object on hover?
     * @type {boolean}
     * @defaultValue true
     */
    get hoverFade() {
      return this.#hoverFade;
    }

    set hoverFade(value) {
      if ( this.#hoverFade === value ) return;
      this.#hoverFade = value;
      const state = this._hoverFadeState;
      state.hovered = false;
      state.faded = false;
      state.fading = false;
      state.occlusion = 0;
    }

    /**
     * Fade this object on hover?
     * @type {boolean}
     */
    #hoverFade = true;

    /**
     * @typedef OcclusionState
     * @property {number} fade            The amount of FADE occlusion
     * @property {number} radial          The amount of RADIAL occlusion
     * @property {number} vision          The amount of VISION occlusion
     * @property {number} surface         The amount of SURFACE occlusion
     */

    /**
     * The amount of rendered FADE, RADIAL, VISION, and SURFACE occlusion.
     * @type {OcclusionState}
     * @internal
     */
    _occlusionState = {
      fade: 0.0,
      radial: 0.0,
      vision: 0.0,
      surface: 0.0
    };

    /**
     * @typedef HoverFadeState
     * @property {boolean} hovered        The hovered state
     * @property {number} hoveredTime     The last time when a mouse event was hovering this object
     * @property {boolean} faded          The faded state
     * @property {boolean} fading         The fading state
     * @property {number} fadingTime      The time the fade animation started
     * @property {number} occlusion       The amount of occlusion
     */

    /**
     * The state of hover-fading.
     * @type {HoverFadeState}
     * @internal
     */
    _hoverFadeState = {
      hovered: false,
      hoveredTime: 0,
      _hoveredTime: 0,
      faded: false,
      fading: false,
      fadingTime: 0,
      occlusion: 0.0
    };

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

    /**
     * Get the blocking option bitmask value.
     * @returns {number}
     * @internal
     */
    get _restrictionState() {
      return this.#restrictionState.valueOf();
    }

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

    /**
     * Is this object blocking light?
     * @type {boolean}
     */
    get restrictsLight() {
      return this.#restrictionState.hasState(this.#restrictionState.states.light);
    }

    set restrictsLight(enabled) {
      this.#restrictionState.toggleState(this.#restrictionState.states.light, enabled);
    }

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

    /**
     * Is this object blocking weather?
     * @type {boolean}
     */
    get restrictsWeather() {
      return this.#restrictionState.hasState(this.#restrictionState.states.weather);
    }

    set restrictsWeather(enabled) {
      this.#restrictionState.toggleState(this.#restrictionState.states.weather, enabled);
    }

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

    /**
     * Is this occludable object... occludable?
     * @type {boolean}
     */
    get isOccludable() {
      const M = CONST.OCCLUSION_MODES;
      return !!(this.occlusionMode & M.FADE) || (!!(this.occlusionMode & M.VISION) && !canvas.masks.occlusion.vision);
    }

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

    /** @override */
    get elevation() {
      return super.elevation;
    }

    /** @override */
    set elevation(value) {
      super.elevation = value;
      this.#elevationNextDown = undefined;
    }

    /**
     * The elevation minus eplison used for {@link _occludedBySameElevationSurfaces}.
     * @type {number|undefined}
     */
    #elevationNextDown;

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

    /**
     * The occlusion elevation of this object.
     * @type {number}
     * @internal
     */
    get _occlusionElevation() {
      let elevation;
      if ( this._occludedBySameElevationSurfaces ) elevation = this.elevation;
      else elevation = this.#elevationNextDown ??= Math.nextDown(this.elevation);
      return canvas.masks.occlusion.mapElevation(elevation);
    }

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

    /**
     * Debounce assignment of the PCO occluded state to avoid cases like animated token movement which can rapidly
     * change PCO appearance.
     * Uses a 50ms debounce threshold.
     * Objects which are in the hovered state remain occluded until their hovered state ends.
     * @type {function(occluded: boolean): void}
     */
    debounceSetOcclusion = foundry.utils.debounce(occluded => this.occluded = occluded, 50);

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

    /** @inheritDoc */
    updateTransform() {
      super.updateTransform();
      this.#updateHoverFadeState();
      this.#updateOcclusionState();
    }

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

    /**
     * Update the occlusion state.
     */
    #updateOcclusionState() {
      const state = this._occlusionState;
      state.fade = 0;
      state.radial = 0;
      state.vision = 0;
      state.surface = 0;
      const M = CONST.OCCLUSION_MODES;
      if ( (this.occlusionMode & M.FADE) && this.occluded ) state.fade = 1;
      if ( this.occlusionMode & M.SURFACE ) state.surface = 1;
      if ( this.occlusionMode & M.RADIAL ) state.radial = 1;
      if ( this.occlusionMode & M.VISION ) {
        if ( canvas.masks.occlusion.vision ) state.vision = 1;
        else if ( this.occluded ) state.fade = 1;
      }
      const hoverFade = this._hoverFadeState.occlusion;
      if ( canvas.masks.occlusion.vision ) state.vision = Math.max(state.vision, hoverFade);
      else state.fade = Math.max(state.fade, hoverFade);
    }

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

    /**
     * Update the hover-fade state.
     */
    #updateHoverFadeState() {
      if ( !this.#hoverFade ) return;
      const state = this._hoverFadeState;
      const time = canvas.app.ticker.lastTime;
      const {delay, duration} = CONFIG.Canvas.hoverFade;
      if ( state.fading ) {
        const dt = time - state.fadingTime;
        if ( dt >= duration ) state.fading = false;
      } else if ( state.faded !== state.hovered ) {
        const dt = time - state.hoveredTime;
        if ( dt >= delay ) {
          state.faded = state.hovered;
          if ( dt - delay < duration ) {
            state.fading = true;
            state.fadingTime = time;
          }
        }
      }
      let occlusion = 1;
      if ( state.fading ) {
        if ( state.faded !== state.hovered ) {
          state.faded = state.hovered;
          state.fadingTime = time - (state.fadingTime + duration - time);
        }
        occlusion = CanvasAnimation.easeInOutCosine((time - state.fadingTime) / duration);
      }
      state.occlusion = state.faded ? occlusion : 1 - occlusion;
    }

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

    /** @override */
    _shouldRenderDepth() {
      return !this.#restrictionState.isEmpty && !this.hidden;
    }

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

    /**
     * Test whether a specific Token occludes this PCO.
     * @param {Token} token       The Token to test
     * @returns {boolean}         Is the Token occluded by the PCO?
     */
    testOcclusion(token) {
      if ( token.document.elevation >= this.elevation ) return false;
      if ( !token.bounds.intersects(this.canvasBounds) ) return false;
      for ( const point of token.document.getOcclusionTestPoints() ) {
        if ( this.containsCanvasPoint(point) ) return true;
      }
      return false;
    }
  }
  return PrimaryOccludableObject;
}
