import Check from "../Core/Check.js";
import defined from "../Core/defined.js";
import BillboardLoadState from "./BillboardLoadState.js";

/**
 * Tracks a reference to an image and it's loading state, as used in a BillboardCollection and stored in a texture atlas.
 * @constructor
 * @private
 * @see BillboardCollection
 * @see Billboard#image
 * @alias BillboardTexture
 * @param {BillboardCollection} billboardCollection The associated billboard collecion.
 */
function BillboardTexture(billboardCollection) {
  //>>includeStart('debug', pragmas.debug);
  Check.defined("billboardCollection", billboardCollection);
  //>>includeEnd('debug');

  this._billboardCollection = billboardCollection;

  this._id = undefined;
  this._loadState = BillboardLoadState.NONE;
  this._loadError = undefined;

  this._index = -1;
  this._width = undefined;
  this._height = undefined;

  this._hasSubregion = false;

  /**
   * Used by billboardCollection to track whcih billboards to update.
   * @type {boolean}
   * @private
   */
  this.dirty = false;
}

Object.defineProperties(BillboardTexture.prototype, {
  /**
   * If defined, this error was encountered during the loading process.
   * @memberof BillboardTexture.prototype
   * @type {Error|undefined}
   * @readonly
   * @private
   */
  loadError: {
    get: function () {
      return this._loadError;
    },
  },

  /**
   * The current status of the image load. When <code>BillboardLoadState.LOADED</code>, this billboard is ready to render, i.e., the image
   * has been downloaded and the WebGL resources are created.
   * @memberof BillboardTexture.prototype
   * @type {BillboardLoadState}
   * @readonly
   * @default BillboardLoadState.NONE
   * @private
   */
  loadState: {
    get: function () {
      return this._loadState;
    },
  },

  /**
   * When <code>true</code>, this texture is ready to render, i.e., the image
   * has been downloaded and the WebGL resources are created.
   * @memberof BillboardTexture.prototype
   * @type {boolean}
   * @readonly
   * @default false
   * @private
   */
  ready: {
    get: function () {
      return this._loadState === BillboardLoadState.LOADED;
    },
  },

  /**
   * Returns <code>true</code> if there is image data associated with this instance.
   * @memberof BillboardTexture.prototype
   * @type {boolean}
   * @readonly
   * @private
   */
  hasImage: {
    get: function () {
      return this._loadState !== BillboardLoadState.NONE;
    },
  },

  /**
   * A unique identifier for the image, or undefined if no image data has been associated with this instance.
   * @memberof BillboardTexture.prototype
   * @type {string|undefined}
   * @readonly
   * @private
   */
  id: {
    get: function () {
      return this._id;
    },
  },

  /**
   * The width of the associated image. Before the instance is <code>ready</code>, this will be <code>undefined</code>.
   * @memberof BillboardTexture.prototype
   * @type {number|undefined}
   * @readonly
   * @private
   */
  width: {
    get: function () {
      return this._width;
    },
  },

  /**
   * The height of the associated image. Before the instance is <code>ready</code>, this will be <code>undefined</code>.
   * @memberof BillboardTexture.prototype
   * @type {number|undefined}
   * @readonly
   * @private
   */
  height: {
    get: function () {
      return this._height;
    },
  },
});

/**
 * Releases reference to any associated image data.
 * @private
 */
BillboardTexture.prototype.unload = async function () {
  if (this._loadState === BillboardLoadState.NONE) {
    return;
  }

  this._id = undefined;
  this._loadError = undefined;
  this._loadState = BillboardLoadState.NONE;

  this._index = -1;
  this._width = undefined;
  this._height = undefined;

  this.dirty = true;
};

/**
 * Starts loading an image into the texture atlas.
 * @see {TextureAtlas#addImage}
 * @private
 * @param {string} id An identifier to detect whether the image already exists in the atlas.
 * @param {HTMLImageElement|HTMLCanvasElement|string|Resource|Promise|TextureAtlas.CreateImageCallback} image An image or canvas to add to the texture atlas,
 *        or a URL to an Image, or a Promise for an image, or a function that creates an image.
 * @param {number} width A number specifying the width of the texture. If undefined, the image width will be used.
 * @param {number} height A number specifying the height of the texture. If undefined, the image height will be used.
 */
BillboardTexture.prototype.loadImage = async function (
  id,
  image,
  width,
  height,
) {
  if (this._id === id) {
    // This image has already been loaded
    return;
  }

  const collection = this._billboardCollection;
  const cache = collection.billboardTextureCache;
  let billboardTexture = cache.get(id);
  if (
    (defined(billboardTexture) &&
      image.loadState === BillboardLoadState.LOADING) ||
    image.loadState === BillboardLoadState.LOADED
  ) {
    // Use the cached texture if it is in progress or successful.
    BillboardTexture.clone(billboardTexture, this);
    return;
  }
  // Otherwise, load if not yet assigned an image, and try the load again if anything failed during the last billboard creation
  if (!defined(billboardTexture)) {
    billboardTexture = new BillboardTexture(collection);
    cache.set(id, billboardTexture);
  }

  billboardTexture._id = this._id = id;
  billboardTexture._loadState = this._loadState = BillboardLoadState.LOADING;
  billboardTexture._loadError = this._loadError = undefined;

  let index;
  const atlas = this._billboardCollection.textureAtlas;
  try {
    index = atlas.addImage(id, image, width, height);
    if (index instanceof Promise) {
      index = await index;
    }
  } catch (error) {
    // There was an error loading the image
    billboardTexture._loadState = BillboardLoadState.ERROR;
    billboardTexture._loadError = error;

    if (this._id !== id) {
      // Another load was initiated and resolved resolved before this one. This operation is cancelled.
      return;
    }

    this._loadState = BillboardLoadState.ERROR;
    this._loadError = error;
    return;
  }

  if (!defined(index) || index === -1) {
    // Resources destroyed or otherwise
    billboardTexture._loadState = BillboardLoadState.FAILED;
    billboardTexture._index = -1;

    if (this._id !== id) {
      // Another load was initiated and resolved resolved before this one. This operation is cancelled.
      return;
    }

    this._loadState = BillboardLoadState.FAILED;
    this._index = -1;

    return;
  }

  billboardTexture._index = index;
  billboardTexture._loadState = BillboardLoadState.LOADED;

  const rectangle = atlas.rectangles[index];
  billboardTexture._width = rectangle.width;
  billboardTexture._height = rectangle.height;

  if (this._id !== id) {
    // Another load was initiated and resolved resolved before this one. This operation is cancelled.
    return;
  }

  this._index = index;
  this._loadState = BillboardLoadState.LOADED;
  this._width = rectangle.width;
  this._height = rectangle.height;

  this.dirty = true;
};

