/**
 * The base shader class for weather shaders.
 */
class AbstractWeatherShader extends AbstractBaseShader {
  constructor(...args) {
    super(...args);
    Object.defineProperties(this, Object.keys(this.constructor.defaultUniforms).reduce((obj, k) => {
      obj[k] = {
        get() {
          return this.uniforms[k];
        },
        set(value) {
          this.uniforms[k] = value;
        },
        enumerable: false
      };
      return obj;
    }, {}));
  }

  /**
   * Compute the weather masking value.
   * @type {string}
   */
  static COMPUTE_MASK = `
    // Base mask value 
    float mask = 1.0;
    
    // Process the occlusion mask
    if ( useOcclusion ) {
      float oMask = step(depthElevation, (254.5 / 255.0) - dot(occlusionWeights, texture2D(occlusionTexture, vUvsOcclusion)));
      if ( reverseOcclusion ) oMask = 1.0 - oMask;
      mask *= oMask;
    }
                  
    // Process the terrain mask 
    if ( useTerrain ) {
      float tMask = dot(terrainWeights, texture2D(terrainTexture, vUvsTerrain));
      if ( reverseTerrain ) tMask = 1.0 - tMask;
      mask *= tMask;
    }
  `;

  /**
   * Compute the weather masking value.
   * @type {string}
   */
  static FRAGMENT_HEADER = `
    precision ${PIXI.settings.PRECISION_FRAGMENT} float;
    
    // Occlusion mask uniforms
    uniform bool useOcclusion;
    uniform sampler2D occlusionTexture;
    uniform bool reverseOcclusion;
    uniform vec4 occlusionWeights;
    
    // Terrain mask uniforms
    uniform bool useTerrain;
    uniform sampler2D terrainTexture;
    uniform bool reverseTerrain;
    uniform vec4 terrainWeights;

    // Other uniforms and varyings
    uniform vec3 tint;
    uniform float time;
    uniform float depthElevation;
    uniform float alpha;
    varying vec2 vUvsOcclusion;
    varying vec2 vUvsTerrain;
    varying vec2 vStaticUvs;
    varying vec2 vUvs;
  `;

  /**
   * Common uniforms for all weather shaders.
   * @type {{
   *  useOcclusion: boolean,
   *  occlusionTexture: PIXI.Texture|null,
   *  reverseOcclusion: boolean,
   *  occlusionWeights: number[],
   *  useTerrain: boolean,
   *  terrainTexture: PIXI.Texture|null,
   *  reverseTerrain: boolean,
   *  terrainWeights: number[],
   *  alpha: number,
   *  tint: number[],
   *  screenDimensions: [number, number],
   *  effectDimensions: [number, number],
   *  depthElevation: number,
   *  time: number
   * }}
   */
  static commonUniforms = {
    terrainUvMatrix: new PIXI.Matrix(),
    useOcclusion: false,
    occlusionTexture: null,
    reverseOcclusion: false,
    occlusionWeights: [0, 0, 1, 0],
    useTerrain: false,
    terrainTexture: null,
    reverseTerrain: false,
    terrainWeights: [1, 0, 0, 0],
    alpha: 1,
    tint: [1, 1, 1],
    screenDimensions: [1, 1],
    effectDimensions: [1, 1],
    depthElevation: 1,
    time: 0
  };

  /**
   * Default uniforms for a specific class
   * @abstract
   */
  static defaultUniforms;

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

  /** @override */
  static create(initialUniforms) {
    const program = this.createProgram();
    const uniforms = {...this.commonUniforms, ...this.defaultUniforms, ...initialUniforms};
    return new this(program, uniforms);
  }

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

  /**
   * Create the shader program.
   * @returns {PIXI.Program}
   */
  static createProgram() {
    return PIXI.Program.from(this.vertexShader, this.fragmentShader);
  }

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

  /** @inheritdoc */
  static vertexShader = `
    precision ${PIXI.settings.PRECISION_VERTEX} float;
    attribute vec2 aVertexPosition;
    uniform mat3 translationMatrix;
    uniform mat3 projectionMatrix;
    uniform mat3 terrainUvMatrix;
    uniform vec2 screenDimensions;
    uniform vec2 effectDimensions;
    varying vec2 vUvsOcclusion;
    varying vec2 vUvsTerrain;
    varying vec2 vUvs;
    varying vec2 vStaticUvs;
  
    void main() {    
      vec3 tPos = translationMatrix * vec3(aVertexPosition, 1.0);
      vStaticUvs = aVertexPosition;
      vUvs = vStaticUvs * effectDimensions;
      vUvsOcclusion = tPos.xy / screenDimensions;
      vUvsTerrain = (terrainUvMatrix * vec3(aVertexPosition, 1.0)).xy;
      gl_Position = vec4((projectionMatrix * tPos).xy, 0.0, 1.0);
    }
  `;

  /* -------------------------------------------- */
  /*  Common Management and Parameters            */
  /* -------------------------------------------- */

  /**
   * Update the scale of this effect with new values
   * @param {number|{x: number, y: number}} scale    The desired scale
   */
  set scale(scale) {
    this.#scale.x = typeof scale === "object" ? scale.x : scale;
    this.#scale.y = (typeof scale === "object" ? scale.y : scale) ?? this.#scale.x;
  }

  set scaleX(x) {
    this.#scale.x = x ?? 1;
  }

  set scaleY(y) {
    this.#scale.y = y ?? 1;
  }

  #scale = {
    x: 1,
    y: 1
  };

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

  /**
   * The speed multiplier applied to animation.
   * 0 stops animation.
   * @type {number}
   */
  speed = 1;

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

  /** @override */
  _preRender(mesh, renderer) {
    this.uniforms.alpha = mesh.worldAlpha;
    this.uniforms.depthElevation = canvas.masks.depth.mapElevation(canvas.weather.elevation);
    this.uniforms.time += (canvas.app.ticker.deltaMS / 1000 * this.speed);
    this.uniforms.screenDimensions = canvas.screenDimensions;
    this.uniforms.effectDimensions[0] = this.#scale.x * mesh.scale.x / 10000;
    this.uniforms.effectDimensions[1] = this.#scale.y * mesh.scale.y / 10000;
  }
}
