const express = require('express');
const dgram = require('dgram');
const oscParser = require('./osc-parser');
const bodyParser = require('body-parser');
const { Bundle, Client } = require('node-osc');
const path = require('path');
const config = require('../frontend/assets/config.json');

const app = express();

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended:true}));
app.use(express.static('./frontend/assets'));

app.get('/', function (req, res) {
	res.sendFile('./frontend/index.html', {
		root: path.resolve(__dirname + '/..')
	});
});

const httpServer = require('http').Server(app);

/* --------
 * RECEIVER
 * --------
 */

const receiverIo = require('socket.io')(httpServer, {
	transports: ['websocket']
});

const onSocketListening = function() {
	const address = receiverUdpSocket.address();
	console.log('Serveur TUIO en écoute sur : ' + address.address + ':' + address.port);
};

const onSocketConnection = function(socket) {
	receiverUdpSocket.on('message', function(msg) {
		socket.emit('osc', oscParser.decode(msg));
	});
};

const receiverUdpSocket = dgram.createSocket('udp4');
receiverUdpSocket.on('listening', onSocketListening);
receiverUdpSocket.bind(config.app.oscUdpPort, '127.0.0.1');

app.get('/receiver/json', function (req, res) {
	res.status(200).send();
});

receiverIo.sockets.on('connection', (socket) =>{
	console.log(`Connecté au client ${socket.id}`);

	const dgramCallback = function (buf) {
		if (config.app.debug && config.app.debugLog.backend.receiver.oscDatagram) {
			console.log(oscParser.decode(buf));
		}
		socket.emit('osc', oscParser.decode(buf));
	};

	// forward UDP packets via socket.io
	receiverUdpSocket.on('message', dgramCallback);

	// prevent memory leak on disconnect
	socket.on('disconnect', function (socket) {
		receiverUdpSocket.removeListener('message', dgramCallback);
	});
});

/* -------
 * EMITTER
 * -------
 */

const emitterOscClient = new Client('127.0.0.1', config.app.oscUdpPort);
let alive = [];
let fseq;

let objectsAlive = [] ;
let lastDots = [];

function getHypotenuse(touch1, touch2) {
	const x = Math.abs(touch1.x - touch2.x);
	const y = Math.abs(touch1.y - touch2.y);
	return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
}

function getTop(dotTrio) {
	const dist01 = getHypotenuse(dotTrio[0], dotTrio[1]);
	const dist02 = getHypotenuse(dotTrio[0], dotTrio[2]);
	const dist12 = getHypotenuse(dotTrio[1], dotTrio[2]);

	const diff01m02 = Math.abs(dist01 - dist02);
	const diff01m12 = Math.abs(dist01 - dist12);
	const diff02m12 = Math.abs(dist02 - dist12);

	if (diff01m02 < diff02m12 && diff01m02 < diff01m12) {
		return 0;
	}
	else if (diff01m12 < diff01m02 && diff01m12 < diff02m12) {
		return 1;
	}
	else if (diff02m12 < diff01m02 && diff02m12 < diff01m12) {
		return 2;
	}
}

function getAngleApex(dotTrio, topIndex) {
	let dotA;
	let dotB;
	let dotC;
	dotB = dotTrio[topIndex];
	if (topIndex == 0) {
		dotA = dotTrio[1];
		dotC = dotTrio[2];
	}
	else if (topIndex == 1) {
		dotA = dotTrio[0];
		dotC = dotTrio[2];
	}
	else if (topIndex == 2) {
		dotA = dotTrio[0];
		dotC = dotTrio[1];
	}

	const AB = [dotB.x - dotA.x, dotB.y - dotA.y];
	const CB = [dotB.x - dotC.x, dotB.y - dotC.y];

	const dotProd = (AB[0] * CB[0] + AB[1] * CB[1]);
	const crossProd = (AB[0] * CB[1] - AB[1] * CB[0]);

	const alpha = Math.atan2(crossProd, dotProd);
	//return alpha ;
	return Math.abs(Math.floor(alpha * 180. / Math.PI + 0.5)) ;
}

function getOrientation(dotTrio, topIndex) {
	let dotA;
	let dotB;
	let dotC;
	dotB = dotTrio[topIndex];
	if (topIndex == 0) {
		dotA = dotTrio[1];
		dotC = dotTrio[2];
	}
	else if (topIndex == 1) {
		dotA = dotTrio[0];
		dotC = dotTrio[2];
	}
	else if (topIndex == 2) {
		dotA = dotTrio[0];
		dotC = dotTrio[1];
	}

	const middlePt = [(dotA.x + dotC.x) / 2, (dotA.y + dotC.y) / 2 ] ;
	let diff = [dotB.x - middlePt[0], dotB.y - middlePt[1]] ;
	//const length = Math.sqrt(Math.pow(diff[0], 2) + Math.pow(diff[1], 2) ) ;
	//normalize diff
	//diff = [diff[0] / length, diff[1] / length];
	const rad = Math.atan2(diff[1], diff[0]) ;
 	return Math.floor(rad * 180 / Math.PI)  ;
	 //return length ;
}

