import BoundingRectangle from "../Core/BoundingRectangle.js";
import Cartesian2 from "../Core/Cartesian2.js";
import Cartesian3 from "../Core/Cartesian3.js";
import Cartesian4 from "../Core/Cartesian4.js";
import Cartographic from "../Core/Cartographic.js";
import Color from "../Core/Color.js";
import defined from "../Core/defined.js";
import Ellipsoid from "../Core/Ellipsoid.js";
import EncodedCartesian3 from "../Core/EncodedCartesian3.js";
import CesiumMath from "../Core/Math.js";
import Matrix3 from "../Core/Matrix3.js";
import Matrix4 from "../Core/Matrix4.js";
import OrthographicFrustum from "../Core/OrthographicFrustum.js";
import Simon1994PlanetaryPositions from "../Core/Simon1994PlanetaryPositions.js";
import Transforms from "../Core/Transforms.js";
import SceneMode from "../Scene/SceneMode.js";
import SunLight from "../Scene/SunLight.js";

/**
 * @private
 * @constructor
 */
function UniformState() {
  /**
   * @type {Texture}
   */
  this.globeDepthTexture = undefined;
  /**
   * @type {Texture}
   */
  this.edgeIdTexture = undefined;
  /**
   * @type {Texture}
   */
  this.edgeColorTexture = undefined;
  /**
   * @type {Texture}
   */
  this.edgeDepthTexture = undefined; // packed depth color attachment from edge pass
  /**
   * @type {number}
   */
  this.gamma = undefined;

  this._viewport = new BoundingRectangle();
  this._viewportCartesian4 = new Cartesian4();
  this._viewportDirty = false;
  this._viewportOrthographicMatrix = Matrix4.clone(Matrix4.IDENTITY);
  this._viewportTransformation = Matrix4.clone(Matrix4.IDENTITY);

  this._model = Matrix4.clone(Matrix4.IDENTITY);
  this._view = Matrix4.clone(Matrix4.IDENTITY);
  this._inverseView = Matrix4.clone(Matrix4.IDENTITY);
  this._projection = Matrix4.clone(Matrix4.IDENTITY);
  this._infiniteProjection = Matrix4.clone(Matrix4.IDENTITY);
  this._entireFrustum = new Cartesian2();
  this._currentFrustum = new Cartesian2();
  this._frustumPlanes = new Cartesian4();
  this._farDepthFromNearPlusOne = undefined;
  this._log2FarDepthFromNearPlusOne = undefined;
  this._oneOverLog2FarDepthFromNearPlusOne = undefined;

  this._frameState = undefined;
  this._temeToPseudoFixed = Matrix3.clone(Matrix4.IDENTITY);

  // Derived members
  this._view3DDirty = true;
  this._view3D = new Matrix4();

  this._inverseView3DDirty = true;
  this._inverseView3D = new Matrix4();

  this._inverseModelDirty = true;
  this._inverseModel = new Matrix4();

  this._inverseTransposeModelDirty = true;
  this._inverseTransposeModel = new Matrix3();

  this._viewRotation = new Matrix3();
  this._inverseViewRotation = new Matrix3();

  this._viewRotation3D = new Matrix3();
  this._inverseViewRotation3D = new Matrix3();

  this._inverseProjectionDirty = true;
  this._inverseProjection = new Matrix4();

  this._modelViewDirty = true;
  this._modelView = new Matrix4();

  this._modelView3DDirty = true;
  this._modelView3D = new Matrix4();

  this._modelViewRelativeToEyeDirty = true;
  this._modelViewRelativeToEye = new Matrix4();

  this._inverseModelViewDirty = true;
  this._inverseModelView = new Matrix4();

  this._inverseModelView3DDirty = true;
  this._inverseModelView3D = new Matrix4();

  this._viewProjectionDirty = true;
  this._viewProjection = new Matrix4();

  this._inverseViewProjectionDirty = true;
  this._inverseViewProjection = new Matrix4();

  this._modelViewProjectionDirty = true;
  this._modelViewProjection = new Matrix4();

  this._inverseModelViewProjectionDirty = true;
  this._inverseModelViewProjection = new Matrix4();

  this._modelViewProjectionRelativeToEyeDirty = true;
  this._modelViewProjectionRelativeToEye = new Matrix4();

  this._modelViewInfiniteProjectionDirty = true;
  this._modelViewInfiniteProjection = new Matrix4();

  this._normalDirty = true;
  this._normal = new Matrix3();

  this._normal3DDirty = true;
  this._normal3D = new Matrix3();

  this._inverseNormalDirty = true;
  this._inverseNormal = new Matrix3();

  this._inverseNormal3DDirty = true;
  this._inverseNormal3D = new Matrix3();

  this._encodedCameraPositionMCDirty = true;
  this._encodedCameraPositionMC = new EncodedCartesian3();
  this._cameraPosition = new Cartesian3();

  this._sunPositionWC = new Cartesian3();
  this._sunPositionColumbusView = new Cartesian3();
  this._sunDirectionWC = new Cartesian3();
  this._sunDirectionEC = new Cartesian3();
  this._moonDirectionEC = new Cartesian3();

  this._lightDirectionWC = new Cartesian3();
  this._lightDirectionEC = new Cartesian3();
  this._lightColor = new Cartesian3();
  this._lightColorHdr = new Cartesian3();

  this._pass = undefined;
  this._mode = undefined;
  this._mapProjection = undefined;
  this._ellipsoid = undefined;
  this._cameraDirection = new Cartesian3();
  this._cameraRight = new Cartesian3();
  this._cameraUp = new Cartesian3();
  this._frustum2DWidth = 0.0;
  this._eyeHeight = 0.0;
  this._eyeHeight2D = new Cartesian2();
  this._eyeEllipsoidNormalEC = new Cartesian3();
  this._eyeEllipsoidCurvature = new Cartesian2();
  this._modelToEnu = new Matrix4();
  this._enuToModel = new Matrix4();
  this._pixelRatio = 1.0;
  this._orthographicIn3D = false;
  this._backgroundColor = new Color();

  this._brdfLut = undefined;
  this._environmentMap = undefined;

  this._sphericalHarmonicCoefficients = undefined;
  this._specularEnvironmentMaps = undefined;
  this._specularEnvironmentMapsMaximumLOD = undefined;

  this._fogDensity = undefined;
  this._fogVisualDensityScalar = undefined;
  this._fogMinimumBrightness = undefined;

  this._atmosphereHsbShift = undefined;
  this._atmosphereLightIntensity = undefined;
  this._atmosphereRayleighCoefficient = new Cartesian3();
  this._atmosphereRayleighScaleHeight = new Cartesian3();
  this._atmosphereMieCoefficient = new Cartesian3();
  this._atmosphereMieScaleHeight = undefined;
  this._atmosphereMieAnisotropy = undefined;
  this._atmosphereDynamicLighting = undefined;

  this._invertClassificationColor = undefined;

  this._splitPosition = 0.0;
  this._pixelSizePerMeter = undefined;
  this._geometricToleranceOverMeter = undefined;

  this._minimumDisableDepthTestDistance = undefined;
}

