/**
 * The batch data that is needed by {@link DepthSamplerShader} to render an element with batching.
 * @typedef {object} DepthBatchData
 * @property {PIXI.Texture} _texture                       The texture
 * @property {Float32Array} vertexData                     The vertices
 * @property {Uint16Array|Uint32Array|number[]} indices    The indices
 * @property {Float32Array} uvs                            The texture UVs
 * @property {number} elevation                            The elevation
 * @property {number} textureAlphaThreshold                The texture alpha threshold
 * @property {number} fadeOcclusion                        The amount of FADE occlusion
 * @property {number} radialOcclusion                      The amount of RADIAL occlusion
 * @property {number} visionOcclusion                      The amount of VISION occlusion
 */

/**
 * The depth sampler shader.
 */
class DepthSamplerShader extends BaseSamplerShader {

  /* -------------------------------------------- */
  /*  Batched version Rendering                   */
  /* -------------------------------------------- */

  /** @override */
  static classPluginName = "batchDepth";

  /* ---------------------------------------- */

  /** @override */
  static batchGeometry = [
    {id: "aVertexPosition", size: 2, normalized: false, type: PIXI.TYPES.FLOAT},
    {id: "aTextureCoord", size: 2, normalized: false, type: PIXI.TYPES.FLOAT},
    {id: "aTextureId", size: 1, normalized: false, type: PIXI.TYPES.UNSIGNED_BYTE},
    {id: "aTextureAlphaThreshold", size: 1, normalized: true, type: PIXI.TYPES.UNSIGNED_BYTE},
    {id: "aDepthElevation", size: 1, normalized: true, type: PIXI.TYPES.UNSIGNED_BYTE},
    {id: "aRestrictionState", size: 1, normalized: false, type: PIXI.TYPES.UNSIGNED_BYTE},
    {id: "aOcclusionData", size: 4, normalized: true, type: PIXI.TYPES.UNSIGNED_BYTE}
  ];

  /* ---------------------------------------- */

  /** @override */
  static batchVertexSize = 6;

  /* -------------------------------------------- */

  /** @override */
  static reservedTextureUnits = 1; // We need a texture unit for the occlusion texture

  /* -------------------------------------------- */

  /** @override */
  static defaultUniforms = {
    screenDimensions: [1, 1],
    sampler: null,
    occlusionTexture: null,
    textureAlphaThreshold: 0,
    depthElevation: 0,
    occlusionElevation: 0,
    fadeOcclusion: 0,
    radialOcclusion: 0,
    visionOcclusion: 0,
    restrictsLight: false,
    restrictsWeather: false
  };

  /* -------------------------------------------- */

  /** @override */
  static batchDefaultUniforms(maxTex) {
    return {
      screenDimensions: [1, 1],
      occlusionTexture: maxTex
    };
  }

  /* -------------------------------------------- */

  /** @override */
  static _preRenderBatch(batchRenderer) {
    const uniforms = batchRenderer._shader.uniforms;
    uniforms.screenDimensions = canvas.screenDimensions;
    batchRenderer.renderer.texture.bind(canvas.masks.occlusion.renderTexture, uniforms.occlusionTexture);
  }

  /* ---------------------------------------- */

  /** @override */
  static _packInterleavedGeometry(element, attributeBuffer, indexBuffer, aIndex, iIndex) {
    const {float32View, uint8View} = attributeBuffer;

    // Write indices into buffer
    const packedVertices = aIndex / this.vertexSize;
    const indices = element.indices;
    for ( let i = 0; i < indices.length; i++ ) {
      indexBuffer[iIndex++] = packedVertices + indices[i];
    }

    // Prepare attributes
    const vertexData = element.vertexData;
    const uvs = element.uvs;
    const textureId = element._texture.baseTexture._batchLocation;
    const restrictionState = element.restrictionState;
    const textureAlphaThreshold = (element.textureAlphaThreshold * 255) | 0;
    const depthElevation = (canvas.masks.depth.mapElevation(element.elevation) * 255) | 0;
    const occlusionElevation = (canvas.masks.occlusion.mapElevation(element.elevation) * 255) | 0;
    const fadeOcclusion = (element.fadeOcclusion * 255) | 0;
    const radialOcclusion = (element.radialOcclusion * 255) | 0;
    const visionOcclusion = (element.visionOcclusion * 255) | 0;

    // Write attributes into buffer
    const vertexSize = this.vertexSize;
    for ( let i = 0, j = 0; i < vertexData.length; i += 2, j += vertexSize ) {
      let k = aIndex + j;
      float32View[k++] = vertexData[i];
      float32View[k++] = vertexData[i + 1];
      float32View[k++] = uvs[i];
      float32View[k++] = uvs[i + 1];
      k <<= 2;
      uint8View[k++] = textureId;
      uint8View[k++] = textureAlphaThreshold;
      uint8View[k++] = depthElevation;
      uint8View[k++] = restrictionState;
      uint8View[k++] = occlusionElevation;
      uint8View[k++] = fadeOcclusion;
      uint8View[k++] = radialOcclusion;
      uint8View[k++] = visionOcclusion;
    }
  }

