import Check from "../Core/Check.js";
import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import when from "../ThirdParty/when.js";
import parseFeatureMetadata from "./parseFeatureMetadata.js";
import ResourceCache from "./ResourceCache.js";
import ResourceLoader from "./ResourceLoader.js";
import ResourceLoaderState from "./ResourceLoaderState.js";

/**
 * Loads glTF feature metadata.
 * <p>
 * Implements the {@link ResourceLoader} interface.
 * </p>
 *
 * @alias GltfFeatureMetadataLoader
 * @constructor
 * @augments ResourceLoader
 *
 * @param {Object} options Object with the following properties:
 * @param {Object} options.gltf The glTF JSON.
 * @param {String} options.extension The feature metadata extension object.
 * @param {Resource} options.gltfResource The {@link Resource} containing the glTF.
 * @param {Resource} options.baseResource The {@link Resource} that paths in the glTF JSON are relative to.
 * @param {SupportedImageFormats} options.supportedImageFormats The supported image formats.
 * @param {String} [options.cacheKey] The cache key of the resource.
 * @param {Boolean} [options.asynchronous=true] Determines if WebGL resource creation will be spread out over several frames or block until all WebGL resources are created.
 *
 * @private
 * @experimental This feature is using part of the 3D Tiles spec that is not final and is subject to change without Cesium's standard deprecation policy.
 */
export default function GltfFeatureMetadataLoader(options) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);
  var gltf = options.gltf;
  var extension = options.extension;
  var gltfResource = options.gltfResource;
  var baseResource = options.baseResource;
  var supportedImageFormats = options.supportedImageFormats;
  var cacheKey = options.cacheKey;
  var asynchronous = defaultValue(options.asynchronous, true);

  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("options.gltf", gltf);
  Check.typeOf.object("options.extension", extension);
  Check.typeOf.object("options.gltfResource", gltfResource);
  Check.typeOf.object("options.baseResource", baseResource);
  Check.typeOf.object("options.supportedImageFormats", supportedImageFormats);
  //>>includeEnd('debug');

  this._gltfResource = gltfResource;
  this._baseResource = baseResource;
  this._gltf = gltf;
  this._extension = extension;
  this._supportedImageFormats = supportedImageFormats;
  this._cacheKey = cacheKey;
  this._asynchronous = asynchronous;
  this._bufferViewLoaders = [];
  this._textureLoaders = [];
  this._schemaLoader = undefined;
  this._featureMetadata = undefined;
  this._state = ResourceLoaderState.UNLOADED;
  this._promise = when.defer();
}

if (defined(Object.create)) {
  GltfFeatureMetadataLoader.prototype = Object.create(ResourceLoader.prototype);
  GltfFeatureMetadataLoader.prototype.constructor = GltfFeatureMetadataLoader;
}

Object.defineProperties(GltfFeatureMetadataLoader.prototype, {
  /**
   * A promise that resolves to the resource when the resource is ready.
   *
   * @memberof GltfFeatureMetadataLoader.prototype
   *
   * @type {Promise.<GltfFeatureMetadataLoader>}
   * @readonly
   * @private
   */
  promise: {
    get: function () {
      return this._promise.promise;
    },
  },
  /**
   * The cache key of the resource.
   *
   * @memberof GltfFeatureMetadataLoader.prototype
   *
   * @type {String}
   * @readonly
   * @private
   */
  cacheKey: {
    get: function () {
      return this._cacheKey;
    },
  },
  /**
   * Feature metadata.
   *
   * @memberof GltfFeatureMetadataLoader.prototype
   *
   * @type {FeatureMetadata}
   * @readonly
   * @private
   */
  featureMetadata: {
    get: function () {
      return this._featureMetadata;
    },
  },
});

/**
 * Loads the resource.
 * @private
 */
