/**
 * Builds geometries on quad points
 *
 *
 */
import {Vector2, Vector3, Plane, Object3D, BoxGeometry, BufferGeometry} from 'three';
import {QuadSopNode} from './_BaseQuad';
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {CoreGroup} from '../../../core/geometry/Group';
import {SopType} from '../../poly/registers/nodes/types/Sop';
import {QuadObject} from '../../../core/geometry/modules/quad/QuadObject';
import {QuadPrimitive} from '../../../core/geometry/modules/quad/QuadPrimitive';
import {InputCloneMode} from '../../poly/InputCloneMode';
import {quadGraphFromQuadObject} from '../../../core/geometry/modules/quad/graph/QuadGraphUtils';
import {QuadPoint} from '../../../core/geometry/modules/quad/QuadPoint';
import {quadPrimitiveOppositePoints, QuadOppositePoints} from '../../../core/geometry/modules/quad/QuadPrimitiveUtils';
import {ConvexGeometry} from 'three/examples/jsm/geometries/ConvexGeometry';
import {ObjectType} from '../../../core/geometry/Constant';
import {ThreejsCoreObject} from '../../../core/geometry/modules/three/ThreejsCoreObject';
import {mergeGeometries} from 'three/examples/jsm/utils/BufferGeometryUtils';

const _currentPointPosition = new Vector3();
const _neighbourPosition = new Vector3();
const _neighbourPositionOnPlane = new Vector3();
const _delta = new Vector3();
const _tmp = new Vector3();
const _normal = new Vector3();
const _plane = new Plane();
const _positions: Vector3[] = [];
const quadOppositePoints: QuadOppositePoints = {
	p0: -1,
	p1: -1,
};
const _pointIdsSet: Set<number> = new Set();
const BOX_DIVISIONS = 1;
const _newObjectsForPoint: Object3D[] = [];

enum StarMode {
	ALONG_EDGES = 'along edges',
	TO_CENTER = 'to center',
}
const STAR_MODES: StarMode[] = [StarMode.ALONG_EDGES, StarMode.TO_CENTER];

class QuadCornersSopParamsConfig extends NodeParamsConfig {
	center = ParamConfig.BOOLEAN(1);
	star = ParamConfig.BOOLEAN(1);
	height = ParamConfig.FLOAT(0.1, {
		range: [0, 1],
		rangeLocked: [false, false],
	});
	centerSize = ParamConfig.FLOAT(0.1, {
		range: [0, 1],
		rangeLocked: [false, false],
		visibleIf: {center: 1},
	});
	starSize = ParamConfig.VECTOR2([0.05, 0.3], {
		visibleIf: {star: 1},
	});
	starMode = ParamConfig.INTEGER(STAR_MODES.indexOf(StarMode.ALONG_EDGES), {
		visibleIf: {star: 1},
		menu: {
			entries: STAR_MODES.map((name, value) => ({name, value})),
		},
	});
	cornersAttribName = ParamConfig.STRING('cornersCount');
	quadsAttribName = ParamConfig.STRING('quadsCount');
}
const ParamsConfig = new QuadCornersSopParamsConfig();

export class QuadCornersSopNode extends QuadSopNode<QuadCornersSopParamsConfig> {
	override paramsConfig = ParamsConfig;
	static override type() {
		return SopType.QUAD_CORNERS;
	}

	protected override initializeNode() {
		this.io.inputs.setCount(1);
		this.io.inputs.initInputsClonedState(InputCloneMode.FROM_NODE);
	}

	override cook(inputCoreGroups: CoreGroup[]) {
		const coreGroup = inputCoreGroups[0];
		const objects = coreGroup.quadObjects();
		if (!objects) {
			this.states.error.set(`no quad objects found`);
			return;
		}

		const newObjects: Object3D[] = [];
		for (const object of objects) {
			this._processObject(object, newObjects);
		}

		this.setObjects(newObjects);
	}

