/**
 * A fog of war management class which is the singleton canvas.fog instance.
 * @category - Canvas
 */
class FogManager {

  /**
   * The FogExploration document which applies to this canvas view
   * @type {FogExploration|null}
   */
  exploration = null;

  /**
   * A status flag for whether the layer initialization workflow has succeeded
   * @type {boolean}
   * @private
   */
  #initialized = false;

  /**
   * Track whether we have pending fog updates which have not yet been saved to the database
   * @type {boolean}
   * @internal
   */
  _updated = false;

  /**
   * Texture extractor
   * @type {TextureExtractor}
   */
  get extractor() {
    return this.#extractor;
  }

  #extractor;

  /**
   * The fog refresh count.
   * If > to the refresh threshold, the fog texture is saved to database. It is then reinitialized to 0.
   * @type {number}
   */
  #refreshCount = 0;

  /**
   * Matrix used for fog rendering transformation.
   * @type {PIXI.Matrix}
   */
  #renderTransform = new PIXI.Matrix();

  /**
   * Define the number of fog refresh needed before the fog texture is extracted and pushed to the server.
   * @type {number}
   */
  static COMMIT_THRESHOLD = 70;

  /**
   * A debounced function to save fog of war exploration once a continuous stream of updates has concluded.
   * @type {Function}
   */
  #debouncedSave;

  /**
   * Handling of the concurrency for fog loading, saving and reset.
   * @type {Semaphore}
   */
  #queue = new foundry.utils.Semaphore();

  /* -------------------------------------------- */
  /*  Fog Manager Properties                      */
  /* -------------------------------------------- */

  /**
   * The exploration SpriteMesh which holds the fog exploration texture.
   * @type {SpriteMesh}
   */
  get sprite() {
    return this.#explorationSprite || (this.#explorationSprite = this._createExplorationObject());
  }

  #explorationSprite;

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

  /**
   * The configured options used for the saved fog-of-war texture.
   * @type {FogTextureConfiguration}
   */
  get textureConfiguration() {
    return canvas.visibility.textureConfiguration;
  }

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

  /**
   * Does the currently viewed Scene support Token field of vision?
   * @type {boolean}
   */
  get tokenVision() {
    return canvas.scene.tokenVision;
  }

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

  /**
   * Does the currently viewed Scene support fog of war exploration?
   * @type {boolean}
   */
  get fogExploration() {
    return canvas.scene.fog.exploration;
  }

  /* -------------------------------------------- */
  /*  Fog of War Management                       */
  /* -------------------------------------------- */

  /**
   * Create the exploration display object with or without a provided texture.
   * @param {PIXI.Texture|PIXI.RenderTexture} [tex] Optional exploration texture.
   * @returns {DisplayObject}
   * @internal
   */
  _createExplorationObject(tex) {
    return new SpriteMesh(tex ?? Canvas.getRenderTexture({
      clearColor: [0, 0, 0, 1],
      textureConfiguration: this.textureConfiguration
    }), FogSamplerShader);
  }

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