  /* ---------------------------------------- */

  /** @override */
  static get batchVertexShader() {
    return `
      #version 300 es

      ${this.GLSL1_COMPATIBILITY_VERTEX}

      precision ${PIXI.settings.PRECISION_VERTEX} float;

      in vec2 aVertexPosition;
      in vec2 aTextureCoord;

      uniform vec2 screenDimensions;

      ${this._batchVertexShader}

      in float aTextureId;
      in float aTextureAlphaThreshold;
      in float aDepthElevation;
      in vec4 aOcclusionData;
      in float aRestrictionState;

      uniform mat3 projectionMatrix;
      uniform mat3 translationMatrix;

      out vec2 vTextureCoord;
      out vec2 vOcclusionCoord;
      flat out float vTextureId;
      flat out float vTextureAlphaThreshold;
      flat out float vDepthElevation;
      flat out float vOcclusionElevation;
      flat out float vFadeOcclusion;
      flat out float vRadialOcclusion;
      flat out float vVisionOcclusion;
      flat out uint vRestrictionState;

      void main() {
        vec2 vertexPosition;
        vec2 textureCoord;
        _main(vertexPosition, textureCoord);
        vec3 tPos = translationMatrix * vec3(vertexPosition, 1.0);
        gl_Position = vec4((projectionMatrix * tPos).xy, 0.0, 1.0);
        vTextureCoord = textureCoord;
        vOcclusionCoord = tPos.xy / screenDimensions;
        vTextureId = aTextureId;
        vTextureAlphaThreshold = aTextureAlphaThreshold;
        vDepthElevation = aDepthElevation;
        vOcclusionElevation = aOcclusionData.x;
        vFadeOcclusion = aOcclusionData.y;
        vRadialOcclusion = aOcclusionData.z;
        vVisionOcclusion = aOcclusionData.w;
        vRestrictionState = uint(aRestrictionState);
      }
    `;
  }

  /* -------------------------------------------- */

  /**
   * The batch vertex shader source. Subclasses can override it.
   * @type {string}
   * @protected
   */
  static _batchVertexShader = `
    void _main(out vec2 vertexPosition, out vec2 textureCoord) {
      vertexPosition = aVertexPosition;
      textureCoord = aTextureCoord;
    }
  `;

  /* ---------------------------------------- */

  /** @override */
  static get batchFragmentShader() {
    return `
      #version 300 es

      ${this.GLSL1_COMPATIBILITY_FRAGMENT}

      precision ${PIXI.settings.PRECISION_FRAGMENT} float;

      in vec2 vTextureCoord;
      flat in float vTextureId;

      uniform sampler2D uSamplers[%count%];

      ${DepthSamplerShader.#OPTIONS_CONSTANTS}
      ${this._batchFragmentShader}

      in vec2 vOcclusionCoord;
      flat in float vTextureAlphaThreshold;
      flat in float vDepthElevation;
      flat in float vOcclusionElevation;
      flat in float vFadeOcclusion;
      flat in float vRadialOcclusion;
      flat in float vVisionOcclusion;
      flat in uint vRestrictionState;
      
      uniform sampler2D occlusionTexture;

      out vec3 fragColor;

      void main() {
        float textureAlpha = _main();
        float textureAlphaThreshold = vTextureAlphaThreshold;
        float depthElevation = vDepthElevation;
        float occlusionElevation = vOcclusionElevation;
        float fadeOcclusion = vFadeOcclusion;
        float radialOcclusion = vRadialOcclusion;
        float visionOcclusion = vVisionOcclusion;
        bool restrictsLight = ((vRestrictionState & RESTRICTS_LIGHT) == RESTRICTS_LIGHT);
        bool restrictsWeather = ((vRestrictionState & RESTRICTS_WEATHER) == RESTRICTS_WEATHER);
        ${DepthSamplerShader.#FRAGMENT_MAIN}
      }
    `;
  }

  /* -------------------------------------------- */

  /**
   * The batch fragment shader source. Subclasses can override it.
   * @type {string}
   * @protected
   */
  static _batchFragmentShader = `
    float _main() {
      vec4 color;
      %forloop%
      return color.a;
    }
  `;

  /* -------------------------------------------- */
  /*  Non-Batched version Rendering               */
  /* -------------------------------------------- */

