import {
	Vector3D,
	Matrix3D,
	Box, Sphere,
	AbstractionBase,
	Plane3D,
	Point,
} from '@awayjs/core';

import { IPartitionTraverser } from '../partition/IPartitionTraverser';

import { BoundsPickerPool, PickGroup } from '../PickGroup';
import { BoundingVolumePool } from '../bounds/BoundingVolumePool';
import { BoundingVolumeType } from '../bounds/BoundingVolumeType';
import { BoundingVolumeBase } from '../bounds/BoundingVolumeBase';
import { BoundingBox } from '../bounds/BoundingBox';
import { BoundingSphere } from '../bounds/BoundingSphere';
import { IBoundsPicker } from './IBoundsPicker';
import { PickEntity } from '../base/PickEntity';
import { ContainerNode } from '../partition/ContainerNode';
import { INode } from '../partition/INode';

/**
 * Picks a 3d object from a view or scene by 3D raycast calculations.
 * Performs an initial coarse boundary calculation to return a subset
 * of entities whose bounding volumes intersect with the specified ray,
 * then triggers an optional picking collider on individual renderable
 * objects to further determine the precise values of the picking ray collision.
 *
 * @class away.pick.RaycastPicker
 */
export class BoundsPicker extends AbstractionBase implements IPartitionTraverser, IBoundsPicker {
	private static tmpMatrix: Matrix3D = new Matrix3D();
	private static tmpPoint: Point = new Point();
	private static tmpBox: Box = new Box();

	public static MINIMAL_SCALE = 0.00001;

	private _boundingVolumePools: Record<string, BoundingVolumePool>;

	private _pickGroup: PickGroup;

	private _boundsPickers: IBoundsPicker[] = [];

	// private _orientedBoxBounds: Box[] = [];
	// private _orientedBoxBoundsDirty: boolean[] = [true, true];
	// private _orientedSphereBounds: Sphere[] = [];
	// private _orientedSphereBoundsDirty: boolean[] = [true, true];

	/**
     *
     * @returns {ContainerNode}
     */
	public get node(): ContainerNode {
		return <ContainerNode> this._asset;
	}

	/**
	 * Indicates the width of the display object, in pixels. The width is
	 * calculated based on the bounds of the content of the display object. When
	 * you set the <code>width</code> property, the <code>scaleX</code> property
	 * is adjusted accordingly, as shown in the following code:
	 *
	 * <p>Except for TextField and Video objects, a display object with no
	 * content(such as an empty sprite) has a width of 0, even if you try to set
	 * <code>width</code> to a different value.</p>
	 */
	public get width(): number {
		const box: Box = this.getBoxBounds();

		if (box == null)
			return 0;

		// scale already should be applied, because we request width relative self
		return box.width;
	}

	public set width(val: number) {
		const transform = (<INode> this._asset).container.transform;
		const selfBox = this.getBoxBounds();

		//return if box is empty ie setting width for no content is impossible
		if (selfBox == null)
			return;

		const rotation = transform.rotation;
		const baseMatrix = transform.matrix3D;

		const box = baseMatrix.transformBox(selfBox, BoundsPicker.tmpBox);

		const scaleFactor = box.width > 0 ? val / box.width : 1;

		// without rotation, fast case
		if (rotation.z === 0) {
			const s = transform.scale;
			transform.scaleTo(
				s.x * scaleFactor || BoundsPicker.MINIMAL_SCALE,
				s.y,
				s.z
			);

			return;
		}

		const matrix = BoundsPicker.tmpMatrix;

		matrix.copyFrom(baseMatrix);
		matrix.appendScale(
			scaleFactor || BoundsPicker.MINIMAL_SCALE,
			1,
			1
		);

		// decompose matrix for grabbing transformed scale of transform
		// this is target scale that applied (real?) by width
		const realScale = matrix.decompose()[2];

		transform.scaleTo(
			realScale.x,
			realScale.y,
			realScale.z
		);
	}

	/**
	 * Indicates the height of the display object, in pixels. The height is
	 * calculated based on the bounds of the content of the display object. When
	 * you set the <code>height</code> property, the <code>scaleY</code> property
	 * is adjusted accordingly, as shown in the following code:
	 *
	 * <p>Except for TextField and Video objects, a display object with no
	 * content (such as an empty sprite) has a height of 0, even if you try to
	 * set <code>height</code> to a different value.</p>
	 */
	public get height(): number {
		const box: Box = this.getBoxBounds();

		if (box == null)
			return 0;

		// if (this._node._registrationMatrix3D)
		// 	return box.height*this._node.scaleY*this._node._registrationMatrix3D._rawData[5];

		// already should be applied
		return box.height;// * this._node.container.transform.scale.y;
	}

