import TempNode from '../core/TempNode.js';
import { uv } from '../accessors/UVNode.js';
import { addNodeElement, tslFn, nodeObject, float, vec2, vec4, int, If } from '../shadernode/ShaderNode.js';
import { NodeUpdateType } from '../core/constants.js';
import { uniform } from '../core/UniformNode.js';
import { abs, max, min, mix, pow } from '../math/MathNode.js';
import { sub } from '../math/OperatorNode.js';
import { loop, Break } from '../utils/LoopNode.js';

import { Vector2 } from '../../math/Vector2.js';

class FXAANode extends TempNode {

	constructor( textureNode ) {

		super();

		this.textureNode = textureNode;

		this.updateBeforeType = NodeUpdateType.RENDER;

		this._invSize = uniform( new Vector2() );

	}

	updateBefore() {

		const map = this.textureNode.value;

		this._invSize.value.set( 1 / map.image.width, 1 / map.image.height );

	}

	setup() {

		const textureNode = this.textureNode.bias( - 100 );
		const uvNode = textureNode.uvNode || uv();

		// FXAA 3.11 implementation by NVIDIA, ported to WebGL by Agost Biro (biro@archilogic.com)

		//----------------------------------------------------------------------------------
		// File:        es3-kepler\FXAA\assets\shaders/FXAA_DefaultES.frag
		// SDK Version: v3.00
		// Email:       gameworks@nvidia.com
		// Site:        http://developer.nvidia.com/
		//
		// Copyright (c) 2014-2015, NVIDIA CORPORATION. All rights reserved.
		//
		// Redistribution and use in source and binary forms, with or without
		// modification, are permitted provided that the following conditions
		// are met:
		//  * Redistributions of source code must retain the above copyright
		//    notice, this list of conditions and the following disclaimer.
		//  * Redistributions in binary form must reproduce the above copyright
		//    notice, this list of conditions and the following disclaimer in the
		//    documentation and/or other materials provided with the distribution.
		//  * Neither the name of NVIDIA CORPORATION nor the names of its
		//    contributors may be used to endorse or promote products derived
		//    from this software without specific prior written permission.
		//
		// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY
		// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
		// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
		// PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
		// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
		// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
		// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
		// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
		// OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
		// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
		// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
		//
		//----------------------------------------------------------------------------------

		const FxaaTexTop = ( p ) => textureNode.uv( p );
		const FxaaTexOff = ( p, o, r ) => textureNode.uv( p.add( o.mul( r ) ) );

		const NUM_SAMPLES = int( 5 );

		const contrast = tslFn( ( [ a_immutable, b_immutable ] ) => {

			// assumes colors have premultipliedAlpha, so that the calculated color contrast is scaled by alpha

			const b = vec4( b_immutable ).toVar();
			const a = vec4( a_immutable ).toVar();
			const diff = vec4( abs( a.sub( b ) ) ).toVar();

			return max( max( max( diff.r, diff.g ), diff.b ), diff.a );

		} );

		// FXAA3 QUALITY - PC

		const FxaaPixelShader = tslFn( ( [ uv, fxaaQualityRcpFrame, fxaaQualityEdgeThreshold, fxaaQualityinvEdgeThreshold ] ) => {

			const rgbaM = FxaaTexTop( uv ).toVar();
			const rgbaS = FxaaTexOff( uv, vec2( 0.0, - 1.0 ), fxaaQualityRcpFrame.xy ).toVar();
			const rgbaE = FxaaTexOff( uv, vec2( 1.0, 0.0 ), fxaaQualityRcpFrame.xy ).toVar();
			const rgbaN = FxaaTexOff( uv, vec2( 0.0, 1.0 ), fxaaQualityRcpFrame.xy ).toVar();
			const rgbaW = FxaaTexOff( uv, vec2( - 1.0, 0.0 ), fxaaQualityRcpFrame.xy ).toVar();
			// . S .
			// W M E
			// . N .

			const contrastN = contrast( rgbaM, rgbaN ).toVar();
			const contrastS = contrast( rgbaM, rgbaS ).toVar();
			const contrastE = contrast( rgbaM, rgbaE ).toVar();
			const contrastW = contrast( rgbaM, rgbaW ).toVar();

			const maxValue = max( contrastN, max( contrastS, max( contrastE, contrastW ) ) ).toVar();

			// . 0 .
			// 0 0 0
			// . 0 .

			If( maxValue.lessThan( fxaaQualityEdgeThreshold ), () => {

				return rgbaM; // assuming define FXAA_DISCARD is always 0

			} );

			//

			const relativeVContrast = sub( contrastN.add( contrastS ), ( contrastE.add( contrastW ) ) ).toVar();
			relativeVContrast.mulAssign( fxaaQualityinvEdgeThreshold );

			// 45 deg edge detection and corners of objects, aka V/H contrast is too similar

			If( abs( relativeVContrast ).lessThan( 0.3 ), () => {

				// locate the edge

				const x = contrastE.greaterThan( contrastW ).cond( 1, - 1 ).toVar();
				const y = contrastS.greaterThan( contrastN ).cond( 1, - 1 ).toVar();

				const dirToEdge = vec2( x, y ).toVar();
				// . 2 .      . 1 .
				// 1 0 2  ~=  0 0 1
				// . 1 .      . 0 .

				// tap 2 pixels and see which ones are "outside" the edge, to
				// determine if the edge is vertical or horizontal

				const rgbaAlongH = FxaaTexOff( uv, vec2( dirToEdge.x, dirToEdge.y ), fxaaQualityRcpFrame.xy );
				const matchAlongH = contrast( rgbaM, rgbaAlongH ).toVar();
				// . 1 .
				// 0 0 1
				// . 0 H

				const rgbaAlongV = FxaaTexOff( uv, vec2( dirToEdge.x.negate(), dirToEdge.y.negate() ), fxaaQualityRcpFrame.xy );
				const matchAlongV = contrast( rgbaM, rgbaAlongV ).toVar();
				// V 1 .
				// 0 0 1
				// . 0 .

				relativeVContrast.assign( matchAlongV.sub( matchAlongH ) );
				relativeVContrast.mulAssign( fxaaQualityinvEdgeThreshold );

				If( abs( relativeVContrast ).lessThan( 0.3 ), () => { // 45 deg edge

					// 1 1 .
					// 0 0 1
					// . 0 1

					// do a simple blur
					const sum = rgbaN.add( rgbaS ).add( rgbaE ).add( rgbaW );
					return mix( rgbaM, sum.mul( 0.25 ), 0.4 );

				} );

			} );

			const offNP = vec2().toVar();

			If( relativeVContrast.lessThanEqual( 0 ), () => {

				rgbaN.assign( rgbaW );
				rgbaS.assign( rgbaE );

				// . 0 .      1
				// 1 0 1  ->  0
				// . 0 .      1

				offNP.x.assign( 0 );
				offNP.y.assign( fxaaQualityRcpFrame.y );

			 } ).else( () => {

				offNP.x.assign( fxaaQualityRcpFrame.x );
				offNP.y.assign( 0 );

			 } );

			const mn = contrast( rgbaM, rgbaN ).toVar();
			const ms = contrast( rgbaM, rgbaS ).toVar();

			If( mn.lessThanEqual( ms ), () => {

				rgbaN.assign( rgbaS );

			} );

			const doneN = int( 0 ).toVar();
			const doneP = int( 0 ).toVar();

			const nDist = float( 0 ).toVar();
			const pDist = float( 0 ).toVar();

			const posN = vec2( uv ).toVar();
			const posP = vec2( uv ).toVar();

			const iterationsUsedN = int( 0 ).toVar();
			const iterationsUsedP = int( 0 ).toVar();

			loop( NUM_SAMPLES, ( { i } ) => {

				const increment = i.add( 1 ).toVar();

				If( doneN.equal( 0 ), () => {

					nDist.addAssign( increment );
					posN.assign( uv.add( offNP.mul( nDist ) ) );
					const rgbaEndN = FxaaTexTop( posN.xy );

					const nm = contrast( rgbaEndN, rgbaM ).toVar();
					const nn = contrast( rgbaEndN, rgbaN ).toVar();

					If( nm.greaterThan( nn ), () => {

						doneN.assign( 1 );

					} );

					iterationsUsedN.assign( i );

				} );

				If( doneP.equal( 0 ), () => {

					pDist.addAssign( increment );
					posP.assign( uv.sub( offNP.mul( pDist ) ) );
					const rgbaEndP = FxaaTexTop( posP.xy );

					const pm = contrast( rgbaEndP, rgbaM ).toVar();
					const pn = contrast( rgbaEndP, rgbaN ).toVar();

					If( pm.greaterThan( pn ), () => {

						doneP.assign( 1 );

					} );

					iterationsUsedP.assign( i );

				} );

				If( doneN.equal( 1 ).or( doneP.equal( 1 ) ), () => {

					Break();

				} );

			} );

			If( doneN.equal( 0 ).and( doneP.equal( 0 ) ), () => {

				return rgbaM; // failed to find end of edge

			} );

			const distN = float( 1 ).toVar();
			const distP = float( 1 ).toVar();

			If( doneN.equal( 1 ), () => {

				distN.assign( float( iterationsUsedN ).div( float( NUM_SAMPLES.sub( 1 ) ) ) );

			} );

			If( doneP.equal( 1 ), () => {

				distP.assign( float( iterationsUsedP ).div( float( NUM_SAMPLES.sub( 1 ) ) ) );

			} );

			const dist = min( distN, distP );

			// hacky way of reduces blurriness of mostly diagonal edges
			// but reduces AA quality
			dist.assign( pow( dist, 0.5 ) );
			dist.assign( float( 1 ).sub( dist ) );

			return mix( rgbaM, rgbaN, dist.mul( 0.5 ) );

		} ).setLayout( {
			name: 'FxaaPixelShader',
			type: 'vec4',
			inputs: [
				{ name: 'uv', type: 'vec2' },
				{ name: 'fxaaQualityRcpFrame', type: 'vec2' },
				{ name: 'fxaaQualityEdgeThreshold', type: 'float' },
				{ name: 'fxaaQualityinvEdgeThreshold', type: 'float' },
			]
		} );

		const fxaa = tslFn( () => {

			const edgeDetectionQuality = float( 0.2 );
			const invEdgeDetectionQuality = float( 1 ).div( edgeDetectionQuality );

			return FxaaPixelShader( uvNode, this._invSize, edgeDetectionQuality, invEdgeDetectionQuality );

		} );

		const outputNode = fxaa();

		return outputNode;

	}

}

export const fxaa = ( node ) => nodeObject( new FXAANode( nodeObject( node ).toTexture() ) );

addNodeElement( 'fxaa', fxaa );

export default FXAANode;
