import { RenderPipeline }	from "../framework/RenderPipeline.js";

import { Block }			from "../framework/Block.js";

import Camera				from "../framework/Camera.js";

import EventManager			from "../framework/eventManager.js";

import { SpectrumPass }		from "../passes/SpectrumPass.js";

import { RowFFTPass }		from "../passes/RowFFTPass.js";

import { ColFFTPass }		from "../passes/ColFFTPass.js";

import { OceanRenderPass }	from "../passes/OceanRenderPass.js";

import { OceanSolidRenderPass }	from "../passes/OceanSolidRenderPass.js";

import { SkySpherePass }	from "../passes/SkySpherePass.js";


export class OceanPipeline extends RenderPipeline {

	constructor( engine, canvas ) {

		super( engine );

		this.canvas				= canvas;
		this.canvasConfigured	= false;

		this.gridSize			= 64;
		this.meshResolution		= 64;

		this.offsetX			= 0;
		this.offsetZ			= 0;

		this.wavelengthScale	= 1.0;

		this.tiling				= 1;

		this.handleInput		= true;

		this.renderMode			= "solid";
		this.shadingMode		= "lighting";

		this.isPaused			= false;
		this.elapsedTime		= 0;

	}


	async create( ) {

		this.patchSize		= 80;
		this.heightScale	= 54.0;
		this.timeScale		= 0.35;

		this.startTime		= performance.now();

		const simSize		= this.gridSize;

		this.memory.set( "h0Real", new Float32Array( simSize * simSize ) );

		this.memory.set( "h0Imag", new Float32Array( simSize * simSize ) );

		this.memory.set( "spectrumReal", new Float32Array( simSize * simSize ) );

		this.memory.set( "spectrumImag", new Float32Array( simSize * simSize ) );

		this.memory.set( "rowReal", new Float32Array( simSize * simSize ) );

		this.memory.set( "rowImag", new Float32Array( simSize * simSize ) );

		this.memory.set( "colReal", new Float32Array( simSize * simSize ) );

		this.memory.set( "colImag", new Float32Array( simSize * simSize ) );

		this.memory.set( "heightField", new Float32Array( simSize * simSize ) );

		this.memory.set( "computeParams", new Float32Array( 4 ) );

		this.memory.set( "renderParams", new Float32Array( [ simSize, this.patchSize, this.heightScale, 0, this.meshResolution, this.tiling, this.wavelengthScale ] ) );

		this._seedSpectrum();

		const geometry	= this._buildGridGeometry( this.meshResolution, this.patchSize );

		this.memory.set( "positions", geometry.positions );

		this.memory.set( "uvs", geometry.uvs );

		this.memory.set( "indices", geometry.indices );

		this.memory.set( "lineIndices", geometry.lineIndices );

		const skySphere	= this._buildSkySphereGeometry( 220, 32, 64 );

		this.memory.set( "skyPositions", skySphere.positions );

		this.memory.set( "skyNormals", skySphere.normals );

		this.memory.set( "skyIndices", skySphere.indices );

		const cubeGeometry			= this._buildCubeGeometry();

		this.memory.set( "cubePositions", cubeGeometry.positions );

		this.memory.set( "cubeColors", cubeGeometry.colors );

		this.memory.set( "cubeIndices", cubeGeometry.indices );


		const block				= new Block( "ocean", this );

		const spectrumPass		= new SpectrumPass( );

		const rowFFTPass		= new RowFFTPass( );

		const colFFTPass		= new ColFFTPass( );

		const renderWirePass	= new OceanRenderPass( );

		const renderSolidPass	= new OceanSolidRenderPass( );

	 	const skySpherePass		= new SkySpherePass( );

		block.addPass( "Spectrum", spectrumPass );

		block.addPass( "RowFFT", rowFFTPass );

		block.addPass( "ColFFT", colFFTPass );

		block.addPass( "RenderWire", renderWirePass );

		block.addPass( "RenderSolid", renderSolidPass );

		block.addPass( "SkySphere", skySpherePass );

		this.addBlock( block );

		this.camera				= new Camera( [ 0, 90, 120 ], [ 0, 0, 0 ], [ 0, 1, 0 ] );

		this.camera.far		= 6000.0;

		this.camera.pitch		= Math.PI / 3;

		this.camera.update();

		this.eventManager		= new EventManager( );

		if ( this.canvas && this.handleInput ) {

			this.eventManager.setup( this.canvas, this.camera );

			this.eventManager.registerEventListeners();

		}

		await super.create();

	}


