import { math } from '../../core/math/math.js';
import { Vec3 } from '../../core/math/vec3.js';
import { Mat4 } from '../../core/math/mat4.js';
import { Ray } from '../../core/shape/ray.js';
import { EventHandler } from '../../core/event-handler.js';
import { SORTMODE_NONE, PROJECTION_PERSPECTIVE } from '../../scene/constants.js';
import { Entity } from '../../framework/entity.js';
import { Layer } from '../../scene/layer.js';

const v = new Vec3();
const position = new Vec3();
const angles = new Vec3();
const dir = new Vec3();
const m1 = new Mat4();
const m2 = new Mat4();
const ray = new Ray();
const MIN_SCALE = 1e-4;
const PERS_SCALE_RATIO = 0.3;
const ORTHO_SCALE_RATIO = 0.32;
const UPDATE_EPSILON = 1e-6;
const DIST_EPSILON = 1e-4;
class Gizmo extends EventHandler {
		static{
				this.EVENT_POINTERDOWN = 'pointer:down';
		}
		static{
				this.EVENT_POINTERMOVE = 'pointer:move';
		}
		static{
				this.EVENT_POINTERUP = 'pointer:up';
		}
		static{
				this.EVENT_POSITIONUPDATE = 'position:update';
		}
		static{
				this.EVENT_ROTATIONUPDATE = 'rotation:update';
		}
		static{
				this.EVENT_SCALEUPDATE = 'scale:update';
		}
		static{
				this.EVENT_NODESATTACH = 'nodes:attach';
		}
		static{
				this.EVENT_NODESDETACH = 'nodes:detach';
		}
		static{
				this.EVENT_RENDERUPDATE = 'render:update';
		}
		static createLayer(app, layerName = 'Gizmo', layerIndex = app.scene.layers.layerList.length) {
				const layer = new Layer({
						name: layerName,
						clearDepthBuffer: true,
						opaqueSortMode: SORTMODE_NONE,
						transparentSortMode: SORTMODE_NONE
				});
				app.scene.layers.insert(layer, layerIndex);
				return layer;
		}
		constructor(camera, layer, name = 'gizmo'){
				super(), this._size = 1, this._scale = 1, this._coordSpace = 'world', this._handles = [], this.nodes = [], this.intersectShapes = [];
				this._layer = layer;
				this._camera = camera;
				this._camera.layers = this._camera.layers.concat(this._layer.id);
				this._app = this._camera.system.app;
				this._device = this._app.graphicsDevice;
				this.root = new Entity(name);
				this._app.root.addChild(this.root);
				this.root.enabled = false;
				this._updateScale();
				this._onPointerDown = this._onPointerDown.bind(this);
				this._onPointerMove = this._onPointerMove.bind(this);
				this._onPointerUp = this._onPointerUp.bind(this);
				this._device.canvas.addEventListener('pointerdown', this._onPointerDown);
				this._device.canvas.addEventListener('pointermove', this._onPointerMove);
				this._device.canvas.addEventListener('pointerup', this._onPointerUp);
				this._handles.push(this._app.on('prerender', ()=>this.prerender()));
				this._handles.push(this._app.on('update', ()=>this.update()));
				this._handles.push(this._app.on('destroy', ()=>this.destroy()));
		}
		set enabled(state) {
				const cameraDist = this.root.getLocalPosition().distance(this.camera.entity.getPosition());
				const enabled = state ? this.nodes.length > 0 && cameraDist > DIST_EPSILON : false;
				if (enabled !== this.root.enabled) {
						this.root.enabled = enabled;
						this.fire(Gizmo.EVENT_RENDERUPDATE);
				}
		}
		get enabled() {
				return this.root.enabled;
		}
		set layer(layer) {
				if (this._layer === layer) {
						return;
				}
				this._camera.layers = this._camera.layers.filter((id)=>id !== this._layer.id);
				this._layer = layer;
				this._camera.layers = this._camera.layers.concat(this._layer.id);
				this.enabled = true;
		}
		get layer() {
				return this._layer;
		}
		set camera(camera) {
				if (this._camera === camera) {
						return;
				}
				this._camera.layers = this._camera.layers.filter((id)=>id !== this._layer.id);
				this._camera = camera;
				this._camera.layers = this._camera.layers.concat(this._layer.id);
				this.enabled = true;
		}
		get camera() {
				return this._camera;
		}
		set coordSpace(value) {
				this._coordSpace = value ?? this._coordSpace;
				this._updateRotation();
		}
		get coordSpace() {
				return this._coordSpace;
		}
		set size(value) {
				this._size = value;
				this._updateScale();
		}
		get size() {
				return this._size;
		}
		get facingDir() {
				if (this._camera.projection === PROJECTION_PERSPECTIVE) {
						const gizmoPos = this.root.getLocalPosition();
						const cameraPos = this._camera.entity.getPosition();
						return dir.sub2(cameraPos, gizmoPos).normalize();
				}
				return dir.copy(this._camera.entity.forward).mulScalar(-1);
		}
		get cameraDir() {
				const cameraPos = this._camera.entity.getPosition();
				const gizmoPos = this.root.getLocalPosition();
				return dir.sub2(cameraPos, gizmoPos).normalize();
		}
		_onPointerDown(e) {
				if (!this.enabled || document.pointerLockElement) {
						return;
				}
				const selection = this._getSelection(e.offsetX, e.offsetY);
				if (selection[0]) {
						e.preventDefault();
						e.stopPropagation();
				}
				const { canvas } = this._device;
				canvas.setPointerCapture(e.pointerId);
				this.fire(Gizmo.EVENT_POINTERDOWN, e.offsetX, e.offsetY, selection[0]);
		}
		_onPointerMove(e) {
				if (!this.enabled || document.pointerLockElement) {
						return;
				}
				const selection = this._getSelection(e.offsetX, e.offsetY);
				if (selection[0]) {
						e.preventDefault();
						e.stopPropagation();
				}
				this.fire(Gizmo.EVENT_POINTERMOVE, e.offsetX, e.offsetY, selection[0]);
		}
		_onPointerUp(e) {
				if (!this.enabled || document.pointerLockElement) {
						return;
				}
				const selection = this._getSelection(e.offsetX, e.offsetY);
				if (selection[0]) {
						e.preventDefault();
						e.stopPropagation();
				}
				const { canvas } = this._device;
				canvas.releasePointerCapture(e.pointerId);
				this.fire(Gizmo.EVENT_POINTERUP, e.offsetX, e.offsetY, selection[0]);
		}
		_updatePosition() {
				position.set(0, 0, 0);
				for(let i = 0; i < this.nodes.length; i++){
						const node = this.nodes[i];
						position.add(node.getPosition());
				}
				position.mulScalar(1.0 / (this.nodes.length || 1));
				if (position.distance(this.root.getLocalPosition()) < UPDATE_EPSILON) {
						return;
				}
				this.root.setLocalPosition(position);
				this.fire(Gizmo.EVENT_POSITIONUPDATE, position);
				this.fire(Gizmo.EVENT_RENDERUPDATE);
		}
		_updateRotation() {
				angles.set(0, 0, 0);
				if (this._coordSpace === 'local' && this.nodes.length !== 0) {
						angles.copy(this.nodes[this.nodes.length - 1].getEulerAngles());
				}
				if (angles.distance(this.root.getLocalEulerAngles()) < UPDATE_EPSILON) {
						return;
				}
				this.root.setLocalEulerAngles(angles);
				this.fire(Gizmo.EVENT_ROTATIONUPDATE, angles);
				this.fire(Gizmo.EVENT_RENDERUPDATE);
		}
		_updateScale() {
				if (this._camera.projection === PROJECTION_PERSPECTIVE) {
						const gizmoPos = this.root.getLocalPosition();
						const cameraPos = this._camera.entity.getPosition();
						const dist = gizmoPos.distance(cameraPos);
						this._scale = Math.tan(0.5 * this._camera.fov * math.DEG_TO_RAD) * dist * PERS_SCALE_RATIO;
				} else {
						this._scale = this._camera.orthoHeight * ORTHO_SCALE_RATIO;
				}
				this._scale = Math.max(this._scale * this._size, MIN_SCALE);
				if (Math.abs(this._scale - this.root.getLocalScale().x) < UPDATE_EPSILON) {
						return;
				}
				this.root.setLocalScale(this._scale, this._scale, this._scale);
				this.fire(Gizmo.EVENT_SCALEUPDATE, this._scale);
				this.fire(Gizmo.EVENT_RENDERUPDATE);
		}
		_getSelection(x, y) {
				const start = this._camera.screenToWorld(x, y, 0);
				const end = this._camera.screenToWorld(x, y, this._camera.farClip - this._camera.nearClip);
				const dir = v.copy(end).sub(start).normalize();
				const selection = [];
				for(let i = 0; i < this.intersectShapes.length; i++){
						const shape = this.intersectShapes[i];
						if (shape.disabled || !shape.entity.enabled) {
								continue;
						}
						const parentTM = shape.entity.getWorldTransform();
						for(let j = 0; j < shape.triData.length; j++){
								const { tris, transform, priority } = shape.triData[j];
								const triWTM = m1.copy(parentTM).mul(transform);
								const invTriWTM = m2.copy(triWTM).invert();
								invTriWTM.transformPoint(start, ray.origin);
								invTriWTM.transformVector(dir, ray.direction);
								ray.direction.normalize();
								for(let k = 0; k < tris.length; k++){
										if (tris[k].intersectsRay(ray, v)) {
												selection.push({
														dist: triWTM.transformPoint(v).sub(start).length(),
														meshInstances: shape.meshInstances,
														priority: priority
												});
										}
								}
						}
				}
				if (selection.length) {
						selection.sort((s0, s1)=>{
								if (s0.priority !== 0 && s1.priority !== 0) {
										return s1.priority - s0.priority;
								}
								return s0.dist - s1.dist;
						});
						return selection[0].meshInstances;
				}
				return [];
		}
		attach(nodes = []) {
				if (Array.isArray(nodes)) {
						if (nodes.length === 0) {
								return;
						}
						this.nodes = nodes;
				} else {
						this.nodes = [
								nodes
						];
				}
				this._updatePosition();
				this._updateRotation();
				this._updateScale();
				this.fire(Gizmo.EVENT_NODESATTACH);
				this.enabled = true;
		}
		detach() {
				this.enabled = false;
				this.fire(Gizmo.EVENT_NODESDETACH);
				this.nodes = [];
		}
		prerender() {}
		update() {
				this._updatePosition();
				this._updateRotation();
				this._updateScale();
		}
		destroy() {
				this.detach();
				this._device.canvas.removeEventListener('pointerdown', this._onPointerDown);
				this._device.canvas.removeEventListener('pointermove', this._onPointerMove);
				this._device.canvas.removeEventListener('pointerup', this._onPointerUp);
				this._handles.forEach((handle)=>handle.off());
				this.root.destroy();
		}
}

export { Gizmo };
