import {
  flatMap,
  isEmpty
} from 'lodash'
import * as util from './util'

export default class TextureManager {
  constructor (
    regl,
    tileSize = 256,
    maxTextureDimension,
    flipY = false,
    textureFormat = 'rgba',
    textureType = 'uint8'
  ) {
    const tilesAcross = Math.floor(maxTextureDimension / tileSize)
    const pixelsAcross = tilesAcross * tileSize
    const tileCapacity = tilesAcross * tilesAcross

    const texture = regl.texture({
      width: pixelsAcross,
      height: pixelsAcross,
      flipY: flipY,
      format: textureFormat,
      type: textureType
    })

    const contents = new Map()
    const available = this.allTextureCoordinates(tilesAcross, tileSize)

    Object.assign(this, {
      tileSize,
      tilesAcross,
      pixelsAcross,
      tileCapacity,
      texture,
      contents,
      available
    })
  }

  addTile (
    tileCoordinates,
    data
  ) {
    const {
      available,
      contents,
      texture,
      tileSize
    } = this

    const hashKey = this.hashTileCoordinates(tileCoordinates)
    if (contents.has(hashKey)) {
      const textureCoordinates = contents.get(hashKey)
      // We use a least-recently-used eviction policy for the tile cache. Map iterators are
      // convenient for this, because they return entries in insertion order. But for this to work
      // as expected, every time we access a tile, we need to reinsert it so that it moves to the
      // end of that insertion-order list.
      contents.delete(hashKey)
      contents.set(hashKey, textureCoordinates)
      return this.formatOutputTextureCoordinates(textureCoordinates)
    }
    if (isEmpty(available)) {
      // Get the first key inserted. Map.prototype.keys() produces an iterable iterator over the keys
      // in the order of insertion, so we can just use the iterator's first value.
      const firstInsertedKey = contents.keys().next().value
      this.removeByHashKey(firstInsertedKey)
    }
    // remove from list of available positions
    const textureCoordinates = available.pop()
    // store mapping of tile to texture coordinates
    contents.set(hashKey, textureCoordinates)

    const { x: textureX, y: textureY } = textureCoordinates
    texture.subimage({
      data,
      width: tileSize,
      height: tileSize
    }, textureX, textureY)

    return this.formatOutputTextureCoordinates(textureCoordinates)
  }

  removeTile (tileCoordinates) {
    this.removeByHashKey(this.hashTileCoordinates(tileCoordinates))
  }

  clearTiles () {
    for (const hashKey of Array.from(this.contents.keys())) {
      this.removeByHashKey(hashKey)
    }
  }

  destroy () {
    this.texture.destroy()
  }

  removeByHashKey (hashKey) {
    // This method only removes the key. The pixel data remains in the texture.
    if (this.contents.has(hashKey)) {
      const textureCoordinates = this.contents.get(hashKey)
      this.contents.delete(hashKey)
      this.available.push(textureCoordinates)
    }
  }

  formatOutputTextureCoordinates (textureCoordinates) {
    const { x, y } = textureCoordinates
    const { pixelsAcross, tileSize } = this
    return [
      {
        x: x / pixelsAcross,
        y: y / pixelsAcross
      },
      {
        x: (x + tileSize) / pixelsAcross,
        y: (y + tileSize) / pixelsAcross
      }
    ]
  }

  hashTileCoordinates ({ x, y, z }) {
    return `${x}:${y}:${z}`
  }

  getTextureCoordinates (tileCoordinates) {
    const hashKey = this.hashTileCoordinates(tileCoordinates)
    const textureCoordinates = this.contents.get(hashKey)
    return this.formatOutputTextureCoordinates(textureCoordinates)
  }

  allTextureCoordinates (tilesAcross, tileSize) {
    return flatMap(util.range(tilesAcross), x =>
      util.range(tilesAcross).map(y => ({
        x: x * tileSize,
        y: y * tileSize
      }))
    )
  }
}