	setHeightScale( value ) {

		this.heightScale					= value;

		this.memory.computeParams[ 2 ]		= value;
		this.memory.renderParams[ 2 ]		= value;

	}


	setWavelengthScale( value ) {

		if ( value <= 0 ) {

			value = 0.25;

		}

		this.wavelengthScale	= value;

		if ( this.memory && this.memory.renderParams ) {

			this.memory.renderParams[ 6 ] = this.wavelengthScale;

		}

		this._seedSpectrum( );

		const block			= this.getBlockByName( "ocean" );

		if ( block ) {

			const spectrumPass	= block.getPass( "Spectrum" );

			if ( spectrumPass && spectrumPass.shader ) {

				spectrumPass.shader.setVariable( "h0Real", this.memory.h0Real );

				spectrumPass.shader.setVariable( "h0Imag", this.memory.h0Imag );

			}

		}

	}


	setRenderMode( mode ) {

		if ( mode !== "wireframe" && mode !== "solid" ) {

			return;

		}

		this.renderMode	= mode;

	}


	setShadingMode( mode ) {

		let code = 0;

		if ( mode === "normals" ) {

			code = 1;

		} else if ( mode === "solid" ) {

			code = 2;

		} else if ( mode === "height" ) {

			code = 3;

		} else if ( mode === "realistic" ) {

			code = 4;

		} else {

			mode = "lighting";
			code = 0;

		}

		this.shadingMode				= mode;
		this.memory.renderParams[ 3 ]	= code;

	}


	setTiling( value ) {

		if ( value < 1 ) {

			value = 1;

		}

		this.tiling					= value;
		this.memory.renderParams[ 5 ]	= value;

	}

	setOffset( x, z ) {

		this.offsetX					= x;
		this.offsetZ					= z;

		// Offsets are now handled in the vertex shader via instancing.

	}


	setPaused( paused ) {

		if ( paused === this.isPaused ) {

			return;

		}

		this.isPaused	= paused;

		if ( !paused ) {

			this.startTime	= performance.now() - this.elapsedTime * 1000.0;

		}

	}


	async stepOnce( stepSeconds = 1 / 60 ) {

		if ( !this.isPaused ) {

			return;

		}

		this.elapsedTime					+= stepSeconds;

		this.memory.computeParams[ 0 ]		= this.elapsedTime * this.timeScale;
		this.memory.computeParams[ 1 ]		= this.gridSize;

		await this.bindBuffers( );
		await this.execute( );

	}


	async bindBuffers( ) {

		const nowSeconds			= ( performance.now() - this.startTime ) / 1000;

		if ( !this.isPaused ) {

			this.elapsedTime		= nowSeconds;

		}

		this.memory.computeParams[ 0 ]	= this.elapsedTime * this.timeScale;
		this.memory.computeParams[ 1 ]	= this.gridSize;


		const block				= this.getBlockByName( "ocean" );

		const spectrumPass		= block.getPass( "Spectrum" );

		const rowFFTPass		= block.getPass( "RowFFT" );

		const colFFTPass		= block.getPass( "ColFFT" );

		const renderWirePass	= block.getPass( "RenderWire" );

		const renderSolidPass	= block.getPass( "RenderSolid" );

		const skyPass			= block.getPass( "SkySphere" );


		await spectrumPass.bindBuffers( );

		await rowFFTPass.bindBuffers( );

		await colFFTPass.bindBuffers( );


		if ( this.renderMode === "solid" ) {

			await renderSolidPass.bindBuffers( );

		} else {

			await renderWirePass.bindBuffers( );

		}

		if ( skyPass ) {

			await skyPass.bindBuffers( );

		}

	}


	async execute( ) {

		const block				= this.getBlockByName( "ocean" );

		const spectrumPass		= block.getPass( "Spectrum" );

		const rowFFTPass		= block.getPass( "RowFFT" );

		const colFFTPass		= block.getPass( "ColFFT" );


		await spectrumPass.execute( );

		await rowFFTPass.execute( );

		await colFFTPass.execute( );

 }


