import { BoundingSphere } from "../../Source/Cesium.js";
import { BoxGeometry } from "../../Source/Cesium.js";
import { Cartesian3 } from "../../Source/Cesium.js";
import { Color } from "../../Source/Cesium.js";
import { ColorGeometryInstanceAttribute } from "../../Source/Cesium.js";
import { ComponentDatatype } from "../../Source/Cesium.js";
import { EllipsoidTerrainProvider } from "../../Source/Cesium.js";
import { GeometryInstance } from "../../Source/Cesium.js";
import { HeadingPitchRange } from "../../Source/Cesium.js";
import { HeadingPitchRoll } from "../../Source/Cesium.js";
import { HeightmapTerrainData } from "../../Source/Cesium.js";
import { JulianDate } from "../../Source/Cesium.js";
import { Math as CesiumMath } from "../../Source/Cesium.js";
import { OrthographicOffCenterFrustum } from "../../Source/Cesium.js";
import { PixelFormat } from "../../Source/Cesium.js";
import { Transforms } from "../../Source/Cesium.js";
import { WebGLConstants } from "../../Source/Cesium.js";
import { Context } from "../../Source/Cesium.js";
import { Framebuffer } from "../../Source/Cesium.js";
import { PixelDatatype } from "../../Source/Cesium.js";
import { Texture } from "../../Source/Cesium.js";
import { Camera } from "../../Source/Cesium.js";
import { DirectionalLight } from "../../Source/Cesium.js";
import { Globe } from "../../Source/Cesium.js";
import { Model } from "../../Source/Cesium.js";
import { PerInstanceColorAppearance } from "../../Source/Cesium.js";
import { Primitive } from "../../Source/Cesium.js";
import { ShadowMap } from "../../Source/Cesium.js";
import { ShadowMode } from "../../Source/Cesium.js";
import createScene from "../createScene.js";
import pollToPromise from "../pollToPromise.js";
import { when } from "../../Source/Cesium.js";

