import * as RE from 'rogue-engine'
import * as THREE from 'three'

// https://codesandbox.io/s/prototypes-pygsc7?file=/prototypes/water/001_gerstner.html:0-10387
// https://catlikecoding.com/unity/tutorials/flow/waves/

// Old Fungi Version
// https://sketchpunk.bitbucket.io/src/fungi_v4/002_gerstner_waves.html
// https://bitbucket.org/sketchpunk/sketchpunk.bitbucket.io/src/c036dfb4b5a93425220b4bb482c0e96e1edc77cf/src/fungi_v4/002_gerstner_waves.html#lines-304

export default class GerstnerWavesComponent extends RE.Component {
    wave: GerstnerWave = new GerstnerWave(2, 12)
    vertexShader    = `#version 300 es
    in vec3 position;
    in vec3 normal;
    in vec2 uv;
    
    uniform mat4 modelMatrix;
    uniform mat4 viewMatrix;
    uniform mat4 projectionMatrix;
    
    uniform float clock;

    out vec3 fragNorm;
    out vec3 fragWPos;

    /////////////////////////////////////////////////////////////////
    // #region GERSTNER WAVE
    const float PI_2	= 6.283185307179586;

    uniform 	vec4	Waves[3];   // XY : Direction, Z: Steepness, W: Wavelength
    uniform 	int 	WaveCnt;
    uniform		float 	WaveSpeed;
    uniform 	int 	WaveXZ;
    uniform     float   GridSize;
    uniform     float   GridRes;

    // https://catlikecoding.com/unity/tutorials/flow/waves/
    // XY : norm( Dir.xy ), Z : Steepness( 0->1 ), W : 0 to Total Grid Divisions, Max gives one wave
    vec3 gerstnerWave( vec2 dir, float steepness, float wavelength, vec2 pnt, float time, out vec3 tangent, out vec3 binormal ){
        float k = PI_2 / wavelength;	                // Phase Increment
        float c = sqrt( 9.8 / k );	                    // Phase Speed. Higher the wave, the faster it moves, Gravity Constant
        float f = k * ( dot( dir, pnt ) - c * time );   // Frequency - Specific time in Phase : PhaseInc * ( Angle - PhaseSpeed * Time )
        float a = steepness / k;		                // Amptitude, Steep=1, app is at max where mesh will start to loop onto self.
        
        // cache results for multi reuse.
        float sin_f  = sin( f );
        float cos_f  = cos( f );
        float acos_f = a * cos_f;
        float scos_f = steepness * cos_f;
        float ssin_f = steepness * sin_f;

        /*====================================
        Must Init outside function as
        vec3 tangent = vec3( 1.0, 0.0, 0.0 );
        vec3 binormal = vec3( 0.0, 0.0, 1.0 );
        normal is normalize(cross(binormal, tangent)); */

        tangent += vec3(
          -dir.x * dir.x * ssin_f,
           dir.x * scos_f,
          -dir.x * dir.y * ssin_f
      );

      binormal += vec3(
          -dir.x * dir.y * ssin_f,
           dir.y * scos_f,
          -dir.y * dir.y * ssin_f
      );
      
      return vec3( 
          dir.x * acos_f, 
          a * sin_f,       
          dir.y * acos_f
      );
  }
  // #endregion

  /////////////////////////////////////////////////////////////////
  // #region GRID
  const float FLOOR = 0.001;

  vec2 gridCenterCoord( vec2 p ){
      float inc = GridSize / GridRes;
      p += vec2( GridSize * 0.5 );       // Shift so top Left is at origin
      return floor( p / vec2( inc ) );
  }

  vec2 gridCoord( vec2 p ){
      float inc = GridSize / GridRes;
      return floor( p / vec2( inc ) );
      // return p / vec2( inc );
  }
  // #endregion
  
  /////////////////////////////////////////////////////////////////

  void main(){
      vec3 pos = position;
      fragNorm = normal;

      if( pos.y > FLOOR ){
          vec2    coord       = gridCoord( pos.xz );
          vec3    offset      = vec3( 0.0 );
          vec3    tangent     = vec3( 1.0, 0.0, 0.0 ); // Normal is Up, So tangent is Right
          vec3    binormal    = vec3( 0.0, 0.0, 1.0 ); // ... and Binormal is Forward
          float   time        = clock * WaveSpeed;    

          if( WaveCnt > 0 ) offset += gerstnerWave( normalize(Waves[0].xy), Waves[0].z, Waves[0].w, coord, time, tangent, binormal );
          if( WaveCnt > 1 ) offset += gerstnerWave( normalize(Waves[1].xy), Waves[1].z, Waves[1].w, coord, time, tangent, binormal );
          if( WaveCnt > 2 ) offset += gerstnerWave( normalize(Waves[2].xy), Waves[2].z, Waves[2].w, coord, time, tangent, binormal );
          
          if( WaveXZ == 1 ){
              // Only keep height change
              offset.x = 0.0;
              offset.z = 0.0;
          }else{
              // keep Wave in cube bounds
              if( coord.x < 1.0 || coord.x >= GridRes ) offset.x = 0.0;
              if( coord.y < 1.0 || coord.y >= GridRes ) offset.z = 0.0;
          }

          pos += offset;

          // Only Apply Wave Normals on top face, use normal to find it
          if( normal.x < 0.001 && normal.z < 0.001 && normal.y > 0.9 ){
              fragNorm = normalize( cross( binormal, tangent ) );
          }
      }

      vec4 wPos   = modelMatrix * vec4( pos, 1.0 );
      fragWPos    = wPos.xyz;
      gl_Position = projectionMatrix * viewMatrix * wPos;
  }`