	_seedSpectrum( ) {

		const size			= this.gridSize;

		const phillipsA		= 0.0006;

		const windSpeed		= 24.0;

		const windDir		= { x: 0.8, y: 0.6 };

		const baseL			= windSpeed * windSpeed / 9.81;

		let wlScale			= this.wavelengthScale;

		if ( wlScale <= 0 ) {

			wlScale = 0.25;

		}

		const L				= baseL;

		for ( let y = 0; y < size; y++ ) {

			for ( let x = 0; x < size; x++ ) {

				const kx			= ( x - size / 2 ) * ( Math.PI * 2 / this.patchSize );

				const ky			= ( y - size / 2 ) * ( Math.PI * 2 / this.patchSize );

				const kLength		= Math.sqrt( kx * kx + ky * ky );

				if ( kLength === 0 ) {

					continue;

				}

				const kxNorm		= kx / kLength;

				const kyNorm		= ky / kLength;

				const windDotK		= kxNorm * windDir.x + kyNorm * windDir.y;

				const kScaled		= kLength / wlScale;

				const phillips		= phillipsA * Math.exp( -1 / ( kScaled * kScaled * L * L ) ) / Math.pow( kScaled, 4 ) * ( windDotK * windDotK );

				const gaussianR		= this._gaussianRandom();

				const gaussianI		= this._gaussianRandom();

				const amplitude		= Math.sqrt( phillips ) * Math.SQRT1_2;

				const idx			= y * size + x;

				this.memory.h0Real[ idx ]	= gaussianR * amplitude;

				this.memory.h0Imag[ idx ]	= gaussianI * amplitude;

			}
		}

		// enforce conjugate symmetry: h0(-k) = conj( h0(k) ) for a real height field
		for ( let y = 0; y < size; y++ ) {

			for ( let x = 0; x < size; x++ ) {

				const idx			= y * size + x;

				const mx			= ( size - x ) % size;

				const my			= ( size - y ) % size;

				const mirrorIdx		= my * size + mx;

				this.memory.h0Real[ mirrorIdx ]	= this.memory.h0Real[ idx ];

				this.memory.h0Imag[ mirrorIdx ]	= -this.memory.h0Imag[ idx ];

			}
		}

		this.memory.computeParams[ 0 ] = 0;
		this.memory.computeParams[ 1 ] = this.gridSize;
		this.memory.computeParams[ 2 ] = this.heightScale;
		this.memory.computeParams[ 3 ] = this.patchSize;
		this.memory.renderParams[ 2 ]  = this.heightScale;

	}