	public set height(val: number) {
		const transform = (<INode> this._asset).container.transform;
		const selfBox = this.getBoxBounds();

		//return if box is empty ie setting height for no content is impossible
		if (selfBox == null)
			return;

		const baseMatrix = transform.matrix3D;
		const rotation = transform.rotation;

		const box = baseMatrix.transformBox(selfBox, BoundsPicker.tmpBox);

		const scaleFactor = box.height > 0 ? val / box.height : 1;

		// without rotation, fast case
		if (rotation.z === 0) {
			const s = transform.scale;
			transform.scaleTo(
				s.x,
				s.y * scaleFactor || BoundsPicker.MINIMAL_SCALE,
				s.z
			);

			return;
		}

		// or we should use decomposition
		const matrix = BoundsPicker.tmpMatrix;

		matrix.copyFrom(baseMatrix);
		matrix.appendScale(
			1,
			scaleFactor || BoundsPicker.MINIMAL_SCALE,
			1
		);

		const realScale = matrix.decompose()[2];

		transform.scaleTo(
			realScale.x,
			realScale.y,
			realScale.z
		);
	}

	/**
	 * Indicates the depth of the display object, in pixels. The depth is
	 * calculated based on the bounds of the content of the display object. When
	 * you set the <code>depth</code> property, the <code>scaleZ</code> property
	 * is adjusted accordingly, as shown in the following code:
	 *
	 * <p>Except for TextField and Video objects, a display object with no
	 * content (such as an empty sprite) has a depth of 0, even if you try to
	 * set <code>depth</code> to a different value.</p>
	 */
	public get depth(): number {
		const box: Box = this.getBoxBounds();

		if (box == null)
			return 0;

		// if (this._node._registrationMatrix3D)
		// 	return  box.depth*this._node.scaleZ*this._node._registrationMatrix3D._rawData[10];

		return box.depth * (<INode> this._asset).container.transform.scale.z;
	}

	public set depth(val: number) {
		const box: Box = this.getBoxBounds();

		//return if box is empty ie setting depth for no content is impossible
		if (box == null || box.depth == 0)
			return;

		//this._updateAbsoluteDimension();
		const container = (<INode> this._asset).container;

		container.transform.scaleTo(
			container.transform.scale.x,
			container.transform.scale.y,
			val / box.depth
		);
	}

	public init(node: INode, pool: BoundsPickerPool) {
		super.init(node, pool);

		this._pickGroup = pool.pickGroup;

		this._boundingVolumePools = {};
	}

	public onInvalidate(): void {
		super.onInvalidate();

		// this._orientedBoxBoundsDirty[0] = true;
		// this._orientedBoxBoundsDirty[1] = true;
		// this._orientedSphereBoundsDirty[0] = true;
		// this._orientedSphereBoundsDirty[1] = true;

		for (const key in this._boundingVolumePools)
			this._boundingVolumePools[key].abstractions.forEach((boundingVolume: BoundingVolumeBase) => boundingVolume.onInvalidate());
	}

	public traverse(): void {
		this._invalid = false;
		this._boundsPickers.length = 0;
		(<INode> this._asset).acceptTraverser(this);

	}

	public getTraverser(node: INode): IPartitionTraverser {
		// const traverser: BoundsPicker = this._pickGroup.getBoundsPicker(node);

		// this._boundsPickers.push(traverser);

		return this;
	}

	/**
	 * Returns true if the current node is at least partly in the frustum.
	 * If so, the partition node knows to pass on the traverser to its children.
	 *
	 * @param node The Partition3DNode object to frustum-test.
	 */
	public enterNode(node: INode): boolean {
		return !(node.container.assetType == '[asset TextSprite]');
	}

	public getBoundingVolume(target: INode = null, type: BoundingVolumeType = null): BoundingVolumeBase {
		if (target == null)
			target = (<INode> this._asset);

		if (type == null)
			type = (<INode> this._asset).container.defaultBoundingVolume;

		const pool: BoundingVolumePool = this._boundingVolumePools[type]
									|| (this._boundingVolumePools[type] = new BoundingVolumePool(this, type));

		return pool.abstractions.getAbstraction<BoundingVolumeBase>(target);
	}