  /**
   * Initialize fog of war - resetting it when switching scenes or re-drawing the canvas
   * @returns {Promise<void>}
   */
  async initialize() {
    this.#initialized = false;

    // Create a TextureExtractor instance
    if ( this.#extractor === undefined ) {
      try {
        this.#extractor = new TextureExtractor(canvas.app.renderer, {
          callerName: "FogExtractor",
          controlHash: true,
          format: PIXI.FORMATS.RED
        });
      } catch(e) {
        this.#extractor = null;
        console.error(e);
      }
    }
    this.#extractor?.reset();

    // Bind a debounced save handler
    this.#debouncedSave = foundry.utils.debounce(this.save.bind(this), 2000);

    // Load the initial fog texture
    await this.load();
    this.#initialized = true;
  }

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

  /**
   * Clear the fog and reinitialize properties (commit and save in non reset mode)
   * @returns {Promise<void>}
   */
  async clear() {
    // Save any pending exploration
    try {
      await this.save();
    } catch(e) {
      ui.notifications.error("Failed to save fog exploration");
      console.error(e);
    }

    // Deactivate current fog exploration
    this.#initialized = false;
    this.#deactivate();
  }

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

  /**
   * Once a new Fog of War location is explored, composite the explored container with the current staging sprite.
   * Once the number of refresh is > to the commit threshold, save the fog texture to the database.
   */
  commit() {
    const vision = canvas.visibility.vision;
    if ( !vision?.children.length || !this.fogExploration || !this.tokenVision ) return;
    if ( !this.#explorationSprite?.texture.valid ) return;

    // Get a staging texture or clear and render into the sprite if its texture is a RT
    // and render the entire fog container to it
    const dims = canvas.dimensions;
    const isRenderTex = this.#explorationSprite.texture instanceof PIXI.RenderTexture;
    const tex = isRenderTex ? this.#explorationSprite.texture : Canvas.getRenderTexture({
      clearColor: [0, 0, 0, 1],
      textureConfiguration: this.textureConfiguration
    });
    this.#renderTransform.tx = -dims.sceneX;
    this.#renderTransform.ty = -dims.sceneY;

    // Render the currently revealed vision (preview excluded) to the texture
    vision.containmentFilter.enabled = canvas.visibility.needsContainment;
    vision.light.preview.visible = false;
    vision.light.mask.preview.visible = false;
    vision.sight.preview.visible = false;
    canvas.app.renderer.render(isRenderTex ? vision : this.#explorationSprite, {
      renderTexture: tex,
      clear: false,
      transform: this.#renderTransform
    });
    vision.light.preview.visible = true;
    vision.light.mask.preview.visible = true;
    vision.sight.preview.visible = true;
    vision.containmentFilter.enabled = false;

    if ( !isRenderTex ) this.#explorationSprite.texture.destroy(true);
    this.#explorationSprite.texture = tex;
    this._updated = true;

    if ( !this.exploration ) {
      const fogExplorationCls = getDocumentClass("FogExploration");
      this.exploration = new fogExplorationCls();
    }

    // Schedule saving the texture to the database
    if ( this.#refreshCount > FogManager.COMMIT_THRESHOLD ) {
      this.#debouncedSave();
      this.#refreshCount = 0;
    }
    else this.#refreshCount++;
  }

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

  /**
   * Load existing fog of war data from local storage and populate the initial exploration sprite
   * @returns {Promise<(PIXI.Texture|void)>}
   */
  async load() {
    return await this.#queue.add(this.#load.bind(this));
  }

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

  /**
   * Load existing fog of war data from local storage and populate the initial exploration sprite
   * @returns {Promise<(PIXI.Texture|void)>}
   */
  async #load() {
    if ( CONFIG.debug.fog.manager ) console.debug("FogManager | Loading saved FogExploration for Scene.");

    this.#deactivate();

    // Take no further action if token vision is not enabled
    if ( !this.tokenVision ) return;

    // Load existing FOW exploration data or create a new placeholder
    const fogExplorationCls = /** @type {typeof FogExploration} */ getDocumentClass("FogExploration");
    this.exploration = await fogExplorationCls.load();

    // Extract and assign the fog data image
    const assign = (tex, resolve) => {
      if ( this.#explorationSprite?.texture === tex ) return resolve(tex);
      this.#explorationSprite?.destroy(true);
      this.#explorationSprite = this._createExplorationObject(tex);
      canvas.visibility.resetExploration();
      canvas.perception.initialize();
      resolve(tex);
    };

    // Initialize the exploration sprite if no exploration data exists
    if ( !this.exploration ) {
      return await new Promise(resolve => {
        assign(Canvas.getRenderTexture({
          clearColor: [0, 0, 0, 1],
          textureConfiguration: this.textureConfiguration
        }), resolve);
      });
    }
    // Otherwise load the texture from the exploration data
    return await new Promise(resolve => {
      let tex = this.exploration.getTexture();
      if ( tex === null ) assign(Canvas.getRenderTexture({
        clearColor: [0, 0, 0, 1],
        textureConfiguration: this.textureConfiguration
      }), resolve);
      else if ( tex.baseTexture.valid ) assign(tex, resolve);
      else tex.on("update", tex => assign(tex, resolve));
    });
  }

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

  /**
   * Dispatch a request to reset the fog of war exploration status for all users within this Scene.
   * Once the server has deleted existing FogExploration documents, the _onReset handler will re-draw the canvas.
   */
  async reset() {
    if ( CONFIG.debug.fog.manager ) console.debug("FogManager | Resetting fog of war exploration for Scene.");
    game.socket.emit("resetFog", canvas.scene.id);
  }

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

  /**
   * Request a fog of war save operation.
   * Note: if a save operation is pending, we're waiting for its conclusion.
   */
  async save() {
    return await this.#queue.add(this.#save.bind(this));
  }

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

  /**
   * Request a fog of war save operation.
   * Note: if a save operation is pending, we're waiting for its conclusion.
   */
  async #save() {
    if ( !this._updated ) return;
    this._updated = false;
    const exploration = this.exploration;
    if ( CONFIG.debug.fog.manager ) {
      console.debug("FogManager | Initiate non-blocking extraction of the fog of war progress.");
    }
    if ( !this.#extractor ) {
      console.error("FogManager | Browser does not support texture extraction.");
      return;
    }

    // Get compressed base64 image from the fog texture
    const base64Image = await this._extractBase64();

    // If the exploration changed, the fog was reloaded while the pixels were extracted
    if ( this.exploration !== exploration ) return;

    // Need to skip?
    if ( !base64Image ) {
      if ( CONFIG.debug.fog.manager ) console.debug("FogManager | Fog of war has not changed. Skipping db operation.");
      return;
    }

    // Update the fog exploration document
    const updateData = this._prepareFogUpdateData(base64Image);
    await this.#updateFogExploration(updateData);
  }

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

  /**
   * Extract fog data as a base64 string
   * @returns {Promise<string>}
   * @protected
   */
  async _extractBase64() {
    try {
      return this.#extractor.extract({
        texture: this.#explorationSprite.texture,
        compression: TextureExtractor.COMPRESSION_MODES.BASE64,
        type: "image/webp",
        quality: 0.8,
        debug: CONFIG.debug.fog.extractor
      });
    } catch(err) {
      // FIXME this is needed because for some reason .extract() may throw a boolean false instead of an Error
      throw new Error("Fog of War base64 extraction failed");
    }
  }

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

  /**
   * Prepare the data that will be used to update the FogExploration document.
   * @param {string} base64Image              The extracted base64 image data
   * @returns {Partial<FogExplorationData>}   Exploration data to update
   * @protected
   */
  _prepareFogUpdateData(base64Image) {
    return {explored: base64Image, timestamp: Date.now()};
  }

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

  /**
   * Update the fog exploration document with provided data.
   * @param {object} updateData
   * @returns {Promise<void>}
   */
  async #updateFogExploration(updateData) {
    if ( !game.scenes.has(canvas.scene?.id) ) return;
    if ( !this.exploration ) return;
    if ( CONFIG.debug.fog.manager ) console.debug("FogManager | Saving fog of war progress into exploration document.");
    if ( !this.exploration.id ) {
      this.exploration.updateSource(updateData);
      this.exploration = await this.exploration.constructor.create(this.exploration.toJSON(), {loadFog: false});
    }
    else await this.exploration.update(updateData, {loadFog: false});
  }

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

  /**
   * Deactivate fog of war.
   * Clear all shared containers by unlinking them from their parent.
   * Destroy all stored textures and graphics.
   */
  #deactivate() {
    // Remove the current exploration document
    this.exploration = null;
    this.#extractor?.reset();

    // Destroy current exploration texture and provide a new one with transparency
    if ( this.#explorationSprite && !this.#explorationSprite.destroyed ) this.#explorationSprite.destroy(true);
    this.#explorationSprite = undefined;

    this._updated = false;
    this.#refreshCount = 0;
  }

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

  /**
   * If fog of war data is reset from the server, deactivate the current fog and initialize the exploration.
   * @returns {Promise}
   * @internal
   */
  async _handleReset() {
    return await this.#queue.add(this.#handleReset.bind(this));
  }

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

  /**
   * If fog of war data is reset from the server, deactivate the current fog and initialize the exploration.
   * @returns {Promise}
   */
  async #handleReset() {
    ui.notifications.info("Fog of War exploration progress was reset for this Scene");

    // Remove the current exploration document
    this.#deactivate();

    // Reset exploration in the visibility layer
    canvas.visibility.resetExploration();

    // Refresh perception
    canvas.perception.initialize();
  }

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

  /**
   * @deprecated since v11
   * @ignore
   */
  get pending() {
    const msg = "pending is deprecated and redirected to the exploration container";
    foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
    return canvas.visibility.explored;
  }

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

  /**
   * @deprecated since v11
   * @ignore
   */
  get revealed() {
    const msg = "revealed is deprecated and redirected to the exploration container";
    foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
    return canvas.visibility.explored;
  }

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

  /**
   * @deprecated since v11
   * @ignore
   */
  update(source, force=false) {
    const msg = "update is obsolete and always returns true. The fog exploration does not record position anymore.";
    foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
    return true;
  }

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

  /**
   * @deprecated since v11
   * @ignore
   */
  get resolution() {
    const msg = "resolution is deprecated and redirected to CanvasVisibility#textureConfiguration";
    foundry.utils.logCompatibilityWarning(msg, {since: 11, until: 13});
    return canvas.visibility.textureConfiguration;
  }
}
