/**
 * @module ol/renderer/webgl/TileLayer
 */
import TileState from '../../TileState.js';
import {
  boundingExtent,
  containsCoordinate,
  getIntersection,
} from '../../extent.js';
import {fromUserExtent} from '../../proj.js';
import {toSize} from '../../size.js';
import {apply as applyTransform} from '../../transform.js';
import {fromTransform as mat4FromTransform} from '../../vec/mat4.js';
import WebGLArrayBuffer from '../../webgl/Buffer.js';
import {AttributeType} from '../../webgl/Helper.js';
import TileTexture from '../../webgl/TileTexture.js';
import {ELEMENT_ARRAY_BUFFER, STATIC_DRAW} from '../../webgl.js';
import WebGLBaseTileLayerRenderer, {
  Uniforms as BaseUniforms,
  getCacheKey,
} from './TileLayerBase.js';

export const Uniforms = {
  ...BaseUniforms,
  TILE_TEXTURE_ARRAY: 'u_tileTextures',
  TEXTURE_PIXEL_WIDTH: 'u_texturePixelWidth',
  TEXTURE_PIXEL_HEIGHT: 'u_texturePixelHeight',
  TEXTURE_RESOLUTION: 'u_textureResolution', // map units per texture pixel
  TEXTURE_ORIGIN_X: 'u_textureOriginX', // map x coordinate of left edge of texture
  TEXTURE_ORIGIN_Y: 'u_textureOriginY', // map y coordinate of top edge of texture
};

export const Attributes = {
  TEXTURE_COORD: 'a_textureCoord',
};

/**
 * @type {Array<import('../../webgl/Helper.js').AttributeDescription>}
 */
const attributeDescriptions = [
  {
    name: Attributes.TEXTURE_COORD,
    size: 2,
    type: AttributeType.FLOAT,
  },
];

/**
 * @typedef {Object} Options
 * @property {string} vertexShader Vertex shader source.
 * @property {string} fragmentShader Fragment shader source.
 * @property {Object<string, import("../../webgl/Helper").UniformValue>} [uniforms] Additional uniforms
 * made available to shaders.
 * @property {Array<import("../../webgl/PaletteTexture.js").default>} [paletteTextures] Palette textures.
 * @property {number} [cacheSize=512] The texture cache size.
 * @property {Array<import('./Layer.js').PostProcessesOptions>} [postProcesses] Post-processes definitions.
 */

/**
 * @typedef {import("../../webgl/TileTexture.js").TileType} TileTextureType
 */

/**
 * @typedef {import("../../webgl/TileTexture.js").default} TileTextureRepresentation
 */

/**
 * @classdesc
 * WebGL renderer for tile layers.
 * @template {import("../../layer/WebGLTile.js").default|import("../../layer/Flow.js").default} LayerType
 * @extends {WebGLBaseTileLayerRenderer<LayerType, TileTextureType, TileTextureRepresentation>}
 * @api
 */
class WebGLTileLayerRenderer extends WebGLBaseTileLayerRenderer {
  /**
   * @param {LayerType} tileLayer Tile layer.
   * @param {Options} options Options.
   */
  constructor(tileLayer, options) {
    super(tileLayer, options);

    /**
     * @type {WebGLProgram}
     * @private
     */
    this.program_;

    /**
     * @private
     */
    this.vertexShader_ = options.vertexShader;

    /**
     * @private
     */
    this.fragmentShader_ = options.fragmentShader;

    /**
     * Tiles are rendered as a quad with the following structure:
     *
     *  [P3]---------[P2]
     *   |`           |
     *   |  `     B   |
     *   |    `       |
     *   |      `     |
     *   |   A    `   |
     *   |          ` |
     *  [P0]---------[P1]
     *
     * Triangle A: P0, P1, P3
     * Triangle B: P1, P2, P3
     *
     * @private
     */
    this.indices_ = new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, STATIC_DRAW);
    this.indices_.fromArray([0, 1, 3, 1, 2, 3]);

