import { BoundingSphere } from "../../Source/Cesium.js";
import { Cartesian3 } from "../../Source/Cesium.js";
import { Clock } from "../../Source/Cesium.js";
import { ClockStep } from "../../Source/Cesium.js";
import { defaultValue } from "../../Source/Cesium.js";
import { defined } from "../../Source/Cesium.js";
import { HeadingPitchRange } from "../../Source/Cesium.js";
import { HeadingPitchRoll } from "../../Source/Cesium.js";
import { JulianDate } from "../../Source/Cesium.js";
import { Matrix4 } from "../../Source/Cesium.js";
import { Resource } from "../../Source/Cesium.js";
import { TimeIntervalCollection } from "../../Source/Cesium.js";
import { Transforms } from "../../Source/Cesium.js";
import { Cesium3DTileStyle } from "../../Source/Cesium.js";
import { ClippingPlane } from "../../Source/Cesium.js";
import { ClippingPlaneCollection } from "../../Source/Cesium.js";
import { DracoLoader } from "../../Source/Cesium.js";
import { PointCloudEyeDomeLighting } from "../../Source/Cesium.js";
import { ShadowMode } from "../../Source/Cesium.js";
import { TimeDynamicPointCloud } from "../../Source/Cesium.js";
import createCanvas from "../createCanvas.js";
import createScene from "../createScene.js";
import pollToPromise from "../pollToPromise.js";
import { when } from "../../Source/Cesium.js";