function objectGarbageCollector(){
	//si un point dans last dots est detecté dans un des triangle alors on ne réduit pas sa duration
	for(const triangle of objectsAlive){
		if(triangle.dots.some(dot => lastDots.some(lastDot => lastDot.x == dot.x))){	
		} else {
			triangle.remainingDuration -= 1;
		}
	};
	objectsAlive = objectsAlive.filter(triangle => triangle.remainingDuration > 0);
	createBundle();
}

setInterval(objectGarbageCollector, config.app.garbageCollectorInterval);

let currentOscBundle = null;
let hasPending = false;

function createBundle(){
	currentOscBundle = new Bundle ;
	currentOscBundle.append([ '/tuio/2Dobj', 'alive'].concat(objectsAlive.map(t => t.matchingObject.apexAngle) ));
	for(const triangle of objectsAlive){
		currentOscBundle.append([
					'/tuio/2Dobj',
					'set',
					triangle.matchingObject.apexAngle,
					triangle.matchingObject.apexAngle,
					triangle.center[0],
					triangle.center[1],
					triangle.orientation,
					0.0,
					0.0,
					0.0,
					0.0,
					0.0
				]);
	}
	currentOscBundle.append(['/tuio/2Dobj', 'fseq', fseq]);
	hasPending = true;
}

function listDots(touches){
	const dots = [];
	for(const touch of touches){
		dots.push({
			id: touch.identifier,
			x: touch.clientX,
			y: touch.clientY
		});
	};
	if (config.app.debug && config.app.debugLog.backend.emitter.dots) {
		console.log('-- dots --', dots);
	}
	return dots
}

function listSegments(dots){
	const segments = [];
	if (dots.length > 2) {
		for (var i = 0; i < dots.length; i++) {
			for (var j = 0; j < dots.length; j++) {
				if (j !== i) {
					/* on vérifie que le segment n'est pas déjà listé */
					const alreadyExists = segments.find(segment => {
						return segment.identifiers.includes(i) && segment.identifiers.includes(j);
					});
					/* on calcule la taille du segment (l'hypoténuse) */
					var hyp = getHypotenuse(dots[i], dots[j]);
					/* on garde uniquement les segments inférieurs à 750px (valeur par défaut)
						* cette valeur est la variable de configuration "maxDistanceBetweenPoints" */
					if (!alreadyExists && hyp <= config.app.maxDistanceBetweenPoints) {
						segments.push({
							identifiers: [i, j],
							x1: dots[i].x,
							x2: dots[j].x,
							y1: dots[i].y,
							y2: dots[j].y,
							hyp
						});
					}
				}
			}
		}
	}
	if (config.app.debug && config.app.debugLog.backend.emitter.segments) {
		console.log('-- segments --', segments);
	}
	return segments;
}

function listTriangles(segments){
	const triangles = [];
	/* on boucle sur les segments */
	for(const segment of segments) {
		const dot1 = segment.identifiers[0];
		const dot2 = segment.identifiers[1];
		/* on vérifie que le triangle n'est pas déjà listé */
		const alreadyExists = triangles.find(triangle => {
			return triangle.includes(dot1) && triangle.includes(dot2);
		});
		if (!alreadyExists) {
			/* on cherche les segments qui contiennent un des 2 points du segment actuel
				* ex: si le segment actuel est AB, on cherche un segment contenant A (pour AC) et un autre contenant B (pour BC) */
			const found1 = segments.findIndex(seg => {
				return (seg.identifiers.includes(dot1) && !seg.identifiers.includes(dot2));
			});
			const found2 = segments.findIndex(seg => {
				return (seg.identifiers.includes(dot2) && !seg.identifiers.includes(dot1));
			});
			/* si on trouve bien les 2 segments (AC et BC), on peut créer un triangle */
			if (found1 !== -1 && found2 !== -1) {
				/* on devine quel est le 3ème point du triangle par rapport au segment actuel (le point C par rapport au segment AB) */
				const dot3 = segments[found1].identifiers.find(identifier => {
					return identifier !== dot1 && identifier !== dot2;
				});
				triangles.push([dot1, dot2, dot3]);
			}
		}
	};
	if (config.app.debug && config.app.debugLog.backend.emitter.triangles) {
		console.log('-- triangles --', triangles);
	}
	return triangles
}

