import { math } from '../../core/math/math.js';
import { Color } from '../../core/math/color.js';
import { Quat } from '../../core/math/quat.js';
import { Vec3 } from '../../core/math/vec3.js';
import { PROJECTION_PERSPECTIVE } from '../../scene/constants.js';
import { ArcShape } from './shape/arc-shape.js';
import { TransformGizmo } from './transform-gizmo.js';
import { MeshLine } from './mesh-line.js';

const point = new Vec3();
const v1 = new Vec3();
const v2 = new Vec3();
const v3 = new Vec3();
const v4 = new Vec3();
const q1 = new Quat();
const q2 = new Quat();
const color = new Color();
const ROTATE_FACING_EPSILON = 0.1;
const RING_FACING_EPSILON = 1e-4;
class RotateGizmo extends TransformGizmo {
		constructor(camera, layer){
				super(camera, layer, 'gizmo:rotate'), this._shapes = {
						z: new ArcShape(this._device, {
								axis: 'z',
								layers: [
										this._layer.id
								],
								rotation: new Vec3(90, 0, 90),
								defaultColor: this._theme.shapeBase.z,
								hoverColor: this._theme.shapeHover.z,
								disabledColor: this._theme.disabled,
								sectorAngle: 180
						}),
						x: new ArcShape(this._device, {
								axis: 'x',
								layers: [
										this._layer.id
								],
								rotation: new Vec3(0, 0, -90),
								defaultColor: this._theme.shapeBase.x,
								hoverColor: this._theme.shapeHover.x,
								disabledColor: this._theme.disabled,
								sectorAngle: 180
						}),
						y: new ArcShape(this._device, {
								axis: 'y',
								layers: [
										this._layer.id
								],
								rotation: new Vec3(0, 0, 0),
								defaultColor: this._theme.shapeBase.y,
								hoverColor: this._theme.shapeHover.y,
								disabledColor: this._theme.disabled,
								sectorAngle: 180
						}),
						f: new ArcShape(this._device, {
								axis: 'f',
								layers: [
										this._layer.id
								],
								defaultColor: this._theme.shapeBase.f,
								hoverColor: this._theme.shapeHover.f,
								disabledColor: this._theme.disabled,
								ringRadius: 0.55
						})
				}, this._selectionStartAngle = 0, this._nodeLocalRotations = new Map(), this._nodeRotations = new Map(), this._nodeOffsets = new Map(), this._guideAngleStart = new Vec3(), this._guideAngleEnd = new Vec3(), this.snapIncrement = 5, this.orbitRotation = false;
				this._createTransform();
				this._guideAngleLines = [
						new MeshLine(this._app, this._layer),
						new MeshLine(this._app, this._layer)
				];
				this._guideAngleLines.forEach((line)=>{
						this._app.root.addChild(line.entity);
						line.entity.enabled = false;
				});
				this.on(TransformGizmo.EVENT_TRANSFORMSTART, (point, x, y)=>{
						this._selectionStartAngle = this._calculateArcAngle(point, x, y);
						this._storeNodeRotations();
						this._storeGuidePoints();
						this._drag(true);
						this._angleGuide(true);
				});
				this.on(TransformGizmo.EVENT_TRANSFORMMOVE, (point, x, y)=>{
						const axis = this._selectedAxis;
						if (!axis) {
								return;
						}
						let angleDelta = this._calculateArcAngle(point, x, y) - this._selectionStartAngle;
						if (this.snap) {
								angleDelta = Math.round(angleDelta / this.snapIncrement) * this.snapIncrement;
						}
						const angleAxis = this._dirFromAxis(axis, v1);
						this._setNodeRotations(axis, angleAxis, angleDelta);
						this._updateGuidePoints(angleDelta);
						this._angleGuide(true);
				});
				this.on(TransformGizmo.EVENT_TRANSFORMEND, ()=>{
						this._drag(false);
						this._angleGuide(false);
				});
				this.on(TransformGizmo.EVENT_NODESDETACH, ()=>{
						this._nodeLocalRotations.clear();
						this._nodeRotations.clear();
						this._nodeOffsets.clear();
				});
		}
		set xyzTubeRadius(value) {
				this._setDiskProp('tubeRadius', value);
		}
		get xyzTubeRadius() {
				return this._shapes.x.tubeRadius;
		}
		set xyzRingRadius(value) {
				this._setDiskProp('ringRadius', value);
		}
		get xyzRingRadius() {
				return this._shapes.x.ringRadius;
		}
		set faceTubeRadius(value) {
				this._shapes.f.tubeRadius = value;
		}
		get faceTubeRadius() {
				return this._shapes.f.tubeRadius;
		}
		set faceRingRadius(value) {
				this._shapes.f.ringRadius = value;
		}
		get faceRingRadius() {
				return this._shapes.f.ringRadius;
		}
		set ringTolerance(value) {
				this._setDiskProp('tolerance', value);
				this._shapes.f.tolerance = value;
		}
		get ringTolerance() {
				return this._shapes.x.tolerance;
		}
		set angleGuideThickness(value) {
				this._guideAngleLines[0].thickness = value;
				this._guideAngleLines[1].thickness = value;
		}
		get angleGuideThickness() {
				return this._guideAngleLines[0].thickness;
		}
		_setDiskProp(prop, value) {
				this._shapes.x[prop] = value;
				this._shapes.y[prop] = value;
				this._shapes.z[prop] = value;
		}
		_storeGuidePoints() {
				const gizmoPos = this.root.getLocalPosition();
				const axis = this._selectedAxis;
				const isFacing = axis === 'f';
				const scale = isFacing ? this.faceRingRadius : this.xyzRingRadius;
				this._guideAngleStart.copy(this._selectionStartPoint).sub(gizmoPos).normalize();
				this._guideAngleStart.mulScalar(scale);
				this._guideAngleEnd.copy(this._guideAngleStart);
		}
		_updateGuidePoints(angleDelta) {
				const axis = this._selectedAxis;
				const isFacing = axis === 'f';
				if (isFacing) {
						v1.copy(this.facingDir);
				} else {
						v1.set(0, 0, 0);
						v1[axis] = 1;
						this._rootStartRot.transformVector(v1, v1);
				}
				q1.setFromAxisAngle(v1, angleDelta);
				q1.transformVector(this._guideAngleStart, this._guideAngleEnd);
		}
		_angleGuide(state) {
				const axis = this._selectedAxis;
				if (state && this.dragMode !== 'show' && axis !== 'xyz') {
						const gizmoPos = this.root.getLocalPosition();
						const baseColor = this._theme.guideBase[axis];
						const startColor = color.copy(baseColor);
						startColor.a *= 0.3;
						this._guideAngleLines[0].draw(gizmoPos, v1.copy(this._guideAngleStart).add(gizmoPos), this._scale, startColor);
						this._guideAngleLines[1].draw(gizmoPos, v1.copy(this._guideAngleEnd).add(gizmoPos), this._scale, baseColor);
						this._guideAngleLines[0].entity.enabled = true;
						this._guideAngleLines[1].entity.enabled = true;
				} else {
						this._guideAngleLines[0].entity.enabled = false;
						this._guideAngleLines[1].entity.enabled = false;
				}
		}
		_shapesLookAtCamera() {
				if (this._camera.projection === PROJECTION_PERSPECTIVE) {
						const dir = this._camera.entity.getPosition().sub(this.root.getPosition()).normalize();
						const elev = Math.atan2(-dir.y, Math.sqrt(dir.x * dir.x + dir.z * dir.z)) * math.RAD_TO_DEG;
						const azim = Math.atan2(-dir.x, -dir.z) * math.RAD_TO_DEG;
						this._shapes.f.entity.setEulerAngles(-elev + 90, azim, 0);
				} else {
						q1.copy(this._camera.entity.getRotation()).getEulerAngles(v1);
						this._shapes.f.entity.setEulerAngles(v1);
						this._shapes.f.entity.rotateLocal(-90, 0, 0);
				}
				let angle, dot, sector;
				const facingDir = v1.copy(this.facingDir);
				q1.copy(this.root.getRotation()).invert().transformVector(facingDir, facingDir);
				angle = Math.atan2(facingDir.z, facingDir.y) * math.RAD_TO_DEG;
				this._shapes.x.entity.setLocalEulerAngles(0, angle - 90, -90);
				angle = Math.atan2(facingDir.x, facingDir.z) * math.RAD_TO_DEG;
				this._shapes.y.entity.setLocalEulerAngles(0, angle, 0);
				angle = Math.atan2(facingDir.y, facingDir.x) * math.RAD_TO_DEG;
				this._shapes.z.entity.setLocalEulerAngles(90, 0, angle + 90);
				if (!this._dragging) {
						dot = facingDir.dot(this.root.right);
						sector = 1 - Math.abs(dot) > RING_FACING_EPSILON;
						this._shapes.x.show(sector ? 'sector' : 'ring');
						dot = facingDir.dot(this.root.up);
						sector = 1 - Math.abs(dot) > RING_FACING_EPSILON;
						this._shapes.y.show(sector ? 'sector' : 'ring');
						dot = facingDir.dot(this.root.forward);
						sector = 1 - Math.abs(dot) > RING_FACING_EPSILON;
						this._shapes.z.show(sector ? 'sector' : 'ring');
						this.fire(TransformGizmo.EVENT_RENDERUPDATE);
				}
		}
		_drag(state) {
				for(const axis in this._shapes){
						const shape = this._shapes[axis];
						if (!(shape instanceof ArcShape)) {
								continue;
						}
						switch(this.dragMode){
								case 'show':
										{
												break;
										}
								case 'hide':
										{
												shape.show(state ? axis === this._selectedAxis ? 'ring' : 'none' : 'sector');
												continue;
										}
								case 'selected':
										{
												shape.show(state ? axis === this._selectedAxis ? 'ring' : 'sector' : 'sector');
												break;
										}
						}
				}
				this.fire(TransformGizmo.EVENT_RENDERUPDATE);
		}
		_storeNodeRotations() {
				const gizmoPos = this.root.getLocalPosition();
				for(let i = 0; i < this.nodes.length; i++){
						const node = this.nodes[i];
						this._nodeLocalRotations.set(node, node.getLocalRotation().clone());
						this._nodeRotations.set(node, node.getRotation().clone());
						this._nodeOffsets.set(node, node.getPosition().clone().sub(gizmoPos));
				}
		}
		_setNodeRotations(axis, angleAxis, angleDelta) {
				const gizmoPos = this.root.getLocalPosition();
				q1.setFromAxisAngle(angleAxis, angleDelta);
				for(let i = 0; i < this.nodes.length; i++){
						const node = this.nodes[i];
						if ((axis === 'x' || axis === 'y' || axis === 'z') && this._coordSpace === 'local') {
								const rot = this._nodeLocalRotations.get(node);
								if (!rot) {
										continue;
								}
								q2.copy(rot).mul(q1);
								node.setLocalRotation(q2);
						} else {
								const rot = this._nodeRotations.get(node);
								if (!rot) {
										continue;
								}
								const offset = this._nodeOffsets.get(node);
								if (!offset) {
										continue;
								}
								v1.copy(offset);
								q1.transformVector(v1, v1);
								q2.copy(q1).mul(rot);
								node.setEulerAngles(q2.getEulerAngles());
								node.setPosition(v1.add(gizmoPos));
						}
				}
				if (this._coordSpace === 'local') {
						this._updateRotation();
				}
		}
		_screenToPoint(x, y) {
				const mouseWPos = this._camera.screenToWorld(x, y, 1);
				const axis = this._selectedAxis;
				const ray = this._createRay(mouseWPos);
				const plane = this._createPlane(axis, axis === 'f', false);
				if (!plane.intersectsRay(ray, point)) {
						return point;
				}
				return point;
		}
		_calculateArcAngle(point, x, y) {
				const gizmoPos = this.root.getLocalPosition();
				const axis = this._selectedAxis;
				const plane = this._createPlane(axis, axis === 'f', false);
				let angle = 0;
				const facingDir = v2.copy(this.facingDir);
				const facingDot = plane.normal.dot(facingDir);
				if (this.orbitRotation || 1 - Math.abs(facingDot) < ROTATE_FACING_EPSILON) {
						v1.sub2(point, gizmoPos);
						switch(axis){
								case 'x':
										{
												q1.copy(this._rootStartRot).invert().transformVector(v1, v1);
												angle = Math.atan2(v1.z, v1.y) * math.RAD_TO_DEG;
												break;
										}
								case 'y':
										{
												q1.copy(this._rootStartRot).invert().transformVector(v1, v1);
												angle = Math.atan2(v1.x, v1.z) * math.RAD_TO_DEG;
												break;
										}
								case 'z':
										{
												q1.copy(this._rootStartRot).invert().transformVector(v1, v1);
												angle = Math.atan2(v1.y, v1.x) * math.RAD_TO_DEG;
												break;
										}
								case 'f':
										{
												q1.copy(this._camera.entity.getRotation()).invert().transformVector(v1, v1);
												angle = Math.sign(facingDot) * Math.atan2(v1.y, v1.x) * math.RAD_TO_DEG;
												break;
										}
						}
				} else {
						v1.copy(gizmoPos);
						v2.cross(plane.normal, facingDir).normalize().add(gizmoPos);
						this._camera.worldToScreen(v1, v3);
						this._camera.worldToScreen(v2, v4);
						v1.sub2(v4, v3).normalize();
						v2.set(x, y, 0);
						angle = v1.dot(v2);
				}
				return angle;
		}
		prerender() {
				super.prerender();
				if (!this.enabled) {
						return;
				}
				this._shapesLookAtCamera();
		}
}

export { RotateGizmo };
