import { Box3Helper, Group, MeshBasicMaterial } from 'three';
import { TilesRenderer } from './TilesRenderer.js';
import { SphereHelper } from './SphereHelper.js';

const ORIGINAL_MATERIAL = Symbol( 'ORIGINAL_MATERIAL' );

function emptyRaycast() {}

export const NONE = 0;
export const SCREEN_ERROR = 1;
export const GEOMETRIC_ERROR = 2;
export const DISTANCE = 3;
export const DEPTH = 4;
export const RELATIVE_DEPTH = 5;
export const IS_LEAF = 6;
export const RANDOM_COLOR = 7;

export class DebugTilesRenderer extends TilesRenderer {

	constructor( ...args ) {

		super( ...args );

		const tilesGroup = this.group;
		const boxGroup = new Group();
		tilesGroup.add( boxGroup );

		const sphereGroup = new Group();
		tilesGroup.add( sphereGroup );

		this.displayBoxBounds = false;
		this.displaySphereBounds = false;
		this.colorMode = NONE;
		this.boxGroup = boxGroup;
		this.sphereGroup = sphereGroup;
		this.maxDepth = - 1;
		this.maxDistance = - 1;
		this.maxError = - 1;

		this.extremeDepth = - 1;
		this.extremeError = - 1;

	}

	initExtremes() {

		let maxDepth = - 1;
		this.traverse( tile => {

			maxDepth = Math.max( maxDepth, tile.__depth );

		} );

		let maxError = - 1;
		this.traverse( tile => {

			maxError = Math.max( maxError, tile.geometricError );

		} );

		this.extremeDepth = maxDepth;
		this.extremeError = maxError;

	}

	loadTileSet( ...args ) {

		const pr = super.loadTileSet( ...args );
		pr.then( () => this.initExtremes() );

		return pr;

	}

	getTileInformationFromActiveObject( object ) {

		let targetTile = null;
		const activeTiles = this.activeTiles;
		activeTiles.forEach( tile => {

			if ( targetTile ) {

				return true;

			}

			const scene = tile.cached.scene;
			if ( scene ) {

				scene.traverse( c => {

					if ( c === object ) {

						targetTile = tile;

					}

				} );

			}

		} );

		if ( targetTile ) {

			return {

				distanceToCamera: targetTile.cached.distance,
				geometricError: targetTile.geometricError,
				screenSpaceError: targetTile.__error,
				depth: targetTile.__depth,
				isLeaf: targetTile.__isLeaf

			};

		} else {

			return null;

		}

	}

	update() {

		super.update();

		if ( ! this.root ) {

			return;

		}

		this.boxGroup.visible = this.displayBoxBounds;
		this.sphereGroup.visible = this.displaySphereBounds;

		let maxDepth = - 1;
		if ( this.maxDepth === - 1 ) {

			maxDepth = this.extremeDepth;

		} else {

			maxDepth = this.maxDepth;

		}

		let maxError = - 1;
		if ( this.maxError === - 1 ) {

			maxError = this.extremeError;

		} else {

			maxError = this.maxError;

		}

		let maxDistance = - 1;
		if ( this.maxDistance === - 1 ) {

			maxDistance = this.root.cached.sphere.radius;

		} else {

			maxDistance = this.maxDistance;

		}

		// TODO: Support i3dm, pnts, cmpt here
		const errorTarget = this.errorTarget;
		const colorMode = this.colorMode;
		const visibleTiles = this.visibleTiles;
		visibleTiles.forEach( tile => {

			const scene = tile.cached.scene;
			scene.traverse( c => {

				const currMaterial = c.material;
				if ( currMaterial ) {

					const originalMaterial = c[ ORIGINAL_MATERIAL ];
					if ( colorMode === NONE && currMaterial !== originalMaterial ) {

						c.material.dispose();
						c.material = c[ ORIGINAL_MATERIAL ];

					} else if ( colorMode !== NONE && currMaterial === originalMaterial ) {

						c.material = new MeshBasicMaterial();

					}

					if ( colorMode !== RANDOM_COLOR ) {

						delete c.material.__randomColor;

					}

					switch ( colorMode ) {

						case DEPTH: {

							const val = tile.__depth / maxDepth;
							c.material.color.setRGB( val, val, val );
							break;

						}
						case RELATIVE_DEPTH: {

							const val = tile.__depthFromRenderedParent / maxDepth;
							c.material.color.setRGB( val, val, val );
							break;

						}
						case SCREEN_ERROR: {

							const val = tile.__error / errorTarget;
							if ( val > 1.0 ) {

								c.material.color.setRGB( 1.0, 0.0, 0.0 );

							} else {

								c.material.color.setRGB( val, val, val );

							}
							break;

						}
						case GEOMETRIC_ERROR: {

							const val = Math.min( tile.geometricError / maxError, 1 );
							c.material.color.setRGB( val, val, val );
							break;

						}
						case DISTANCE: {

							// We don't update the distance if the geometric error is 0.0 so
							// it will always be black.
							const val = Math.min( tile.cached.distance / maxDistance, 1 );
							c.material.color.setRGB( val, val, val );
							break;

						}
						case IS_LEAF: {

							if ( ! tile.children || tile.children.length === 0 ) {

								c.material.color.set( 0xffffff );

							} else {

								c.material.color.set( 0 );

							}
							break;

						}
						case RANDOM_COLOR: {

							if ( ! c.material.__randomColor ) {

								const h = Math.random();
								const s = 0.5 + Math.random() * 0.5;
								const l = 0.375 + Math.random() * 0.25;
								c.material.color.setHSL( h, s, l );
								c.material.__randomColor = true;

							}
							break;

						}

					}

				}

			} );

		} );

	}