GltfFeatureMetadataLoader.prototype.load = function () {
  var bufferViewsPromise = loadBufferViews(this);
  var texturesPromise = loadTextures(this);
  var schemaPromise = loadSchema(this);

  this._gltf = undefined; // No longer need to hold onto the glTF
  this._state = ResourceLoaderState.LOADING;

  var that = this;

  when
    .all([bufferViewsPromise, texturesPromise, schemaPromise])
    .then(function (results) {
      if (that.isDestroyed()) {
        return;
      }
      var bufferViews = results[0];
      var textures = results[1];
      var schema = results[2];

      that._featureMetadata = parseFeatureMetadata({
        extension: that._extension,
        schema: schema,
        bufferViews: bufferViews,
        textures: textures,
      });
      that._state = ResourceLoaderState.READY;
      that._promise.resolve(that);
    })
    .otherwise(function (error) {
      if (that.isDestroyed()) {
        return;
      }
      that.unload();
      that._state = ResourceLoaderState.FAILED;
      var errorMessage = "Failed to load feature metadata";
      that._promise.reject(that.getError(errorMessage, error));
    });
};

function loadBufferViews(featureMetadataLoader) {
  var extension = featureMetadataLoader._extension;
  var featureTables = extension.featureTables;

  // Gather the used buffer views
  var bufferViewIds = {};
  if (defined(featureTables)) {
    for (var featureTableId in featureTables) {
      if (featureTables.hasOwnProperty(featureTableId)) {
        var featureTable = featureTables[featureTableId];
        var properties = featureTable.properties;
        if (defined(properties)) {
          for (var propertyId in properties) {
            if (properties.hasOwnProperty(propertyId)) {
              var property = properties[propertyId];
              var bufferView = property.bufferView;
              var arrayOffsetBufferView = property.arrayOffsetBufferView;
              var stringOffsetBufferView = property.stringOffsetBufferView;
              if (defined(bufferView)) {
                bufferViewIds[bufferView] = true;
              }
              if (defined(arrayOffsetBufferView)) {
                bufferViewIds[arrayOffsetBufferView] = true;
              }
              if (defined(stringOffsetBufferView)) {
                bufferViewIds[stringOffsetBufferView] = true;
              }
            }
          }
        }
      }
    }
  }

  // Load the buffer views
  var bufferViewPromises = [];
  var bufferViewLoaders = {};
  for (var bufferViewId in bufferViewIds) {
    if (bufferViewIds.hasOwnProperty(bufferViewId)) {
      var bufferViewLoader = ResourceCache.loadBufferView({
        gltf: featureMetadataLoader._gltf,
        bufferViewId: parseInt(bufferViewId),
        gltfResource: featureMetadataLoader._gltfResource,
        baseResource: featureMetadataLoader._baseResource,
      });
      bufferViewPromises.push(bufferViewLoader.promise);
      featureMetadataLoader._bufferViewLoaders.push(bufferViewLoader);
      bufferViewLoaders[bufferViewId] = bufferViewLoader;
    }
  }

  // Return a promise to a map of buffer view IDs to typed arrays
  return when.all(bufferViewPromises).then(function () {
    var bufferViews = {};
    for (var bufferViewId in bufferViewLoaders) {
      if (bufferViewLoaders.hasOwnProperty(bufferViewId)) {
        var bufferViewLoader = bufferViewLoaders[bufferViewId];
        // Copy the typed array and let the underlying ArrayBuffer be freed
        var bufferViewTypedArray = new Uint8Array(bufferViewLoader.typedArray);
        bufferViews[bufferViewId] = bufferViewTypedArray;
      }
    }

    // Buffer views can be unloaded after the data has been copied
    unloadBufferViews(featureMetadataLoader);

    return bufferViews;
  });
}