/**
 * Track a reference to a sub-region of an existing image.
 * @see {TextureAtlas#addImageSubRegion}
 * @private
 * @param {string} id An identifier to detect whether the image already exists in the atlas.
 * @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
 */
BillboardTexture.prototype.addImageSubRegion = function (id, subRegion) {
  this._id = id;
  this._loadError = undefined;
  this._hasSubregion = true;

  const atlas = this._billboardCollection.textureAtlas;
  const indexOrPromise = atlas.addImageSubRegion(id, subRegion);

  if (typeof indexOrPromise === "number") {
    this.setImageSubRegion(indexOrPromise, subRegion);
    return;
  }

  this.loadImageSubRegion(id, subRegion, indexOrPromise);
};

/**
 * @see {TextureAtlas#addImageSubRegion}
 * @private
 * @param {string} id An identifier to detect whether the image already exists in the atlas.
 * @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
 * @param {Promise<number>} indexPromise A promise that resolves to the image region index.
 */
BillboardTexture.prototype.loadImageSubRegion = async function (
  id,
  subRegion,
  indexPromise,
) {
  let index;
  try {
    this._loadState = BillboardLoadState.LOADING;
    index = await indexPromise;
  } catch (error) {
    // There was an error loading the referenced image
    this._loadState = BillboardLoadState.ERROR;
    this._loadError = error;
    return;
  }

  if (this._id !== id) {
    // Another load was initiated and resolved resolved before this one. This operation is cancelled.
    return;
  }

  this._loadState = BillboardLoadState.LOADED;

  this.setImageSubRegion(index, subRegion);
};

/**
 * @see {TextureAtlas#addImageSubRegion}
 * @private
 * @param {number} index The resolved index in the {@link TextureAtlas}
 * @param {BoundingRectangle} subRegion An {@link BoundingRectangle} defining a region of an existing image, measured in pixels from the bottom-left of the image.
 */
BillboardTexture.prototype.setImageSubRegion = function (index, subRegion) {
  if (this._index === index) {
    return;
  }

  if (!defined(index) || index === -1) {
    this._loadState = BillboardLoadState.FAILED;
    this._index = -1;
    this._width = undefined;
    this._height = undefined;
    return;
  }

  this._width = subRegion.width;
  this._height = subRegion.height;

  this._index = index;

  this.dirty = true;
};

/**
 * Get the texture coordinates for reading the loaded texture in shaders.
 * @private
 * @param {BoundingRectangle} [result] The modified result parameter or a new BoundingRectangle instance if one was not provided.
 * @return {BoundingRectangle} The modified result parameter or a new BoundingRectangle instance if one was not provided.
 */
BillboardTexture.prototype.computeTextureCoordinates = function (result) {
  const atlas = this._billboardCollection.textureAtlas;
  return atlas.computeTextureCoordinates(this._index, result);
};

/**
 * Clones an existing billboard texture, inlcuding any in-flight tracking, into the target billboard texture.
 * @param {BillboardTexture} billboardTexture
 * @param {BillboardTexture} target
 * @returns {BillboardTexture} target
 */
BillboardTexture.clone = function (billboardTexture, target) {
  target._id = billboardTexture._id;
  target._loadState = billboardTexture._loadState;
  target._loadError = undefined;
  target._index = billboardTexture._index;
  target._width = billboardTexture._width;
  target._height = billboardTexture._height;
  target._hasSubregion = billboardTexture._hasSubregion;

  if (billboardTexture.ready) {
    target.dirty = true;
    return;
  }

  const completeLoad = async () => {
    const id = billboardTexture._id;
    const atlas = billboardTexture._billboardCollection.textureAtlas;
    await atlas._indexPromiseById.get(id);

    // Any errors should have already been handled
    if (target._id !== id) {
      // Another load was initiated and resolved resolved before this one. This operation is cancelled.
      return;
    }

    if (billboardTexture._hasSubregion) {
      // Subregions must wait an additional frame to be ready
      await Promise.resolve();
    }

    target._id = id;
    target._loadState = billboardTexture._loadState;
    target._loadError = billboardTexture._loadError;
    target._index = billboardTexture._index;
    target._width = billboardTexture._width;
    target._height = billboardTexture._height;
    target.dirty = true;
  };

  completeLoad();
  return target;
};

export default BillboardTexture;