	public getBoxBounds(
		targetCoordinateSpace: INode = null, strokeFlag: boolean = false, fastFlag: boolean = false): Box {

		return (<BoundingBox> this.getBoundingVolume(
			targetCoordinateSpace,
			strokeFlag
				? (fastFlag ? BoundingVolumeType.BOX_BOUNDS_FAST : BoundingVolumeType.BOX_BOUNDS)
				: (fastFlag ? BoundingVolumeType.BOX_FAST : BoundingVolumeType.BOX))
		).getBox();
	}

	public getSphereBounds(
		targetCoordinateSpace: INode = null, strokeFlag: boolean = false, fastFlag: boolean = false): Sphere {

		return (<BoundingSphere> this.getBoundingVolume(
			targetCoordinateSpace,
			strokeFlag
				? (fastFlag ? BoundingVolumeType.SPHERE_BOUNDS_FAST : BoundingVolumeType.SPHERE_BOUNDS)
				: (fastFlag ? BoundingVolumeType.SPHERE_FAST : BoundingVolumeType.SPHERE))
		).getSphere();
	}

	public hitTestPoint(x: number, y: number, shapeFlag: boolean = false): boolean {
		return this._hitTestPointInternal(<INode> this._asset, x, y, shapeFlag, false);
	}

	public _hitTestPointInternal(
		rootNode: INode,
		x: number, y: number,
		shapeFlag: boolean = false,
		maskFlag: boolean = false
	): boolean {
		const node: ContainerNode = <ContainerNode> this._asset;

		if (node.getMaskId() != -1 && (!maskFlag || !shapeFlag))//allow masks for bounds hit tests
			return false;

		if (this._invalid)
			this.traverse();

		//set local tempPoint for later reference
		const tempPoint: Point = BoundsPicker.tmpPoint;
		tempPoint.setTo(x, y);

		node.globalToLocal(tempPoint, tempPoint);

		//early out for box test
		const box: Box = this.getBoxBounds(null, false, true);

		if (box == null || !box.contains(tempPoint.x, tempPoint.y, 0))
			return false;

		//early out for non-shape tests
		if (!shapeFlag ||
			node.container.assetType == '[asset TextField]' ||
			node.container.assetType == '[asset Billboard]'
		)
			return true;

		const numPickers: number = this._boundsPickers.length;
		if (numPickers)
			for (let i: number = 0; i < numPickers; ++i)
				if (this._boundsPickers[i]._hitTestPointInternal(rootNode, x, y, shapeFlag, maskFlag))
					return true;

		return false;
	}

	/**
	 * Evaluates the bounding box of the display object to see if it overlaps or
	 * intersects with the bounding box of the <code>obj</code> display object.
	 *
	 * @param obj The display object to test against.
	 * @return <code>true</code> if the bounding boxes of the display objects
	 *         intersect; <code>false</code> if not.
	 */
	public hitTestObject(obj: BoundsPicker): boolean {
		const node: INode = <INode> this._asset;
		//TODO: getBoxBounds should be using the root partition root

		//first do a fast box comparision
		const objBox: Box = obj.getBoxBounds(node, true, true);

		if (objBox == null)
			return false;

		const box: Box = this.getBoxBounds(node, true, true);

		if (box == null)
			return false;

		if (!objBox.intersects(box))
			return false;

		//if the fast box passes, do the slow test
		return obj.getBoxBounds(node, true).intersects(this.getBoxBounds(node, true));
	}