function loadTextures(featureMetadataLoader) {
  var extension = featureMetadataLoader._extension;
  var featureTextures = extension.featureTextures;

  var gltf = featureMetadataLoader._gltf;
  var gltfResource = featureMetadataLoader._gltfResource;
  var baseResource = featureMetadataLoader._baseResource;
  var supportedImageFormats = featureMetadataLoader._supportedImageFormats;
  var asynchronous = featureMetadataLoader._asynchronous;

  // Gather the used textures
  var textureIds = {};
  if (defined(featureTextures)) {
    for (var featureTextureId in featureTextures) {
      if (featureTextures.hasOwnProperty(featureTextureId)) {
        var featureTexture = featureTextures[featureTextureId];
        var properties = featureTexture.properties;
        if (defined(properties)) {
          for (var propertyId in properties) {
            if (properties.hasOwnProperty(propertyId)) {
              var property = properties[propertyId];
              var textureInfo = property.texture;
              textureIds[textureInfo.index] = textureInfo;
            }
          }
        }
      }
    }
  }

  // Load the textures
  var texturePromises = [];
  var textureLoaders = {};
  for (var textureId in textureIds) {
    if (textureIds.hasOwnProperty(textureId)) {
      var textureLoader = ResourceCache.loadTexture({
        gltf: gltf,
        textureInfo: textureIds[textureId],
        gltfResource: gltfResource,
        baseResource: baseResource,
        supportedImageFormats: supportedImageFormats,
        asynchronous: asynchronous,
      });
      texturePromises.push(textureLoader.promise);
      featureMetadataLoader._textureLoaders.push(textureLoader);
      textureLoaders[textureId] = textureLoader;
    }
  }

  // Return a promise to a map of texture IDs to Texture objects
  return when.all(texturePromises).then(function () {
    var textures = {};
    for (var textureId in textureLoaders) {
      if (textureLoaders.hasOwnProperty(textureId)) {
        var textureLoader = textureLoaders[textureId];
        textures[textureId] = textureLoader.texture;
      }
    }
    return textures;
  });
}

function loadSchema(featureMetadataLoader) {
  var extension = featureMetadataLoader._extension;

  var schemaLoader;
  if (defined(extension.schemaUri)) {
    var resource = featureMetadataLoader._baseResource.getDerivedResource({
      url: extension.schemaUri,
    });
    schemaLoader = ResourceCache.loadSchema({
      resource: resource,
    });
  } else {
    schemaLoader = ResourceCache.loadSchema({
      schema: extension.schema,
    });
  }

  featureMetadataLoader._schemaLoader = schemaLoader;

  return schemaLoader.promise.then(function (schemaLoader) {
    return schemaLoader.schema;
  });
}

/**
 * Processes the resource until it becomes ready.
 *
 * @param {FrameState} frameState The frame state.
 * @private
 */
GltfFeatureMetadataLoader.prototype.process = function (frameState) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("frameState", frameState);
  //>>includeEnd('debug');

  if (this._state !== ResourceLoaderState.LOADING) {
    return;
  }

  var textureLoaders = this._textureLoaders;
  var textureLoadersLength = textureLoaders.length;

  for (var i = 0; i < textureLoadersLength; ++i) {
    var textureLoader = textureLoaders[i];
    textureLoader.process(frameState);
  }
};

function unloadBufferViews(featureMetadataLoader) {
  var bufferViewLoaders = featureMetadataLoader._bufferViewLoaders;
  var bufferViewLoadersLength = bufferViewLoaders.length;
  for (var i = 0; i < bufferViewLoadersLength; ++i) {
    ResourceCache.unload(bufferViewLoaders[i]);
  }
  featureMetadataLoader._bufferViewLoaders.length = 0;
}

function unloadTextures(featureMetadataLoader) {
  var textureLoaders = featureMetadataLoader._textureLoaders;
  var textureLoadersLength = textureLoaders.length;
  for (var i = 0; i < textureLoadersLength; ++i) {
    ResourceCache.unload(textureLoaders[i]);
  }
  featureMetadataLoader._textureLoaders.length = 0;
}

/**
 * Unloads the resource.
 * @private
 */
GltfFeatureMetadataLoader.prototype.unload = function () {
  unloadBufferViews(this);
  unloadTextures(this);

  if (defined(this._schemaLoader)) {
    ResourceCache.unload(this._schemaLoader);
  }
  this._schemaLoader = undefined;

  this._featureMetadata = undefined;
};