import OpenSimplexNoise from 'open-simplex-noise'
import { vec3 } from 'gl-matrix'
import Resource from '../../resource'

class HeightMap {
  constructor (size) {
    this.size = size
  }

  static async fromFile (file, amplitude) {
    const img = await Resource.loadImage(file)
    if (img.width / img.height !== 1) throw new Error('Height Map needs to be of 1:1 aspect ratio.')

    const hmap = new HeightMap(img.width)
    let sampler = Resource.imageToSampler(img)

    for (let x = 0; x < img.width; x++) {
      for (let y = 0; y < img.width; y++) {
        hmap['h' + x + ';' + y] = (sampler(x, y)[0] / 255 * 2 - 1) * amplitude
      }
    }

    sampler = null

    return hmap
  }

  getHeight (x, y) {
    if (x > this.size || y > this.size || x < 0 || y < 0) return 0
    if (!this['h' + x + ';' + y]) return 0
    return this['h' + x + ';' + y]
  }

  getNormal (x, y) {
    const hL = this.getHeight(x - 1, y)
    const hR = this.getHeight(x + 1, y)
    const hD = this.getHeight(x, y - 1)
    const hU = this.getHeight(x, y + 1)
    const normal = vec3.fromValues(hL - hR, 2.0, hD - hU)
    vec3.normalize(normal, normal)
    return normal
  }

  // Vertex position based getters with LOD subnode support

  getHeightPlaneMesh (mesh, vx, vz, absolute) {
    let pos = mesh.pos
    if (absolute) pos = mesh.absolutePosition
    return this.getHeight(pos[0] + vx, pos[2] + vz)
  }

  getNormalPlaneMesh (mesh, vx, vz, absolute) {
    let pos = mesh.pos
    if (absolute) pos = mesh.absolutePosition
    return this.getNormal(pos[0] + vx, pos[2] + vz)
  }
}

class SimplexHeightMap extends HeightMap {
  // amplitude    - Controls the amount the height changes. The higher, the taller the hills.
  // persistence  - Controls details, value in [0,1]. Higher increases grain, lower increases smoothness.
  // octaves      - Number of noise layers
  // period       - Distance above which we start to see similarities. The higher, the longer "hills" will be on a terrain.
  // lacunarity   - Controls period change across octaves. 2 is usually a good value to address all detail levels.
  constructor (seed, amplitude = 15, period = 80, persistence = 0.4, lacunarity = 2, octaves = 5) {
    super(0)
    this.seed = seed

    this.osn = new OpenSimplexNoise(seed)

    this.amplitude = amplitude
    this.period = period
    this.lacunarity = lacunarity
    this.persistence = persistence
    this.octaves = octaves
  }

  // Fractal/Fractional Brownian Motion (fBm) summation of 3D Perlin Simplex noise
  getNoise2D (o, x, y) {
    let output = 0.0
    let denom = 0.0
    let frequency = this.period
    let amplitude = this.amplitude

    for (let i = 0; i < o; i++) {
      output += (amplitude * this.osn.noise2D(x * frequency, y * frequency))
      denom += amplitude

      frequency *= this.lacunarity
      amplitude *= this.persistence
    }

    return (output / denom)
  }

  getNoise3D (o, x, y, z) {
    let output = 0.0
    let denom = 0.0
    let frequency = this.period
    let amplitude = this.amplitude

    for (let i = 0; i < o; i++) {
      output += (amplitude * this.osn.noise3D(x * frequency, y * frequency, z * frequency))
      denom += amplitude

      frequency *= this.lacunarity
      amplitude *= this.persistence
    }

    return (output / denom)
  }

  // 2D Height Map
  getHeight (x, y) {
    return this.getNoise2D(this.octaves, x, y)
  }
}

export { HeightMap, SimplexHeightMap }
