import defaultOptions from './default_options'
import * as util from './util'
import Renderer from './renderer'
import { EARTH_CIRCUMFERENCE } from './constants'

export default class GLOperations {
  constructor (options) {
    this.options = Object.assign({}, defaultOptions, options)
    this._checkColorScaleAndSentinels()
    this.setHillshadeOptions()
    const {
      nodataValue,
      tileSize
    } = this.options

    this._renderer = new Renderer(
      this,
      tileSize,
      nodataValue,
      this.options.colorScale,
      this.options.sentinelValues,
      this.options.colorscaleMaxLength,
      this.options.sentinelMaxLength,
      this.options.aboveColor,
      this.options.belowColor
    )

    this._renderer.generateAmbientDirections(this.options.hsAdvAmbientIterations)
    this._renderer.generateSunDirections(
      this.options.hsAdvSoftIterations,
      this.options.hsAdvSunRadiusMultiplier
    )
  }

  destroy () {
    if (this._renderer) this._renderer.destroy()
    this._renderer = null
  }

  updateOptions (options) {
    const {
      url: prevUrl,
      operationUrlA: prevUrlA,
      operationUrlB: prevUrlB,
      operationUrlC: prevUrlC,
      operationUrlD: prevUrlD,
      colorScale: prevColorScale,
      sentinelValues: prevSentinelValues,
      hillshadeType: prevHillshadeType,
      hsAdvSoftIterations: prevHsAdvSoftIterations,
      hsAdvAmbientIterations: prevHsAdvAmbientIterations,
      hsAdvSunRadiusMultiplier: prevHsAdvSunRadiusMultiplier,
      hsAdvBaselayerUrl: prevHsAdvBaselayerUrl,
      colorscaleMaxLength: prevScaleMaxLength,
      sentinelMaxLength: prevSentinelMaxLength,
      aboveColor: prevAboveColor,
      belowColor: prevBelowColor
    } = this.options
    Object.assign(this.options, options)
    // create new renderer if max length of sentinels or colorscale changes
    if (this.options.colorscaleMaxLength !== prevScaleMaxLength || this.options.sentinelMaxLength !== prevSentinelMaxLength) {
      const tileSize = this._tileSizeAsNumber()
      const renderer = new Renderer(
        this,
        tileSize,
        this.options.nodataValue,
        this.options.colorScale,
        this.options.sentinelValues,
        this.options.colorscaleMaxLength,
        this.options.sentinelMaxLength
      )

      this._renderer.regl.destroy()
      // @ts-ignore
      delete this._renderer

      Object.assign(this, {
        _renderer: renderer
      })
    }
    this._checkColorScaleAndSentinels()
    if (this.options.colorScale !== prevColorScale) {
      this._renderer.updateColorscale(this.options.colorScale)
    }
    if (this.options.aboveColor !== prevAboveColor || this.options.belowColor !== prevBelowColor) {
      this._renderer.updateOuteRangeColor(this.options.aboveColor, this.options.belowColor)
    }
    if (this.options.sentinelValues !== prevSentinelValues) {
      this._renderer.updateSentinels(this.options.sentinelValues)
    }
    if (this.options.hsAdvAmbientIterations !== prevHsAdvAmbientIterations) {
      this._renderer.generateAmbientDirections(this.options.hsAdvAmbientIterations)
    }
    if (
      this.options.hsAdvSoftIterations !== prevHsAdvSoftIterations ||
      this.options.hsAdvSunRadiusMultiplier !== prevHsAdvSunRadiusMultiplier
    ) {
      this._renderer.generateSunDirections(this.options.hsAdvSoftIterations, this.options.hsAdvSunRadiusMultiplier)
    }
    this.setHillshadeOptions()

    if (this.options.extraPixelLayers > 0 && this.options.glOperation === 'none') {
      this._maybeLoadExtraLayers(prevUrlA, prevUrlB, prevUrlC, prevUrlD)
    }
    // TODO: Fix shader so simple hillshading works ok with larger texture than tileSize
    if (this.options.hillshadeType !== prevHillshadeType) {
      // reduce textureManager size as simple hillshading type currently gets "edges" around the tiles with larger texture.
      if (this.options.hillshadeType === 'simple') {
        this._renderer.setMaxTextureDimension(this._tileSizeAsNumber())
      } else if (prevHillshadeType === 'simple') {
        this._renderer.setMaxTextureDimension(this._renderer.findMaxTextureDimension())
      }
    }

    if (this.options.url !== prevUrl) {
      // need to clear tiles so they are not reused with adv.hs.
      this._renderer.textureManager.clearTiles()
    }

    if (this.options.hsAdvBaselayerUrl !== prevHsAdvBaselayerUrl) {
      // need to clear tiles so they are not reused
      // TODO: Check why necessary
      this._renderer.textureManagerHillshade.clearTiles()
    }
  }