	setTileVisible( tile, visible ) {

		super.setTileVisible( tile, visible );

		const cached = tile.cached;
		const sphereGroup = this.sphereGroup;
		const boxGroup = this.boxGroup;
		const boxHelperGroup = cached.boxHelperGroup;
		const sphereHelper = cached.sphereHelper;

		if ( ! visible ) {

			boxGroup.remove( boxHelperGroup );
			sphereGroup.remove( sphereHelper );

		} else {

			boxGroup.add( boxHelperGroup );
			boxHelperGroup.updateMatrixWorld( true );

			sphereGroup.add( sphereHelper );
			sphereHelper.updateMatrixWorld( true );

		}

	}

	parseTile( buffer, tile, extension ) {

		return super
			.parseTile( buffer, tile, extension )
			.then( () => {

				const cached = tile.cached;
				const scene = cached.scene;
				if ( scene ) {

					const cachedBox = cached.box;
					const cachedBoxMat = cached.boxTransform;

					const boxHelperGroup = new Group();
					boxHelperGroup.matrix.copy( cachedBoxMat );
					boxHelperGroup.matrix.decompose( boxHelperGroup.position, boxHelperGroup.quaternion, boxHelperGroup.scale );

					const boxHelper = new Box3Helper( cachedBox );
					boxHelper.raycast = emptyRaycast;
					boxHelperGroup.add( boxHelper );

					cached.boxHelperGroup = boxHelperGroup;

					if ( this.visibleTiles.has( tile ) && this.displayBoxBounds ) {

						this.boxGroup.add( boxHelperGroup );
						boxHelperGroup.updateMatrixWorld( true );

					}

					const cachedSphere = cached.sphere;
					const sphereHelper = new SphereHelper( cachedSphere );
					sphereHelper.raycast = emptyRaycast;
					cached.sphereHelper = sphereHelper;

					if ( this.visibleTiles.has( tile ) && this.displaySphereBounds ) {

						this.sphereGroup.add( sphereHelper );
						sphereHelper.updateMatrixWorld( true );

					}

					scene.traverse( c => {

						const material = c.material;
						if ( material ) {

							c[ ORIGINAL_MATERIAL ] = material;

						}

					} );

				}

			} );

	}

	disposeTile( tile ) {

		super.disposeTile( tile );

		const cached = tile.cached;
		if ( cached.boxHelperGroup ) {

			cached.boxHelperGroup.children[ 0 ].geometry.dispose();
			cached.sphereHelper.geometry.dispose();

			delete cached.boxHelperGroup;
			delete cached.sphereHelper;

		}

	}

}