	private _processObject(quadObject: QuadObject, newObjects: Object3D[]) {
		const {center, star, centerSize, starSize, height, cornersAttribName, quadsAttribName} = this.pv;
		const graph = quadGraphFromQuadObject(quadObject);
		const pointsCount = QuadPoint.entitiesCount(quadObject);
		const starMode = STAR_MODES[this.pv.starMode];

		for (let i = 0; i < pointsCount; i++) {
			_newObjectsForPoint.length = 0;
			QuadPoint.position(quadObject, i, _currentPointPosition);
			_plane.normal;
			_pointIdsSet.clear();
			const quadIds = graph.quadIdsByPointIndex(i);
			if (!quadIds) {
				continue;
			}
			_plane.normal.set(0, 0, 0);
			quadIds.forEach((quadId) => {
				QuadPrimitive.normal(quadObject, quadId, _tmp);
				_plane.normal.add(_tmp);
				quadPrimitiveOppositePoints(quadObject, quadId, i, quadOppositePoints);
				_pointIdsSet.add(quadOppositePoints.p0);
				_pointIdsSet.add(quadOppositePoints.p1);
			});
			if (_pointIdsSet.size <= 2) {
				continue;
			}

			_plane.normal.divideScalar(quadIds.size).normalize();
			_plane.constant = _plane.distanceToPoint(_currentPointPosition);

			if (center) {
				_normal.copy(_plane.normal).multiplyScalar(height / 2);
				_positions.length = 0;
				_pointIdsSet.forEach((pointId, neighbourPointIndex) => {
					QuadPoint.position(quadObject, pointId, _neighbourPosition);
					_plane.projectPoint(_neighbourPosition, _neighbourPositionOnPlane);
					_delta.copy(_neighbourPositionOnPlane).sub(_currentPointPosition);

					_delta.normalize().multiplyScalar(centerSize);
					_neighbourPositionOnPlane.copy(_delta);
					_tmp.copy(_neighbourPositionOnPlane).add(_normal);
					_positions.push(_tmp.clone());
					_tmp.copy(_neighbourPositionOnPlane).sub(_normal);
					_positions.push(_tmp.clone());
				});

				const newGeo = new ConvexGeometry(_positions);
				const object = this.createObject(newGeo, ObjectType.MESH);
				object.position.copy(_currentPointPosition);
				_newObjectsForPoint.push(object);
			}
			if (star) {
				const boxGeometry = new BoxGeometry(
					starSize.x,
					height,
					starSize.y,
					BOX_DIVISIONS,
					BOX_DIVISIONS,
					BOX_DIVISIONS
				);
				const geometries: BufferGeometry[] = [];
				this._applyStarMode(starMode, quadObject, boxGeometry, starSize, geometries, _pointIdsSet, quadIds);

				const mergedGeometry = mergeGeometries(geometries);
				const object = this.createObject(mergedGeometry, ObjectType.MESH);
				object.position.copy(_currentPointPosition);
				_newObjectsForPoint.push(object);
			}
			for (const object of _newObjectsForPoint) {
				ThreejsCoreObject.addAttribute(object, cornersAttribName, _pointIdsSet.size);
				ThreejsCoreObject.addAttribute(object, quadsAttribName, quadIds.size);
				newObjects.push(object);
			}
		}
	}
	private _applyStarMode(
		starMode: StarMode,
		quadObject: QuadObject,
		boxGeometry: BoxGeometry,
		starSize: Vector2,
		geometries: BufferGeometry[],
		pointIdsSet: Set<number>,
		quadIdsSet: Set<number>
	) {
		switch (starMode) {
			case StarMode.ALONG_EDGES: {
				pointIdsSet.forEach((pointId) => {
					QuadPoint.position(quadObject, pointId, _neighbourPosition);
					const currentBoxGeometry = _createBoxTowardPoint(boxGeometry, _neighbourPosition, starSize);
					geometries.push(currentBoxGeometry);
				});
				return;
			}
			case StarMode.TO_CENTER: {
				quadIdsSet.forEach((quadId) => {
					QuadPrimitive.position(quadObject, quadId, _neighbourPosition);
					const currentBoxGeometry = _createBoxTowardPoint(boxGeometry, _neighbourPosition, starSize);
					geometries.push(currentBoxGeometry);
				});
				return;
			}
		}
	}
}

function _createBoxTowardPoint(boxGeometry: BoxGeometry, target: Vector3, starSize: Vector2) {
	_plane.projectPoint(target, _neighbourPositionOnPlane);
	_delta.copy(_neighbourPositionOnPlane).sub(_currentPointPosition);

	const currentBoxGeometry = boxGeometry.clone();
	currentBoxGeometry.translate(0, 0, starSize.y * 0.5);
	currentBoxGeometry.lookAt(_delta);
	return currentBoxGeometry;
}