  createTile (coords) {
    const {
      extraPixelLayers,
      tileSize,
      url,
      operationUrlA,
      operationUrlB,
      operationUrlC,
      operationUrlD,
      operationUrlE,
      operationUrlF,
      filterLowA,
      filterHighA,
      filterLowB,
      filterHighB,
      filterLowC,
      filterHighC,
      filterLowD,
      filterHighD,
      filterLowE,
      filterHighE,
      filterLowF,
      filterHighF,
      multiplierA,
      multiplierB,
      multiplierC,
      multiplierD,
      multiplierE,
      multiplierF
    } = this.options

    // Create a <canvas> element to contain the rendered image.
    const tileCanvas = document.createElement('canvas')
    // Configure the element.
    Object.assign(tileCanvas, {
      className: 'gl-tile',
      width: tileSize,
      height: tileSize
    })

    return new Promise((resolve) => {
      if (this.options.glOperation === 'none') {
        // Download an extra layer if required
        if (extraPixelLayers === 1) {
          this._fetchTileData(coords, operationUrlA).then((pixelDataA) => {
            tileCanvas.pixelDataA = pixelDataA
          })
        }
  
        this._fetchTileData(coords, url).then((pixelData) => {
          let [sourceX, sourceY] = this._renderer.renderTile(
            { coords, pixelData },
            this.options._hillshadeOptions,
            coords.z
          )

          // Copy pixel data to a property on tile canvas element (for later retrieval).
          tileCanvas.pixelData = pixelData

          this._copyToTileCanvas(tileCanvas, sourceX, sourceY)

          resolve(tileCanvas)
        })
      } else if (this.options.glOperation === 'diff') {
        Promise.all([
          this._fetchTileData(coords, operationUrlA),
          this._fetchTileData(coords, operationUrlB)
        ]).then((pixelDataArray) => {
          const pixelDataA = pixelDataArray[0]
          const pixelDataB = pixelDataArray[1]
          const [sourceX, sourceY, resultEncodedPixels] = this._renderer.renderTileDiff(
            { coords: coords, pixelData: pixelDataA },
            { coords: coords, pixelData: pixelDataB }
          )
  
          tileCanvas.pixelData = resultEncodedPixels
          tileCanvas.pixelDataA = pixelDataA
          tileCanvas.pixelDataB = pixelDataB
  
          // Copy contents to tileCanvas.
          this._copyToTileCanvas(tileCanvas, sourceX, sourceY)
        })
      } else if (this.options.glOperation === 'multi' && this.options.multiLayers === 1) {
        Promise.all([
          this._fetchTileData(coords, operationUrlA)
        ]).then((pixelDataArray) => {
          const pixelDataA = pixelDataArray[0]
          const [sourceX, sourceY, resultEncodedPixels] = this._renderer.renderTileMulti1(
            { coords: coords, pixelData: pixelDataA },
            filterLowA,
            filterHighA,
            multiplierA
          )
  
          // Copy pixel data to a property on tile canvas element (for later retrieval).
          tileCanvas.pixelData = resultEncodedPixels
          tileCanvas.pixelDataA = pixelDataA
  
          // Copy contents to tileCanvas.
          this._copyToTileCanvas(tileCanvas, sourceX, sourceY)
        })
      } else if (this.options.glOperation === 'multi' && this.options.multiLayers === 2) {
        Promise.all([
          this._fetchTileData(coords, operationUrlA),
          this._fetchTileData(coords, operationUrlB)
        ]).then((pixelDataArray) => {
          const pixelDataA = pixelDataArray[0]
          const pixelDataB = pixelDataArray[1]
          const [sourceX, sourceY, resultEncodedPixels] = this._renderer.renderTileMulti2(
            { coords: coords, pixelData: pixelDataA },
            { coords: coords, pixelData: pixelDataB },
            filterLowA,
            filterHighA,
            filterLowB,
            filterHighB,
            multiplierA,
            multiplierB
          )
  
          // Copy pixel data to a property on tile canvas element (for later retrieval).
          tileCanvas.pixelData = resultEncodedPixels
          tileCanvas.pixelDataA = pixelDataA
          tileCanvas.pixelDataB = pixelDataB
  
          // Copy contents to tileCanvas.
          this._copyToTileCanvas(tileCanvas, sourceX, sourceY)
        })
      } else if (this.options.glOperation === 'multi' && this.options.multiLayers === 3) {
        Promise.all([
          this._fetchTileData(coords, operationUrlA),
          this._fetchTileData(coords, operationUrlB),
          this._fetchTileData(coords, operationUrlC)
        ]).then((pixelDataArray) => {
          const pixelDataA = pixelDataArray[0]
          const pixelDataB = pixelDataArray[1]
          const pixelDataC = pixelDataArray[2]
          const [sourceX, sourceY, resultEncodedPixels] = this._renderer.renderTileMulti3(
            { coords: coords, pixelData: pixelDataA },
            { coords: coords, pixelData: pixelDataB },
            { coords: coords, pixelData: pixelDataC },
            filterLowA,
            filterHighA,
            filterLowB,
            filterHighB,
            filterLowC,
            filterHighC,
            multiplierA,
            multiplierB,
            multiplierC
          )
  
          // Copy pixel data to a property on tile canvas element (for later retrieval).
          tileCanvas.pixelData = resultEncodedPixels
          tileCanvas.pixelDataA = pixelDataA
          tileCanvas.pixelDataB = pixelDataB
          tileCanvas.pixelDataC = pixelDataC
  
          // Copy contents to tileCanvas.
          this._copyToTileCanvas(tileCanvas, sourceX, sourceY)
        })
      } else if (this.options.glOperation === 'multi' && this.options.multiLayers === 4) {
        Promise.all([
          this._fetchTileData(coords, operationUrlA),
          this._fetchTileData(coords, operationUrlB),
          this._fetchTileData(coords, operationUrlC),
          this._fetchTileData(coords, operationUrlD)
        ]).then((pixelDataArray) => {
          const pixelDataA = pixelDataArray[0]
          const pixelDataB = pixelDataArray[1]
          const pixelDataC = pixelDataArray[2]
          const pixelDataD = pixelDataArray[3]
          const [sourceX, sourceY, resultEncodedPixels] = this._renderer.renderTileMulti4(
            { coords: coords, pixelData: pixelDataA },
            { coords: coords, pixelData: pixelDataB },
            { coords: coords, pixelData: pixelDataC },
            { coords: coords, pixelData: pixelDataD },
            filterLowA,
            filterHighA,
            filterLowB,
            filterHighB,
            filterLowC,
            filterHighC,
            filterLowD,
            filterHighD,
            multiplierA,
            multiplierB,
            multiplierC,
            multiplierD
          )
  
          // Copy pixel data to a property on tile canvas element (for later retrieval).
          tileCanvas.pixelData = resultEncodedPixels
          tileCanvas.pixelDataA = pixelDataA
          tileCanvas.pixelDataB = pixelDataB
          tileCanvas.pixelDataC = pixelDataC
          tileCanvas.pixelDataD = pixelDataD
  
          // Copy contents to tileCanvas.
          this._copyToTileCanvas(tileCanvas, sourceX, sourceY)
        })
      } else if (this.options.glOperation === 'multi' && this.options.multiLayers === 5) {
        Promise.all([
          this._fetchTileData(coords, operationUrlA),
          this._fetchTileData(coords, operationUrlB),
          this._fetchTileData(coords, operationUrlC),
          this._fetchTileData(coords, operationUrlD),
          this._fetchTileData(coords, operationUrlE)
        ]).then((pixelDataArray) => {
          const pixelDataA = pixelDataArray[0]
          const pixelDataB = pixelDataArray[1]
          const pixelDataC = pixelDataArray[2]
          const pixelDataD = pixelDataArray[3]
          const pixelDataE = pixelDataArray[4]
          const [sourceX, sourceY, resultEncodedPixels] = this._renderer.renderTileMulti5(
            { coords: coords, pixelData: pixelDataA },
            { coords: coords, pixelData: pixelDataB },
            { coords: coords, pixelData: pixelDataC },
            { coords: coords, pixelData: pixelDataD },
            { coords: coords, pixelData: pixelDataE },
            filterLowA,
            filterHighA,
            filterLowB,
            filterHighB,
            filterLowC,
            filterHighC,
            filterLowD,
            filterHighD,
            filterLowE,
            filterHighE,
            multiplierA,
            multiplierB,
            multiplierC,
            multiplierD,
            multiplierE
          )
  
          // Copy pixel data to a property on tile canvas element (for later retrieval).
          tileCanvas.pixelData = resultEncodedPixels
          tileCanvas.pixelDataA = pixelDataA
          tileCanvas.pixelDataB = pixelDataB
          tileCanvas.pixelDataC = pixelDataC
          tileCanvas.pixelDataD = pixelDataD
          tileCanvas.pixelDataE = pixelDataE
  
          // Copy contents to tileCanvas.
          this._copyToTileCanvas(tileCanvas, sourceX, sourceY)
        })
      } else if (this.options.glOperation === 'multi' && this.options.multiLayers === 6) {
        Promise.all([
          this._fetchTileData(coords, operationUrlA),
          this._fetchTileData(coords, operationUrlB),
          this._fetchTileData(coords, operationUrlC),
          this._fetchTileData(coords, operationUrlD),
          this._fetchTileData(coords, operationUrlE),
          this._fetchTileData(coords, operationUrlF)
        ]).then((pixelDataArray) => {
          const pixelDataA = pixelDataArray[0]
          const pixelDataB = pixelDataArray[1]
          const pixelDataC = pixelDataArray[2]
          const pixelDataD = pixelDataArray[3]
          const pixelDataE = pixelDataArray[4]
          const pixelDataF = pixelDataArray[5]
          const [sourceX, sourceY, resultEncodedPixels] = this._renderer.renderTileMulti6(
            { coords: coords, pixelData: pixelDataA },
            { coords: coords, pixelData: pixelDataB },
            { coords: coords, pixelData: pixelDataC },
            { coords: coords, pixelData: pixelDataD },
            { coords: coords, pixelData: pixelDataE },
            { coords: coords, pixelData: pixelDataF },
            filterLowA,
            filterHighA,
            filterLowB,
            filterHighB,
            filterLowC,
            filterHighC,
            filterLowD,
            filterHighD,
            filterLowE,
            filterHighE,
            filterLowF,
            filterHighF,
            multiplierA,
            multiplierB,
            multiplierC,
            multiplierD,
            multiplierE,
            multiplierF
          )
  
          // Copy pixel data to a property on tile canvas element (for later retrieval).
          tileCanvas.pixelData = resultEncodedPixels
          tileCanvas.pixelDataA = pixelDataA
          tileCanvas.pixelDataB = pixelDataB
          tileCanvas.pixelDataC = pixelDataC
          tileCanvas.pixelDataD = pixelDataD
          tileCanvas.pixelDataE = pixelDataE
          tileCanvas.pixelDataF = pixelDataF
  
          // Copy contents to tileCanvas.
          this._copyToTileCanvas(tileCanvas, sourceX, sourceY)
        })
      }
    })
  
  }

