import {
  clone,
  GltfBufferViewLoader,
  GltfFeatureMetadataLoader,
  GltfTextureLoader,
  MetadataSchemaLoader,
  Resource,
  ResourceCache,
  SupportedImageFormats,
  when,
} from "../../Source/Cesium.js";
import createScene from "../createScene.js";
import MetadataTester from "../MetadataTester.js";
import waitForLoaderProcess from "../waitForLoaderProcess.js";

describe(
  "Scene/GltfFeatureMetadataLoader",
  function () {
    if (!MetadataTester.isSupported()) {
      return;
    }

    var image = new Image();
    image.src =
      "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII=";

    var gltfUri = "https://example.com/model.glb";
    var gltfResource = new Resource({
      url: gltfUri,
    });

    var schemaJson = {
      classes: {
        building: {
          properties: {
            name: {
              type: "STRING",
            },
            height: {
              type: "FLOAT64",
            },
          },
        },
        tree: {
          properties: {
            species: {
              type: "ARRAY",
              componentType: "STRING",
            },
          },
        },
        map: {
          properties: {
            color: {
              type: "ARRAY",
              componentType: "UINT8",
              componentCount: 3,
            },
            intensity: {
              type: "UINT8",
            },
          },
        },
        ortho: {
          properties: {
            vegetation: {
              type: "UINT8",
              normalized: true,
            },
          },
        },
      },
    };

    var results = MetadataTester.createGltf({
      schema: schemaJson,
      featureTables: {
        buildings: {
          class: "building",
          properties: {
            name: ["House", "Hospital"],
            height: [10.0, 20.0],
          },
        },
        trees: {
          class: "tree",
          properties: {
            species: [["Sparrow", "Squirrel"], ["Crow"]],
          },
        },
      },
      images: [
        {
          uri: "map.png",
        },
        {
          uri: "ortho.png",
        },
      ],
      textures: [
        {
          source: 0,
        },
        {
          source: 1,
        },
      ],
      featureTextures: {
        mapTexture: {
          class: "map",
          properties: {
            color: {
              channels: "rgb",
              texture: {
                index: 0,
                texCoord: 0,
              },
            },
            intensity: {
              channels: "a",
              texture: {
                index: 0,
                texCoord: 0,
              },
            },
          },
        },
        orthoTexture: {
          class: "ortho",
          properties: {
            vegetation: {
              channels: "r",
              texture: {
                index: 1,
                texCoord: 1,
              },
            },
          },
        },
      },
    });

    var gltf = results.gltf;
    var extension = gltf.extensions.EXT_feature_metadata;
    var buffer = results.buffer.buffer;

    var gltfSchemaUri = clone(gltf, true);
    var extensionSchemaUri = gltfSchemaUri.extensions.EXT_feature_metadata;
    extensionSchemaUri.schemaUri = "schema.json";
    delete extensionSchemaUri.schema;

    var scene;

    beforeAll(function () {
      scene = createScene();
    });

    afterAll(function () {
      scene.destroyForSpecs();
    });

    afterEach(function () {
      ResourceCache.clearForSpecs();
    });

    it("throws if gltf is undefined", function () {
      expect(function () {
        return new GltfFeatureMetadataLoader({
          gltf: undefined,
          extension: extension,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          supportedImageFormats: new SupportedImageFormats(),
        });
      }).toThrowDeveloperError();
    });

    it("throws if extension is undefined", function () {
      expect(function () {
        return new GltfFeatureMetadataLoader({
          gltf: gltf,
          extension: undefined,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          supportedImageFormats: new SupportedImageFormats(),
        });
      }).toThrowDeveloperError();
    });

    it("throws if gltfResource is undefined", function () {
      expect(function () {
        return new GltfFeatureMetadataLoader({
          gltf: gltf,
          extension: extension,
          gltfResource: undefined,
          baseResource: gltfResource,
          supportedImageFormats: new SupportedImageFormats(),
        });
      }).toThrowDeveloperError();
    });

    it("throws if baseResource is undefined", function () {
      expect(function () {
        return new GltfFeatureMetadataLoader({
          gltf: gltf,
          extension: extension,
          gltfResource: gltfResource,
          baseResource: undefined,
          supportedImageFormats: new SupportedImageFormats(),
        });
      }).toThrowDeveloperError();
    });

    it("throws if gltf is undefined", function () {
      expect(function () {
        return new GltfFeatureMetadataLoader({
          gltf: gltf,
          extension: extension,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          supportedImageFormats: undefined,
        });
      }).toThrowDeveloperError();
    });

    it("rejects promise if buffer view fails to load", function () {
      var error = new Error("404 Not Found");
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.reject(error)
      );

      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(image)
      );

      var featureMetadataLoader = new GltfFeatureMetadataLoader({
        gltf: gltf,
        extension: extension,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });

      featureMetadataLoader.load();

      return featureMetadataLoader.promise
        .then(function (featureMetadataLoader) {
          fail();
        })
        .otherwise(function (runtimeError) {
          expect(runtimeError.message).toBe(
            "Failed to load feature metadata\nFailed to load buffer view\nFailed to load external buffer: https://example.com/external.bin\n404 Not Found"
          );
        });
    });

    it("rejects promise if texture fails to load", function () {
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.resolve(buffer)
      );

      var error = new Error("404 Not Found");
      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.reject(error)
      );

      var featureMetadataLoader = new GltfFeatureMetadataLoader({
        gltf: gltf,
        extension: extension,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });

      featureMetadataLoader.load();

      return featureMetadataLoader.promise
        .then(function (featureMetadataLoader) {
          fail();
        })
        .otherwise(function (runtimeError) {
          expect(runtimeError.message).toBe(
            "Failed to load feature metadata\nFailed to load texture\nFailed to load image: map.png\n404 Not Found"
          );
        });
    });

    it("rejects promise if external schema fails to load", function () {
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.resolve(buffer)
      );

      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(image)
      );

      var error = new Error("404 Not Found");
      spyOn(Resource.prototype, "fetchJson").and.returnValue(
        when.reject(error)
      );

      var featureMetadataLoader = new GltfFeatureMetadataLoader({
        gltf: gltfSchemaUri,
        extension: extensionSchemaUri,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });

      featureMetadataLoader.load();

      return featureMetadataLoader.promise
        .then(function (featureMetadataLoader) {
          fail();
        })
        .otherwise(function (runtimeError) {
          expect(runtimeError.message).toBe(
            "Failed to load feature metadata\nFailed to load schema: https://example.com/schema.json\n404 Not Found"
          );
        });
    });

    it("loads feature metadata", function () {
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.resolve(buffer)
      );

      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(image)
      );

      var featureMetadataLoader = new GltfFeatureMetadataLoader({
        gltf: gltf,
        extension: extension,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });

      featureMetadataLoader.load();

      return waitForLoaderProcess(featureMetadataLoader, scene).then(function (
        featureMetadataLoader
      ) {
        featureMetadataLoader.process(scene.frameState); // Check that calling process after load doesn't break anything
        var featureMetadata = featureMetadataLoader.featureMetadata;
        var buildingsTable = featureMetadata.getFeatureTable("buildings");
        var treesTable = featureMetadata.getFeatureTable("trees");
        var mapTexture = featureMetadata.getFeatureTexture("mapTexture");
        var orthoTexture = featureMetadata.getFeatureTexture("orthoTexture");

        expect(buildingsTable.getProperty(0, "name")).toBe("House");
        expect(buildingsTable.getProperty(1, "name")).toBe("Hospital");
        expect(treesTable.getProperty(0, "species")).toEqual([
          "Sparrow",
          "Squirrel",
        ]);
        expect(treesTable.getProperty(1, "species")).toEqual(["Crow"]);

        var colorProperty = mapTexture.getProperty("color");
        var intensityProperty = mapTexture.getProperty("intensity");
        var vegetationProperty = orthoTexture.getProperty("vegetation");

        expect(colorProperty.textureReader.texture.width).toBe(1);
        expect(colorProperty.textureReader.texture.height).toBe(1);
        expect(colorProperty.textureReader.texture).toBe(
          intensityProperty.textureReader.texture
        );

        expect(vegetationProperty.textureReader.texture.width).toBe(1);
        expect(vegetationProperty.textureReader.texture.height).toBe(1);
        expect(vegetationProperty.textureReader.texture).not.toBe(
          colorProperty.textureReader.texture
        );

        expect(Object.keys(featureMetadata.schema.classes).sort()).toEqual([
          "building",
          "map",
          "ortho",
          "tree",
        ]);
      });
    });

    it("loads feature metadata with external schema", function () {
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.resolve(buffer)
      );

      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(image)
      );

      spyOn(Resource.prototype, "fetchJson").and.returnValue(
        when.resolve(schemaJson)
      );

      var featureMetadataLoader = new GltfFeatureMetadataLoader({
        gltf: gltfSchemaUri,
        extension: extensionSchemaUri,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });

      featureMetadataLoader.load();

      return waitForLoaderProcess(featureMetadataLoader, scene).then(function (
        featureMetadataLoader
      ) {
        var featureMetadata = featureMetadataLoader.featureMetadata;
        expect(Object.keys(featureMetadata.schema.classes).sort()).toEqual([
          "building",
          "map",
          "ortho",
          "tree",
        ]);
      });
    });

    it("destroys feature metadata", function () {
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.resolve(buffer)
      );

      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(image)
      );

      spyOn(Resource.prototype, "fetchJson").and.returnValue(
        when.resolve(schemaJson)
      );

      var destroyBufferView = spyOn(
        GltfBufferViewLoader.prototype,
        "destroy"
      ).and.callThrough();

      var destroyTexture = spyOn(
        GltfTextureLoader.prototype,
        "destroy"
      ).and.callThrough();

      var destroySchema = spyOn(
        MetadataSchemaLoader.prototype,
        "destroy"
      ).and.callThrough();

      var featureMetadataLoader = new GltfFeatureMetadataLoader({
        gltf: gltf,
        extension: extension,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });

      featureMetadataLoader.load();

      return waitForLoaderProcess(featureMetadataLoader, scene).then(function (
        featureMetadataLoader
      ) {
        expect(featureMetadataLoader.featureMetadata).toBeDefined();
        expect(featureMetadataLoader.isDestroyed()).toBe(false);

        featureMetadataLoader.destroy();

        expect(featureMetadataLoader.featureMetadata).not.toBeDefined();
        expect(featureMetadataLoader.isDestroyed()).toBe(true);

        expect(destroyBufferView.calls.count()).toBe(6);
        expect(destroyTexture.calls.count()).toBe(2);
        expect(destroySchema.calls.count()).toBe(1);
      });
    });

    function resolveAfterDestroy(reject) {
      spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
        when.resolve(buffer)
      );
      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(image)
      );
      var deferredPromise = when.defer();
      spyOn(Resource.prototype, "fetchJson").and.returnValue(
        deferredPromise.promise
      );

      var destroyBufferView = spyOn(
        GltfBufferViewLoader.prototype,
        "destroy"
      ).and.callThrough();
      var destroyTexture = spyOn(
        GltfTextureLoader.prototype,
        "destroy"
      ).and.callThrough();
      var destroySchema = spyOn(
        MetadataSchemaLoader.prototype,
        "destroy"
      ).and.callThrough();

      // Load a copy of feature metadata into the cache so that the resource
      // promises resolve even if the feature metadata loader is destroyed
      var featureMetadataLoaderCopy = new GltfFeatureMetadataLoader({
        gltf: gltf,
        extension: extension,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });
      // Also load a copy of the schema into the cache
      var schemaResource = gltfResource.getDerivedResource({
        url: "schema.json",
      });
      var schemaCopy = ResourceCache.loadSchema({
        resource: schemaResource,
      });

      featureMetadataLoaderCopy.load();

      return waitForLoaderProcess(featureMetadataLoaderCopy, scene).then(
        function (featureMetadataLoaderCopy) {
          // Ignore featureMetadataLoaderCopy destroying its buffer views
          destroyBufferView.calls.reset();

          var featureMetadataLoader = new GltfFeatureMetadataLoader({
            gltf: gltfSchemaUri,
            extension: extensionSchemaUri,
            gltfResource: gltfResource,
            baseResource: gltfResource,
            supportedImageFormats: new SupportedImageFormats(),
          });
          expect(featureMetadataLoader.featureMetadata).not.toBeDefined();
          featureMetadataLoader.load();
          featureMetadataLoader.destroy();

          if (reject) {
            deferredPromise.reject(new Error());
          } else {
            deferredPromise.resolve(schemaJson);
          }

          expect(featureMetadataLoader.featureMetadata).not.toBeDefined();
          expect(featureMetadataLoader.isDestroyed()).toBe(true);

          featureMetadataLoaderCopy.destroy();

          expect(destroyBufferView.calls.count()).toBe(6);
          expect(destroyTexture.calls.count()).toBe(2);
          expect(destroySchema.calls.count()).toBe(1);

          ResourceCache.unload(schemaCopy);
        }
      );
    }

    it("handles resolving resources after destroy", function () {
      resolveAfterDestroy(false);
    });

    it("handles rejecting resources after destroy", function () {
      resolveAfterDestroy(true);
    });
  },
  "WebGL"
);
