import {
  clone,
  GltfImageLoader,
  GltfTextureLoader,
  GltfLoaderUtil,
  JobScheduler,
  Resource,
  ResourceCache,
  SupportedImageFormats,
  Texture,
  TextureMinificationFilter,
  when,
} from "../../Source/Cesium.js";
import createScene from "../createScene.js";
import loaderProcess from "../loaderProcess.js";
import waitForLoaderProcess from "../waitForLoaderProcess.js";

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

    var imageNpot = new Image();
    imageNpot.src =
      "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACAQMAAACnuvRZAAAAA3NCSVQICAjb4U/gAAAABlBMVEUAAAD///+l2Z/dAAAAAnRSTlP/AOW3MEoAAAAJcEhZcwAACxIAAAsSAdLdfvwAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzQGstOgAAAAFnRFWHRDcmVhdGlvbiBUaW1lADAxLzA0LzE0Kb6O2wAAAAxJREFUCJljeMDwAAADhAHBgGgjpQAAAABJRU5ErkJggg==";

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

    var gltf = {
      images: [
        {
          uri: "image.png",
        },
      ],
      textures: [
        {
          source: 0,
        },
        {
          source: 0,
          sampler: 0,
        },
        {
          source: 0,
          sampler: 1,
        },
      ],
      materials: [
        {
          emissiveTexture: {
            index: 0,
          },
          occlusionTexture: {
            index: 1,
          },
          normalTexture: {
            index: 2,
          },
        },
      ],
      samplers: [
        {
          magFilter: 9728, // NEAREST
          minFilter: 9984, // NEAREST_MIPMAP_NEAREST
          wrapS: 10497, // REPEAT
          wrapT: 10497, // REPEAT
        },
        {
          magFilter: 9728, // NEAREST
          minFilter: 9728, // NEAREST
          wrapS: 33071, // CLAMP_TO_EDGE
          wrapT: 33071, // CLAMP_TO_EDGE
        },
      ],
    };

    var gltfKtx2BaseResource = new Resource({
      url: "./Data/Images/",
    });

    var gltfKtx2 = {
      images: [
        {
          uri: "image.png",
        },
        {
          uri: "Green4x4_ETC1S.ktx2",
        },
        {
          uri: "Green4x4Mipmap_ETC1S.ktx2",
        },
      ],
      textures: [
        {
          source: 0,
          sampler: 0,
          extensions: {
            KHR_texture_basisu: {
              source: 1,
            },
          },
        },
        {
          source: 0,
          sampler: 1,
          extensions: {
            KHR_texture_basisu: {
              source: 2,
            },
          },
        },
      ],
      materials: [
        {
          emissiveTexture: {
            index: 0,
          },
          occlusionTexture: {
            index: 1,
          },
        },
      ],
      samplers: [
        {
          magFilter: 9728, // NEAREST
          minFilter: 9728, // NEAREST
          wrapS: 10497, // REPEAT
          wrapT: 10497, // REPEAT
        },
        {
          magFilter: 9728, // NEAREST
          minFilter: 9984, // NEAREST_MIPMAP_NEAREST
          wrapS: 33071, // CLAMP_TO_EDGE
          wrapT: 33071, // CLAMP_TO_EDGE
        },
      ],
    };

    var scene;

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

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

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

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

    it("throws if gltf is undefined", function () {
      expect(function () {
        return new GltfTextureLoader({
          resourceCache: ResourceCache,
          gltf: undefined,
          textureInfo: gltf.materials[0].emissiveTexture,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          supportedImageFormats: new SupportedImageFormats(),
        });
      }).toThrowDeveloperError();
    });

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

    it("throws if gltfResource is undefined", function () {
      expect(function () {
        return new GltfTextureLoader({
          resourceCache: ResourceCache,
          gltf: gltf,
          textureInfo: gltf.materials[0].emissiveTexture,
          gltfResource: undefined,
          baseResource: gltfResource,
          supportedImageFormats: new SupportedImageFormats(),
        });
      }).toThrowDeveloperError();
    });

    it("throws if baseResource is undefined", function () {
      expect(function () {
        return new GltfTextureLoader({
          resourceCache: ResourceCache,
          gltf: gltf,
          textureInfo: gltf.materials[0].emissiveTexture,
          gltfResource: gltfResource,
          baseResource: undefined,
          supportedImageFormats: new SupportedImageFormats(),
        });
      }).toThrowDeveloperError();
    });

    it("throws if supportedImageFormats is undefined", function () {
      expect(function () {
        return new GltfTextureLoader({
          resourceCache: ResourceCache,
          gltf: gltf,
          textureInfo: gltf.materials[0].emissiveTexture,
          gltfResource: gltfResource,
          baseResource: gltfResource,
          supportedImageFormats: undefined,
        });
      }).toThrowDeveloperError();
    });

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

      var textureLoader = new GltfTextureLoader({
        resourceCache: ResourceCache,
        gltf: gltf,
        textureInfo: gltf.materials[0].emissiveTexture,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });

      textureLoader.load();

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

    it("loads texture", function () {
      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(image)
      );

      // Simulate JobScheduler not being ready for a few frames
      var processCallsTotal = 3;
      var processCallsCount = 0;
      var jobScheduler = scene.frameState.jobScheduler;
      var originalJobSchedulerExecute = jobScheduler.execute;
      spyOn(JobScheduler.prototype, "execute").and.callFake(function (
        job,
        jobType
      ) {
        if (processCallsCount++ >= processCallsTotal) {
          return originalJobSchedulerExecute.call(jobScheduler, job, jobType);
        }
        return false;
      });

      var textureLoader = new GltfTextureLoader({
        resourceCache: ResourceCache,
        gltf: gltf,
        textureInfo: gltf.materials[0].emissiveTexture,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });

      loaderProcess(textureLoader, scene); // Check that calling process before load doesn't break anything

      textureLoader.load();

      return waitForLoaderProcess(textureLoader, scene).then(function (
        textureLoader
      ) {
        loaderProcess(textureLoader, scene); // Check that calling process after load doesn't break anything
        expect(textureLoader.texture.width).toBe(1);
        expect(textureLoader.texture.height).toBe(1);
      });
    });

    it("creates texture synchronously", function () {
      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(image)
      );

      var textureLoader = new GltfTextureLoader({
        resourceCache: ResourceCache,
        gltf: gltf,
        textureInfo: gltf.materials[0].emissiveTexture,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
        asynchronous: false,
      });

      textureLoader.load();

      return waitForLoaderProcess(textureLoader, scene).then(function (
        textureLoader
      ) {
        loaderProcess(textureLoader, scene); // Check that calling process after load doesn't break anything
        expect(textureLoader.texture.width).toBe(1);
        expect(textureLoader.texture.height).toBe(1);
      });
    });

    it("loads KTX2/Basis texture", function () {
      if (!scene.context.supportsBasis) {
        return;
      }

      var gl = scene.context._gl;
      spyOn(gl, "compressedTexImage2D").and.callThrough();

      var textureLoader = new GltfTextureLoader({
        resourceCache: ResourceCache,
        gltf: gltfKtx2,
        textureInfo: gltf.materials[0].emissiveTexture,
        gltfResource: gltfResource,
        baseResource: gltfKtx2BaseResource,
        supportedImageFormats: new SupportedImageFormats({
          basis: true,
        }),
      });

      textureLoader.load();

      return waitForLoaderProcess(textureLoader, scene).then(function (
        textureLoader
      ) {
        expect(textureLoader.texture.width).toBe(4);
        expect(textureLoader.texture.height).toBe(4);
        expect(gl.compressedTexImage2D.calls.count()).toEqual(1);
      });
    });

    it("loads KTX2/Basis texture with mipmap", function () {
      if (!scene.context.supportsBasis) {
        return;
      }

      var gl = scene.context._gl;
      spyOn(gl, "compressedTexImage2D").and.callThrough();

      var textureLoader = new GltfTextureLoader({
        resourceCache: ResourceCache,
        gltf: gltfKtx2,
        textureInfo: gltf.materials[0].occlusionTexture,
        gltfResource: gltfResource,
        baseResource: gltfKtx2BaseResource,
        supportedImageFormats: new SupportedImageFormats({
          basis: true,
        }),
      });

      textureLoader.load();

      return waitForLoaderProcess(textureLoader, scene).then(function (
        textureLoader
      ) {
        expect(textureLoader.texture.width).toBe(4);
        expect(textureLoader.texture.height).toBe(4);
        expect(gl.compressedTexImage2D.calls.count()).toEqual(3);
      });
    });

    it("loads KTX2/Basis texture with incompatible mipmap sampler", function () {
      if (!scene.context.supportsBasis) {
        return;
      }

      spyOn(GltfLoaderUtil, "createSampler").and.callThrough();

      var gltfKtx2MissingMipmap = clone(gltfKtx2, true);
      gltfKtx2MissingMipmap.samplers[0].minFilter =
        TextureMinificationFilter.NEAREST_MIPMAP_NEAREST;

      var textureLoader = new GltfTextureLoader({
        resourceCache: ResourceCache,
        gltf: gltfKtx2MissingMipmap,
        textureInfo: gltfKtx2MissingMipmap.materials[0].emissiveTexture,
        gltfResource: gltfResource,
        baseResource: gltfKtx2BaseResource,
        supportedImageFormats: new SupportedImageFormats({
          basis: true,
        }),
        asynchronous: false,
      });

      textureLoader.load();

      return waitForLoaderProcess(textureLoader, scene).then(function (
        textureLoader
      ) {
        expect(GltfLoaderUtil.createSampler).toHaveBeenCalledWith({
          gltf: gltfKtx2MissingMipmap,
          textureInfo: gltf.materials[0].emissiveTexture,
          compressedTextureNoMipmap: true,
        });
        expect(textureLoader.texture.sampler.minificationFilter).toBe(
          TextureMinificationFilter.NEAREST
        );
      });
    });

    it("generates mipmap if sampler requires it", function () {
      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(image)
      );

      var generateMipmap = spyOn(
        Texture.prototype,
        "generateMipmap"
      ).and.callThrough();

      var textureLoader = new GltfTextureLoader({
        resourceCache: ResourceCache,
        gltf: gltf,
        textureInfo: gltf.materials[0].occlusionTexture, // This texture has a sampler that require a mipmap
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });

      textureLoader.load();

      return waitForLoaderProcess(textureLoader, scene).then(function (
        textureLoader
      ) {
        expect(textureLoader.texture.width).toBe(1);
        expect(textureLoader.texture.height).toBe(1);
        expect(generateMipmap).toHaveBeenCalled();
      });
    });

    it("generates power-of-two texture if sampler requires it", function () {
      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(imageNpot)
      );

      var textureLoader = new GltfTextureLoader({
        resourceCache: ResourceCache,
        gltf: gltf,
        textureInfo: gltf.materials[0].occlusionTexture, // This texture has a sampler that require power-of-two texture
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });

      textureLoader.load();

      return waitForLoaderProcess(textureLoader, scene).then(function (
        textureLoader
      ) {
        expect(textureLoader.texture.width).toBe(4);
        expect(textureLoader.texture.height).toBe(2);
      });
    });

    it("does not generate power-of-two texture if sampler does not require it", function () {
      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(imageNpot)
      );

      var textureLoader = new GltfTextureLoader({
        resourceCache: ResourceCache,
        gltf: gltf,
        textureInfo: gltf.materials[0].normalTexture, // This texture has a sampler that does not require power-of-two texture
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });

      textureLoader.load();

      return waitForLoaderProcess(textureLoader, scene).then(function (
        textureLoader
      ) {
        expect(textureLoader.texture.width).toBe(3);
        expect(textureLoader.texture.height).toBe(2);
      });
    });

    it("destroys texture loader", function () {
      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        when.resolve(image)
      );

      var unloadImage = spyOn(
        GltfImageLoader.prototype,
        "unload"
      ).and.callThrough();

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

      var textureLoader = new GltfTextureLoader({
        resourceCache: ResourceCache,
        gltf: gltf,
        textureInfo: gltf.materials[0].emissiveTexture,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });

      expect(textureLoader.texture).not.toBeDefined();

      textureLoader.load();

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

        textureLoader.destroy();

        expect(textureLoader.texture).not.toBeDefined();
        expect(textureLoader.isDestroyed()).toBe(true);
        expect(unloadImage).toHaveBeenCalled();
        expect(destroyTexture).toHaveBeenCalled();
      });
    });

    function resolveImageAfterDestroy(reject) {
      var deferredPromise = when.defer();
      spyOn(Resource.prototype, "fetchImage").and.returnValue(
        deferredPromise.promise
      );

      // Load a copy of the image into the cache so that the image
      // promise resolves even if the texture loader is destroyed
      var imageLoaderCopy = ResourceCache.loadImage({
        gltf: gltf,
        imageId: 0,
        gltfResource: gltfResource,
        baseResource: gltfResource,
      });

      var textureLoader = new GltfTextureLoader({
        resourceCache: ResourceCache,
        gltf: gltf,
        textureInfo: gltf.materials[0].emissiveTexture,
        gltfResource: gltfResource,
        baseResource: gltfResource,
        supportedImageFormats: new SupportedImageFormats(),
      });

      expect(textureLoader.texture).not.toBeDefined();

      textureLoader.load();
      textureLoader.destroy();

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

      expect(textureLoader.texture).not.toBeDefined();
      expect(textureLoader.isDestroyed()).toBe(true);

      ResourceCache.unload(imageLoaderCopy);
    }

    it("handles resolving image after destroy", function () {
      resolveImageAfterDestroy(false);
    });

    it("handles rejecting image after destroy", function () {
      resolveImageAfterDestroy(true);
    });
  },
  "WebGL"
);
