text: import { math } from '../math/math.js';
  import { Color } from '../math/color.js';
  import { Mat4 } from '../math/mat4.js';
  import { Vec2 } from '../math/vec2.js';
  import { Vec3 } from '../math/vec3.js';
  import { Vec4 } from '../math/vec4.js';
  
  import {
      BLUR_GAUSSIAN,
      LIGHTTYPE_DIRECTIONAL, LIGHTTYPE_OMNI, LIGHTTYPE_SPOT,
      MASK_LIGHTMAP, MASK_DYNAMIC,
      SHADOW_PCF3, SHADOW_PCF5, SHADOW_VSM8, SHADOW_VSM16, SHADOW_VSM32,
      SHADOWUPDATE_NONE, SHADOWUPDATE_REALTIME, SHADOWUPDATE_THISFRAME,
      LIGHTSHAPE_PUNCTUAL
  } from './constants.js';
  import { ShadowRenderer } from './renderer/shadow-renderer.js';
  
  const spotCenter = new Vec3();
  const spotEndPoint = new Vec3();
  const tmpVec = new Vec3();
  const tmpBiases = {
      bias: 0,
      normalBias: 0
  };
  
  const chanId = { r: 0, g: 1, b: 2, a: 3 };
  
  // viewport in shadows map for cascades for directional light
  const directionalCascades = [
      [new Vec4(0, 0, 1, 1)],
      [new Vec4(0, 0, 0.5, 0.5), new Vec4(0, 0.5, 0.5, 0.5)],
      [new Vec4(0, 0, 0.5, 0.5), new Vec4(0, 0.5, 0.5, 0.5), new Vec4(0.5, 0, 0.5, 0.5)],
      [new Vec4(0, 0, 0.5, 0.5), new Vec4(0, 0.5, 0.5, 0.5), new Vec4(0.5, 0, 0.5, 0.5), new Vec4(0.5, 0.5, 0.5, 0.5)]
  ];
  
  // Class storing shadow rendering related private information
  class LightRenderData {
      constructor(device, camera, face, light) {
  
          // light this data belongs to
          this.light = light;
  
          // camera this applies to. Only used by directional light, as directional shadow map
          // is culled and rendered for each camera. Local lights' shadow is culled and rendered one time
          // and shared between cameras (even though it's not strictly correct and we can get shadows
          // from a mesh that is not visible by the camera)
          this.camera = camera;
  
          // camera used to cull / render the shadow map
          this.shadowCamera = ShadowRenderer.createShadowCamera(device, light._shadowType, light._type, face);
  
          // shadow view-projection matrix
          this.shadowMatrix = new Mat4();
  
          // viewport for the shadow rendering to the texture (x, y, width, height)
          this.shadowViewport = new Vec4(0, 0, 1, 1);
  
          // scissor rectangle for the shadow rendering to the texture (x, y, width, height)
          this.shadowScissor = new Vec4(0, 0, 1, 1);
  
          // face index, value is based on light type:
          // - spot: always 0
          // - omni: cubemap face, 0..5
          // - directional: 0 for simple shadows, cascade index for cascaded shadow map
          this.face = face;
  
          // visible shadow casters
          this.visibleCasters = [];
      }
  
      // returns shadow buffer currently attached to the shadow camera
      get shadowBuffer() {
          const rt = this.shadowCamera.renderTarget;
          if (rt) {
              const light = this.light;
              if (light._type === LIGHTTYPE_OMNI) {
                  return rt.colorBuffer;
              }
  
              return light._isPcf && light.device.webgl2 ? rt.depthBuffer : rt.colorBuffer;
          }
  
          return null;
      }
  }
  
  /**
   * @private
   * @class
   * @name Light
   * @classdesc A light.
   */
  class Light {
      constructor(graphicsDevice) {
          this.device = graphicsDevice;
  
          // Light properties (defaults)
          this._type = LIGHTTYPE_DIRECTIONAL;
          this._color = new Color(0.8, 0.8, 0.8);
          this._intensity = 1;
          this._castShadows = false;
          this._enabled = false;
          this.mask = MASK_DYNAMIC;
          this.isStatic = false;
          this.key = 0;
          this.bakeDir = true;
          this.bakeNumSamples = 1;
          this.bakeArea = 0;
  
          // Omni and spot properties
          this.attenuationStart = 10;
          this.attenuationEnd = 10;
          this._falloffMode = 0;
          this._shadowType = SHADOW_PCF3;
          this._vsmBlurSize = 11;
          this.vsmBlurMode = BLUR_GAUSSIAN;
          this.vsmBias = 0.01 * 0.25;
          this._cookie = null; // light cookie texture (2D for spot, cubemap for omni)
          this.cookieIntensity = 1;
          this._cookieFalloff = true;
          this._cookieChannel = "rgb";
          this._cookieTransform = null; // 2d rotation/scale matrix (spot only)
          this._cookieTransformUniform = new Float32Array(4);
          this._cookieOffset = null; // 2d position offset (spot only)
          this._cookieOffsetUniform = new Float32Array(2);
          this._cookieTransformSet = false;
          this._cookieOffsetSet = false;
  
          // Spot properties
          this._innerConeAngle = 40;
          this._outerConeAngle = 45;
  
          // Directional properties
          this.cascades = null;               // an array of Vec4 viewports per cascade
          this._shadowMatrixPalette = null;   // a float array, 16 floats per cascade
          this._shadowCascadeDistances = null;
          this.numCascades = 1;
          this.cascadeDistribution = 0.5;
  
          // Light source shape properties
          this._shape = LIGHTSHAPE_PUNCTUAL;
  
          // Cache of light property data in a format more friendly for shader uniforms
          this._finalColor = new Float32Array([0.8, 0.8, 0.8]);
          const c = Math.pow(this._finalColor[0], 2.2);
          this._linearFinalColor = new Float32Array([c, c, c]);
  
          this._position = new Vec3(0, 0, 0);
          this._direction = new Vec3(0, 0, 0);
          this._innerConeAngleCos = Math.cos(this._innerConeAngle * Math.PI / 180);
          this._outerConeAngleCos = Math.cos(this._outerConeAngle * Math.PI / 180);
  
          // Shadow mapping resources
          this._shadowMap = null;
          this._shadowRenderParams = [];
  
          // Shadow mapping properties
          this.shadowDistance = 40;
          this._shadowResolution = 1024;
          this.shadowBias = -0.0005;
          this._normalOffsetBias = 0.0;
          this.shadowUpdateMode = SHADOWUPDATE_REALTIME;
          this._isVsm = false;
          this._isPcf = true;
  
          // cookie matrix (used in case the shadow mapping is disabled and so the shadow matrix cannot be used)
          this._cookieMatrix = null;
  
          // viewport of the cookie texture / shadow in the atlas
          this._atlasViewport = null;
  
          this._scene = null;
          this._node = null;
  
          // private rendering data
          this._renderData = [];
  
          // true if the light is visible by any camera within a frame
          this.visibleThisFrame = false;
      }
  
      destroy() {
          this._destroyShadowMap();
          this._renderData = null;
      }
  
      // destroys shadow map related resources, called when shadow properties change and resources
      // need to be recreated
      _destroyShadowMap() {
  
          if (this._renderData) {
              this._renderData.length = 0;
          }
  
          if (this._shadowMap) {
              if (!this._shadowMap.cached) {
                  this._shadowMap.destroy();
              }
              this._shadowMap = null;
          }
  
          if (this.shadowUpdateMode === SHADOWUPDATE_NONE) {
              this.shadowUpdateMode = SHADOWUPDATE_THISFRAME;
          }
      }
  
      // returns LightRenderData with matching camera and face
      getRenderData(camera, face) {
  
          // returns existing
          for (let i = 0; i < this._renderData.length; i++) {
              const current = this._renderData[i];
              if (current.camera === camera && current.face === face) {
                  return current;
              }
          }
  
          // create new one
          const rd = new LightRenderData(this.device, camera, face, this);
          this._renderData.push(rd);
          return rd;
      }
  
      /**
       * @private
       * @function
       * @name Light#clone
       * @description Duplicates a light node but does not 'deep copy' the hierarchy.
       * @returns {Light} A cloned Light.
       */
      clone() {
          const clone = new Light(this.device);
  
          // Clone Light properties
          clone.type = this._type;
          clone.setColor(this._color);
          clone.intensity = this._intensity;
          clone.castShadows = this.castShadows;
          clone._enabled = this._enabled;
  
          // Omni and spot properties
          clone.attenuationStart = this.attenuationStart;
          clone.attenuationEnd = this.attenuationEnd;
          clone.falloffMode = this._falloffMode;
          clone.shadowType = this._shadowType;
          clone.vsmBlurSize = this._vsmBlurSize;
          clone.vsmBlurMode = this.vsmBlurMode;
          clone.vsmBias = this.vsmBias;
          clone.shadowUpdateMode = this.shadowUpdateMode;
          clone.mask = this.mask;
  
          // Spot properties
          clone.innerConeAngle = this._innerConeAngle;
          clone.outerConeAngle = this._outerConeAngle;
  
          // Directional properties
          clone.numCascades = this.numCascades;
          clone.cascadeDistribution = this.cascadeDistribution;
  
          // shape properties
          clone.shape = this._shape;
  
          // Shadow properties
          clone.shadowBias = this.shadowBias;
          clone.normalOffsetBias = this._normalOffsetBias;
          clone.shadowResolution = this._shadowResolution;
          clone.shadowDistance = this.shadowDistance;
  
          // Cookies properties
          // clone.cookie = this._cookie;
          // clone.cookieIntensity = this.cookieIntensity;
          // clone.cookieFalloff = this._cookieFalloff;
          // clone.cookieChannel = this._cookieChannel;
          // clone.cookieTransform = this._cookieTransform;
          // clone.cookieOffset = this._cookieOffset;
  
          return clone;
      }
  
      // returns the bias (.x) and normalBias (.y) value for lights as passed to shaders by uniforms
      // Note: this needs to be revisited and simplified
      // Note: vsmBias is not used at all for omni light, even though it is editable in the Editor
      _getUniformBiasValues(lightRenderData) {
  
          const farClip = lightRenderData.shadowCamera._farClip;
  
          switch (this._type) {
              case LIGHTTYPE_OMNI:
                  tmpBiases.bias = this.shadowBias;
                  tmpBiases.normalBias = this._normalOffsetBias;
                  break;
              case LIGHTTYPE_SPOT:
                  if (this._isVsm) {
                      tmpBiases.bias = -0.00001 * 20;
                  } else {
                      tmpBiases.bias = this.shadowBias * 20; // approx remap from old bias values
                      if (!this.device.webgl2 && this.device.extStandardDerivatives) tmpBiases.bias *= -100;
                  }
                  tmpBiases.normalBias = this._isVsm ? this.vsmBias / (this.attenuationEnd / 7.0) : this._normalOffsetBias;
                  break;
              case LIGHTTYPE_DIRECTIONAL:
                  // make bias dependent on far plane because it's not constant for direct light
                  // clip distance used is based on the nearest shadow cascade
                  if (this._isVsm) {
                      tmpBiases.bias = -0.00001 * 20;
                  } else {
                      tmpBiases.bias = (this.shadowBias / farClip) * 100;
                      if (!this.device.webgl2 && this.device.extStandardDerivatives) tmpBiases.bias *= -100;
                  }
                  tmpBiases.normalBias = this._isVsm ? this.vsmBias / (farClip / 7.0) : this._normalOffsetBias;
                  break;
          }
  
          return tmpBiases;
      }
  
      get numCascades() {
          return this.cascades.length;
      }
  
      set numCascades(value) {
          if (!this.cascades || this.numCascades != value) {
              this.cascades = directionalCascades[value - 1];
              this._shadowMatrixPalette = new Float32Array(4 * 16);   // always 4
              this._shadowCascadeDistances = new Float32Array(4);     // always 4
              this._destroyShadowMap();
              this.updateKey();
          }
      }
  
      get shadowMap() {
          return this._shadowMap;
      }
  
      set shadowMap(shadowMap) {
          if (this._shadowMap !== shadowMap) {
              this._destroyShadowMap();
              this._shadowMap = shadowMap;
          }
      }
  
      // returns number of render targets to render the shadow map
      get numShadowFaces() {
          const type = this._type;
          if (type === LIGHTTYPE_DIRECTIONAL) {
              return this.numCascades;
          } else if (type === LIGHTTYPE_OMNI) {
              return 6;
          }
  
          return 1;
      }
  
      getColor() {
          return this._color;
      }
  
      getBoundingSphere(sphere) {
          if (this._type === LIGHTTYPE_SPOT) {
              const range = this.attenuationEnd;
              const angle = this._outerConeAngle;
              const f = Math.cos(angle * math.DEG_TO_RAD);
              const node = this._node;
  
              spotCenter.copy(node.up);
              spotCenter.mulScalar(-range * 0.5 * f);
              spotCenter.add(node.getPosition());
              sphere.center = spotCenter;
  
              spotEndPoint.copy(node.up);
              spotEndPoint.mulScalar(-range);
  
              tmpVec.copy(node.right);
              tmpVec.mulScalar(Math.sin(angle * math.DEG_TO_RAD) * range);
              spotEndPoint.add(tmpVec);
  
              sphere.radius = spotEndPoint.length() * 0.5;
  
          } else if (this._type === LIGHTTYPE_OMNI) {
              sphere.center = this._node.getPosition();
              sphere.radius = this.attenuationEnd;
          }
      }
  
      getBoundingBox(box) {
          if (this._type === LIGHTTYPE_SPOT) {
              const range = this.attenuationEnd;
              const angle = this._outerConeAngle;
              const node = this._node;
  
              const scl = Math.abs(Math.sin(angle * math.DEG_TO_RAD) * range);
  
              box.center.set(0, -range * 0.5, 0);
              box.halfExtents.set(scl, range * 0.5, scl);
  
              box.setFromTransformedAabb(box, node.getWorldTransform());
  
          } else if (this._type === LIGHTTYPE_OMNI) {
              box.center.copy(this._node.getPosition());
              box.halfExtents.set(this.attenuationEnd, this.attenuationEnd, this.attenuationEnd);
          }
      }
  
      _updateFinalColor() {
          const color = this._color;
          const r = color.r;
          const g = color.g;
          const b = color.b;
  
          const i = this._intensity;
  
          const finalColor = this._finalColor;
          const linearFinalColor = this._linearFinalColor;
  
          finalColor[0] = r * i;
          finalColor[1] = g * i;
          finalColor[2] = b * i;
          if (i >= 1) {
              linearFinalColor[0] = Math.pow(r, 2.2) * i;
              linearFinalColor[1] = Math.pow(g, 2.2) * i;
              linearFinalColor[2] = Math.pow(b, 2.2) * i;
          } else {
              linearFinalColor[0] = Math.pow(finalColor[0], 2.2);
              linearFinalColor[1] = Math.pow(finalColor[1], 2.2);
              linearFinalColor[2] = Math.pow(finalColor[2], 2.2);
          }
      }
  
      setColor() {
          if (arguments.length === 1) {
              this._color.set(arguments[0].r, arguments[0].g, arguments[0].b);
          } else if (arguments.length === 3) {
              this._color.set(arguments[0], arguments[1], arguments[2]);
          }
  
          this._updateFinalColor();
      }
  
      updateShadow() {
          if (this.shadowUpdateMode !== SHADOWUPDATE_REALTIME) {
              this.shadowUpdateMode = SHADOWUPDATE_THISFRAME;
          }
      }
  
      layersDirty() {
          if (this._scene?.layers) {
              this._scene.layers._dirtyLights = true;
          }
      }
  
      updateKey() {
          // Key definition:
          // Bit
          // 31      : sign bit (leave)
          // 29 - 30 : type
          // 28      : cast shadows
          // 25 - 27 : shadow type
          // 23 - 24 : falloff mode
          // 22      : normal offset bias
          // 21      : cookie
          // 20      : cookie falloff
          // 18 - 19 : cookie channel R
          // 16 - 17 : cookie channel G
          // 14 - 15 : cookie channel B
          // 12      : cookie transform
          // 10 - 11 : light source shape
          //  8 -  9 : light num cascades
          let key =
                 (this._type                                << 29) |
                 ((this._castShadows ? 1 : 0)               << 28) |
                 (this._shadowType                          << 25) |
                 (this._falloffMode                         << 23) |
                 ((this._normalOffsetBias !== 0.0 ? 1 : 0)  << 22) |
                 ((this._cookie ? 1 : 0)                    << 21) |
                 ((this._cookieFalloff ? 1 : 0)             << 20) |
                 (chanId[this._cookieChannel.charAt(0)]     << 18) |
                 ((this._cookieTransform ? 1 : 0)           << 12) |
                 ((this._shape)                             << 10) |
                 ((this.numCascades - 1)                    <<  8);
  
          if (this._cookieChannel.length === 3) {
              key |= (chanId[this._cookieChannel.charAt(1)] << 16);
              key |= (chanId[this._cookieChannel.charAt(2)] << 14);
          }
  
          if (key !== this.key && this._scene !== null) {
              // TODO: most of the changes to the key should not invalidate the composition,
              // probably only _type and _castShadows
              this.layersDirty();
          }
  
          this.key = key;
      }
  
      get type() {
          return this._type;
      }
  
      set type(value) {
          if (this._type === value)
              return;
  
          this._type = value;
          this._destroyShadowMap();
          this.updateKey();
  
          const stype = this._shadowType;
          this._shadowType = null;
          this.shadowType = stype; // refresh shadow type; switching from direct/spot to omni and back may change it
      }
  
      get shape() {
          return this._shape;
      }
  
      set shape(value) {
          if (this._shape === value)
              return;
  
          this._shape = value;
          this._destroyShadowMap();
          this.updateKey();
  
          const stype = this._shadowType;
          this._shadowType = null;
          this.shadowType = stype; // refresh shadow type; switching shape and back may change it
      }
  
      get shadowType() {
          return this._shadowType;
      }
  
      set shadowType(value) {
          if (this._shadowType === value)
              return;
  
          const device = this.device;
  
          if (this._type === LIGHTTYPE_OMNI)
              value = SHADOW_PCF3; // VSM or HW PCF for omni lights is not supported yet
  
          if (value === SHADOW_PCF5 && !device.webgl2) {
              value = SHADOW_PCF3; // fallback from HW PCF to old PCF
          }
  
          if (value === SHADOW_VSM32 && !device.textureFloatRenderable) // fallback from vsm32 to vsm16
              value = SHADOW_VSM16;
  
          if (value === SHADOW_VSM16 && !device.textureHalfFloatRenderable) // fallback from vsm16 to vsm8
              value = SHADOW_VSM8;
  
          this._isVsm = value >= SHADOW_VSM8 && value <= SHADOW_VSM32;
          this._isPcf = value === SHADOW_PCF5 || value === SHADOW_PCF3;
  
          this._shadowType = value;
          this._destroyShadowMap();
          this.updateKey();
      }
  
      get enabled() {
          return this._enabled;
      }
  
      set enabled(value) {
          if (this._enabled !== value) {
              this._enabled = value;
              this.layersDirty();
          }
      }
  
      get castShadows() {
          return this._castShadows && this.mask !== MASK_LIGHTMAP && this.mask !== 0;
      }
  
      set castShadows(value) {
          if (this._castShadows !== value) {
              this._castShadows = value;
              this._destroyShadowMap();
              this.layersDirty();
              this.updateKey();
          }
      }
  
      get shadowResolution() {
          return this._shadowResolution;
      }
  
      set shadowResolution(value) {
          if (this._shadowResolution !== value) {
              if (this._type === LIGHTTYPE_OMNI) {
                  value = Math.min(value, this.device.maxCubeMapSize);
              } else {
                  value = Math.min(value, this.device.maxTextureSize);
              }
              this._shadowResolution = value;
              this._destroyShadowMap();
          }
      }
  
      get vsmBlurSize() {
          return this._vsmBlurSize;
      }
  
      set vsmBlurSize(value) {
          if (this._vsmBlurSize === value)
              return;
  
          if (value % 2 === 0) value++; // don't allow even size
          this._vsmBlurSize = value;
      }
  
      get normalOffsetBias() {
          return this._normalOffsetBias;
      }
  
      set normalOffsetBias(value) {
          if (this._normalOffsetBias === value)
              return;
  
          if ((!this._normalOffsetBias && value) || (this._normalOffsetBias && !value)) {
              this.updateKey();
          }
          this._normalOffsetBias = value;
      }
  
      get falloffMode() {
          return this._falloffMode;
      }
  
      set falloffMode(value) {
          if (this._falloffMode === value)
              return;
  
          this._falloffMode = value;
          this.updateKey();
      }
  
      get innerConeAngle() {
          return this._innerConeAngle;
      }
  
      set innerConeAngle(value) {
          if (this._innerConeAngle === value)
              return;
  
          this._innerConeAngle = value;
          this._innerConeAngleCos = Math.cos(value * Math.PI / 180);
      }
  
      get outerConeAngle() {
          return this._outerConeAngle;
      }
  
      set outerConeAngle(value) {
          if (this._outerConeAngle === value)
              return;
  
          this._outerConeAngle = value;
          this._outerConeAngleCos = Math.cos(value * Math.PI / 180);
      }
  
      get intensity() {
          return this._intensity;
      }
  
      set intensity(value) {
          if (this._intensity !== value) {
              this._intensity = value;
              this._updateFinalColor();
          }
      }
  
      get cookieMatrix() {
          if (!this._cookieMatrix) {
              this._cookieMatrix = new Mat4();
          }
          return this._cookieMatrix;
      }
  
      get atlasViewport() {
          if (!this._atlasViewport) {
              this._atlasViewport = new Vec4(0, 0, 1, 1);
          }
          return this._atlasViewport;
      }
  
      get cookie() {
          return this._cookie;
      }
  
      set cookie(value) {
          if (this._cookie === value)
              return;
  
          this._cookie = value;
          this.updateKey();
      }
  
      get cookieFalloff() {
          return this._cookieFalloff;
      }
  
      set cookieFalloff(value) {
          if (this._cookieFalloff === value)
              return;
  
          this._cookieFalloff = value;
          this.updateKey();
      }
  
      get cookieChannel() {
          return this._cookieChannel;
      }
  
      set cookieChannel(value) {
          if (this._cookieChannel === value)
              return;
  
          if (value.length < 3) {
              const chr = value.charAt(value.length - 1);
              const addLen = 3 - value.length;
              for (let i = 0; i < addLen; i++)
                  value += chr;
          }
          this._cookieChannel = value;
          this.updateKey();
      }
  
      get cookieTransform() {
          return this._cookieTransform;
      }
  
      set cookieTransform(value) {
          if (this._cookieTransform === value)
              return;
  
          this._cookieTransform = value;
          this._cookieTransformSet = !!value;
          if (value && !this._cookieOffset) {
              this.cookieOffset = new Vec2(); // using transform forces using offset code
              this._cookieOffsetSet = false;
          }
          this.updateKey();
      }
  
      get cookieOffset() {
          return this._cookieOffset;
      }
  
      set cookieOffset(value) {
          if (this._cookieOffset === value)
              return;
  
          const xformNew = !!(this._cookieTransformSet || value);
          if (xformNew && !value && this._cookieOffset) {
              this._cookieOffset.set(0, 0);
          } else {
              this._cookieOffset = value;
          }
          this._cookieOffsetSet = !!value;
          if (value && !this._cookieTransform) {
              this.cookieTransform = new Vec4(1, 1, 0, 0); // using offset forces using matrix code
              this._cookieTransformSet = false;
          }
          this.updateKey();
      }
  }
  
  export { Light };
  