    /**
     * @type {Array<import("../../webgl/PaletteTexture.js").default>}
     * @private
     */
    this.paletteTextures_ = options.paletteTextures || [];
  }

  /**
   * @param {Options} options Options.
   * @override
   */
  reset(options) {
    super.reset(options);
    if (this.helper) {
      const gl = this.helper.getGL();
      for (const paletteTexture of this.paletteTextures_) {
        paletteTexture.delete(gl);
      }
    }

    this.vertexShader_ = options.vertexShader;
    this.fragmentShader_ = options.fragmentShader;
    this.paletteTextures_ = options.paletteTextures || [];

    if (this.helper) {
      this.program_ = this.helper.getProgram(
        this.fragmentShader_,
        this.vertexShader_,
      );
      const gl = this.helper.getGL();
      for (const paletteTexture of this.paletteTextures_) {
        // upload the texture data
        paletteTexture.getTexture(gl);
      }
    }
  }

  /**
   * @override
   */
  afterHelperCreated() {
    super.afterHelperCreated();

    const gl = this.helper.getGL();
    for (const paletteTexture of this.paletteTextures_) {
      // upload the texture data
      paletteTexture.getTexture(gl);
    }

    this.program_ = this.helper.getProgram(
      this.fragmentShader_,
      this.vertexShader_,
    );
    this.helper.flushBufferData(this.indices_);
  }

  /**
   * @override
   */
  removeHelper() {
    if (this.helper) {
      const gl = this.helper.getGL();
      for (const paletteTexture of this.paletteTextures_) {
        paletteTexture.delete(gl);
      }
    }

    super.removeHelper();
  }

  /**
   * @override
   */
  createTileRepresentation(options) {
    return new TileTexture(options);
  }

  /**
   * @override
   */
  beforeTilesRender(frameState, tilesWithAlpha) {
    super.beforeTilesRender(frameState, tilesWithAlpha);
    this.helper.useProgram(this.program_, frameState);
  }

  /**
   * @override
   */
  renderTile(
    tileTexture,
    tileTransform,
    frameState,
    renderExtent,
    tileResolution,
    tileSize,
    tileOrigin,
    tileExtent,
    depth,
    gutter,
    alpha,
  ) {
    const gl = this.helper.getGL();
    this.helper.bindBuffer(tileTexture.coords);
    this.helper.bindBuffer(this.indices_);
    this.helper.enableAttributes(attributeDescriptions);

    let textureSlot = 0;
    while (textureSlot < tileTexture.textures.length) {
      const uniformName = `${Uniforms.TILE_TEXTURE_ARRAY}[${textureSlot}]`;
      this.helper.bindTexture(
        tileTexture.textures[textureSlot],
        textureSlot,
        uniformName,
      );
      ++textureSlot;
    }

    for (
      let paletteIndex = 0;
      paletteIndex < this.paletteTextures_.length;
      ++paletteIndex
    ) {
      const paletteTexture = this.paletteTextures_[paletteIndex];
      const texture = paletteTexture.getTexture(gl);
      this.helper.bindTexture(texture, textureSlot, paletteTexture.name);
      ++textureSlot;
    }

    const viewState = frameState.viewState;

    const tileWidthWithGutter = tileSize[0] + 2 * gutter;
    const tileHeightWithGutter = tileSize[1] + 2 * gutter;

    const tile = tileTexture.tile;
    const tileCoord = tile.tileCoord;

    const tileCenterI = tileCoord[1];
    const tileCenterJ = tileCoord[2];

    this.helper.setUniformMatrixValue(
      Uniforms.TILE_TRANSFORM,
      mat4FromTransform(this.tempMat4, tileTransform),
    );

    this.helper.setUniformFloatValue(Uniforms.TRANSITION_ALPHA, alpha);
    this.helper.setUniformFloatValue(Uniforms.DEPTH, depth);

    let gutterExtent = renderExtent;
    if (gutter > 0) {
      gutterExtent = tileExtent;
      getIntersection(gutterExtent, renderExtent, gutterExtent);
    }
    this.helper.setUniformFloatVec4(Uniforms.RENDER_EXTENT, gutterExtent);

    this.helper.setUniformFloatValue(Uniforms.RESOLUTION, viewState.resolution);
    this.helper.setUniformFloatValue(Uniforms.ZOOM, viewState.zoom);

    this.helper.setUniformFloatValue(
      Uniforms.TEXTURE_PIXEL_WIDTH,
      tileWidthWithGutter,
    );
    this.helper.setUniformFloatValue(
      Uniforms.TEXTURE_PIXEL_HEIGHT,
      tileHeightWithGutter,
    );
    this.helper.setUniformFloatValue(
      Uniforms.TEXTURE_RESOLUTION,
      tileResolution,
    );
    this.helper.setUniformFloatValue(
      Uniforms.TEXTURE_ORIGIN_X,
      tileOrigin[0] +
        tileCenterI * tileSize[0] * tileResolution -
        gutter * tileResolution,
    );
    this.helper.setUniformFloatValue(
      Uniforms.TEXTURE_ORIGIN_Y,
      tileOrigin[1] -
        tileCenterJ * tileSize[1] * tileResolution +
        gutter * tileResolution,
    );

    this.helper.drawElements(0, this.indices_.getSize());
  }

  /**
   * @param {import("../../pixel.js").Pixel} pixel Pixel.
   * @return {Uint8ClampedArray|Uint8Array|Float32Array|DataView} Data at the pixel location.
   * @override
   */
  getData(pixel) {
    const gl = this.helper.getGL();
    if (!gl) {
      return null;
    }

    const frameState = this.frameState;
    if (!frameState) {
      return null;
    }

    const layer = this.getLayer();
    const coordinate = applyTransform(
      frameState.pixelToCoordinateTransform,
      pixel.slice(),
    );

    const viewState = frameState.viewState;
    const layerExtent = layer.getExtent();
    if (layerExtent) {
      if (
        !containsCoordinate(
          fromUserExtent(layerExtent, viewState.projection),
          coordinate,
        )
      ) {
        return null;
      }
    }

    // determine last source suitable for rendering at coordinate
    const sources = layer.getSources(
      boundingExtent([coordinate]),
      viewState.resolution,
    );
    let i, source, tileGrid;
    for (i = sources.length - 1; i >= 0; --i) {
      source = sources[i];
      if (source.getState() === 'ready') {
        tileGrid = source.getTileGridForProjection(viewState.projection);
        if (source.getWrapX()) {
          break;
        }
        const gridExtent = tileGrid.getExtent();
        if (!gridExtent || containsCoordinate(gridExtent, coordinate)) {
          break;
        }
      }
    }
    if (i < 0) {
      return null;
    }

    const tileTextureCache = this.tileRepresentationCache;
    for (
      let z = tileGrid.getZForResolution(viewState.resolution);
      z >= tileGrid.getMinZoom();
      --z
    ) {
      const tileCoord = tileGrid.getTileCoordForCoordAndZ(coordinate, z);
      const cacheKey = getCacheKey(source, tileCoord);
      if (!tileTextureCache.containsKey(cacheKey)) {
        continue;
      }
      const tileTexture = tileTextureCache.get(cacheKey);
      const tile = tileTexture.tile;
      if (tile.getState() === TileState.EMPTY) {
        return null;
      }
      if (!tileTexture.loaded) {
        continue;
      }
      const tileOrigin = tileGrid.getOrigin(z);
      const tileSize = toSize(tileGrid.getTileSize(z));
      const tileResolution = tileGrid.getResolution(z);

      const col =
        (coordinate[0] - tileOrigin[0]) / tileResolution -
        tileCoord[1] * tileSize[0];

      const row =
        (tileOrigin[1] - coordinate[1]) / tileResolution -
        tileCoord[2] * tileSize[1];

      return tileTexture.getPixelData(col, row);
    }
    return null;
  }

  /**
   * Clean up.
   * @override
   */
  disposeInternal() {
    const helper = this.helper;
    if (helper) {
      const gl = helper.getGL();
      for (const paletteTexture of this.paletteTextures_) {
        paletteTexture.delete(gl);
      }
      this.paletteTextures_.length = 0;

      gl.deleteProgram(this.program_);
      delete this.program_;
      helper.deleteBuffer(this.indices_);
    }
    super.disposeInternal();
    delete this.indices_;
  }
}

export default WebGLTileLayerRenderer;
