import * as THREE from 'three';
import { threejsLessonUtils } from './threejs-lesson-utils.js';

{

	const loader = new THREE.TextureLoader();

	function loadTextureAndPromise( url ) {

		let textureResolve;
		const promise = new Promise( ( resolve ) => {

			textureResolve = resolve;

		} );
		const texture = loader.load( url, ( texture ) => {

			textureResolve( texture );

		} );
		return {
			texture,
			promise,
		};

	}

	const filterTextureInfo = loadTextureAndPromise( '/manual/resources/images/mip-example.png' );
	const filterTexture = filterTextureInfo.texture;
	const filterTexturePromise = filterTextureInfo.promise;

	function filterCube( scale, texture ) {

		const size = 8;
		const geometry = new THREE.BoxGeometry( size, size, size );
		const material = new THREE.MeshBasicMaterial( {
			map: texture || filterTexture,
		} );
		const mesh = new THREE.Mesh( geometry, material );
		mesh.scale.set( scale, scale, scale );
		return mesh;

	}

	function lowResCube( scale, pixelSize = 16 ) {

		const mesh = filterCube( scale );
		const renderTarget = new THREE.WebGLRenderTarget( 1, 1, {
			magFilter: THREE.NearestFilter,
			minFilter: THREE.NearestFilter,
		} );

		const planeScene = new THREE.Scene();

		const plane = new THREE.PlaneGeometry( 1, 1 );
		const planeMaterial = new THREE.MeshBasicMaterial( {
			map: renderTarget.texture,
		} );
		const planeMesh = new THREE.Mesh( plane, planeMaterial );
		planeScene.add( planeMesh );

		const planeCamera = new THREE.OrthographicCamera( 0, 1, 0, 1, - 1, 1 );
		planeCamera.position.z = 1;

		return {
			obj3D: mesh,
			update( time, renderInfo ) {

				const { width, height, scene, camera, renderer, pixelRatio } = renderInfo;
				const rtWidth = Math.ceil( width / pixelRatio / pixelSize );
				const rtHeight = Math.ceil( height / pixelRatio / pixelSize );
				renderTarget.setSize( rtWidth, rtHeight );

				camera.aspect = rtWidth / rtHeight;
				camera.updateProjectionMatrix();

				renderer.setRenderTarget( renderTarget );

				renderer.render( scene, camera );
				renderer.setRenderTarget( null );

			},
			render( renderInfo ) {

				const { width, height, renderer, pixelRatio } = renderInfo;
				const viewWidth = width / pixelRatio / pixelSize;
				const viewHeight = height / pixelRatio / pixelSize;
				planeCamera.left = - viewWidth / 2;
				planeCamera.right = viewWidth / 2;
				planeCamera.top = viewHeight / 2;
				planeCamera.bottom = - viewHeight / 2;
				planeCamera.updateProjectionMatrix();

				// compute the difference between our renderTarget size
				// and the view size. The renderTarget is a multiple pixels magnified pixels
				// so for example if the view is 15 pixels wide and the magnified pixel size is 10
				// the renderTarget will be 20 pixels wide. We only want to display 15 of those 20
				// pixels so

				planeMesh.scale.set( renderTarget.width, renderTarget.height, 1 );

				renderer.render( planeScene, planeCamera );

			},
		};

	}

	function createMip( level, numLevels, scale ) {

		const u = level / numLevels;
		const size = 2 ** ( numLevels - level - 1 );
		const halfSize = Math.ceil( size / 2 );
		const ctx = document.createElement( 'canvas' ).getContext( '2d' );
		ctx.canvas.width = size * scale;
		ctx.canvas.height = size * scale;
		ctx.scale( scale, scale );
		ctx.fillStyle = `hsl(${180 + u * 360 | 0},100%,20%)`;
		ctx.fillRect( 0, 0, size, size );
		ctx.fillStyle = `hsl(${u * 360 | 0},100%,50%)`;
		ctx.fillRect( 0, 0, halfSize, halfSize );
		ctx.fillRect( halfSize, halfSize, halfSize, halfSize );
		return ctx.canvas;

	}

	threejsLessonUtils.init( {
		threejsOptions: { antialias: false },
	} );
	threejsLessonUtils.addDiagrams( {
		filterCube: {
			create() {

				return filterCube( 1 );

			},
		},
		filterCubeSmall: {
			create( info ) {

				return lowResCube( .1, info.renderInfo.pixelRatio );

			},
		},
		filterCubeSmallLowRes: {
			create() {

				return lowResCube( 1 );

			},
		},
		filterCubeMagNearest: {
			async create() {

				const texture = await filterTexturePromise;
				const newTexture = texture.clone();
				newTexture.magFilter = THREE.NearestFilter;
				newTexture.needsUpdate = true;
				return filterCube( 1, newTexture );

			},
		},
		filterCubeMagLinear: {
			async create() {

				const texture = await filterTexturePromise;
				const newTexture = texture.clone();
				newTexture.magFilter = THREE.LinearFilter;
				newTexture.needsUpdate = true;
				return filterCube( 1, newTexture );

			},
		},
		filterModes: {
			async create( props ) {

				const { scene, camera, renderInfo } = props;
				scene.background = new THREE.Color( 'black' );
				camera.far = 150;
				const texture = await filterTexturePromise;
				const root = new THREE.Object3D();
				const depth = 50;
				const plane = new THREE.PlaneGeometry( 1, depth );
				const mipmap = [];
				const numMips = 7;
				for ( let i = 0; i < numMips; ++ i ) {

					mipmap.push( createMip( i, numMips, 1 ) );

				}

				// Is this a design flaw in three.js?
				// AFAIK there's no way to clone a texture really
				// Textures can share an image and I guess deep down
				// if the image is the same they might share a WebGLTexture
				// but no checks for mipmaps I'm guessing. It seems like
				// they shouldn't be checking for same image, the should be
				// checking for same WebGLTexture. Given there is more than
				// WebGL to support maybe they need to abtract WebGLTexture to
				// PlatformTexture or something?

				const meshInfos = [
					{ x: - 1, y: 1, minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter },
					{ x: 0, y: 1, minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter },
					{ x: 1, y: 1, minFilter: THREE.NearestMipmapNearestFilter, magFilter: THREE.LinearFilter },
					{ x: - 1, y: - 1, minFilter: THREE.NearestMipmapLinearFilter, magFilter: THREE.LinearFilter },
					{ x: 0, y: - 1, minFilter: THREE.LinearMipmapNearestFilter, magFilter: THREE.LinearFilter },
					{ x: 1, y: - 1, minFilter: THREE.LinearMipmapLinearFilter, magFilter: THREE.LinearFilter },
				].map( ( info ) => {

					const copyTexture = texture.clone();
					copyTexture.minFilter = info.minFilter;
					copyTexture.magFilter = info.magFilter;
					copyTexture.wrapT = THREE.RepeatWrapping;
					copyTexture.repeat.y = depth;
					copyTexture.needsUpdate = true;

					const mipTexture = new THREE.CanvasTexture( mipmap[ 0 ] );
					mipTexture.mipmaps = mipmap;
					mipTexture.minFilter = info.minFilter;
					mipTexture.magFilter = info.magFilter;
					mipTexture.wrapT = THREE.RepeatWrapping;
					mipTexture.repeat.y = depth;

					const material = new THREE.MeshBasicMaterial( {
						map: copyTexture,
					} );

					const mesh = new THREE.Mesh( plane, material );
					mesh.rotation.x = Math.PI * .5 * info.y;
					mesh.position.x = info.x * 1.5;
					mesh.position.y = info.y;
					root.add( mesh );
					return {
						material,
						copyTexture,
						mipTexture,
					};

				} );
				scene.add( root );

				renderInfo.elem.addEventListener( 'click', () => {

					for ( const meshInfo of meshInfos ) {

						const { material, copyTexture, mipTexture } = meshInfo;
						material.map = material.map === copyTexture ? mipTexture : copyTexture;

					}

				} );

				return {
					update( time, renderInfo ) {

						const { camera } = renderInfo;
						camera.position.y = Math.sin( time * .2 ) * .5;

					},
					trackball: false,
				};

			},
		},
	} );

	const textureDiagrams = {
		differentColoredMips( parent ) {

			const numMips = 7;
			for ( let i = 0; i < numMips; ++ i ) {

				const elem = createMip( i, numMips, 4 );
				elem.className = 'border';
				elem.style.margin = '1px';
				parent.appendChild( elem );

			}

		},
	};

	function createTextureDiagram( elem ) {

		const name = elem.dataset.textureDiagram;
		const info = textureDiagrams[ name ];
		info( elem );

	}

	[ ...document.querySelectorAll( '[data-texture-diagram]' ) ].forEach( createTextureDiagram );

}