  _checkColorScaleAndSentinels () {
    const {
      colorScale,
      sentinelValues,
      colorscaleMaxLength,
      sentinelMaxLength
    } = this.options
    if (colorScale.length === 0 && sentinelValues.length === 0) {
      throw new Error('Either `colorScale` or `sentinelValues` must be of non-zero length.')
    }
    if (colorScale.length > colorscaleMaxLength) {
      throw new Error(
        `Color scale length ${colorScale.length} exceeds the maximum, ${colorscaleMaxLength}.`
      )
    }
    if (sentinelValues.length > sentinelMaxLength) {
      throw new Error(
        `Sentinel values length ${sentinelValues.length} exceeds the maximum, ${sentinelMaxLength}.`
      )
    }
  }

  setHillshadeOptions () {
    this.options._hillshadeOptions = {
      hillshadeType: this.options.hillshadeType,
      hsAdvValueScale: this.options.hsAdvValueScale,
      hsAdvPixelScale: this.options.hsAdvPixelScale,
      hsSimpleSlopescale: this.options.hsSimpleSlopescale,
      hsSimpleAzimuth: this.options.hsSimpleAzimuth,
      hsSimpleAltitude: this.options.hsSimpleAltitude,
      hsSimpleZoomdelta: this.options.hsSimpleZoomdelta,
      hsAdvSoftIterations: this.options.hsAdvSoftIterations,
      hsAdvAmbientIterations: this.options.hsAdvAmbientIterations,
      hsAdvSunRadiusMultiplier: this.options.hsAdvSunRadiusMultiplier,
      hsAdvFinalSoftMultiplier: this.options.hsAdvFinalSoftMultiplier,
      hsAdvFinalAmbientMultiplier: this.options.hsAdvFinalAmbientMultiplier,
      hsAdvBaselayerUrl: this.options.hsAdvBaselayerUrl,
      hsAdvSmoothInput: this.options.hsAdvSmoothInput,
      hsAdvSmoothInputKernel: this.options.hsAdvSmoothInputKernel,
      hsPregenUrl: this.options.hsPregenUrl
    }
  }