describe(
  "Scene/ShadowMap",
  function () {
    var scene;
    var sunShadowMap;
    var backgroundColor = [0, 0, 0, 255];

    var longitude = -1.31968;
    var latitude = 0.4101524;
    var height = 0.0;
    var boxHeight = 4.0;
    var floorHeight = -1.0;

    var boxUrl = "./Data/Models/Shadows/Box.gltf";
    var boxTranslucentUrl = "./Data/Models/Shadows/BoxTranslucent.gltf";
    var boxCutoutUrl = "./Data/Models/Shadows/BoxCutout.gltf";
    var boxInvertedUrl = "./Data/Models/Shadows/BoxInverted.gltf";

    var box;
    var boxTranslucent;
    var boxCutout;
    var room;
    var floor;
    var floorTranslucent;

    var primitiveBox;
    var primitiveBoxRTC;
    var primitiveBoxTranslucent;
    var primitiveFloor;
    var primitiveFloorRTC;

    beforeAll(function () {
      scene = createScene();
      scene.frameState.scene3DOnly = true;
      Color.unpack(backgroundColor, 0, scene.backgroundColor);

      sunShadowMap = scene.shadowMap;

      var boxOrigin = new Cartesian3.fromRadians(
        longitude,
        latitude,
        boxHeight
      );
      var boxTransform = Transforms.headingPitchRollToFixedFrame(
        boxOrigin,
        new HeadingPitchRoll()
      );

      var floorOrigin = new Cartesian3.fromRadians(
        longitude,
        latitude,
        floorHeight
      );
      var floorTransform = Transforms.headingPitchRollToFixedFrame(
        floorOrigin,
        new HeadingPitchRoll()
      );

      var roomOrigin = new Cartesian3.fromRadians(longitude, latitude, height);
      var roomTransform = Transforms.headingPitchRollToFixedFrame(
        roomOrigin,
        new HeadingPitchRoll()
      );

      var modelPromises = [];
      modelPromises.push(
        loadModel({
          url: boxUrl,
          modelMatrix: boxTransform,
          scale: 0.5,
          show: false,
        }).then(function (model) {
          box = model;
        })
      );
      modelPromises.push(
        loadModel({
          url: boxTranslucentUrl,
          modelMatrix: boxTransform,
          scale: 0.5,
          show: false,
        }).then(function (model) {
          boxTranslucent = model;
        })
      );
      modelPromises.push(
        loadModel({
          url: boxCutoutUrl,
          modelMatrix: boxTransform,
          scale: 0.5,
          incrementallyLoadTextures: false,
          show: false,
        }).then(function (model) {
          boxCutout = model;
        })
      );
      modelPromises.push(
        loadModel({
          url: boxUrl,
          modelMatrix: floorTransform,
          scale: 2.0,
          show: false,
        }).then(function (model) {
          floor = model;
        })
      );
      modelPromises.push(
        loadModel({
          url: boxTranslucentUrl,
          modelMatrix: floorTransform,
          scale: 2.0,
          show: false,
        }).then(function (model) {
          floorTranslucent = model;
        })
      );
      modelPromises.push(
        loadModel({
          url: boxInvertedUrl,
          modelMatrix: roomTransform,
          scale: 8.0,
          show: false,
        }).then(function (model) {
          room = model;
        })
      );

      primitiveBox = createPrimitive(boxTransform, 0.5, Color.RED);
      primitiveBoxRTC = createPrimitiveRTC(boxTransform, 0.5, Color.RED);
      primitiveBoxTranslucent = createPrimitive(
        boxTransform,
        0.5,
        Color.RED.withAlpha(0.5)
      );
      primitiveFloor = createPrimitive(floorTransform, 2.0, Color.RED);
      primitiveFloorRTC = createPrimitiveRTC(floorTransform, 2.0, Color.RED);

      return when.all(modelPromises);
    });

    function createPrimitive(transform, size, color) {
      return scene.primitives.add(
        new Primitive({
          geometryInstances: new GeometryInstance({
            geometry: BoxGeometry.fromDimensions({
              dimensions: new Cartesian3(size, size, size),
              vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
            }),
            modelMatrix: transform,
            attributes: {
              color: ColorGeometryInstanceAttribute.fromColor(color),
            },
          }),
          appearance: new PerInstanceColorAppearance({
            translucent: false,
            closed: true,
          }),
          asynchronous: false,
          show: false,
          shadows: ShadowMode.ENABLED,
        })
      );
    }

    function createPrimitiveRTC(transform, size, color) {
      var boxGeometry = BoxGeometry.createGeometry(
        BoxGeometry.fromDimensions({
          vertexFormat: PerInstanceColorAppearance.VERTEX_FORMAT,
          dimensions: new Cartesian3(size, size, size),
        })
      );

      var positions = boxGeometry.attributes.position.values;
      var newPositions = new Float32Array(positions.length);
      for (var i = 0; i < positions.length; ++i) {
        newPositions[i] = positions[i];
      }
      boxGeometry.attributes.position.values = newPositions;
      boxGeometry.attributes.position.componentDatatype =
        ComponentDatatype.FLOAT;

      BoundingSphere.transform(
        boxGeometry.boundingSphere,
        transform,
        boxGeometry.boundingSphere
      );

      var boxGeometryInstance = new GeometryInstance({
        geometry: boxGeometry,
        attributes: {
          color: ColorGeometryInstanceAttribute.fromColor(color),
        },
      });

      return scene.primitives.add(
        new Primitive({
          geometryInstances: boxGeometryInstance,
          appearance: new PerInstanceColorAppearance({
            translucent: false,
            closed: true,
          }),
          asynchronous: false,
          rtcCenter: boxGeometry.boundingSphere.center,
          show: false,
          shadows: ShadowMode.ENABLED,
        })
      );
    }

    function loadModel(options) {
      var model = scene.primitives.add(Model.fromGltf(options));
      return pollToPromise(
        function () {
          // Render scene to progressively load the model
          scene.render();
          return model.ready;
        },
        { timeout: 10000 }
      ).then(function () {
        return model;
      });
    }

    /**
     * Repeatedly calls render until the load queue is empty. Returns a promise that resolves
     * when the load queue is empty.
     */
    function loadGlobe() {
      return pollToPromise(function () {
        scene.render();
        var globe = scene.globe;
        return (
          globe._surface.tileProvider.ready &&
          globe._surface._tileLoadQueueHigh.length === 0 &&
          globe._surface._tileLoadQueueMedium.length === 0 &&
          globe._surface._tileLoadQueueLow.length === 0 &&
          globe._surface._debug.tilesWaitingForChildren === 0
        );
      });
    }

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

    afterEach(function () {
      var length = scene.primitives.length;
      for (var i = 0; i < length; ++i) {
        scene.primitives.get(i).show = false;
      }

      scene.globe = undefined;
      scene.shadowMap = scene.shadowMap && scene.shadowMap.destroy();
    });

    function createCascadedShadowMap() {
      var center = new Cartesian3.fromRadians(longitude, latitude, height);
      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
      );

      // Create light camera pointing straight down
      var lightCamera = new Camera(scene);
      lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, 1.0));

      scene.shadowMap = new ShadowMap({
        context: scene.context,
        lightCamera: lightCamera,
      });
    }

    function createSingleCascadeShadowMap() {
      var center = new Cartesian3.fromRadians(longitude, latitude, height);
      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
      );

      // Create light camera pointing straight down
      var lightCamera = new Camera(scene);
      lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, 1.0));

      scene.shadowMap = new ShadowMap({
        context: scene.context,
        lightCamera: lightCamera,
        numberOfCascades: 1,
      });
    }

    function createShadowMapForDirectionalLight() {
      var center = new Cartesian3.fromRadians(longitude, latitude, height);
      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
      );

      var frustum = new OrthographicOffCenterFrustum();
      frustum.left = -50.0;
      frustum.right = 50.0;
      frustum.bottom = -50.0;
      frustum.top = 50.0;
      frustum.near = 1.0;
      frustum.far = 1000;

      // Create light camera pointing straight down
      var lightCamera = new Camera(scene);
      lightCamera.frustum = frustum;
      lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, 20.0));

      scene.shadowMap = new ShadowMap({
        context: scene.context,
        lightCamera: lightCamera,
        cascadesEnabled: false,
      });
    }

    function createShadowMapForSpotLight() {
      var center = new Cartesian3.fromRadians(longitude, latitude, height);
      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
      );

      var lightCamera = new Camera(scene);
      lightCamera.frustum.fov = CesiumMath.PI_OVER_TWO;
      lightCamera.frustum.aspectRatio = 1.0;
      lightCamera.frustum.near = 1.0;
      lightCamera.frustum.far = 1000.0;
      lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, 20.0));

      scene.shadowMap = new ShadowMap({
        context: scene.context,
        lightCamera: lightCamera,
        cascadesEnabled: false,
      });
    }

    function createShadowMapForPointLight() {
      var center = new Cartesian3.fromRadians(longitude, latitude, height);
      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
      );

      var lightCamera = new Camera(scene);
      lightCamera.position = center;

      scene.shadowMap = new ShadowMap({
        context: scene.context,
        lightCamera: lightCamera,
        isPointLight: true,
      });
    }

    function renderAndExpect(rgba, time) {
      expect({
        scene: scene,
        time: time,
        primeShadowMap: true,
      }).toRender(rgba);
    }

    function renderAndReadPixels() {
      var color;

      expect({
        scene: scene,
        primeShadowMap: true,
      }).toRenderAndCall(function (rgba) {
        color = rgba;
      });

      return color;
    }

    function renderAndCall(expectationCallback, time) {
      expect({
        scene: scene,
        time: time,
        primeShadowMap: true,
      }).toRenderAndCall(function (rgba) {
        expectationCallback(rgba);
      });
    }

    function verifyShadows(caster, receiver) {
      caster.shadows = ShadowMode.ENABLED;
      receiver.shadows = ShadowMode.ENABLED;

      // Render without shadows
      scene.shadowMap.enabled = false;
      var unshadowedColor;
      renderAndCall(function (rgba) {
        unshadowedColor = rgba;
        expect(unshadowedColor).not.toEqual(backgroundColor);
      });

      // Render with shadows
      scene.shadowMap.enabled = true;
      var shadowedColor;
      renderAndCall(function (rgba) {
        shadowedColor = rgba;
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).not.toEqual(unshadowedColor);
      });

      // Turn shadow casting off/on
      caster.shadows = ShadowMode.DISABLED;
      renderAndExpect(unshadowedColor);
      caster.shadows = ShadowMode.ENABLED;
      renderAndExpect(shadowedColor);

      // Turn shadow receiving off/on
      receiver.shadows = ShadowMode.DISABLED;
      renderAndExpect(unshadowedColor);
      receiver.shadows = ShadowMode.ENABLED;
      renderAndExpect(shadowedColor);

      // Move the camera away from the shadow
      scene.camera.moveRight(0.5);
      renderAndExpect(unshadowedColor);
    }

    it("sets default shadow map properties", function () {
      scene.shadowMap = new ShadowMap({
        context: scene.context,
        lightCamera: new Camera(scene),
      });

      expect(scene.shadowMap.enabled).toBe(true);
      expect(scene.shadowMap.softShadows).toBe(false);
      expect(scene.shadowMap.isPointLight).toBe(false);
      expect(scene.shadowMap._isSpotLight).toBe(false);
      expect(scene.shadowMap._cascadesEnabled).toBe(true);
      expect(scene.shadowMap._numberOfCascades).toBe(4);
      expect(scene.shadowMap._normalOffset).toBe(true);
    });

    it("throws without options.context", function () {
      expect(function () {
        scene.shadowMap = new ShadowMap({
          lightCamera: new Camera(scene),
        });
      }).toThrowDeveloperError();
    });

    it("throws without options.lightCamera", function () {
      expect(function () {
        scene.shadowMap = new ShadowMap({
          context: scene.context,
        });
      }).toThrowDeveloperError();
    });

    it("throws when options.numberOfCascades is not one or four", function () {
      expect(function () {
        scene.shadowMap = new ShadowMap({
          context: scene.context,
          lightCamera: new Camera(scene),
          numberOfCascades: 3,
        });
      }).toThrowDeveloperError();
    });

    it("model casts shadows onto another model", function () {
      box.show = true;
      floor.show = true;
      createCascadedShadowMap();
      verifyShadows(box, floor);
    });

    it("translucent model casts shadows onto another model", function () {
      boxTranslucent.show = true;
      floor.show = true;
      createCascadedShadowMap();
      verifyShadows(boxTranslucent, floor);
    });

    it("model with cutout texture casts shadows onto another model", function () {
      boxCutout.show = true;
      floor.show = true;
      createCascadedShadowMap();

      // Render without shadows
      scene.shadowMap.enabled = false;

      var unshadowedColor;
      renderAndCall(function (rgba) {
        unshadowedColor = rgba;
        expect(rgba).not.toEqual(backgroundColor);
      });

      // Render with shadows. The area should not be shadowed because the box's texture is transparent in the center.
      scene.shadowMap.enabled = true;
      renderAndExpect(unshadowedColor);

      // Move the camera into the shadowed area
      scene.camera.moveRight(0.2);

      renderAndCall(function (rgba) {
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).not.toEqual(unshadowedColor);
      });

      // Move the camera away from the shadow
      scene.camera.moveRight(0.3);
      renderAndExpect(unshadowedColor);
    });

    it("primitive casts shadows onto another primitive", function () {
      primitiveBox.show = true;
      primitiveFloor.show = true;
      createCascadedShadowMap();
      verifyShadows(primitiveBox, primitiveFloor);
    });

    it("RTC primitive casts shadows onto another RTC primitive", function () {
      primitiveBoxRTC.show = true;
      primitiveFloorRTC.show = true;
      createCascadedShadowMap();
      verifyShadows(primitiveBoxRTC, primitiveFloorRTC);
    });

    it("translucent primitive casts shadows onto another primitive", function () {
      primitiveBoxTranslucent.show = true;
      primitiveFloor.show = true;
      createCascadedShadowMap();
      verifyShadows(primitiveBoxTranslucent, primitiveFloor);
    });

    it("model casts shadow onto globe", function () {
      box.show = true;
      scene.globe = new Globe();
      scene.camera.frustum._sseDenominator = 0.005;
      createCascadedShadowMap();

      return loadGlobe().then(function () {
        verifyShadows(box, scene.globe);
      });
    });

    it("globe casts shadow onto globe", function () {
      scene.globe = new Globe();
      scene.camera.frustum._sseDenominator = 0.01;

      var center = new Cartesian3.fromRadians(longitude, latitude, height);
      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
      );

      // Create light camera that is angled horizontally
      var lightCamera = new Camera(scene);
      lightCamera.lookAt(center, new Cartesian3(1.0, 0.0, 0.1));

      scene.shadowMap = new ShadowMap({
        context: scene.context,
        lightCamera: lightCamera,
      });

      // Instead of the default flat tile, add a ridge that will cast shadows
      spyOn(
        EllipsoidTerrainProvider.prototype,
        "requestTileGeometry"
      ).and.callFake(function () {
        var width = 16;
        var height = 16;
        var buffer = new Uint8Array(width * height);
        for (var i = 0; i < buffer.length; ++i) {
          var row = i % width;
          if (row > 6 && row < 10) {
            buffer[i] = 1;
          }
        }
        return new HeightmapTerrainData({
          buffer: buffer,
          width: width,
          height: height,
        });
      });

      return loadGlobe().then(function () {
        // Render without shadows
        scene.shadowMap.enabled = false;

        var unshadowedColor;
        renderAndCall(function (rgba) {
          unshadowedColor = rgba;
          expect(rgba).not.toEqual(backgroundColor);
        });

        // Render with globe casting off
        scene.shadowMap.enabled = true;
        scene.globe.shadows = ShadowMode.DISABLED;
        renderAndExpect(unshadowedColor);

        // Render with globe casting on
        scene.globe.shadows = ShadowMode.ENABLED;
        renderAndCall(function (rgba) {
          expect(rgba).not.toEqual(backgroundColor);
          expect(rgba).not.toEqual(unshadowedColor);
        });
      });
    });

    it("changes light direction", function () {
      box.show = true;
      floor.show = true;

      var center = new Cartesian3.fromRadians(longitude, latitude, height);
      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
      );

      // Create light camera pointing straight down
      var lightCamera = new Camera(scene);
      lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, 1.0));

      scene.shadowMap = new ShadowMap({
        context: scene.context,
        lightCamera: lightCamera,
      });

      // Render with shadows
      var shadowedColor = renderAndReadPixels();

      // Move the camera away from the shadow
      scene.camera.moveLeft(0.5);
      renderAndCall(function (rgba) {
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).not.toEqual(shadowedColor);
      });

      // Change the light direction so the unshadowed area is now shadowed
      lightCamera.lookAt(center, new Cartesian3(0.1, 0.0, 1.0));
      renderAndExpect(shadowedColor);
    });

    it("sun shadow map works", function () {
      box.show = true;
      floor.show = true;

      var startTime = new JulianDate(2457561.211806); // Sun pointing straight above
      var endTime = new JulianDate(2457561.276389); // Sun at an angle

      var center = new Cartesian3.fromRadians(longitude, latitude, height);
      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
      );

      // Use the default shadow map which uses the sun as a light source
      scene.shadowMap = sunShadowMap;

      // Render without shadows
      scene.shadowMap.enabled = false;

      var unshadowedColor;
      renderAndCall(function (rgba) {
        unshadowedColor = rgba;
        expect(rgba).not.toEqual(backgroundColor);
      });

      // Render with shadows
      scene.shadowMap.enabled = true;
      renderAndCall(function (rgba) {
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).not.toEqual(unshadowedColor);
      }, startTime);

      // Change the time so that the shadows are no longer pointing straight down
      renderAndExpect(unshadowedColor, endTime);

      scene.shadowMap = undefined;
    });

    it("uses scene's light source", function () {
      var originalLight = scene.light;

      box.show = true;
      floor.show = true;

      var lightDirectionAbove = new Cartesian3(
        -0.22562675028973597,
        0.8893549458095356,
        -0.3976686433675793
      ); // Light pointing straight above
      var lightDirectionAngle = new Cartesian3(
        0.14370705890272903,
        0.9062077731227641,
        -0.3976628636840613
      ); // Light at an angle

      var center = new Cartesian3.fromRadians(longitude, latitude, height);
      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
      );

      // Use the default shadow map which uses the scene's light source
      scene.light = new DirectionalLight({
        direction: lightDirectionAbove,
      });
      scene.shadowMap = sunShadowMap;

      // Render without shadows
      scene.shadowMap.enabled = false;

      var unshadowedColor;
      renderAndCall(function (rgba) {
        unshadowedColor = rgba;
        expect(rgba).not.toEqual(backgroundColor);
      });

      // Render with shadows
      scene.shadowMap.enabled = true;
      renderAndCall(function (rgba) {
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).not.toEqual(unshadowedColor);
      });

      // Change the light so that the shadows are no longer pointing straight down
      scene.light = new DirectionalLight({
        direction: lightDirectionAngle,
      });
      renderAndExpect(unshadowedColor);

      scene.shadowMap = undefined;
      scene.light = originalLight;
    });

    it("single cascade shadow map", function () {
      box.show = true;
      floor.show = true;
      createSingleCascadeShadowMap();
      verifyShadows(box, floor);
    });

    it("directional shadow map", function () {
      box.show = true;
      floor.show = true;
      createShadowMapForDirectionalLight();
      verifyShadows(box, floor);
    });

    it("spot light shadow map", function () {
      box.show = true;
      floor.show = true;
      createShadowMapForSpotLight();
      verifyShadows(box, floor);
    });

    it("point light shadows", function () {
      // Check that shadows are cast from all directions.
      // Place the point light in the middle of an enclosed area and place a box on each side.
      room.show = true;
      createShadowMapForPointLight();

      var longitudeSpacing = 0.0000003419296208325038;
      var latitudeSpacing = 0.000000315782;
      var heightSpacing = 2.0;

      var origins = [
        Cartesian3.fromRadians(longitude, latitude + latitudeSpacing, height),
        Cartesian3.fromRadians(longitude, latitude - latitudeSpacing, height),
        Cartesian3.fromRadians(longitude + longitudeSpacing, latitude, height),
        Cartesian3.fromRadians(longitude - longitudeSpacing, latitude, height),
        Cartesian3.fromRadians(longitude, latitude, height - heightSpacing),
        Cartesian3.fromRadians(longitude, latitude, height + heightSpacing),
      ];

      var offsets = [
        new HeadingPitchRange(0.0, 0.0, 0.1),
        new HeadingPitchRange(CesiumMath.PI, 0.0, 0.1),
        new HeadingPitchRange(CesiumMath.PI_OVER_TWO, 0.0, 0.1),
        new HeadingPitchRange(CesiumMath.THREE_PI_OVER_TWO, 0.0, 0.1),
        new HeadingPitchRange(0, -CesiumMath.PI_OVER_TWO, 0.1),
        new HeadingPitchRange(0, CesiumMath.PI_OVER_TWO, 0.1),
      ];

      for (var i = 0; i < 6; ++i) {
        var box = scene.primitives.add(
          Model.fromGltf({
            url: boxUrl,
            modelMatrix: Transforms.headingPitchRollToFixedFrame(
              origins[i],
              new HeadingPitchRoll()
            ),
            scale: 0.2,
          })
        );
        scene.render(); // Model is pre-loaded, render one frame to make it ready

        scene.camera.lookAt(origins[i], offsets[i]);
        scene.camera.moveForward(0.5);

        // Render without shadows
        scene.shadowMap.enabled = false;
        var unshadowedColor;
        //eslint-disable-next-line no-loop-func
        renderAndCall(function (rgba) {
          unshadowedColor = rgba;
          expect(rgba).not.toEqual(backgroundColor);
        });

        // Render with shadows
        scene.shadowMap.enabled = true;
        //eslint-disable-next-line no-loop-func
        renderAndCall(function (rgba) {
          expect(rgba).not.toEqual(backgroundColor);
          expect(rgba).not.toEqual(unshadowedColor);
        });

        // Check that setting a smaller radius works
        var radius = scene.shadowMap._pointLightRadius;
        scene.shadowMap._pointLightRadius = 3.0;
        renderAndExpect(unshadowedColor);
        scene.shadowMap._pointLightRadius = radius;

        // Move the camera away from the shadow
        scene.camera.moveRight(0.5);
        renderAndExpect(unshadowedColor);

        scene.primitives.remove(box);
      }
    });

    it("changes size", function () {
      box.show = true;
      floor.show = true;
      createCascadedShadowMap();

      // Render with shadows
      var shadowedColor = renderAndReadPixels();

      // Change size
      scene.shadowMap.size = 256;
      renderAndExpect(shadowedColor);

      // Cascaded shadows combine four maps into one texture
      expect(scene.shadowMap._shadowMapTexture.width).toBe(512);
      expect(scene.shadowMap._shadowMapTexture.height).toBe(512);
      expect(scene.shadowMap.size).toBe(256);
    });

    it("enable debugCascadeColors", function () {
      box.show = true;
      floor.show = true;
      createCascadedShadowMap();

      // Render with shadows
      var shadowedColor = renderAndReadPixels();

      // Render cascade colors
      scene.shadowMap.debugCascadeColors = true;
      expect(scene.shadowMap.dirty).toBe(true);
      renderAndCall(function (rgba) {
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).not.toEqual(shadowedColor);
      });
    });

    it("enable soft shadows", function () {
      box.show = true;
      floor.show = true;
      createCascadedShadowMap();

      // Render without shadows
      scene.shadowMap.enabled = false;
      var unshadowedColor = renderAndReadPixels();

      // Render with shadows
      scene.shadowMap.enabled = true;
      expect(scene.shadowMap.dirty).toBe(true);
      var shadowedColor = renderAndReadPixels();

      // Render with soft shadows
      scene.shadowMap.softShadows = true;
      scene.shadowMap.size = 256; // Make resolution smaller to more easily verify soft edges
      scene.camera.moveRight(0.25);
      renderAndCall(function (rgba) {
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).not.toEqual(unshadowedColor);
        expect(rgba).not.toEqual(shadowedColor);
      });
    });

    it("changes darkness", function () {
      box.show = true;
      floor.show = true;
      createCascadedShadowMap();

      // Render without shadows
      scene.shadowMap.enabled = false;
      var unshadowedColor = renderAndReadPixels();

      // Render with shadows
      scene.shadowMap.enabled = true;
      var shadowedColor = renderAndReadPixels();

      scene.shadowMap.darkness = 0.5;
      renderAndCall(function (rgba) {
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).not.toEqual(unshadowedColor);
        expect(rgba).not.toEqual(shadowedColor);
      });
    });

    it("disables shadow fading", function () {
      box.show = true;
      floor.show = true;

      var center = new Cartesian3.fromRadians(longitude, latitude, height);
      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
      );

      // Create light camera pointing straight down
      var lightCamera = new Camera(scene);
      lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, 1.0));

      scene.shadowMap = new ShadowMap({
        context: scene.context,
        lightCamera: lightCamera,
      });

      // Render with light looking straight down
      var shadowedColor = renderAndReadPixels();

      // Move the light close to the horizon
      lightCamera.lookAt(center, new Cartesian3(1.0, 0.0, 0.01));

      // Render with faded shadows
      var horizonShadowedColor = renderAndReadPixels();

      // Render with unfaded shadows
      scene.shadowMap.fadingEnabled = false;
      renderAndCall(function (rgba) {
        expect(horizonShadowedColor).not.toEqual(shadowedColor);
        expect(rgba).not.toEqual(horizonShadowedColor);
      });
    });

    function depthFramebufferSupported() {
      var framebuffer = new Framebuffer({
        context: scene.context,
        depthStencilTexture: new Texture({
          context: scene.context,
          width: 1,
          height: 1,
          pixelFormat: PixelFormat.DEPTH_STENCIL,
          pixelDatatype: PixelDatatype.UNSIGNED_INT_24_8,
        }),
      });

      return framebuffer.status === WebGLConstants.FRAMEBUFFER_COMPLETE;
    }

    it("defaults to color texture if depth texture extension is not supported", function () {
      box.show = true;
      floor.show = true;

      createCascadedShadowMap();

      renderAndCall(function (rgba) {
        if (scene.context.depthTexture) {
          if (depthFramebufferSupported()) {
            expect(scene.shadowMap._usesDepthTexture).toBe(true);
            expect(scene.shadowMap._shadowMapTexture.pixelFormat).toEqual(
              PixelFormat.DEPTH_STENCIL
            );
          } else {
            // Depth texture extension is supported, but it fails to create create a depth-only FBO
            expect(scene.shadowMap._usesDepthTexture).toBe(false);
            expect(scene.shadowMap._shadowMapTexture.pixelFormat).toEqual(
              PixelFormat.RGBA
            );
          }
        }
      });

      scene.shadowMap = scene.shadowMap && scene.shadowMap.destroy();

      // Disable extension
      var depthTexture = scene.context._depthTexture;
      scene.context._depthTexture = false;
      createCascadedShadowMap();

      renderAndCall(function (rgba) {
        expect(scene.shadowMap._usesDepthTexture).toBe(false);
        expect(scene.shadowMap._shadowMapTexture.pixelFormat).toEqual(
          PixelFormat.RGBA
        );
      });

      // Re-enable extension
      scene.context._depthTexture = depthTexture;
    });

    it("does not render shadows when the camera is far away from any shadow receivers", function () {
      box.show = true;
      floor.show = true;
      createCascadedShadowMap();

      renderAndCall(function (rgba) {
        expect(scene.shadowMap.outOfView).toBe(false);
      });

      var center = new Cartesian3.fromRadians(longitude, latitude, 200000);
      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
      );

      renderAndCall(function (rgba) {
        expect(scene.shadowMap.outOfView).toBe(true);
      });
    });

    it("does not render shadows when the light direction is below the horizon", function () {
      box.show = true;
      floor.show = true;

      var center = new Cartesian3.fromRadians(longitude, latitude, height);
      scene.camera.lookAt(
        center,
        new HeadingPitchRange(0.0, CesiumMath.toRadians(-70.0), 5.0)
      );

      // Create light camera pointing straight down
      var lightCamera = new Camera(scene);
      lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, 1.0));

      scene.shadowMap = new ShadowMap({
        context: scene.context,
        lightCamera: lightCamera,
      });

      renderAndCall(function (rgba) {
        expect(scene.shadowMap.outOfView).toBe(false);
      });

      // Change light direction
      lightCamera.lookAt(center, new Cartesian3(0.0, 0.0, -1.0));
      renderAndCall(function (rgba) {
        expect(scene.shadowMap.outOfView).toBe(true);
      });
    });

    it("enable debugShow for cascaded shadow map", function () {
      createCascadedShadowMap();

      // Shadow overlay command, shadow volume outline, camera outline, four cascade outlines, four cascade planes
      scene.shadowMap.debugShow = true;
      scene.shadowMap.debugFreezeFrame = true;
      renderAndCall(function (rgba) {
        expect(scene.frameState.commandList.length).toBe(13);
      });

      scene.shadowMap.debugShow = false;
      renderAndCall(function (rgba) {
        expect(scene.frameState.commandList.length).toBe(0);
      });
    });

    it("enable debugShow for fixed shadow map", function () {
      createShadowMapForDirectionalLight();

      // Overlay command, shadow volume outline, shadow volume planes
      scene.shadowMap.debugShow = true;
      renderAndCall(function (rgba) {
        expect(scene.frameState.commandList.length).toBe(3);
      });

      scene.shadowMap.debugShow = false;
      renderAndCall(function (rgba) {
        expect(scene.frameState.commandList.length).toBe(0);
      });
    });

    it("enable debugShow for point light shadow map", function () {
      createShadowMapForPointLight();

      // Overlay command and shadow volume outline
      scene.shadowMap.debugShow = true;
      renderAndCall(function (rgba) {
        expect(scene.frameState.commandList.length).toBe(2);
      });

      scene.shadowMap.debugShow = false;
      renderAndCall(function (rgba) {
        expect(scene.frameState.commandList.length).toBe(0);
      });
    });

    it("enable fitNearFar", function () {
      box.show = true;
      floor.show = true;
      createShadowMapForDirectionalLight();
      scene.shadowMap._fitNearFar = true; // True by default

      var shadowNearFit;
      var shadowFarFit;
      renderAndCall(function (rgba) {
        shadowNearFit = scene.shadowMap._sceneCamera.frustum.near;
        shadowFarFit = scene.shadowMap._sceneCamera.frustum.far;
      });

      scene.shadowMap._fitNearFar = false;
      renderAndCall(function (rgba) {
        var shadowNear = scene.shadowMap._sceneCamera.frustum.near;
        var shadowFar = scene.shadowMap._sceneCamera.frustum.far;

        // When fitNearFar is true the shadowed region is smaller
        expect(shadowNear).toBeLessThan(shadowNearFit);
        expect(shadowFar).toBeGreaterThan(shadowFarFit);
      });
    });

    it("set normalOffset", function () {
      createCascadedShadowMap();
      scene.shadowMap.normalOffset = false;

      expect(scene.shadowMap._normalOffset, false);
      expect(scene.shadowMap._terrainBias, false);
      expect(scene.shadowMap._primitiveBias, false);
      expect(scene.shadowMap._pointBias, false);
    });

    it("set maximumDistance", function () {
      box.show = true;
      floor.show = true;
      createCascadedShadowMap();

      // Render without shadows
      scene.shadowMap.enabled = false;
      var unshadowedColor;
      renderAndCall(function (rgba) {
        expect(rgba).not.toEqual(backgroundColor);
        unshadowedColor = rgba;
      });

      // Render with shadows
      scene.shadowMap.enabled = true;
      var shadowedColor;
      renderAndCall(function (rgba) {
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).not.toEqual(unshadowedColor);
      });

      // Set a maximum distance where the shadows start to fade out
      scene.shadowMap.maximumDistance = 6.0;
      renderAndCall(function (rgba) {
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).not.toEqual(unshadowedColor);
        expect(rgba).not.toEqual(shadowedColor);
      });

      // Set a maximimum distance where the shadows are not visible
      scene.shadowMap.maximumDistance = 3.0;
      renderAndExpect(unshadowedColor);
    });

    it("shadows are disabled during the pick pass", function () {
      var spy = spyOn(Context.prototype, "draw").and.callThrough();

      boxTranslucent.show = true;
      floorTranslucent.show = true;

      createCascadedShadowMap();

      // Render normally and expect every model shader program to be shadow related.
      renderAndCall(function (rgba) {
        var count = spy.calls.count();
        for (var i = 0; i < count; ++i) {
          var drawCommand = spy.calls.argsFor(i)[0];
          if (drawCommand.owner.primitive instanceof Model) {
            expect(
              drawCommand.shaderProgram._fragmentShaderText.indexOf(
                "czm_shadow"
              ) !== -1
            ).toBe(true);
          }
        }
      });

      // Do the pick pass and expect every model shader program to not be shadow related. This also checks
      // that there are no shadow cast commands.
      spy.calls.reset();
      expect(scene).toPickAndCall(function (result) {
        var count = spy.calls.count();
        for (var i = 0; i < count; ++i) {
          var drawCommand = spy.calls.argsFor(i)[0];
          if (drawCommand.owner.primitive instanceof Model) {
            expect(
              drawCommand.shaderProgram._fragmentShaderText.indexOf(
                "czm_shadow"
              ) !== -1
            ).toBe(false);
          }
        }
      });
    });

    it("model updates derived commands when the shadow map is dirty", function () {
      var spy1 = spyOn(
        ShadowMap,
        "createReceiveDerivedCommand"
      ).and.callThrough();
      var spy2 = spyOn(ShadowMap, "createCastDerivedCommand").and.callThrough();

      box.show = true;
      floor.show = true;
      createCascadedShadowMap();

      // Render without shadows
      scene.shadowMap.enabled = false;
      var unshadowedColor;
      renderAndCall(function (rgba) {
        unshadowedColor = rgba;
        expect(rgba).not.toEqual(backgroundColor);
      });

      // Render with shadows
      scene.shadowMap.enabled = true;
      var shadowedColor;
      renderAndCall(function (rgba) {
        shadowedColor = rgba;
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).not.toEqual(unshadowedColor);
      });

      // Hide floor temporarily and change the shadow map
      floor.show = false;
      scene.shadowMap.debugCascadeColors = true;

      // Render a few frames
      var i;
      for (i = 0; i < 6; ++i) {
        scene.render();
      }

      // Show the floor and render. The receive shadows shader should now be up-to-date.
      floor.show = true;
      renderAndCall(function (rgba) {
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).not.toEqual(unshadowedColor);
        expect(rgba).not.toEqual(shadowedColor);
      });

      // Render a few more frames
      for (i = 0; i < 6; ++i) {
        scene.render();
      }

      // When using WebGL, this value is 8. When using the stub, this value is 4.
      expect(spy1.calls.count()).toBeLessThanOrEqualTo(8);
      expect(spy2.calls.count()).toEqual(4);

      box.show = false;
      floor.show = false;
    });

    it("does not receive shadows if fromLightSource is false", function () {
      box.show = true;
      floorTranslucent.show = true;
      createCascadedShadowMap();
      scene.shadowMap.fromLightSource = false;

      // Render without shadows
      scene.shadowMap.enabled = false;
      var unshadowedColor;
      renderAndCall(function (rgba) {
        unshadowedColor = rgba;
        expect(rgba).not.toEqual(backgroundColor);
      });

      // Render with shadows
      scene.shadowMap.enabled = true;
      renderAndCall(function (rgba) {
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).toEqual(unshadowedColor);
      });
    });

    it("tweaking shadow bias parameters works", function () {
      box.show = true;
      floor.show = true;
      createCascadedShadowMap();

      // Render without shadows
      scene.shadowMap.enabled = false;
      var unshadowedColor;
      renderAndCall(function (rgba) {
        unshadowedColor = rgba;
        expect(rgba).not.toEqual(backgroundColor);
      });

      // Render with shadows
      scene.shadowMap.enabled = true;
      var shadowedColor;
      renderAndCall(function (rgba) {
        shadowedColor = rgba;
        expect(rgba).not.toEqual(backgroundColor);
        expect(rgba).not.toEqual(unshadowedColor);
      });

      scene.shadowMap._primitiveBias.polygonOffsetFactor = 1.2;
      scene.shadowMap._primitiveBias.polygonOffsetFactor = 4.1;
      scene.shadowMap._primitiveBias.normalOffsetScale = 2.1;
      scene.shadowMap._primitiveBias.normalShadingSmooth = 0.4;
      scene.shadowMap.debugCreateRenderStates();
      scene.shadowMap.dirty = true;
      renderAndExpect(shadowedColor);

      scene.shadowMap._primitiveBias.normalOffset = false;
      scene.shadowMap._primitiveBias.normalShading = false;
      scene.shadowMap._primitiveBias.polygonOffset = false;
      scene.shadowMap.debugCreateRenderStates();
      scene.shadowMap.dirty = true;
      renderAndExpect(shadowedColor);
    });

    it("destroys", function () {
      box.show = true;
      floor.show = true;
      createCascadedShadowMap();

      expect(scene.shadowMap.isDestroyed()).toEqual(false);
      scene.shadowMap.destroy();
      expect(scene.shadowMap.isDestroyed()).toEqual(true);
      scene.shadowMap = undefined;
    });
  },
  "WebGL"
);