import {
  BufferLoader,
  clone,
  CompressedTextureBuffer,
  GltfBufferViewLoader,
  GltfImageLoader,
  FeatureDetection,
  Resource,
  ResourceCache,
  when,
} from "../../Source/Cesium.js";
import createContext from "../createContext.js";
import dataUriToBuffer from "../dataUriToBuffer.js";
import pollToPromise from "../pollToPromise.js";

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

  var pngBuffer = dataUriToBuffer(
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII="
  );
  var jpgBuffer = dataUriToBuffer(
    "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wAALCAABAAEBAREA/8QAJgABAAAAAAAAAAAAAAAAAAAAAxABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAAPwBH/9k"
  );

  var webpBuffer = dataUriToBuffer(
    "data:image/webp;base64,UklGRiIAAABXRUJQVlA4IBYAAAAwAQCdASoBAAEADsD+JaQAA3AAAAAA"
  );

  var gifBuffer = dataUriToBuffer(
    "data:image/gif;base64,R0lGODdhBAAEAIAAAP///////ywAAAAABAAEAAACBISPCQUAOw=="
  );

  var ktx2BasisBuffer;
  var ktx2BasisMipmapBuffer;

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

  var gltf = {
    buffers: [
      {
        uri: "external.bin",
        byteLength: 0, // updated in getGltf
      },
    ],
    bufferViews: [
      {
        buffer: 0,
        byteOffset: 0,
        byteLength: 0, // updated in getGltf
      },
    ],
    images: [
      {
        mimeType: "image/png",
        bufferView: 0,
      },
      {
        uri: "image.png",
      },
      {
        mimeType: "image/ktx2",
        bufferView: 0,
      },
      {
        uri: "image.ktx2",
      },
    ],
  };

  var context;

  function getGltf(imageBuffer) {
    var clonedGltf = clone(gltf, true);
    clonedGltf.buffers[0].byteLength = imageBuffer.byteLength;
    clonedGltf.bufferViews[0].byteLength = imageBuffer.byteLength;
    return clonedGltf;
  }

  beforeAll(function () {
    context = createContext();
    var ktx2BasisBufferPromise = Resource.fetchArrayBuffer({
      url: "./Data/Images/Green4x4_ETC1S.ktx2",
    }).then(function (arrayBuffer) {
      ktx2BasisBuffer = new Uint8Array(arrayBuffer);
    });
    var ktx2BasisMipmapBufferPromise = Resource.fetchArrayBuffer({
      url: "./Data/Images/Green4x4Mipmap_ETC1S.ktx2",
    }).then(function (arrayBuffer) {
      ktx2BasisMipmapBuffer = new Uint8Array(arrayBuffer);
    });

    return when.all([ktx2BasisBufferPromise, ktx2BasisMipmapBufferPromise]);
  });

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

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

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

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

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

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

  it("throws if baseResource is undefined", function () {
    expect(function () {
      return new GltfImageLoader({
        resourceCache: ResourceCache,
        gltf: gltf,
        imageId: 0,
        gltfResource: gltfResource,
        baseResource: 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)
    );

    var imageLoader = new GltfImageLoader({
      resourceCache: ResourceCache,
      gltf: getGltf(pngBuffer),
      imageId: 0,
      gltfResource: gltfResource,
      baseResource: gltfResource,
    });

    imageLoader.load();

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

  it("rejects promise if image format is not recognized", function () {
    spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
      when.resolve(gifBuffer)
    );

    var imageLoader = new GltfImageLoader({
      resourceCache: ResourceCache,
      gltf: getGltf(gifBuffer),
      imageId: 0,
      gltfResource: gltfResource,
      baseResource: gltfResource,
    });

    imageLoader.load();

    return imageLoader.promise
      .then(function (imageLoader) {
        fail();
      })
      .otherwise(function (runtimeError) {
        expect(runtimeError.message).toBe(
          "Failed to load embedded image\nImage format is not recognized"
        );
      });
  });

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

    var imageLoader = new GltfImageLoader({
      resourceCache: ResourceCache,
      gltf: getGltf(pngBuffer),
      imageId: 1,
      gltfResource: gltfResource,
      baseResource: gltfResource,
    });

    imageLoader.load();

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

  function loadsFromBufferView(imageBuffer) {
    spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
      when.resolve(imageBuffer)
    );

    var imageLoader = new GltfImageLoader({
      resourceCache: ResourceCache,
      gltf: getGltf(imageBuffer),
      imageId: 0,
      gltfResource: gltfResource,
      baseResource: gltfResource,
    });

    imageLoader.load();

    return imageLoader.promise.then(function (imageLoader) {
      expect(imageLoader.image.width).toBe(1);
      expect(imageLoader.image.height).toBe(1);
    });
  }

  it("loads PNG from buffer view", function () {
    return loadsFromBufferView(pngBuffer);
  });

  it("loads JPEG from buffer view", function () {
    return loadsFromBufferView(jpgBuffer);
  });

  it("loads WebP from buffer view", function () {
    return pollToPromise(function () {
      FeatureDetection.supportsWebP.initialize();
      return FeatureDetection.supportsWebP.initialized;
    }).then(function () {
      if (!FeatureDetection.supportsWebP()) {
        return;
      }
      return loadsFromBufferView(webpBuffer);
    });
  });

  it("loads KTX2/Basis from buffer view", function () {
    if (!context.supportsBasis) {
      return;
    }

    spyOn(BufferLoader, "_fetchArrayBuffer").and.returnValue(
      when.resolve(ktx2BasisBuffer)
    );

    var imageLoader = new GltfImageLoader({
      resourceCache: ResourceCache,
      gltf: getGltf(ktx2BasisBuffer),
      imageId: 2,
      gltfResource: gltfResource,
      baseResource: gltfResource,
    });

    imageLoader.load();

    return imageLoader.promise.then(function (imageLoader) {
      expect(imageLoader.image instanceof CompressedTextureBuffer).toBe(true);
      expect(imageLoader.image.width).toBe(4);
      expect(imageLoader.image.height).toBe(4);
      expect(imageLoader.mipLevels).toBeUndefined();
    });
  });

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

    spyOn(BufferLoader, "_fetchArrayBuffer").and.returnValue(
      when.resolve(ktx2BasisMipmapBuffer)
    );

    var imageLoader = new GltfImageLoader({
      resourceCache: ResourceCache,
      gltf: getGltf(ktx2BasisMipmapBuffer),
      imageId: 2,
      gltfResource: gltfResource,
      baseResource: gltfResource,
    });

    imageLoader.load();

    return imageLoader.promise.then(function (imageLoader) {
      expect(imageLoader.image instanceof CompressedTextureBuffer).toBe(true);
      expect(imageLoader.image.width).toBe(4);
      expect(imageLoader.image.height).toBe(4);
      expect(imageLoader.mipLevels.length).toBe(2);
    });
  });

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

    var imageLoader = new GltfImageLoader({
      resourceCache: ResourceCache,
      gltf: clone(gltf, true),
      imageId: 1,
      gltfResource: gltfResource,
      baseResource: gltfResource,
    });

    imageLoader.load();

    return imageLoader.promise.then(function (imageLoader) {
      expect(imageLoader.image.width).toBe(1);
      expect(imageLoader.image.height).toBe(1);
    });
  });

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

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

    var clonedGltf = clone(gltf, true);
    clonedGltf.images[3].uri = "Green4x4_ETC1S.ktx2";

    var imageLoader = new GltfImageLoader({
      resourceCache: ResourceCache,
      gltf: clonedGltf,
      imageId: 3,
      gltfResource: gltfResource,
      baseResource: baseResource,
    });

    imageLoader.load();

    return imageLoader.promise.then(function (imageLoader) {
      expect(imageLoader.image instanceof CompressedTextureBuffer).toBe(true);
      expect(imageLoader.image.width).toBe(4);
      expect(imageLoader.image.height).toBe(4);
      expect(imageLoader.mipLevels).toBeUndefined();
    });
  });

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

    var unloadBufferView = spyOn(
      GltfBufferViewLoader.prototype,
      "unload"
    ).and.callThrough();

    var imageLoader = new GltfImageLoader({
      resourceCache: ResourceCache,
      gltf: getGltf(pngBuffer),
      imageId: 0,
      gltfResource: gltfResource,
      baseResource: gltfResource,
    });
    expect(imageLoader.image).not.toBeDefined();

    imageLoader.load();

    return imageLoader.promise.then(function (imageLoader) {
      expect(imageLoader.image).toBeDefined();
      expect(imageLoader.isDestroyed()).toBe(false);

      imageLoader.destroy();

      expect(imageLoader.image).not.toBeDefined();
      expect(imageLoader.isDestroyed()).toBe(true);
      expect(unloadBufferView).toHaveBeenCalled();
    });
  });

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

    // Load a copy of the buffer view into the cache so that the buffer view
    // promise resolves even if the image loader is destroyed
    var bufferViewLoaderCopy = ResourceCache.loadBufferView({
      gltf: getGltf(pngBuffer),
      bufferViewId: 0,
      gltfResource: gltfResource,
      baseResource: gltfResource,
    });

    var imageLoader = new GltfImageLoader({
      resourceCache: ResourceCache,
      gltf: getGltf(pngBuffer),
      imageId: 0,
      gltfResource: gltfResource,
      baseResource: gltfResource,
    });

    expect(imageLoader.image).not.toBeDefined();

    imageLoader.load();
    imageLoader.destroy();

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

    expect(imageLoader.image).not.toBeDefined();
    expect(imageLoader.isDestroyed()).toBe(true);

    ResourceCache.unload(bufferViewLoaderCopy);
  }

  it("handles resolving buffer view after destroy", function () {
    resolveBufferViewAfterDestroy(false);
  });

  it("handles rejecting buffer view after destroy", function () {
    resolveBufferViewAfterDestroy(true);
  });

  function resolveImageFromTypedArrayAfterDestroy(reject) {
    spyOn(Resource.prototype, "fetchArrayBuffer").and.returnValue(
      when.resolve(pngBuffer)
    );

    var deferredPromise = when.defer();
    spyOn(GltfImageLoader, "_loadImageFromTypedArray").and.returnValue(
      deferredPromise.promise
    );

    // Load a copy of the buffer view into the cache so that the buffer view
    // promise resolves even if the image loader is destroyed
    var bufferViewLoaderCopy = ResourceCache.loadBufferView({
      gltf: getGltf(pngBuffer),
      bufferViewId: 0,
      gltfResource: gltfResource,
      baseResource: gltfResource,
    });

    var imageLoader = new GltfImageLoader({
      resourceCache: ResourceCache,
      gltf: getGltf(pngBuffer),
      imageId: 0,
      gltfResource: gltfResource,
      baseResource: gltfResource,
    });

    expect(imageLoader.image).not.toBeDefined();

    imageLoader.load();
    imageLoader.destroy();

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

    expect(imageLoader.image).not.toBeDefined();
    expect(imageLoader.isDestroyed()).toBe(true);

    ResourceCache.unload(bufferViewLoaderCopy);
  }

  it("handles resolving image from typed array after destroy", function () {
    resolveImageFromTypedArrayAfterDestroy(false);
  });

  it("handles rejecting image from typed array after destroy", function () {
    resolveImageFromTypedArrayAfterDestroy(true);
  });

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

    var imageLoader = new GltfImageLoader({
      resourceCache: ResourceCache,
      gltf: getGltf(pngBuffer),
      imageId: 1,
      gltfResource: gltfResource,
      baseResource: gltfResource,
    });

    expect(imageLoader.image).not.toBeDefined();

    imageLoader.load();
    imageLoader.destroy();

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

    expect(imageLoader.image).not.toBeDefined();
    expect(imageLoader.isDestroyed()).toBe(true);
  }

  it("handles resolving uri after destroy", function () {
    resolveUriAfterDestroy(false);
  });

  it("handles rejecting uri after destroy", function () {
    resolveUriAfterDestroy(true);
  });
});