  /** @override */
  static get vertexShader() {
    return `
      #version 300 es

      ${this.GLSL1_COMPATIBILITY_VERTEX}

      precision ${PIXI.settings.PRECISION_VERTEX} float;

      in vec2 aVertexPosition;
      in vec2 aTextureCoord;

      uniform vec2 screenDimensions;

      ${this._vertexShader}

      uniform mat3 projectionMatrix;

      out vec2 vUvs;
      out vec2 vOcclusionCoord;

      void main() {
        vec2 vertexPosition;
        vec2 textureCoord;
        _main(vertexPosition, textureCoord);
        gl_Position = vec4((projectionMatrix * vec3(vertexPosition, 1.0)).xy, 0.0, 1.0);
        vUvs = textureCoord;
        vOcclusionCoord = vertexPosition / screenDimensions;
      }
    `;
  }

  /* -------------------------------------------- */

  /**
   * The vertex shader source. Subclasses can override it.
   * @type {string}
   * @protected
   */
  static _vertexShader = `
    void _main(out vec2 vertexPosition, out vec2 textureCoord) {
      vertexPosition = aVertexPosition;
      textureCoord = aTextureCoord;
    }
  `;

  /* -------------------------------------------- */

  /** @override */
  static get fragmentShader() {
    return `
      #version 300 es

      ${this.GLSL1_COMPATIBILITY_FRAGMENT}

      precision ${PIXI.settings.PRECISION_FRAGMENT} float;

      in vec2 vUvs;

      uniform sampler2D sampler;

      ${DepthSamplerShader.#OPTIONS_CONSTANTS}
      ${this._fragmentShader}

      in vec2 vOcclusionCoord;

      uniform sampler2D occlusionTexture;
      uniform float textureAlphaThreshold;
      uniform float depthElevation;
      uniform float occlusionElevation;
      uniform float fadeOcclusion;
      uniform float radialOcclusion;
      uniform float visionOcclusion;
      uniform bool restrictsLight;
      uniform bool restrictsWeather;

      out vec3 fragColor;

      void main() {
        float textureAlpha = _main();
        ${DepthSamplerShader.#FRAGMENT_MAIN}
      }
    `;
  }

  /* -------------------------------------------- */

  /**
   * The fragment shader source. Subclasses can override it.
   * @type {string}
   * @protected
   */
  static _fragmentShader = `
    float _main() {
      return texture(sampler, vUvs).a;
    }
  `;

  /* -------------------------------------------- */

  /** @inheritdoc */
  _preRender(mesh, renderer) {
    super._preRender(mesh, renderer);
    const uniforms = this.uniforms;
    uniforms.screenDimensions = canvas.screenDimensions;
    uniforms.textureAlphaThreshold = mesh.textureAlphaThreshold;
    const occlusionMask = canvas.masks.occlusion;
    uniforms.occlusionTexture = occlusionMask.renderTexture;
    uniforms.occlusionElevation = occlusionMask.mapElevation(mesh.elevation);
    uniforms.depthElevation = canvas.masks.depth.mapElevation(mesh.elevation);
    const occlusionState = mesh._occlusionState;
    uniforms.fadeOcclusion = occlusionState.fade;
    uniforms.radialOcclusion = occlusionState.radial;
    uniforms.visionOcclusion = occlusionState.vision;
    uniforms.restrictsLight = mesh.restrictsLight;
    uniforms.restrictsWeather = mesh.restrictsWeather;
  }

  /* -------------------------------------------- */

  /**
   * The restriction options bit mask constants.
   * @type {string}
   */
  static #OPTIONS_CONSTANTS = foundry.utils.BitMask.generateShaderBitMaskConstants([
    "RESTRICTS_LIGHT",
    "RESTRICTS_WEATHER"
  ]);

  /* -------------------------------------------- */

  /**
   * The fragment source.
   * @type {string}
   */
  static #FRAGMENT_MAIN = `
    float inverseDepthElevation = 1.0 - depthElevation;
    fragColor = vec3(inverseDepthElevation, depthElevation, inverseDepthElevation);
    fragColor *= step(textureAlphaThreshold, textureAlpha);
    vec3 weight = 1.0 - step(occlusionElevation, texture(occlusionTexture, vOcclusionCoord).rgb);
    float occlusion = step(0.5, max(max(weight.r * fadeOcclusion, weight.g * radialOcclusion), weight.b * visionOcclusion));
    fragColor.r *= occlusion;
    fragColor.g *= 1.0 - occlusion;
    fragColor.b *= occlusion;
    if ( !restrictsLight ) {
      fragColor.r = 0.0;
      fragColor.g = 0.0;
    }
    if ( !restrictsWeather ) {
      fragColor.b = 0.0;
    }
  `;
}
