import {Number2, Number3} from '../../../../../types/GlobalTypes';
import {RaycastEventNode, TargetType, TARGET_TYPES} from '../../Raycast';
import {Object3D} from 'three';
import {Intersection} from 'three';
import {NodeContext} from '../../../../poly/NodeContext';
import {BaseObjNodeType} from '../../../obj/_Base';
import {GeoObjNode} from '../../../obj/Geo';
import {TypeAssert} from '../../../../poly/Assert';
import {Plane} from 'three';
import {Vector3} from 'three';
import {ParamType} from '../../../../poly/ParamType';
import {AttribType, ATTRIBUTE_TYPES} from '../../../../../core/geometry/Constant';
import {Vector3Param} from '../../../../params/Vector3';
import {RaycastCPUVelocityController} from './VelocityController';
import {isString} from '../../../../../core/Type';
import {CPUIntersectWith, CPU_INTERSECT_WITH_OPTIONS} from './CpuConstants';
import {isBooleanTrue} from '../../../../../core/BooleanValue';
import {IntersectDataEventNode} from '../../IntersectData';
import {BaseRaycastController} from './BaseRaycastController';
import {resolveIntersectGeometryAttribute} from '../../../../../core/geometry/intersect/CoreIntersect';
import {EventContext} from '../../../../../core/event/EventContextType';

export class RaycastCPUController extends BaseRaycastController {
	private _cursorArray: Number2 = [0, 0];
	private _resolvedTargets: Object3D[] | undefined;

	public readonly velocityController: RaycastCPUVelocityController;
	constructor(private _node: RaycastEventNode) {
		super();
		this.velocityController = new RaycastCPUVelocityController(this._node);
	}

	updateMouse(eventContext: EventContext<MouseEvent | DragEvent | PointerEvent | TouchEvent>) {
		const viewer = eventContext.viewer;
		if (!viewer) {
			return;
		}
		const camera = viewer.camera();
		if (!camera) {
			return;
		}

		this._cursorHelper.setCursorForCPU(eventContext, this._cursor);
		if (isBooleanTrue(this._node.pv.tmouse)) {
			this._cursor.toArray(this._cursorArray);
			this._node.p.mouse.set(this._cursorArray);
		}
		// this._updateFromCursor(canvas);
		viewer.raycastersController.raycaster0().setFromCamera(this._cursor, camera);
	}

	processEvent(context: EventContext<MouseEvent>) {
		this._prepareRaycaster(context);

		const type = CPU_INTERSECT_WITH_OPTIONS[this._node.pv.intersectWith];
		switch (type) {
			case CPUIntersectWith.GEOMETRY: {
				return this._intersectGeometry(context);
			}
			case CPUIntersectWith.PLANE: {
				return this._intersectPlane(context);
			}
		}
		TypeAssert.unreachable(type);
	}

	private _plane = new Plane();
	private _plane_intersect_target = new Vector3();
	private _intersectPlane(eventContext: EventContext<MouseEvent>) {
		const viewer = eventContext.viewer;
		if (!viewer) {
			return;
		}
		this._plane.normal.copy(this._node.pv.planeDirection);
		this._plane.constant = this._node.pv.planeOffset;
		viewer.raycastersController.raycaster0().ray.intersectPlane(this._plane, this._plane_intersect_target);

		this._setPositionParam(this._plane_intersect_target);
		this._node.triggerHit(eventContext);
	}

	private _intersections: Intersection[] = [];
	private _intersectGeometry(eventContext: EventContext<MouseEvent>) {
		const viewer = eventContext.viewer;
		if (!viewer) {
			return;
		}
		if (!this._resolvedTargets) {
			this.updateTarget();
		}
		if (this._resolvedTargets) {
			// clear array before
			this._intersections.length = 0;
			const intersections = viewer.raycastersController
				.raycaster0()
				.intersectObjects(
					this._resolvedTargets,
					isBooleanTrue(this._node.pv.traverseChildren),
					this._intersections
				);
			const intersection = intersections[0];
			if (intersection) {
				this._node.scene().batchUpdates(() => {
					if (isBooleanTrue(this._node.pv.tposition)) {
						this._setPositionParam(intersection.point);
					}

					if (isBooleanTrue(this._node.pv.geoAttribute)) {
						this._resolveIntersectAttribute(intersection);
					}
				});
				eventContext.value = {intersect: intersection};
				this._node.triggerHit(eventContext);
			} else {
				this._node.triggerMiss(eventContext);
			}
		}
	}
	private _resolveIntersectAttribute(intersection: Intersection) {
		const attribType = ATTRIBUTE_TYPES[this._node.pv.geoAttributeType];
		let attribValue: string | number | null | undefined = IntersectDataEventNode.resolveObjectAttribute(
			intersection,
			this._node.pv.geoAttributeName
		);
		if (attribValue == null) {
			attribValue = resolveIntersectGeometryAttribute(intersection, this._node.pv.geoAttributeName, attribType);
		}
		if (attribValue != null) {
			switch (attribType) {
				case AttribType.NUMERIC: {
					this._node.p.geoAttributeValue1.set(attribValue);
					return;
				}
				case AttribType.STRING: {
					if (isString(attribValue)) {
						this._node.p.geoAttributeValues.set(attribValue);
					}
					return;
				}
			}
			TypeAssert.unreachable(attribType);
		}
	}