describe(
  "Scene/TimeDynamicPointCloud",
  function () {
    var scene;

    var center = new Cartesian3(
      1215012.8828876738,
      -4736313.051199594,
      4081605.22126042
    );

    var clock = new Clock({
      clockStep: ClockStep.TICK_DEPENDENT,
      shouldAnimate: true,
    });

    var dates = [
      JulianDate.fromIso8601("2018-07-19T15:18:00Z"),
      JulianDate.fromIso8601("2018-07-19T15:18:00.5Z"),
      JulianDate.fromIso8601("2018-07-19T15:18:01Z"),
      JulianDate.fromIso8601("2018-07-19T15:18:01.5Z"),
      JulianDate.fromIso8601("2018-07-19T15:18:02Z"),
      JulianDate.fromIso8601("2018-07-19T15:18:02.5Z"),
    ];

    var transforms = [
      Matrix4.fromColumnMajorArray([
        0.968635634376879,
        0.24848542777253735,
        0,
        0,
        -0.15986460794399626,
        0.6231776137472074,
        0.7655670897127491,
        0,
        0.190232265775849,
        -0.7415555636019701,
        0.6433560687121489,
        0,
        1215012.8828876738,
        -4736313.051199594,
        4081605.22126042,
        1,
      ]),
      Matrix4.fromColumnMajorArray([
        0.968634888916237,
        0.24848833367832227,
        0,
        0,
        -0.1598664774761181,
        0.6231771341505793,
        0.7655670897127493,
        0,
        0.19023449044168372,
        -0.7415549929018358,
        0.6433560687121489,
        0,
        1215027.0918213597,
        -4736309.406139632,
        4081605.22126042,
        1,
      ]),
      Matrix4.fromColumnMajorArray([
        0.9686341434468771,
        0.24849123958187078,
        0,
        0,
        -0.1598683470068011,
        0.6231766545483426,
        0.7655670897127493,
        0,
        0.19023671510580634,
        -0.7415544221950274,
        0.6433560687121489,
        0,
        1215041.3007441103,
        -4736305.761037043,
        4081605.22126042,
        1,
      ]),
      Matrix4.fromColumnMajorArray([
        0.9686333979687994,
        0.24849414548318288,
        0,
        0,
        -0.15987021653604533,
        0.6231761749404972,
        0.7655670897127491,
        0,
        0.19023893976821685,
        -0.7415538514815451,
        0.6433560687121489,
        0,
        1215055.5096559257,
        -4736302.115891827,
        4081605.22126042,
        1,
      ]),
      Matrix4.fromColumnMajorArray([
        0.9686326524820043,
        0.2484970513822586,
        0,
        0,
        -0.15987208606385075,
        0.6231756953270434,
        0.7655670897127492,
        0,
        0.19024116442891523,
        -0.7415532807613887,
        0.6433560687121489,
        0,
        1215069.7185568055,
        -4736298.470703985,
        4081605.22126042,
        1,
      ]),
    ];

    function createIntervals(useTransforms, useDraco) {
      var folderName;
      if (useTransforms) {
        folderName =
          "Data/Cesium3DTiles/PointCloud/PointCloudTimeDynamicWithTransform/";
      } else if (useDraco) {
        folderName =
          "Data/Cesium3DTiles/PointCloud/PointCloudTimeDynamicDraco/";
      } else {
        folderName = "Data/Cesium3DTiles/PointCloud/PointCloudTimeDynamic/";
      }

      var uris = [];
      for (var i = 0; i < 5; ++i) {
        uris.push(folderName + i + ".pnts");
      }

      function dataCallback(interval, index) {
        return {
          uri: uris[index],
          transform: useTransforms ? transforms[index] : undefined,
        };
      }

      return TimeIntervalCollection.fromJulianDateArray({
        julianDates: dates,
        dataCallback: dataCallback,
      });
    }

    function createTimeDynamicPointCloud(options) {
      options = defaultValue(options, {});
      var useTransforms = defaultValue(options.useTransforms, false);
      var useDraco = defaultValue(options.useDraco, false);
      options.intervals = createIntervals(useTransforms, useDraco);
      options.clock = clock;
      if (!defined(options.style)) {
        options.style = new Cesium3DTileStyle({
          color: 'color("red")',
          pointSize: 10,
        });
      }
      return scene.primitives.add(new TimeDynamicPointCloud(options));
    }

    function zoomTo(center) {
      scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 5.0));
    }

    function loadFrame(pointCloud, index) {
      index = defaultValue(index, 0);
      goToFrame(index);
      return pollToPromise(function () {
        scene.renderForSpecs();
        var frame = pointCloud._frames[index];
        var ready = defined(frame) && frame.ready;
        if (ready) {
          scene.renderForSpecs();
        }
        return ready;
      });
    }

    function getLoadFrameFunction(pointCloud, index) {
      return function () {
        return loadFrame(pointCloud, index);
      };
    }

    function loadFrames(pointCloud, indexes) {
      var length = indexes.length;
      var promise = getLoadFrameFunction(pointCloud, indexes[0])();
      for (var i = 1; i < length; ++i) {
        promise = promise.then(getLoadFrameFunction(pointCloud, indexes[i]));
      }
      return promise.then(function () {
        goToFrame(indexes[0]);
      });
    }

    function loadAllFrames(pointCloud) {
      return loadFrames(pointCloud, [0, 1, 2, 3, 4]);
    }

    function goToFrame(index) {
      clock.currentTime = dates[index];
      clock.multiplier = 0.0;
    }

    function initializeScene() {
      scene.morphTo3D(0.0);
      zoomTo(center);
      goToFrame(0);
    }

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

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

    beforeEach(function () {
      initializeScene();
    });

    afterEach(function () {
      scene.primitives.removeAll();
    });

    it("throws if options.clock is undefined", function () {
      var intervals = createIntervals();
      expect(function () {
        return new TimeDynamicPointCloud({
          intervals: intervals,
        });
      }).toThrowDeveloperError();
    });

    it("throws if options.intervals is undefined", function () {
      expect(function () {
        return new TimeDynamicPointCloud({
          clock: clock,
        });
      }).toThrowDeveloperError();
    });

    it("renders in 3D", function () {
      var pointCloud = createTimeDynamicPointCloud();
      return loadFrame(pointCloud).then(function () {
        scene.morphTo3D(0.0);
        expect(scene).toRender([255, 0, 0, 255]);
        goToFrame(1);
        expect(scene).toRender([255, 0, 0, 255]);
        scene.camera.moveRight(10.0);
        expect(scene).toRender([0, 0, 0, 255]);
      });
    });

    it("renders in 2D", function () {
      var pointCloud = createTimeDynamicPointCloud();
      return loadFrame(pointCloud).then(function () {
        scene.morphTo2D(0.0);
        expect(scene).toRender([255, 0, 0, 255]);
        goToFrame(1);
        expect(scene).toRender([255, 0, 0, 255]);
        scene.camera.moveRight(10.0);
        expect(scene).toRender([0, 0, 0, 255]);
      });
    });

    it("renders in CV", function () {
      var pointCloud = createTimeDynamicPointCloud();
      return loadFrame(pointCloud).then(function () {
        scene.morphToColumbusView(0.0);
        expect(scene).toRender([255, 0, 0, 255]);
        goToFrame(1);
        expect(scene).toRender([255, 0, 0, 255]);
        scene.camera.moveRight(10.0);
        expect(scene).toRender([0, 0, 0, 255]);
      });
    });

    it("gets bounding sphere of the rendered frame", function () {
      var pointCloud = createTimeDynamicPointCloud({
        useTransforms: true,
      });
      expect(pointCloud.boundingSphere).toBeUndefined(); // Undefined until a frame is rendered
      return loadAllFrames(pointCloud).then(function () {
        var boundingSphereFrame0 = pointCloud.boundingSphere;
        expect(boundingSphereFrame0).toBeDefined();
        goToFrame(1);
        scene.renderForSpecs();
        var boundingSphereFrame1 = pointCloud.boundingSphere;
        expect(boundingSphereFrame1).toBeDefined();
        expect(
          BoundingSphere.equals(boundingSphereFrame0, boundingSphereFrame1)
        ).toBe(false);
      });
    });

    it("resolves ready promise", function () {
      var pointCloud = createTimeDynamicPointCloud();
      return loadFrame(pointCloud).then(function () {
        return pointCloud.readyPromise.then(function (pointCloud) {
          expect(pointCloud.boundingSphere).toBeDefined();
        });
      });
    });

    it("sets show", function () {
      var pointCloud = createTimeDynamicPointCloud();

      return loadFrame(pointCloud).then(function () {
        expect(scene).toRender([255, 0, 0, 255]);
        pointCloud.show = false;
        expect(scene).toRender([0, 0, 0, 255]);
      });
    });

    it("sets model matrix", function () {
      var translation = new Cartesian3(10000, 2000, 100);
      var modelMatrix = Matrix4.fromTranslation(translation);
      var newCenter = Cartesian3.add(center, translation, new Cartesian3());
      var pointCloud = createTimeDynamicPointCloud({
        modelMatrix: modelMatrix,
      });
      return loadFrame(pointCloud).then(function () {
        expect(scene).toRender([0, 0, 0, 255]); // Out of view
        zoomTo(newCenter);
        expect(scene).toRender([255, 0, 0, 255]);
        pointCloud.modelMatrix = Matrix4.IDENTITY;
        expect(scene).toRender([0, 0, 0, 255]); // Out of view
        zoomTo(center);
        expect(scene).toRender([255, 0, 0, 255]);
      });
    });

    it("sets shadows", function () {
      var pointCloud = createTimeDynamicPointCloud({
        shadows: ShadowMode.DISABLED,
      });
      return loadFrame(pointCloud).then(function () {
        scene.renderForSpecs();
        expect(scene.frameState.commandList[0].castShadows).toBe(false);
        expect(scene.frameState.commandList[0].receiveShadows).toBe(false);
        pointCloud.shadows = ShadowMode.ENABLED;
        scene.renderForSpecs();
        expect(scene.frameState.commandList[0].castShadows).toBe(true);
        expect(scene.frameState.commandList[0].receiveShadows).toBe(true);
      });
    });

    it("honors maximumMemoryUsage by unloading all frames not currently being loaded or rendered", function () {
      var pointCloud = createTimeDynamicPointCloud();
      return loadAllFrames(pointCloud).then(function () {
        var singleFrameMemoryUsage = 33000;
        var frames = pointCloud._frames;
        var framesLength = frames.length;
        expect(pointCloud.totalMemoryUsageInBytes).toBe(
          singleFrameMemoryUsage * framesLength
        );
        pointCloud.maximumMemoryUsage = 0;

        // Expect all frames except the current frame to be undefined
        scene.renderForSpecs();
        expect(pointCloud.totalMemoryUsageInBytes).toBe(singleFrameMemoryUsage);
        expect(frames[0].ready).toBe(true);
        for (var i = 1; i < length; ++i) {
          expect(frames[i]).toBeUndefined();
        }

        // The loading frame and last rendered frame are not unloaded
        goToFrame(1);
        scene.renderForSpecs();
        expect(pointCloud.totalMemoryUsageInBytes).toBe(singleFrameMemoryUsage);
        expect(frames[0].ready).toBe(true);
        expect(frames[1].ready).toBe(false);

        // The loaded frame is the only one loaded
        return loadFrame(pointCloud, 1).then(function () {
          expect(pointCloud.totalMemoryUsageInBytes).toBe(
            singleFrameMemoryUsage
          );
          expect(frames[0]).toBeUndefined();
          expect(frames[1].ready).toBe(true);
        });
      });
    });

    it("enables attenuation and eye dome lighting", function () {
      var oldScene = scene;
      scene = createScene({
        canvas: createCanvas(100, 100),
      });
      initializeScene();

      var pointCloud = createTimeDynamicPointCloud({
        shading: {
          attenuation: true,
          eyeDomeLighting: false,
        },
        style: new Cesium3DTileStyle(),
      });

      return loadFrame(pointCloud).then(function () {
        var attenuationPixelCount;
        expect(scene).toRenderPixelCountAndCall(function (pixelCount) {
          attenuationPixelCount = pixelCount;
        });

        // Disable attenuation and expect less pixels to be drawn
        pointCloud.shading.attenuation = false;
        expect(scene).toRenderPixelCountAndCall(function (pixelCount) {
          expect(pixelCount).toBeLessThan(attenuationPixelCount);
        });

        scene.destroyForSpecs();
        scene = oldScene;
      });
    });

    it("enabled eye dome lighting", function () {
      if (!PointCloudEyeDomeLighting.isSupported(scene.frameState.context)) {
        return;
      }

      var pointCloud = createTimeDynamicPointCloud();
      return loadFrame(pointCloud).then(function () {
        expect(scene.frameState.commandList.length).toBe(1);
        pointCloud.shading.attenuation = true;
        pointCloud.shading.eyeDomeLighting = true;
        scene.renderForSpecs();
        expect(scene.frameState.commandList.length).toBe(3); // Added 2 EDL commands
      });
    });

    it("sets style", function () {
      var pointCloud = createTimeDynamicPointCloud({
        style: new Cesium3DTileStyle({
          color: 'color("blue")',
          pointSize: 10,
        }),
      });
      return loadAllFrames(pointCloud).then(function () {
        expect(scene).toRender([0, 0, 255, 255]);
        pointCloud.style = new Cesium3DTileStyle({
          color: 'color("lime")',
          pointSize: 10,
        });
        expect(scene).toRender([0, 255, 0, 255]);
        goToFrame(1); // Also check that the style is updated for the next frame
        expect(scene).toRender([0, 255, 0, 255]);
      });
    });

    it("make style dirty", function () {
      var pointCloud = createTimeDynamicPointCloud({
        style: new Cesium3DTileStyle({
          color: 'color("blue")',
          pointSize: 10,
        }),
      });
      return loadAllFrames(pointCloud).then(function () {
        expect(scene).toRender([0, 0, 255, 255]);
        pointCloud.style.color = 'color("lime")';
        pointCloud.makeStyleDirty();
        expect(scene).toRender([0, 255, 0, 255]);
        goToFrame(1); // Also check that the style is updated for the next frame
        expect(scene).toRender([0, 255, 0, 255]);
      });
    });

    it("sets clipping planes", function () {
      var modelMatrix = new Transforms.headingPitchRollToFixedFrame(
        center,
        new HeadingPitchRoll(0, 0, 0)
      );
      var clippingPlanesX = new ClippingPlaneCollection({
        modelMatrix: modelMatrix,
        planes: [new ClippingPlane(Cartesian3.UNIT_X, 0.0)],
      });
      var clippingPlanesY = new ClippingPlaneCollection({
        modelMatrix: modelMatrix,
        planes: [new ClippingPlane(Cartesian3.UNIT_Y, 0.0)],
      });

      var pointCloud = createTimeDynamicPointCloud({
        clippingPlanes: clippingPlanesX,
      });
      return loadAllFrames(pointCloud).then(function () {
        // Go to unclipped area (right half)
        scene.camera.moveRight(0.1);
        goToFrame(0);
        expect(scene).toRender([255, 0, 0, 255]);
        goToFrame(1);
        expect(scene).toRender([255, 0, 0, 255]);

        // Go to clipped area (left half)
        scene.camera.moveLeft(0.2);
        goToFrame(0);
        expect(scene).toRender([0, 0, 0, 255]);
        goToFrame(1);
        expect(scene).toRender([0, 0, 0, 255]);

        // Same area no longer clipped. Responds to clipping planes updates.
        pointCloud.clippingPlanes.enabled = false;
        goToFrame(0);
        expect(scene).toRender([255, 0, 0, 255]);
        goToFrame(1);
        expect(scene).toRender([255, 0, 0, 255]);

        // Sets a new clipping plane that uses a different axis
        // Go to unclipped area (bottom left)
        pointCloud.clippingPlanes = clippingPlanesY;
        scene.camera.moveRight(0.2);
        scene.camera.moveUp(0.1);
        goToFrame(0);
        expect(scene).toRender([255, 0, 0, 255]);
        goToFrame(1);
        expect(scene).toRender([255, 0, 0, 255]);

        // Go to clipped area (bottom right)
        scene.camera.moveDown(0.2);
        goToFrame(0);
        expect(scene).toRender([0, 0, 0, 255]);
        goToFrame(1);
        expect(scene).toRender([0, 0, 0, 255]);
      });
    });

    it("works with frame transforms", function () {
      var pointCloud = createTimeDynamicPointCloud({
        useTransforms: true,
      });
      return loadAllFrames(pointCloud).then(function () {
        goToFrame(0);
        expect(scene).toRender([255, 0, 0, 255]);
        // The transform shifted the point cloud to the right
        goToFrame(1);
        expect(scene).toRender([0, 0, 0, 255]);
        scene.camera.moveRight(10.0);
        expect(scene).toRender([255, 0, 0, 255]);
      });
    });

    it("does not render during morph", function () {
      var pointCloud = createTimeDynamicPointCloud();
      return loadFrame(pointCloud).then(function () {
        scene.renderForSpecs();
        expect(scene.frameState.commandList.length).toBeGreaterThan(0);
        scene.morphToColumbusView(1.0);
        scene.renderForSpecs();
        expect(scene.frameState.commandList.length).toBe(0);
      });
    });

    it("renders frames using Draco compression", function () {
      var pointCloud = createTimeDynamicPointCloud({
        useDraco: true,
      });
      return loadFrame(pointCloud).then(function () {
        expect(scene).toRender([255, 0, 0, 255]);
      });
    });

    it("picks", function () {
      var pointCloud = createTimeDynamicPointCloud();
      return loadFrame(pointCloud).then(function () {
        pointCloud.show = false;
        expect(scene).toPickPrimitive(undefined);
        pointCloud.show = true;
        expect(scene).toPickPrimitive(pointCloud);
      });
    });

    it("does not render if current time is out of range", function () {
      var pointCloud = createTimeDynamicPointCloud();
      return loadFrame(pointCloud).then(function () {
        // Before
        clock.currentTime = JulianDate.addSeconds(
          dates[0],
          -10.0,
          new JulianDate()
        );
        scene.renderForSpecs();
        expect(scene.frameState.commandList.length).toBe(0);
        // During
        clock.currentTime = dates[0];
        scene.renderForSpecs();
        expect(scene.frameState.commandList.length).toBe(1);
        // After
        clock.currentTime = JulianDate.addSeconds(
          dates[5],
          10.0,
          new JulianDate()
        );
        scene.renderForSpecs();
        expect(scene.frameState.commandList.length).toBe(0);
      });
    });

    it("prefetches different frame when clock multiplier changes", function () {
      var pointCloud = createTimeDynamicPointCloud();
      spyOn(pointCloud, "_getAverageLoadTime").and.returnValue(0.5);
      return loadFrame(pointCloud).then(function () {
        expect(pointCloud._frames[1]).toBeUndefined();
        clock.multiplier = 1.0;
        scene.renderForSpecs();
        expect(pointCloud._frames[1]).toBeDefined();
        clock.multiplier = 4.0;
        scene.renderForSpecs();
        expect(pointCloud._frames[2]).toBeUndefined();
        expect(pointCloud._frames[3]).toBeUndefined();
        expect(pointCloud._frames[4]).toBeDefined();
      });
    });

    it("renders last rendered frame while new frame loads", function () {
      var pointCloud = createTimeDynamicPointCloud();
      return loadFrame(pointCloud).then(function () {
        var commandList = scene.frameState.commandList;
        var firstFrameCommand = commandList[0];
        goToFrame(4);
        return pollToPromise(function () {
          scene.renderForSpecs();
          var frame = pointCloud._frames[4];
          var ready = defined(frame) && frame.ready;
          if (!ready) {
            expect(commandList[0]).toBe(firstFrameCommand);
          }
          return ready;
        }).then(function () {
          scene.renderForSpecs();
          expect(commandList[0]).toBeDefined();
          expect(commandList[0]).not.toBe(firstFrameCommand);
        });
      });
    });

    it("skips frames based on average load time and clock multiplier", function () {
      var pointCloud = createTimeDynamicPointCloud();
      spyOn(pointCloud, "_getAverageLoadTime").and.returnValue(2.0);
      scene.renderForSpecs(); // at 0.0 seconds - loads frame 0
      clock.multiplier = 0.6;
      scene.renderForSpecs(); // at 0.0 seconds - preloads frame 2 at 1.2 seconds
      clock.tick();
      scene.renderForSpecs(); // at 0.6 seconds
      clock.tick();
      scene.renderForSpecs(); // at 1.2 seconds - preloads frame 4 at 2.4 seconds
      clock.tick();
      scene.renderForSpecs(); // at 1.8 seconds
      clock.tick();
      scene.renderForSpecs(); // at 2.4 seconds

      var frames = pointCloud._frames;
      expect(frames[0]).toBeDefined();
      expect(frames[1]).toBeUndefined();
      expect(frames[2]).toBeDefined();
      expect(frames[3]).toBeUndefined();
      expect(frames[4]).toBeDefined();
    });

    it("does not skip frames if clock multiplier is sufficiently slow", function () {
      var pointCloud = createTimeDynamicPointCloud();
      spyOn(pointCloud, "_getAverageLoadTime").and.returnValue(0.5);
      scene.renderForSpecs(); // at 0.0 seconds - loads frame 0
      clock.multiplier = 0.6;
      scene.renderForSpecs(); // at 0.0 seconds - preloads frame 1
      clock.tick();
      scene.renderForSpecs(); // at 0.6 seconds - preloads frame 2
      clock.tick();
      scene.renderForSpecs(); // at 1.2 seconds - preloads frame 3
      clock.tick();
      scene.renderForSpecs(); // at 1.8 seconds - preloads frame 4
      clock.tick();
      scene.renderForSpecs(); // at 2.4 seconds

      var frames = pointCloud._frames;
      expect(frames[0]).toBeDefined();
      expect(frames[1]).toBeDefined();
      expect(frames[2]).toBeDefined();
      expect(frames[3]).toBeDefined();
      expect(frames[4]).toBeDefined();
    });

    it("renders loaded frames between the previous frame and next frame", function () {
      var pointCloud = createTimeDynamicPointCloud();
      spyOn(pointCloud, "_getAverageLoadTime").and.returnValue(3.4);
      var frames = pointCloud._frames;
      return loadFrames(pointCloud, [0, 2]).then(function () {
        clock.multiplier = 0.6;
        scene.renderForSpecs(); // at 0.0 seconds - preloads frame 4 at 2.04 seconds - renders frame 0
        expect(pointCloud._lastRenderedFrame).toBe(frames[0]);
        clock.tick();
        scene.renderForSpecs(); // at 0.6 seconds - renders frame 0
        expect(pointCloud._lastRenderedFrame).toBe(frames[0]);
        clock.tick();
        scene.renderForSpecs(); // at 1.2 seconds - renders frame 2 which has already been loaded
        expect(pointCloud._lastRenderedFrame).toBe(frames[2]);
        clock.tick();
        scene.renderForSpecs(); // at 1.8 seconds - renders frame 2
        expect(pointCloud._lastRenderedFrame).toBe(frames[2]);
      });
    });

    it("works with negative clock multiplier", function () {
      var pointCloud = createTimeDynamicPointCloud();
      spyOn(pointCloud, "_getAverageLoadTime").and.returnValue(2.0);
      goToFrame(4);
      scene.renderForSpecs(); // at 2.0 seconds - loads frame 4
      clock.multiplier = -0.6;
      scene.renderForSpecs(); // at 2.0 seconds - preloads frame 1 at 0.8 seconds
      clock.tick();
      scene.renderForSpecs(); // at 1.4 seconds
      clock.tick();
      scene.renderForSpecs(); // at 0.8 seconds - nothing left to preload
      clock.tick();
      scene.renderForSpecs(); // at 0.2 seconds
      clock.tick();
      scene.renderForSpecs(); // at -0.4 seconds

      var frames = pointCloud._frames;
      expect(frames[0]).toBeUndefined();
      expect(frames[1]).toBeDefined();
      expect(frames[2]).toBeUndefined();
      expect(frames[3]).toBeUndefined();
      expect(frames[4]).toBeDefined();
    });

    it("frames not loaded in sequential updates do not impact average load time", function () {
      var pointCloud = createTimeDynamicPointCloud();
      expect(pointCloud._runningAverage).toBe(0.0);
      return loadFrame(pointCloud).then(function () {
        expect(pointCloud._frames[0].sequential).toBe(true);
        expect(pointCloud._runningLength).toBe(1);
        expect(pointCloud._runningAverage).toBeGreaterThan(0.0);
        goToFrame(2); // Start loading frame 2, but don't finish loading it now
        scene.renderForSpecs();
        return loadFrame(pointCloud, 1).then(function () {
          var twoFrameAverage = pointCloud._runningAverage;
          expect(pointCloud._frames[1].sequential).toBe(true);
          expect(pointCloud._runningLength).toBe(2);
          expect(pointCloud._runningAverage).toBeGreaterThan(0.0);
          return loadFrame(pointCloud, 2).then(function () {
            expect(pointCloud._frames[2].sequential).toBe(false);
            expect(pointCloud._runningLength).toBe(2); // No update
            expect(pointCloud._runningAverage).toBe(twoFrameAverage); // No update
          });
        });
      });
    });

    it("frame failed event is raised from request failure", function () {
      var pointCloud = createTimeDynamicPointCloud();
      spyOn(Resource._Implementations, "loadWithXhr").and.callFake(function (
        request,
        responseType,
        method,
        data,
        headers,
        deferred,
        overrideMimeType
      ) {
        deferred.reject("404");
      });
      var spyUpdate = jasmine.createSpy("listener");
      pointCloud.frameFailed.addEventListener(spyUpdate);

      var i;
      for (i = 0; i < 5; ++i) {
        goToFrame(i);
        scene.renderForSpecs();
      }

      for (i = 0; i < 5; ++i) {
        var arg = spyUpdate.calls.argsFor(i)[0];
        expect(arg).toBeDefined();
        expect(arg.uri).toContain(i + ".pnts");
        expect(arg.message).toBe("404");
      }
    });

    it("failed frame event is raised from Draco failure", function () {
      var pointCloud = createTimeDynamicPointCloud({
        useDraco: true,
      });
      return loadFrame(pointCloud).then(function () {
        var decoder = DracoLoader._getDecoderTaskProcessor();
        spyOn(decoder, "scheduleTask").and.returnValue(
          when.reject({ message: "my error" })
        );
        var spyUpdate = jasmine.createSpy("listener");
        pointCloud.frameFailed.addEventListener(spyUpdate);
        goToFrame(1);
        scene.renderForSpecs();
        var failedPromise;
        var frameFailed = false;
        return pollToPromise(function () {
          var contents = pointCloud._frames[1].pointCloud;
          if (defined(contents) && !defined(failedPromise)) {
            failedPromise = contents.readyPromise.otherwise(function () {
              frameFailed = true;
            });
          }
          scene.renderForSpecs();
          return frameFailed;
        }).then(function () {
          var arg = spyUpdate.calls.argsFor(0)[0];
          expect(arg).toBeDefined();
          expect(arg.uri).toContain("1.pnts");
          expect(arg.message).toBe("my error");
        });
      });
    });

    it("raises frame changed event", function () {
      var pointCloud = createTimeDynamicPointCloud();
      var spyFrameChanged = jasmine.createSpy("listener");
      pointCloud.frameChanged.addEventListener(spyFrameChanged);

      return loadAllFrames(pointCloud).then(function () {
        expect(spyFrameChanged.calls.count()).toBe(5);

        // Go to random frame
        goToFrame(2);
        scene.renderForSpecs();
        expect(spyFrameChanged.calls.count()).toBe(6);

        // Go out of range. No event raised.
        clock.currentTime = JulianDate.addSeconds(
          dates[0],
          -10.0,
          new JulianDate()
        );
        scene.renderForSpecs();
        expect(spyFrameChanged.calls.count()).toBe(6);

        goToFrame(0);
        scene.renderForSpecs();
        expect(spyFrameChanged.calls.count()).toBe(7);

        expect(spyFrameChanged.calls.argsFor(0)[0]).toBe(pointCloud);
      });
    });

    it("destroys", function () {
      var pointCloud = createTimeDynamicPointCloud();
      return loadAllFrames(pointCloud).then(function () {
        expect(pointCloud.isDestroyed()).toEqual(false);
        scene.primitives.remove(pointCloud);
        expect(pointCloud.isDestroyed()).toEqual(true);
        expect(pointCloud.totalMemoryUsageInBytes).toBe(0);
      });
    });
  },
  "WebGL"
);