function filterTriangles(dots, triangles){
	const filteredTriangles = [];

	/* Définition de l'apex, de la position du centre et de l'orientation */
	for(const triangle of triangles){
		const objTriangle = {} ;
		objTriangle.dots = [];
		objTriangle.dots[0] = dots[triangle[0]];
		objTriangle.dots[1] = dots[triangle[1]];
		objTriangle.dots[2] = dots[triangle[2]];

		objTriangle.apex = getTop(objTriangle.dots);
		objTriangle.center = [
			(objTriangle.dots[0].x + objTriangle.dots[1].x + objTriangle.dots[2].x) / 3,
			(objTriangle.dots[0].y + objTriangle.dots[1].y + objTriangle.dots[2].y) / 3
		];

		objTriangle.angleApex = getAngleApex(objTriangle.dots, objTriangle.apex);
		objTriangle.orientation = getOrientation(objTriangle.dots, objTriangle.apex);

		if (config.app.debug && config.app.debugLog.backend.emitter.apex) {
			console.log('-- apex --', objTriangle.apex);
			console.log('angle: ', objTriangle.angleApex);
			console.log('centerPos: ' + objTriangle.center);
			console.log('orientation: ' + objTriangle.orientation);
		}

		//verify if triangle has a corresponding triangle in config
		const matchingObject = config.objects.find(item => {
			return objTriangle.angleApex > item.apexAngle - config.app.matchingTolerance && objTriangle.angleApex < item.apexAngle + config.app.matchingTolerance;
		});
		if (matchingObject) {
			objTriangle.matchingObject = matchingObject;
			filteredTriangles.push(objTriangle);

			if (config.app.debug && config.app.debugLog.backend.emitter.matchingObject) {
				console.log('-- matchingObject --', objTriangle);
			}
		}
	};
	return filteredTriangles;
}

function updateAliveTriangles(filteredTriangles) {
	for (const triangle of filteredTriangles) {
		let idx = objectsAlive.findIndex(item => {
			return triangle.matchingObject.name == item.matchingObject.name;
		});
		if (idx == -1) {
			triangle.remainingDuration = config.app.remainingDuration;
			objectsAlive.push(triangle);
		} else {
			triangle.remainingDuration = config.app.remainingDuration;
			objectsAlive[idx] = triangle;
		}
	}
	if (config.app.debug && config.app.debugLog.backend.emitter.aliveTriangles) {
		console.log('-- aliveTriangles --', objectsAlive);
	}
	
}

const sendBundle = () => {
	if (hasPending) {
		emitterOscClient.send(currentOscBundle, () => {
			hasPending = false;
		});
	}
	setTimeout(() => {
		sendBundle();
	}, config.app.timerRefresh);
};
sendBundle();

app.post('/emitter/json', function (req, res) {
	if (config.app.debug && config.app.debugLog.backend.emitter.httpRequest) {
		console.log('### Emitter POST request ###');
	}
	let oscBundle;
	// if (req.body.type === 'touchend') {
	// 	fseq = fseq ? fseq + 1 : 1;

	// 	const aliveMessage = [ '/tuio/2Dcur', 'alive' ].concat(alive);
	// 	currentOscBundle = new Bundle(
	// 		[ '/tuio/2Dcur', 'source', `tangibles${req.body.section.toString()}@127.0.0.1` ],
	// 		aliveMessage,
	// 		[ '/tuio/2Dcur', 'fseq', fseq ],
	// 		[
	// 			'/tuio/2Dcur',
	// 			'del',
	// 			req.body.touches[0].identifier
	// 		]
	// 	);
	// 	emitterOscClient.send(currentOscBundle, () => {
	// 		const index = alive.indexOf(req.body.touches[0].identifier);
	// 		alive.splice(index, 1);
	// 		if (alive.length === 0) {
	// 			currentOscBundle = new Bundle(
	// 				[ '/tuio/2Dcur', 'source', `tangibles${req.body.section.toString()}@127.0.0.1` ],
	// 				[ '/tuio/2Dcur', 'alive' ],
	// 				[ '/tuio/2Dcur', 'fseq', fseq ]
	// 			);
	// 			emitterOscClient.send(currentOscBundle, () => {
	// 				res.status(200).send();
	// 				fseq = 0;
	// 				hasPending = false;
	// 			});
	// 		} else {
	// 			res.status(200).send();
	// 		}
	// 	});
	// } else {
	if (req.body.touches && req.body.touches.length && req.body.touches.length > 0) {
		fseq = fseq ? fseq + 1 : 1;
		const touches = Object.keys(req.body.touches);
		const aliveMessage = [ '/tuio/2Dcur', 'alive' ].concat(alive);
		touches.forEach(touch => {
			const id = req.body.touches[touch].identifier;
			if (!alive.includes(id)) {
				alive.push(id);
				aliveMessage.push(id);
			}
		});

		/* Listage de tous les points */
		const dots = listDots(req.body.touches);
		
		lastDots = dots;

		const segments = listSegments(dots);

		const triangles = listTriangles(segments);

		const filteredTriangles = filterTriangles(dots, triangles);

		updateAliveTriangles(filteredTriangles);

		createBundle();

		res.status(200).send();
	} else {
		lastDots = [];
		res.status(200).send();
	}
});

httpServer.listen(config.app.httpPort, function () {
	console.log(`Votre app est disponible sur http://localhost:${config.app.httpPort} !`)
});