	_gaussianRandom( ) {

		let u = 0, v = 0;

		while ( u === 0 ) u = Math.random();

		while ( v === 0 ) v = Math.random();

		return Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );

	}


	_buildGridGeometry( size, span ) {

		const vertexCount	= size * size;

		const positions		= new Float32Array( vertexCount * 3 );

		const uvs			= new Float32Array( vertexCount * 2 );

		const quadCount		= ( size - 1 ) * ( size - 1 );

		const indices		= new Uint32Array( quadCount * 6 );

		const horizontalLines	= size * ( size - 1 );

		const verticalLines		= ( size - 1 ) * size;

		const lineIndices		= new Uint32Array( ( horizontalLines + verticalLines ) * 2 );

		let pi = 0;
		let ui = 0;

		for ( let y = 0; y < size; y++ ) {

			for ( let x = 0; x < size; x++ ) {

				const fx	= x / ( size - 1 );

				const fy	= y / ( size - 1 );

				const worldX = ( fx - 0.5 ) * span;

				const worldZ = ( fy - 0.5 ) * span;

				positions[ pi++ ] = worldX;
				positions[ pi++ ] = 0;
				positions[ pi++ ] = worldZ;

				uvs[ ui++ ] = x;
				uvs[ ui++ ] = y;

			}
		}

		let ii = 0;

		let li = 0;

		for ( let y = 0; y < size - 1; y++ ) {

			for ( let x = 0; x < size - 1; x++ ) {

				const topLeft		= y * size + x;

				const topRight		= topLeft + 1;

				const bottomLeft	= topLeft + size;

				const bottomRight	= bottomLeft + 1;

				indices[ ii++ ] = topLeft;
				indices[ ii++ ] = bottomLeft;
				indices[ ii++ ] = topRight;

				indices[ ii++ ] = topRight;
				indices[ ii++ ] = bottomLeft;
				indices[ ii++ ] = bottomRight;

			}

			// horizontal line for row y at each segment
			for ( let x = 0; x < size - 1; x++ ) {

				const a = y * size + x;

				const b = a + 1;

				lineIndices[ li++ ] = a;
				lineIndices[ li++ ] = b;

			}

		}

		// vertical lines
		for ( let x = 0; x < size; x++ ) {

			for ( let y = 0; y < size - 1; y++ ) {

				const a = y * size + x;

				const b = a + size;

				lineIndices[ li++ ] = a;
				lineIndices[ li++ ] = b;

			}
		}

		return {
			positions,
			uvs,
			indices,
			lineIndices
		};

	}


	_buildSkySphereGeometry( radius, latSegments, lonSegments ) {

		const latCount	= latSegments;

		const lonCount	= lonSegments;

		const vertexCount	= ( latCount + 1 ) * ( lonCount + 1 );

		const positions	= new Float32Array( vertexCount * 3 );

		const normals		= new Float32Array( vertexCount * 3 );

		const uvs			= new Float32Array( vertexCount * 2 );

		const indices		= new Uint32Array( latCount * lonCount * 6 );

		let pi	= 0;

		let ni	= 0;

		let ui	= 0;

		for ( let lat = 0; lat <= latCount; lat++ ) {

			const theta	= lat * Math.PI / latCount;

			const sinTheta	= Math.sin( theta );

			const cosTheta	= Math.cos( theta );

			for ( let lon = 0; lon <= lonCount; lon++ ) {

				const phi	= lon * Math.PI * 2.0 / lonCount;

				const sinPhi	= Math.sin( phi );

				const cosPhi	= Math.cos( phi );

				const x		= sinTheta * cosPhi;

				const y		= cosTheta;

				const z		= sinTheta * sinPhi;

				positions[ pi++ ] = x * radius;

				positions[ pi++ ] = y * radius;

				positions[ pi++ ] = z * radius;

				const nx		= -x;

				const ny		= -y;

				const nz		= -z;

				normals[ ni++ ] = nx;

				normals[ ni++ ] = ny;

				normals[ ni++ ] = nz;

				uvs[ ui++ ] = lon / lonCount;

				uvs[ ui++ ] = lat / latCount;

			}

		}

		let ii = 0;

		for ( let y = 0; y < latCount; y++ ) {

			for ( let x = 0; x < lonCount; x++ ) {

				const i0 = y * ( lonCount + 1 ) + x;

				const i1 = i0 + 1;

				const i2 = ( y + 1 ) * ( lonCount + 1 ) + x;

				const i3 = i2 + 1;

				indices[ ii++ ] = i0;

				indices[ ii++ ] = i2;

				indices[ ii++ ] = i1;

				indices[ ii++ ] = i1;

				indices[ ii++ ] = i2;

				indices[ ii++ ] = i3;

			}

		}

		return {

			positions,

			normals,

			uvs,

			indices

		};

	}


	_buildCubeGeometry( ) {

		const positions = new Float32Array( [
			// Front
			-1, -1,  1,   1, -1,  1,   1,  1,  1,  -1,  1,  1,
			// Back
			-1, -1, -1,  -1,  1, -1,   1,  1, -1,   1, -1, -1,
			// Top
			-1,  1, -1,  -1,  1,  1,   1,  1,  1,   1,  1, -1,
			// Bottom
			-1, -1, -1,   1, -1, -1,   1, -1,  1,  -1, -1,  1,
			// Right
			 1, -1, -1,   1,  1, -1,   1,  1,  1,   1, -1,  1,
			// Left
			-1, -1, -1,  -1, -1,  1,  -1,  1,  1,  -1,  1, -1
		] );

		const colors = new Float32Array( [
			// Front
			0.1, 0.6, 1.0,  0.1, 0.6, 1.0,  0.1, 0.6, 1.0,  0.1, 0.6, 1.0,
			// Back
			0.1, 0.2, 0.8,  0.1, 0.2, 0.8,  0.1, 0.2, 0.8,  0.1, 0.2, 0.8,
			// Top
			0.2, 0.8, 0.5,  0.2, 0.8, 0.5,  0.2, 0.8, 0.5,  0.2, 0.8, 0.5,
			// Bottom
			0.9, 0.7, 0.2,  0.9, 0.7, 0.2,  0.9, 0.7, 0.2,  0.9, 0.7, 0.2,
			// Right
			0.8, 0.3, 0.4,  0.8, 0.3, 0.4,  0.8, 0.3, 0.4,  0.8, 0.3, 0.4,
			// Left
			0.4, 0.9, 0.7,  0.4, 0.9, 0.7,  0.4, 0.9, 0.7,  0.4, 0.9, 0.7
		] );

		const indices = new Uint32Array( [
			0, 1, 2,   0, 2, 3,       // Front
			4, 5, 6,   4, 6, 7,       // Back
			8, 9, 10,  8, 10, 11,     // Top
			12, 13, 14, 12, 14, 15,   // Bottom
			16, 17, 18, 16, 18, 19,   // Right
			20, 21, 22, 20, 22, 23    // Left
		] );

		return {
			positions,
			colors,
			indices
		};

	}

}