  _tileSizeAsNumber () {
    return this.options.tileSize
  }

  async _fetchTileData (coords, url, tileFormat = this.options.tileFormat) {
    if (tileFormat === 'float32' || tileFormat === 'image') {
      return util.fetchPNGData(this.getTileUrl(coords, url), this.options.nodataValue, this._tileSizeAsNumber())
    } else if (tileFormat === 'dem') {
      const nodataTile = util.createNoDataTile(this.options.nodataValue, this._tileSizeAsNumber())
      const imageData = await util.fetchPNGData(this.getTileUrl(coords, url), this.options.nodataValue, this._tileSizeAsNumber())
      if (util.typedArraysAreEqual(imageData, nodataTile)) {
        return imageData
      } else {
        const rgbaData = this._renderer.renderConvertDem(imageData)
        return rgbaData
      }
    }
    return util.createNoDataTile(this.options.nodataValue, this._tileSizeAsNumber())
  }

  _copyToTileCanvas (tile, sourceX, sourceY) {
    const tileSize = this._tileSizeAsNumber()
    const tileCanvas2DContext = tile.getContext('2d')
    if (tileCanvas2DContext === null) {
      throw new Error('Tile canvas 2D context is null.')
    }
    // Clear the current contents of the canvas. Otherwise, the new image will be composited with
    // the existing image.
    tileCanvas2DContext.clearRect(0, 0, tileSize, tileSize)
    // Copy the image data from the Renderer's canvas to the tile's canvas.
    tileCanvas2DContext.drawImage(
      this._renderer.canvas,
      sourceX, sourceY, tileSize, tileSize, // source canvas offset (x, y) and size (x, y)
      0, 0, tileSize, tileSize             // destination canvas offset (x, y) and size (x, y)
    )
  }

  getTileUrl (coords, url) {
    return url
      .replace(/{z}/g, String(coords.z))
      .replace(/{x}/g, String(coords.x))
      .replace(/{y}/g, String(coords.y))
  }

  _getPixelScale (coords) {
    let {bbox: {south, north}, z: zoom} = coords
    let pixelScale = 1
    let lat = (south + north) / 2
    if (this.options.hsAdvPixelScale === 'auto') {
      pixelScale = EARTH_CIRCUMFERENCE * Math.abs(
        Math.cos(lat / 180 * Math.PI
        )) / Math.pow(2, zoom + 8)
    } else if (typeof this.options.hsAdvPixelScale === 'number') {
      pixelScale = this.options.hsAdvPixelScale / (this._tileSizeAsNumber() * (2 ** zoom))
    }
    return pixelScale
  }
}