	public _getBoxBoundsInternal(
		invTargetMatrix: Matrix3D = null,
		strokeFlag: boolean = true,
		fastFlag: boolean = true,
		cache: Box = null,
		target: Box = null
	): Box {

		if (this._invalid)
			this.traverse();

		const numPickers: number = this._boundsPickers.length;
		if (numPickers > 0) {
			const node: INode = <INode> this._asset;
			const m: Matrix3D = new Matrix3D();

			let matrix3D;

			// if (fastFlag) {
			// 	let obb: Box;
			// 	const strokeIndex: number = strokeFlag ? 1 : 0;
			// 	const invMatrix3D = (<ContainerNode> this._asset).getInverseMatrix3D().clone();

			// 	if (invTargetMatrix) { // a null invTargetMatrix means local coords to the node so matrix3D is identity
			// 		matrix3D = (<ContainerNode> this._asset).getMatrix3D().clone()
			// 		matrix3D.append(invTargetMatrix);
			// 	}

			// 	if (this._orientedBoxBoundsDirty[strokeIndex]) {
			// 		this._orientedBoxBoundsDirty[strokeIndex] = false;
			// 		for (let i: number = 0; i < numPickers; ++i) {
			// 			obb = this._boundsPickers[i]
			// 				._getBoxBoundsInternal(
			// 					this._boundsPickers[i].node != node
			// 						? invMatrix3D
			// 						: null,
			// 					strokeFlag,
			// 					fastFlag,
			// 					this._orientedBoxBounds[strokeIndex],
			// 					obb);
			// 		}
			// 		this._orientedBoxBounds[strokeIndex] = obb;
			// 	} else {
			// 		obb = this._orientedBoxBounds[strokeIndex];
			// 	}

			// 	if (obb != null) {
			// 		target = (matrix3D)
			// 			? matrix3D.transformBox(obb).union(target, target || cache)
			// 			: obb.union(target, target || cache);
			// 	}

			// } else {
			matrix3D = invTargetMatrix ? invTargetMatrix : (<ContainerNode> this._asset).getInverseMatrix3D();

			for (let i: number = 0; i < numPickers; ++i)
				target = this._boundsPickers[i]
					._getBoxBoundsInternal(
						matrix3D,
						strokeFlag,
						fastFlag,
						cache,
						target);
			// }
		}

		return target;
	}

	public _getSphereBoundsInternal(
		center: Vector3D = null,
		matrix3D: Matrix3D = null,
		strokeFlag: boolean = true,
		fastFlag: boolean = true,
		cache: Sphere = null,
		target: Sphere = null
	): Sphere {

		if (this._invalid)
			this.traverse();

		const box: Box = this._getBoxBoundsInternal(matrix3D, strokeFlag, fastFlag);

		if (box == null)
			return;

		if (!center) {
			center = new Vector3D();
			center.x = box.x + box.width / 2;
			center.y = box.y + box.height / 2;
			center.z = box.z + box.depth / 2;
		}

		const numPickers: number = this._boundsPickers.length;
		if (numPickers > 0) {
			const node: INode = <INode> this._asset;
			const m: Matrix3D = new Matrix3D();
			for (let i: number = 0; i < numPickers; ++i) {
				if (this._boundsPickers[i].node != node) {
					if (matrix3D)
						m.copyFrom(matrix3D);
					else
						m.identity();

					m.prepend(this._boundsPickers[i].node.container.transform.matrix3D);
					if (this._boundsPickers[i].node.container._registrationMatrix3D)
						m.prepend(this._boundsPickers[i].node.container._registrationMatrix3D);

					target = this._boundsPickers[i]._getSphereBoundsInternal(center, m, strokeFlag, fastFlag, cache, target);
				} else {
					target = this._boundsPickers[i]._getSphereBoundsInternal(center, matrix3D, strokeFlag, fastFlag, cache, target);
				}
			}
		}

		return target;
	}

	/**
	 *
	 * @param planes
	 * @param numPlanes
	 * @returns {boolean}
	 */

	public isInFrustum(planes: Array<Plane3D>, numPlanes: number): boolean {
		return this._isInFrustumInternal(<INode> this._asset, planes, numPlanes);
	}

	public _isInFrustumInternal(rootNode: INode, planes: Array<Plane3D>, numPlanes: number): boolean {
		return this.getBoundingVolume(rootNode).isInFrustum(planes, numPlanes);
	}

	public onClear(): void {
		super.onClear();

		for (const key in this._boundingVolumePools)
			this._boundingVolumePools[key].abstractions.forEach((boundingVolume: BoundingVolumeBase) => boundingVolume.onClear());

		this._boundingVolumePools = null;

		this._boundsPickers.length = 0;
		// this._orientedBoxBoundsDirty[0] = true;
		// this._orientedBoxBoundsDirty[1] = true;
		// this._orientedSphereBoundsDirty[0] = true;
		// this._orientedSphereBoundsDirty[1] = true;
	}

	/**
	 *
	 * @param entity
	 */
	public applyEntity(node: INode): void {
		if (node.container.getEntity())
			this._boundsPickers.push(this._pickGroup.abstractions.getAbstraction<PickEntity>(node));
		else
			//check if we have a PickEntity abstraction and if so, clear it!
			this._pickGroup.abstractions.checkAbstraction(node)?.onClear();
	}
}