	private _foundPositionTargetParam: Vector3Param | undefined;
	private _hitPositionArray: Number3 = [0, 0, 0];
	private _setPositionParam(hitPosition: Vector3) {
		hitPosition.toArray(this._hitPositionArray);
		if (isBooleanTrue(this._node.pv.tpositionTarget)) {
			const targetParam = this._node.pv.positionTarget;

			if (this._foundPositionTargetParam == null || isBooleanTrue(this._foundPositionTargetParam.disposed())) {
				this._foundPositionTargetParam = targetParam.paramWithType(ParamType.VECTOR3);
			}

			this._foundPositionTargetParam?.set(this._hitPositionArray);
			// }
		} else {
			this._node.p.position.set(this._hitPositionArray);
		}

		this.velocityController.process(hitPosition);
	}

	private _prepareRaycaster(eventContext: EventContext<MouseEvent>) {
		const viewer = eventContext.viewer;
		if (!viewer) {
			return;
		}
		const pointsParam = viewer.raycastersController.raycaster0().params.Points;
		if (pointsParam) {
			pointsParam.threshold = this._node.pv.pointsThreshold;
		}

		// let camera = context.camera;
		// if (isBooleanTrue(this._node.pv.overrideCamera)) {
		// 	if (isBooleanTrue(this._node.pv.overrideRay)) {
		// 		this._raycaster.ray.origin.copy(this._node.pv.rayOrigin);
		// 		this._raycaster.ray.direction.copy(this._node.pv.rayDirection);
		// 	} else {
		// 		const foundCameraNode = this._node.pv.camera.nodeWithContext(NodeContext.OBJ, this._node.states.error);
		// 		if (foundCameraNode) {
		// 			if ((CAMERA_TYPES as string[]).includes(foundCameraNode.type())) {
		// 				cameraNode = (<unknown>foundCameraNode) as Readonly<BaseCameraObjNodeType>;
		// 			}
		// 		}
		// 	}
		// }

		// if (cameraNode && !isBooleanTrue(this._node.pv.overrideRay)) {
		// 	cameraNode.prepareRaycaster(this._cursor, this._raycaster);
		// }
	}

	updateTarget() {
		const targetType = TARGET_TYPES[this._node.pv.targetType];
		switch (targetType) {
			case TargetType.NODE: {
				return this._updateTargetFromNode();
			}
			case TargetType.SCENE_GRAPH: {
				return this._updateTargetFromSceneGraph();
			}
		}
		TypeAssert.unreachable(targetType);
	}
	private _updateTargetFromNode() {
		const node = this._node.p.targetNode.value.nodeWithContext(NodeContext.OBJ) as BaseObjNodeType;
		if (node) {
			const found_obj = isBooleanTrue(this._node.pv.traverseChildren)
				? node.object
				: (node as GeoObjNode).childrenDisplayController.sopGroup();
			if (found_obj) {
				this._resolvedTargets = [found_obj];
			} else {
				this._resolvedTargets = undefined;
			}
		} else {
			this._node.states.error.set('node is not an object');
		}
	}
	private _updateTargetFromSceneGraph() {
		const objects: Object3D[] = this._node.scene().objectsByMask(this._node.pv.objectMask) as Object3D[];
		if (objects.length > 0) {
			this._resolvedTargets = objects;
		} else {
			this._resolvedTargets = undefined;
		}
	}

	static PARAM_CALLBACK_updateTarget(node: RaycastEventNode) {
		node.cpuController.updateTarget();
	}
}