  fragmentShader  = `#version 300 es
  precision mediump float;

  in vec3 fragNorm;
  in vec3 fragWPos;

  uniform vec3 cameraPosition;

  out vec4 outColor;

  ////////////////////////////////////////////////////////////////////////


  ////////////////////////////////////////////////////////////////////////
  const vec3 lightWPos = vec3( 10.0, 10.0, 10.0 );

  void main(){
      // vec3 N = normalize( cross( dFdx(fragWPos), dFdy(fragWPos) ) );
      vec3 N = normalize( fragNorm );
      
      // float ns  = 6.0; // Normal is to light, scale it up to improve renderering
      // vec3 N    = normalize( vec3( fragNorm.x * ns, fragNorm.y, fragNorm.z * ns ) );

      vec3 L    = normalize( lightWPos - fragWPos );
      float NdL = clamp( dot( L, N ), 0.0, 1.0 );

      outColor  = vec4( vec3( NdL ), 1.0 );

      // if( NdL <= 0.0 ) outColor.rgb = vec3( 1.0, 0.0, 0.0 );
  }`

  elapsed = 0

  awake() {

  }

  start() {

    this.elapsed = 0;
    let objectThree = (this.object3d as any)
    let geometry: THREE.BoxGeometry = objectThree.geometry;

    let gridSize = geometry.parameters.width//2
    let gridRes = geometry.parameters.width * 6//12
    objectThree.material = new THREE.RawShaderMaterial(
      { depthTest: true,
        transparent: false,
        uniforms: {
          clock       : { value : 0 },
          Waves       : { value : new Float32Array( [ 0.5,0.5,0.09,10,  0,1,0.05,5,  0.3,0.7,0.01,2, ]) },
          WaveCnt     : { value : 3 },
          WaveSpeed   : { value : 1.0 },
          WaveXZ      : { value : 1 },
          GridSize    : { value : gridSize },
          GridRes     : { value : gridRes },
        },
        vertexShader: this.vertexShader,
        fragmentShader: this.fragmentShader,
      }
    )

    this.wave = new GerstnerWave(gridSize, gridRes)

  }

  update() {
    this.elapsed += RE.Runtime.deltaTime;
    (this.object3d as any).material.uniforms.clock.value = this.elapsed
  }
}

RE.registerComponent(GerstnerWavesComponent);

// Compute height of the position
class GerstnerWave {
  minH      = 1;
  gridSize  = 2;
  gridRes   = 12;
  waveSpeed = 1.0;

  constructor(gridSize, gridRes) {
    this.gridSize = gridSize//2
    this.gridRes = gridRes//12
  }

  waves = [
      { dir:[ 0.5,0.5 ], steep:0.09, wavelen:10 },
      { dir:[ 0,1 ], steep:0.05, wavelen:5 },
      { dir:[ 0.3,0.7 ], steep:0.01, wavelen:2 },
  ];

  static dot(a: THREE.Vector2, b: THREE.Vector2) {
    return a.x * b.x + a.y * b.y;
  }

  calcWaveH( w, coord, time ){
      const dir = new THREE.Vector2( w.dir ).normalize();
      const k = 6.283185307179586 / w.wavelen;	                // Phase Increment
      const c = Math.sqrt( 9.8 / k );	                            // Phase Speed. Higher the wave, the faster it moves, Gravity Constant
      const f = k * ( GerstnerWave.dot( dir, coord ) - c * time );        // Frequency - Specific time in Phase : PhaseInc * ( Angle - PhaseSpeed * Time )
      const a = w.steep / k;		                                // Amptitude, Steep=1, app is at max where mesh will start to loop onto self.
      
      // cache results for multi reuse.
      const sin_f  = Math.sin( f );
      // const cos_f  = Math.cos( f );
      // const acos_f = a * cos_f;

      // Gerstner Position
      // return [ 
      //     dir.x * acos_f, 
      //     a * sin_f,       
      //     dir.y * acos_f
      // ];

      // Just the Y value
      return a * sin_f;
  }

  getHit( p, clock=0 ){
      const coord = this.gridCoord( p );
      const time  = clock * this.waveSpeed;

      let h = 0;
      for( const w of this.waves ){
          h += this.calcWaveH( w, coord, time );
      }

      return [ p[0], this.minH + h, p[2] ];
  }

  gridCoord( p ){
      const inc = this.gridSize / this.gridRes;
      return {x: p[0] / inc , y: p[2] / inc };
  }
}