Object.defineProperties(UniformState.prototype, {
  /**
   * @memberof UniformState.prototype
   * @type {FrameState}
   * @readonly
   */
  frameState: {
    get: function () {
      return this._frameState;
    },
  },
  /**
   * @memberof UniformState.prototype
   * @type {BoundingRectangle}
   */
  viewport: {
    get: function () {
      return this._viewport;
    },
    set: function (viewport) {
      if (!BoundingRectangle.equals(viewport, this._viewport)) {
        BoundingRectangle.clone(viewport, this._viewport);

        const v = this._viewport;
        const vc = this._viewportCartesian4;
        vc.x = v.x;
        vc.y = v.y;
        vc.z = v.width;
        vc.w = v.height;

        this._viewportDirty = true;
      }
    },
  },

  /**
   * @memberof UniformState.prototype
   * @private
   */
  viewportCartesian4: {
    get: function () {
      return this._viewportCartesian4;
    },
  },

  viewportOrthographic: {
    get: function () {
      cleanViewport(this);
      return this._viewportOrthographicMatrix;
    },
  },

  viewportTransformation: {
    get: function () {
      cleanViewport(this);
      return this._viewportTransformation;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  model: {
    get: function () {
      return this._model;
    },
    set: function (matrix) {
      Matrix4.clone(matrix, this._model);

      this._modelView3DDirty = true;
      this._inverseModelView3DDirty = true;
      this._inverseModelDirty = true;
      this._inverseTransposeModelDirty = true;
      this._modelViewDirty = true;
      this._inverseModelViewDirty = true;
      this._modelViewRelativeToEyeDirty = true;
      this._inverseModelViewDirty = true;
      this._modelViewProjectionDirty = true;
      this._inverseModelViewProjectionDirty = true;
      this._modelViewProjectionRelativeToEyeDirty = true;
      this._modelViewInfiniteProjectionDirty = true;
      this._normalDirty = true;
      this._inverseNormalDirty = true;
      this._normal3DDirty = true;
      this._inverseNormal3DDirty = true;
      this._encodedCameraPositionMCDirty = true;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  inverseModel: {
    get: function () {
      if (this._inverseModelDirty) {
        this._inverseModelDirty = false;

        Matrix4.inverse(this._model, this._inverseModel);
      }

      return this._inverseModel;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @private
   */
  inverseTransposeModel: {
    get: function () {
      const m = this._inverseTransposeModel;
      if (this._inverseTransposeModelDirty) {
        this._inverseTransposeModelDirty = false;

        Matrix4.getMatrix3(this.inverseModel, m);
        Matrix3.transpose(m, m);
      }

      return m;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  view: {
    get: function () {
      return this._view;
    },
  },

  /**
   * The 3D view matrix.  In 3D mode, this is identical to {@link UniformState#view},
   * but in 2D and Columbus View it is a synthetic matrix based on the equivalent position
   * of the camera in the 3D world.
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  view3D: {
    get: function () {
      updateView3D(this);
      return this._view3D;
    },
  },

  /**
   * The 3x3 rotation matrix of the current view matrix ({@link UniformState#view}).
   * @memberof UniformState.prototype
   * @type {Matrix3}
   */
  viewRotation: {
    get: function () {
      updateView3D(this);
      return this._viewRotation;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix3}
   */
  viewRotation3D: {
    get: function () {
      updateView3D(this);
      return this._viewRotation3D;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  inverseView: {
    get: function () {
      return this._inverseView;
    },
  },

  /**
   * the 4x4 inverse-view matrix that transforms from eye to 3D world coordinates.  In 3D mode, this is
   * identical to {@link UniformState#inverseView}, but in 2D and Columbus View it is a synthetic matrix
   * based on the equivalent position of the camera in the 3D world.
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  inverseView3D: {
    get: function () {
      updateInverseView3D(this);
      return this._inverseView3D;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix3}
   */
  inverseViewRotation: {
    get: function () {
      return this._inverseViewRotation;
    },
  },

  /**
   * The 3x3 rotation matrix of the current 3D inverse-view matrix ({@link UniformState#inverseView3D}).
   * @memberof UniformState.prototype
   * @type {Matrix3}
   */
  inverseViewRotation3D: {
    get: function () {
      updateInverseView3D(this);
      return this._inverseViewRotation3D;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  projection: {
    get: function () {
      return this._projection;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  inverseProjection: {
    get: function () {
      cleanInverseProjection(this);
      return this._inverseProjection;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  infiniteProjection: {
    get: function () {
      return this._infiniteProjection;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  modelView: {
    get: function () {
      cleanModelView(this);
      return this._modelView;
    },
  },

  /**
   * The 3D model-view matrix.  In 3D mode, this is equivalent to {@link UniformState#modelView}.  In 2D and
   * Columbus View, however, it is a synthetic matrix based on the equivalent position of the camera in the 3D world.
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  modelView3D: {
    get: function () {
      cleanModelView3D(this);
      return this._modelView3D;
    },
  },

  /**
   * Model-view relative to eye matrix.
   *
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  modelViewRelativeToEye: {
    get: function () {
      cleanModelViewRelativeToEye(this);
      return this._modelViewRelativeToEye;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  inverseModelView: {
    get: function () {
      cleanInverseModelView(this);
      return this._inverseModelView;
    },
  },

  /**
   * The inverse of the 3D model-view matrix.  In 3D mode, this is equivalent to {@link UniformState#inverseModelView}.
   * In 2D and Columbus View, however, it is a synthetic matrix based on the equivalent position of the camera in the 3D world.
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  inverseModelView3D: {
    get: function () {
      cleanInverseModelView3D(this);
      return this._inverseModelView3D;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  viewProjection: {
    get: function () {
      cleanViewProjection(this);
      return this._viewProjection;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  inverseViewProjection: {
    get: function () {
      cleanInverseViewProjection(this);
      return this._inverseViewProjection;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  modelViewProjection: {
    get: function () {
      cleanModelViewProjection(this);
      return this._modelViewProjection;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  inverseModelViewProjection: {
    get: function () {
      cleanInverseModelViewProjection(this);
      return this._inverseModelViewProjection;
    },
  },

  /**
   * Model-view-projection relative to eye matrix.
   *
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  modelViewProjectionRelativeToEye: {
    get: function () {
      cleanModelViewProjectionRelativeToEye(this);
      return this._modelViewProjectionRelativeToEye;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  modelViewInfiniteProjection: {
    get: function () {
      cleanModelViewInfiniteProjection(this);
      return this._modelViewInfiniteProjection;
    },
  },

  /**
   * A 3x3 normal transformation matrix that transforms normal vectors in model coordinates to
   * eye coordinates.
   * @memberof UniformState.prototype
   * @type {Matrix3}
   */
  normal: {
    get: function () {
      cleanNormal(this);
      return this._normal;
    },
  },

  /**
   * A 3x3 normal transformation matrix that transforms normal vectors in 3D model
   * coordinates to eye coordinates.  In 3D mode, this is identical to
   * {@link UniformState#normal}, but in 2D and Columbus View it represents the normal transformation
   * matrix as if the camera were at an equivalent location in 3D mode.
   * @memberof UniformState.prototype
   * @type {Matrix3}
   */
  normal3D: {
    get: function () {
      cleanNormal3D(this);
      return this._normal3D;
    },
  },

  /**
   * An inverse 3x3 normal transformation matrix that transforms normal vectors in model coordinates
   * to eye coordinates.
   * @memberof UniformState.prototype
   * @type {Matrix3}
   */
  inverseNormal: {
    get: function () {
      cleanInverseNormal(this);
      return this._inverseNormal;
    },
  },

  /**
   * An inverse 3x3 normal transformation matrix that transforms normal vectors in eye coordinates
   * to 3D model coordinates.  In 3D mode, this is identical to
   * {@link UniformState#inverseNormal}, but in 2D and Columbus View it represents the normal transformation
   * matrix as if the camera were at an equivalent location in 3D mode.
   * @memberof UniformState.prototype
   * @type {Matrix3}
   */
  inverseNormal3D: {
    get: function () {
      cleanInverseNormal3D(this);
      return this._inverseNormal3D;
    },
  },

  /**
   * The near distance (<code>x</code>) and the far distance (<code>y</code>) of the frustum defined by the camera.
   * This is the largest possible frustum, not an individual frustum used for multi-frustum rendering.
   * @memberof UniformState.prototype
   * @type {Cartesian2}
   */
  entireFrustum: {
    get: function () {
      return this._entireFrustum;
    },
  },

  /**
   * The near distance (<code>x</code>) and the far distance (<code>y</code>) of the frustum defined by the camera.
   * This is the individual frustum used for multi-frustum rendering.
   * @memberof UniformState.prototype
   * @type {Cartesian2}
   */
  currentFrustum: {
    get: function () {
      return this._currentFrustum;
    },
  },

  /**
   * The distances to the frustum planes. The top, bottom, left and right distances are
   * the x, y, z, and w components, respectively.
   * @memberof UniformState.prototype
   * @type {Cartesian4}
   */
  frustumPlanes: {
    get: function () {
      return this._frustumPlanes;
    },
  },

  /**
   * The far plane's distance from the near plane, plus 1.0.
   *
   * @memberof UniformState.prototype
   * @type {number}
   */
  farDepthFromNearPlusOne: {
    get: function () {
      return this._farDepthFromNearPlusOne;
    },
  },

  /**
   * The log2 of {@link UniformState#farDepthFromNearPlusOne}.
   *
   * @memberof UniformState.prototype
   * @type {number}
   */
  log2FarDepthFromNearPlusOne: {
    get: function () {
      return this._log2FarDepthFromNearPlusOne;
    },
  },

  /**
   * 1.0 divided by {@link UniformState#log2FarDepthFromNearPlusOne}.
   *
   * @memberof UniformState.prototype
   * @type {number}
   */
  oneOverLog2FarDepthFromNearPlusOne: {
    get: function () {
      return this._oneOverLog2FarDepthFromNearPlusOne;
    },
  },

  /**
   * The height in meters of the eye (camera) above or below the ellipsoid.
   * @memberof UniformState.prototype
   * @type {number}
   */
  eyeHeight: {
    get: function () {
      return this._eyeHeight;
    },
  },

  /**
   * The height (<code>x</code>) and the height squared (<code>y</code>)
   * in meters of the eye (camera) above the 2D world plane. This uniform is only valid
   * when the {@link SceneMode} is <code>SCENE2D</code>.
   * @memberof UniformState.prototype
   * @type {Cartesian2}
   */
  eyeHeight2D: {
    get: function () {
      return this._eyeHeight2D;
    },
  },

  /**
   * The ellipsoid surface normal at the camera position, in model coordinates.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  eyeEllipsoidNormalEC: {
    get: function () {
      return this._eyeEllipsoidNormalEC;
    },
  },

  /**
   * The ellipsoid radii of curvature at the camera position.
   * The .x component is the prime vertical radius, .y is the meridional.
   * @memberof UniformState.prototype
   * @type {Cartesian2}
   */
  eyeEllipsoidCurvature: {
    get: function () {
      return this._eyeEllipsoidCurvature;
    },
  },

  /**
   * A transform from model coordinates to an east-north-up coordinate system
   * centered at the position on the ellipsoid below the camera
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  modelToEnu: {
    get: function () {
      return this._modelToEnu;
    },
  },

  /**
   * The inverse of {@link UniformState.prototype.modelToEnu}
   * @memberof UniformState.prototype
   * @type {Matrix4}
   */
  enuToModel: {
    get: function () {
      return this._enuToModel;
    },
  },

  /**
   * The sun position in 3D world coordinates at the current scene time.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  sunPositionWC: {
    get: function () {
      return this._sunPositionWC;
    },
  },

  /**
   * The sun position in 2D world coordinates at the current scene time.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  sunPositionColumbusView: {
    get: function () {
      return this._sunPositionColumbusView;
    },
  },

  /**
   * A normalized vector to the sun in 3D world coordinates at the current scene time.  Even in 2D or
   * Columbus View mode, this returns the direction to the sun in the 3D scene.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  sunDirectionWC: {
    get: function () {
      return this._sunDirectionWC;
    },
  },

  /**
   * A normalized vector to the sun in eye coordinates at the current scene time.  In 3D mode, this
   * returns the actual vector from the camera position to the sun position.  In 2D and Columbus View, it returns
   * the vector from the equivalent 3D camera position to the position of the sun in the 3D scene.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  sunDirectionEC: {
    get: function () {
      return this._sunDirectionEC;
    },
  },

  /**
   * A normalized vector to the moon in eye coordinates at the current scene time.  In 3D mode, this
   * returns the actual vector from the camera position to the moon position.  In 2D and Columbus View, it returns
   * the vector from the equivalent 3D camera position to the position of the moon in the 3D scene.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  moonDirectionEC: {
    get: function () {
      return this._moonDirectionEC;
    },
  },

  /**
   * A normalized vector to the scene's light source in 3D world coordinates.  Even in 2D or
   * Columbus View mode, this returns the direction to the light in the 3D scene.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  lightDirectionWC: {
    get: function () {
      return this._lightDirectionWC;
    },
  },

  /**
   * A normalized vector to the scene's light source in eye coordinates.  In 3D mode, this
   * returns the actual vector from the camera position to the light.  In 2D and Columbus View, it returns
   * the vector from the equivalent 3D camera position in the 3D scene.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  lightDirectionEC: {
    get: function () {
      return this._lightDirectionEC;
    },
  },

  /**
   * The color of light emitted by the scene's light source. This is equivalent to the light
   * color multiplied by the light intensity limited to a maximum luminance of 1.0 suitable
   * for non-HDR lighting.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  lightColor: {
    get: function () {
      return this._lightColor;
    },
  },

  /**
   * The high dynamic range color of light emitted by the scene's light source. This is equivalent to
   * the light color multiplied by the light intensity suitable for HDR lighting.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  lightColorHdr: {
    get: function () {
      return this._lightColorHdr;
    },
  },

  /**
   * The high bits of the camera position.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  encodedCameraPositionMCHigh: {
    get: function () {
      cleanEncodedCameraPositionMC(this);
      return this._encodedCameraPositionMC.high;
    },
  },

  /**
   * The low bits of the camera position.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  encodedCameraPositionMCLow: {
    get: function () {
      cleanEncodedCameraPositionMC(this);
      return this._encodedCameraPositionMC.low;
    },
  },

  /**
   * A 3x3 matrix that transforms from True Equator Mean Equinox (TEME) axes to the
   * pseudo-fixed axes at the Scene's current time.
   * @memberof UniformState.prototype
   * @type {Matrix3}
   */
  temeToPseudoFixedMatrix: {
    get: function () {
      return this._temeToPseudoFixed;
    },
  },

  /**
   * Gets the scaling factor for transforming from the canvas
   * pixel space to canvas coordinate space.
   * @memberof UniformState.prototype
   * @type {number}
   */
  pixelRatio: {
    get: function () {
      return this._pixelRatio;
    },
  },

  /**
   * A scalar used to mix a color with the fog color based on the distance to the camera.
   * @memberof UniformState.prototype
   * @type {number}
   */
  fogDensity: {
    get: function () {
      return this._fogDensity;
    },
  },

  /**
   * A scalar used to mix a color with the fog color based on the distance to the camera.
   * @memberof UniformState.prototype
   * @type {number}
   */
  fogVisualDensityScalar: {
    get: function () {
      return this._fogVisualDensityScalar;
    },
  },

  /**
   * A scalar used as a minimum value when brightening fog
   * @memberof UniformState.prototype
   * @type {number}
   */
  fogMinimumBrightness: {
    get: function () {
      return this._fogMinimumBrightness;
    },
  },

  /**
   * A color shift to apply to the atmosphere color in HSB.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  atmosphereHsbShift: {
    get: function () {
      return this._atmosphereHsbShift;
    },
  },
  /**
   * The intensity of the light that is used for computing the atmosphere color
   * @memberof UniformState.prototype
   * @type {number}
   */
  atmosphereLightIntensity: {
    get: function () {
      return this._atmosphereLightIntensity;
    },
  },
  /**
   * The Rayleigh scattering coefficient used in the atmospheric scattering equations for the sky atmosphere.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  atmosphereRayleighCoefficient: {
    get: function () {
      return this._atmosphereRayleighCoefficient;
    },
  },
  /**
   * The Rayleigh scale height used in the atmospheric scattering equations for the sky atmosphere, in meters.
   * @memberof UniformState.prototype
   * @type {number}
   */
  atmosphereRayleighScaleHeight: {
    get: function () {
      return this._atmosphereRayleighScaleHeight;
    },
  },
  /**
   * The Mie scattering coefficient used in the atmospheric scattering equations for the sky atmosphere.
   * @memberof UniformState.prototype
   * @type {Cartesian3}
   */
  atmosphereMieCoefficient: {
    get: function () {
      return this._atmosphereMieCoefficient;
    },
  },
  /**
   * The Mie scale height used in the atmospheric scattering equations for the sky atmosphere, in meters.
   * @memberof UniformState.prototype
   * @type {number}
   */
  atmosphereMieScaleHeight: {
    get: function () {
      return this._atmosphereMieScaleHeight;
    },
  },
  /**
   * The anisotropy of the medium to consider for Mie scattering.
   * @memberof UniformState.prototype
   * @type {number}
   */
  atmosphereMieAnisotropy: {
    get: function () {
      return this._atmosphereMieAnisotropy;
    },
  },
  /**
   * Which light source to use for dynamically lighting the atmosphere
   *
   * @memberof UniformState.prototype
   * @type {DynamicAtmosphereLightingType}
   */
  atmosphereDynamicLighting: {
    get: function () {
      return this._atmosphereDynamicLighting;
    },
  },

  /**
   * A scalar that represents the geometric tolerance per meter
   * @memberof UniformState.prototype
   * @type {number}
   */
  geometricToleranceOverMeter: {
    get: function () {
      return this._geometricToleranceOverMeter;
    },
  },

  /**
   * @memberof UniformState.prototype
   * @type {Pass}
   */
  pass: {
    get: function () {
      return this._pass;
    },
  },

  /**
   * The current background color
   * @memberof UniformState.prototype
   * @type {Color}
   */
  backgroundColor: {
    get: function () {
      return this._backgroundColor;
    },
  },

  /**
   * The look up texture used to find the BRDF for a material
   * @memberof UniformState.prototype
   * @type {Texture}
   */
  brdfLut: {
    get: function () {
      return this._brdfLut;
    },
  },

  /**
   * The environment map of the scene
   * @memberof UniformState.prototype
   * @type {CubeMap}
   */
  environmentMap: {
    get: function () {
      return this._environmentMap;
    },
  },

  /**
   * The spherical harmonic coefficients of the scene.
   * @memberof UniformState.prototype
   * @type {Cartesian3[]}
   */
  sphericalHarmonicCoefficients: {
    get: function () {
      return this._sphericalHarmonicCoefficients;
    },
  },

  /**
   * The specular environment cube map of the scene.
   * @memberof UniformState.prototype
   * @type {Texture}
   */
  specularEnvironmentMaps: {
    get: function () {
      return this._specularEnvironmentMaps;
    },
  },

  /**
   * The maximum level-of-detail of the specular environment cube map of the scene.
   * @memberof UniformState.prototype
   * @type {number}
   */
  specularEnvironmentMapsMaximumLOD: {
    get: function () {
      return this._specularEnvironmentMapsMaximumLOD;
    },
  },

  /**
   * The splitter position to use when rendering with a splitter. This will be in pixel coordinates relative to the canvas.
   * @memberof UniformState.prototype
   * @type {number}
   */
  splitPosition: {
    get: function () {
      return this._splitPosition;
    },
  },

  /**
   * The distance from the camera at which to disable the depth test of billboards, labels and points
   * to, for example, prevent clipping against terrain. When set to zero, the depth test should always
   * be applied. When less than zero, the depth test should never be applied.
   *
   * @memberof UniformState.prototype
   * @type {number}
   */
  minimumDisableDepthTestDistance: {
    get: function () {
      return this._minimumDisableDepthTestDistance;
    },
  },

  /**
   * The highlight color of unclassified 3D Tiles.
   *
   * @memberof UniformState.prototype
   * @type {Color}
   */
  invertClassificationColor: {
    get: function () {
      return this._invertClassificationColor;
    },
  },

  /**
   * Whether or not the current projection is orthographic in 3D.
   *
   * @memberOf UniformState.prototype
   * @type {boolean}
   */
  orthographicIn3D: {
    get: function () {
      return this._orthographicIn3D;
    },
  },

  /**
   * The current ellipsoid.
   *
   * @memberOf UniformState.prototype
   * @type {Ellipsoid}
   */
  ellipsoid: {
    get: function () {
      return this._ellipsoid ?? Ellipsoid.default;
    },
  },
});

function setView(uniformState, matrix) {
  Matrix4.clone(matrix, uniformState._view);
  Matrix4.getMatrix3(matrix, uniformState._viewRotation);

  uniformState._view3DDirty = true;
  uniformState._inverseView3DDirty = true;
  uniformState._modelViewDirty = true;
  uniformState._modelView3DDirty = true;
  uniformState._modelViewRelativeToEyeDirty = true;
  uniformState._inverseModelViewDirty = true;
  uniformState._inverseModelView3DDirty = true;
  uniformState._viewProjectionDirty = true;
  uniformState._inverseViewProjectionDirty = true;
  uniformState._modelViewProjectionDirty = true;
  uniformState._modelViewProjectionRelativeToEyeDirty = true;
  uniformState._modelViewInfiniteProjectionDirty = true;
  uniformState._normalDirty = true;
  uniformState._inverseNormalDirty = true;
  uniformState._normal3DDirty = true;
  uniformState._inverseNormal3DDirty = true;
}

function setInverseView(uniformState, matrix) {
  Matrix4.clone(matrix, uniformState._inverseView);
  Matrix4.getMatrix3(matrix, uniformState._inverseViewRotation);
}

function setProjection(uniformState, matrix) {
  Matrix4.clone(matrix, uniformState._projection);

  uniformState._inverseProjectionDirty = true;
  uniformState._viewProjectionDirty = true;
  uniformState._inverseViewProjectionDirty = true;
  uniformState._modelViewProjectionDirty = true;
  uniformState._modelViewProjectionRelativeToEyeDirty = true;
}

function setInfiniteProjection(uniformState, matrix) {
  Matrix4.clone(matrix, uniformState._infiniteProjection);

  uniformState._modelViewInfiniteProjectionDirty = true;
}

const surfacePositionScratch = new Cartesian3();
const enuTransformScratch = new Matrix4();

function setCamera(uniformState, camera) {
  Cartesian3.clone(camera.positionWC, uniformState._cameraPosition);
  Cartesian3.clone(camera.directionWC, uniformState._cameraDirection);
  Cartesian3.clone(camera.rightWC, uniformState._cameraRight);
  Cartesian3.clone(camera.upWC, uniformState._cameraUp);

  const ellipsoid = uniformState._ellipsoid;
  let surfacePosition;

  const positionCartographic = camera.positionCartographic;
  if (!defined(positionCartographic)) {
    uniformState._eyeHeight = -ellipsoid.maximumRadius;
    if (Cartesian3.magnitude(camera.positionWC) > 0.0) {
      uniformState._eyeEllipsoidNormalEC = Cartesian3.normalize(
        camera.positionWC,
        uniformState._eyeEllipsoidNormalEC,
      );
    }
    surfacePosition = ellipsoid.scaleToGeodeticSurface(
      camera.positionWC,
      surfacePositionScratch,
    );
  } else {
    uniformState._eyeHeight = positionCartographic.height;
    uniformState._eyeEllipsoidNormalEC =
      ellipsoid.geodeticSurfaceNormalCartographic(
        positionCartographic,
        uniformState._eyeEllipsoidNormalEC,
      );
    surfacePosition = Cartesian3.fromRadians(
      positionCartographic.longitude,
      positionCartographic.latitude,
      0.0,
      ellipsoid,
      surfacePositionScratch,
    );
  }

  uniformState._encodedCameraPositionMCDirty = true;

  if (!defined(surfacePosition)) {
    return;
  }

  uniformState._eyeEllipsoidNormalEC = Matrix3.multiplyByVector(
    uniformState._viewRotation,
    uniformState._eyeEllipsoidNormalEC,
    uniformState._eyeEllipsoidNormalEC,
  );

  const enuToWorld = Transforms.eastNorthUpToFixedFrame(
    surfacePosition,
    ellipsoid,
    enuTransformScratch,
  );
  uniformState._enuToModel = Matrix4.multiplyTransformation(
    uniformState.inverseModel,
    enuToWorld,
    uniformState._enuToModel,
  );
  uniformState._modelToEnu = Matrix4.inverseTransformation(
    uniformState._enuToModel,
    uniformState._modelToEnu,
  );

  if (
    !CesiumMath.equalsEpsilon(
      ellipsoid._radii.x,
      ellipsoid._radii.y,
      CesiumMath.EPSILON15,
    )
  ) {
    // Ellipsoid curvature calculations assume radii.x === radii.y as is true for WGS84
    return;
  }

  uniformState._eyeEllipsoidCurvature = ellipsoid.getLocalCurvature(
    surfacePosition,
    uniformState._eyeEllipsoidCurvature,
  );
}

const transformMatrix = new Matrix3();
const sunCartographicScratch = new Cartographic();
function setSunAndMoonDirections(uniformState, frameState) {
  Transforms.computeIcrfToCentralBodyFixedMatrix(
    frameState.time,
    transformMatrix,
  );

  let position =
    Simon1994PlanetaryPositions.computeSunPositionInEarthInertialFrame(
      frameState.time,
      uniformState._sunPositionWC,
    );
  Matrix3.multiplyByVector(transformMatrix, position, position);

  Cartesian3.normalize(position, uniformState._sunDirectionWC);

  position = Matrix3.multiplyByVector(
    uniformState.viewRotation3D,
    position,
    uniformState._sunDirectionEC,
  );
  Cartesian3.normalize(position, position);

  position =
    Simon1994PlanetaryPositions.computeMoonPositionInEarthInertialFrame(
      frameState.time,
      uniformState._moonDirectionEC,
    );
  Matrix3.multiplyByVector(transformMatrix, position, position);
  Matrix3.multiplyByVector(uniformState.viewRotation3D, position, position);
  Cartesian3.normalize(position, position);

  const projection = frameState.mapProjection;
  const ellipsoid = projection.ellipsoid;
  const sunCartographic = ellipsoid.cartesianToCartographic(
    uniformState._sunPositionWC,
    sunCartographicScratch,
  );
  projection.project(sunCartographic, uniformState._sunPositionColumbusView);
}

/**
 * Synchronizes the frustum's state with the camera state.  This is called
 * by the {@link Scene} when rendering to ensure that automatic GLSL uniforms
 * are set to the right value.
 *
 * @param {object} camera The camera to synchronize with.
 */
UniformState.prototype.updateCamera = function (camera) {
  setView(this, camera.viewMatrix);
  setInverseView(this, camera.inverseViewMatrix);
  setCamera(this, camera);

  this._entireFrustum.x = camera.frustum.near;
  this._entireFrustum.y = camera.frustum.far;
  this.updateFrustum(camera.frustum);

  this._orthographicIn3D =
    this._mode !== SceneMode.SCENE2D &&
    camera.frustum instanceof OrthographicFrustum;
};

/**
 * Synchronizes the frustum's state with the uniform state.  This is called
 * by the {@link Scene} when rendering to ensure that automatic GLSL uniforms
 * are set to the right value.
 *
 * @param {object} frustum The frustum to synchronize with.
 */
UniformState.prototype.updateFrustum = function (frustum) {
  // If any frustum parameters have changed, calling the frustum.projectionMatrix
  // getter will recompute the projection before it is copied.
  setProjection(this, frustum.projectionMatrix);

  if (defined(frustum.infiniteProjectionMatrix)) {
    setInfiniteProjection(this, frustum.infiniteProjectionMatrix);
  }
  this._currentFrustum.x = frustum.near;
  this._currentFrustum.y = frustum.far;

  this._farDepthFromNearPlusOne = frustum.far - frustum.near + 1.0;
  this._log2FarDepthFromNearPlusOne = CesiumMath.log2(
    this._farDepthFromNearPlusOne,
  );
  this._oneOverLog2FarDepthFromNearPlusOne =
    1.0 / this._log2FarDepthFromNearPlusOne;

  const offCenterFrustum = frustum.offCenterFrustum;
  if (defined(offCenterFrustum)) {
    frustum = offCenterFrustum;
  }

  this._frustumPlanes.x = frustum.top;
  this._frustumPlanes.y = frustum.bottom;
  this._frustumPlanes.z = frustum.left;
  this._frustumPlanes.w = frustum.right;
};

UniformState.prototype.updatePass = function (pass) {
  this._pass = pass;
};

const EMPTY_ARRAY = [];
const defaultLight = new SunLight();

/**
 * Synchronizes frame state with the uniform state.  This is called
 * by the {@link Scene} when rendering to ensure that automatic GLSL uniforms
 * are set to the right value.
 *
 * @param {FrameState} frameState The frameState to synchronize with.
 */
UniformState.prototype.update = function (frameState) {
  this._mode = frameState.mode;
  this._mapProjection = frameState.mapProjection;
  this._ellipsoid = frameState.mapProjection.ellipsoid;
  this._pixelRatio = frameState.pixelRatio;

  const camera = frameState.camera;
  this.updateCamera(camera);

  if (frameState.mode === SceneMode.SCENE2D) {
    this._frustum2DWidth = camera.frustum.right - camera.frustum.left;
    this._eyeHeight2D.x = this._frustum2DWidth * 0.5;
    this._eyeHeight2D.y = this._eyeHeight2D.x * this._eyeHeight2D.x;
  } else {
    this._frustum2DWidth = 0.0;
    this._eyeHeight2D.x = 0.0;
    this._eyeHeight2D.y = 0.0;
  }

  setSunAndMoonDirections(this, frameState);

  const light = frameState.light ?? defaultLight;
  if (light instanceof SunLight) {
    this._lightDirectionWC = Cartesian3.clone(
      this._sunDirectionWC,
      this._lightDirectionWC,
    );
    this._lightDirectionEC = Cartesian3.clone(
      this._sunDirectionEC,
      this._lightDirectionEC,
    );
  } else {
    this._lightDirectionWC = Cartesian3.normalize(
      Cartesian3.negate(light.direction, this._lightDirectionWC),
      this._lightDirectionWC,
    );
    this._lightDirectionEC = Matrix3.multiplyByVector(
      this.viewRotation3D,
      this._lightDirectionWC,
      this._lightDirectionEC,
    );
  }

  const lightColor = light.color;
  let lightColorHdr = Cartesian3.fromElements(
    lightColor.red,
    lightColor.green,
    lightColor.blue,
    this._lightColorHdr,
  );
  lightColorHdr = Cartesian3.multiplyByScalar(
    lightColorHdr,
    light.intensity,
    lightColorHdr,
  );
  const maximumComponent = Cartesian3.maximumComponent(lightColorHdr);
  if (maximumComponent > 1.0) {
    Cartesian3.divideByScalar(
      lightColorHdr,
      maximumComponent,
      this._lightColor,
    );
  } else {
    Cartesian3.clone(lightColorHdr, this._lightColor);
  }

  const brdfLutGenerator = frameState.brdfLutGenerator;
  const brdfLut = defined(brdfLutGenerator)
    ? brdfLutGenerator.colorTexture
    : undefined;
  this._brdfLut = brdfLut;

  this._environmentMap =
    frameState.environmentMap ?? frameState.context.defaultCubeMap;

  // IE 11 doesn't optimize out uniforms that are #ifdef'd out. So undefined values for the spherical harmonic
  // coefficients cause a crash.
  this._sphericalHarmonicCoefficients =
    frameState.sphericalHarmonicCoefficients ?? EMPTY_ARRAY;
  this._specularEnvironmentMaps = frameState.specularEnvironmentMaps;
  this._specularEnvironmentMapsMaximumLOD =
    frameState.specularEnvironmentMapsMaximumLOD;

  this._fogDensity = frameState.fog.density;
  this._fogVisualDensityScalar = frameState.fog.visualDensityScalar;
  this._fogMinimumBrightness = frameState.fog.minimumBrightness;

  const atmosphere = frameState.atmosphere;
  if (defined(atmosphere)) {
    this._atmosphereHsbShift = Cartesian3.fromElements(
      atmosphere.hueShift,
      atmosphere.saturationShift,
      atmosphere.brightnessShift,
      this._atmosphereHsbShift,
    );
    this._atmosphereLightIntensity = atmosphere.lightIntensity;
    this._atmosphereRayleighCoefficient = Cartesian3.clone(
      atmosphere.rayleighCoefficient,
      this._atmosphereRayleighCoefficient,
    );
    this._atmosphereRayleighScaleHeight = atmosphere.rayleighScaleHeight;
    this._atmosphereMieCoefficient = Cartesian3.clone(
      atmosphere.mieCoefficient,
      this._atmosphereMieCoefficient,
    );
    this._atmosphereMieScaleHeight = atmosphere.mieScaleHeight;
    this._atmosphereMieAnisotropy = atmosphere.mieAnisotropy;
    this._atmosphereDynamicLighting = atmosphere.dynamicLighting;
  }

  this._invertClassificationColor = frameState.invertClassificationColor;

  this._frameState = frameState;
  this._temeToPseudoFixed = Transforms.computeTemeToPseudoFixedMatrix(
    frameState.time,
    this._temeToPseudoFixed,
  );

  // Convert the relative splitPosition to absolute pixel coordinates
  this._splitPosition =
    frameState.splitPosition * frameState.context.drawingBufferWidth;
  const fov = camera.frustum.fov;
  const viewport = this._viewport;
  let pixelSizePerMeter;
  if (defined(fov)) {
    if (viewport.height > viewport.width) {
      pixelSizePerMeter = (Math.tan(0.5 * fov) * 2.0) / viewport.height;
    } else {
      pixelSizePerMeter = (Math.tan(0.5 * fov) * 2.0) / viewport.width;
    }
  } else {
    pixelSizePerMeter = 1.0 / Math.max(viewport.width, viewport.height);
  }

  this._geometricToleranceOverMeter =
    pixelSizePerMeter * frameState.maximumScreenSpaceError;
  Color.clone(frameState.backgroundColor, this._backgroundColor);

  this._minimumDisableDepthTestDistance =
    frameState.minimumDisableDepthTestDistance;
  this._minimumDisableDepthTestDistance *=
    this._minimumDisableDepthTestDistance;
  if (this._minimumDisableDepthTestDistance === Number.POSITIVE_INFINITY) {
    this._minimumDisableDepthTestDistance = -1.0;
  }
};

function cleanViewport(uniformState) {
  if (uniformState._viewportDirty) {
    const v = uniformState._viewport;
    Matrix4.computeOrthographicOffCenter(
      v.x,
      v.x + v.width,
      v.y,
      v.y + v.height,
      0.0,
      1.0,
      uniformState._viewportOrthographicMatrix,
    );
    Matrix4.computeViewportTransformation(
      v,
      0.0,
      1.0,
      uniformState._viewportTransformation,
    );
    uniformState._viewportDirty = false;
  }
}

function cleanInverseProjection(uniformState) {
  if (uniformState._inverseProjectionDirty) {
    uniformState._inverseProjectionDirty = false;

    if (
      uniformState._mode !== SceneMode.SCENE2D &&
      uniformState._mode !== SceneMode.MORPHING &&
      !uniformState._orthographicIn3D
    ) {
      Matrix4.inverse(
        uniformState._projection,
        uniformState._inverseProjection,
      );
    } else {
      Matrix4.clone(Matrix4.ZERO, uniformState._inverseProjection);
    }
  }
}

// Derived
function cleanModelView(uniformState) {
  if (uniformState._modelViewDirty) {
    uniformState._modelViewDirty = false;

    Matrix4.multiplyTransformation(
      uniformState._view,
      uniformState._model,
      uniformState._modelView,
    );
  }
}

function cleanModelView3D(uniformState) {
  if (uniformState._modelView3DDirty) {
    uniformState._modelView3DDirty = false;

    Matrix4.multiplyTransformation(
      uniformState.view3D,
      uniformState._model,
      uniformState._modelView3D,
    );
  }
}

function cleanInverseModelView(uniformState) {
  if (uniformState._inverseModelViewDirty) {
    uniformState._inverseModelViewDirty = false;

    Matrix4.inverse(uniformState.modelView, uniformState._inverseModelView);
  }
}

function cleanInverseModelView3D(uniformState) {
  if (uniformState._inverseModelView3DDirty) {
    uniformState._inverseModelView3DDirty = false;

    Matrix4.inverse(uniformState.modelView3D, uniformState._inverseModelView3D);
  }
}

function cleanViewProjection(uniformState) {
  if (uniformState._viewProjectionDirty) {
    uniformState._viewProjectionDirty = false;

    Matrix4.multiply(
      uniformState._projection,
      uniformState._view,
      uniformState._viewProjection,
    );
  }
}

function cleanInverseViewProjection(uniformState) {
  if (uniformState._inverseViewProjectionDirty) {
    uniformState._inverseViewProjectionDirty = false;

    Matrix4.inverse(
      uniformState.viewProjection,
      uniformState._inverseViewProjection,
    );
  }
}

function cleanModelViewProjection(uniformState) {
  if (uniformState._modelViewProjectionDirty) {
    uniformState._modelViewProjectionDirty = false;

    Matrix4.multiply(
      uniformState._projection,
      uniformState.modelView,
      uniformState._modelViewProjection,
    );
  }
}

function cleanModelViewRelativeToEye(uniformState) {
  if (uniformState._modelViewRelativeToEyeDirty) {
    uniformState._modelViewRelativeToEyeDirty = false;

    const mv = uniformState.modelView;
    const mvRte = uniformState._modelViewRelativeToEye;
    mvRte[0] = mv[0];
    mvRte[1] = mv[1];
    mvRte[2] = mv[2];
    mvRte[3] = mv[3];
    mvRte[4] = mv[4];
    mvRte[5] = mv[5];
    mvRte[6] = mv[6];
    mvRte[7] = mv[7];
    mvRte[8] = mv[8];
    mvRte[9] = mv[9];
    mvRte[10] = mv[10];
    mvRte[11] = mv[11];
    mvRte[12] = 0.0;
    mvRte[13] = 0.0;
    mvRte[14] = 0.0;
    mvRte[15] = mv[15];
  }
}

function cleanInverseModelViewProjection(uniformState) {
  if (uniformState._inverseModelViewProjectionDirty) {
    uniformState._inverseModelViewProjectionDirty = false;

    Matrix4.inverse(
      uniformState.modelViewProjection,
      uniformState._inverseModelViewProjection,
    );
  }
}

function cleanModelViewProjectionRelativeToEye(uniformState) {
  if (uniformState._modelViewProjectionRelativeToEyeDirty) {
    uniformState._modelViewProjectionRelativeToEyeDirty = false;

    Matrix4.multiply(
      uniformState._projection,
      uniformState.modelViewRelativeToEye,
      uniformState._modelViewProjectionRelativeToEye,
    );
  }
}

function cleanModelViewInfiniteProjection(uniformState) {
  if (uniformState._modelViewInfiniteProjectionDirty) {
    uniformState._modelViewInfiniteProjectionDirty = false;

    Matrix4.multiply(
      uniformState._infiniteProjection,
      uniformState.modelView,
      uniformState._modelViewInfiniteProjection,
    );
  }
}

function cleanNormal(uniformState) {
  if (uniformState._normalDirty) {
    uniformState._normalDirty = false;

    const m = uniformState._normal;
    Matrix4.getMatrix3(uniformState.inverseModelView, m);
    Matrix3.transpose(m, m);
  }
}

function cleanNormal3D(uniformState) {
  if (uniformState._normal3DDirty) {
    uniformState._normal3DDirty = false;

    const m = uniformState._normal3D;
    Matrix4.getMatrix3(uniformState.inverseModelView3D, m);
    Matrix3.transpose(m, m);
  }
}

function cleanInverseNormal(uniformState) {
  if (uniformState._inverseNormalDirty) {
    uniformState._inverseNormalDirty = false;

    const m = uniformState._inverseNormal;
    Matrix4.getMatrix3(uniformState.modelView, m);
    Matrix3.transpose(m, m);
  }
}

function cleanInverseNormal3D(uniformState) {
  if (uniformState._inverseNormal3DDirty) {
    uniformState._inverseNormal3DDirty = false;

    const m = uniformState._inverseNormal3D;
    Matrix4.getMatrix3(uniformState.modelView3D, m);
    Matrix3.transpose(m, m);
  }
}

const cameraPositionMC = new Cartesian3();

function cleanEncodedCameraPositionMC(uniformState) {
  if (uniformState._encodedCameraPositionMCDirty) {
    uniformState._encodedCameraPositionMCDirty = false;

    Matrix4.multiplyByPoint(
      uniformState.inverseModel,
      uniformState._cameraPosition,
      cameraPositionMC,
    );
    EncodedCartesian3.fromCartesian(
      cameraPositionMC,
      uniformState._encodedCameraPositionMC,
    );
  }
}

const view2Dto3DPScratch = new Cartesian3();
const view2Dto3DRScratch = new Cartesian3();
const view2Dto3DUScratch = new Cartesian3();
const view2Dto3DDScratch = new Cartesian3();
const view2Dto3DCartographicScratch = new Cartographic();
const view2Dto3DCartesian3Scratch = new Cartesian3();
const view2Dto3DMatrix4Scratch = new Matrix4();

function view2Dto3D(
  position2D,
  direction2D,
  right2D,
  up2D,
  frustum2DWidth,
  mode,
  projection,
  result,
) {
  // The camera position and directions are expressed in the 2D coordinate system where the Y axis is to the East,
  // the Z axis is to the North, and the X axis is out of the map.  Express them instead in the ENU axes where
  // X is to the East, Y is to the North, and Z is out of the local horizontal plane.
  const p = view2Dto3DPScratch;
  p.x = position2D.y;
  p.y = position2D.z;
  p.z = position2D.x;

  const r = view2Dto3DRScratch;
  r.x = right2D.y;
  r.y = right2D.z;
  r.z = right2D.x;

  const u = view2Dto3DUScratch;
  u.x = up2D.y;
  u.y = up2D.z;
  u.z = up2D.x;

  const d = view2Dto3DDScratch;
  d.x = direction2D.y;
  d.y = direction2D.z;
  d.z = direction2D.x;

  // In 2D, the camera height is always 12.7 million meters.
  // The apparent height is equal to half the frustum width.
  if (mode === SceneMode.SCENE2D) {
    p.z = frustum2DWidth * 0.5;
  }

  // Compute the equivalent camera position in the real (3D) world.
  // In 2D and Columbus View, the camera can travel outside the projection, and when it does so
  // there's not really any corresponding location in the real world.  So clamp the unprojected
  // longitude and latitude to their valid ranges.
  const cartographic = projection.unproject(p, view2Dto3DCartographicScratch);
  cartographic.longitude = CesiumMath.clamp(
    cartographic.longitude,
    -Math.PI,
    Math.PI,
  );
  cartographic.latitude = CesiumMath.clamp(
    cartographic.latitude,
    -CesiumMath.PI_OVER_TWO,
    CesiumMath.PI_OVER_TWO,
  );
  const ellipsoid = projection.ellipsoid;
  const position3D = ellipsoid.cartographicToCartesian(
    cartographic,
    view2Dto3DCartesian3Scratch,
  );

  // Compute the rotation from the local ENU at the real world camera position to the fixed axes.
  const enuToFixed = Transforms.eastNorthUpToFixedFrame(
    position3D,
    ellipsoid,
    view2Dto3DMatrix4Scratch,
  );

  // Transform each camera direction to the fixed axes.
  Matrix4.multiplyByPointAsVector(enuToFixed, r, r);
  Matrix4.multiplyByPointAsVector(enuToFixed, u, u);
  Matrix4.multiplyByPointAsVector(enuToFixed, d, d);

  // Compute the view matrix based on the new fixed-frame camera position and directions.
  if (!defined(result)) {
    result = new Matrix4();
  }

  result[0] = r.x;
  result[1] = u.x;
  result[2] = -d.x;
  result[3] = 0.0;
  result[4] = r.y;
  result[5] = u.y;
  result[6] = -d.y;
  result[7] = 0.0;
  result[8] = r.z;
  result[9] = u.z;
  result[10] = -d.z;
  result[11] = 0.0;
  result[12] = -Cartesian3.dot(r, position3D);
  result[13] = -Cartesian3.dot(u, position3D);
  result[14] = Cartesian3.dot(d, position3D);
  result[15] = 1.0;

  return result;
}

function updateView3D(that) {
  if (that._view3DDirty) {
    if (that._mode === SceneMode.SCENE3D) {
      Matrix4.clone(that._view, that._view3D);
    } else {
      view2Dto3D(
        that._cameraPosition,
        that._cameraDirection,
        that._cameraRight,
        that._cameraUp,
        that._frustum2DWidth,
        that._mode,
        that._mapProjection,
        that._view3D,
      );
    }
    Matrix4.getMatrix3(that._view3D, that._viewRotation3D);
    that._view3DDirty = false;
  }
}

function updateInverseView3D(that) {
  if (that._inverseView3DDirty) {
    Matrix4.inverseTransformation(that.view3D, that._inverseView3D);
    Matrix4.getMatrix3(that._inverseView3D, that._inverseViewRotation3D);
    that._inverseView3DDirty = false;
  }
}
export default UniformState;
