/*
 * Copyright (C) 2021 Thomas Van Acker
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <https://www.gnu.org/licenses/>.
 *
 * This project is hosted on https://git.bitscuit.be/bitscuit/Fawkes
 *
*/

// Imports
const http = require("http");
const url = require("url");
const fs = require("fs");
const util = require("util");
const crypto = require("crypto");
const spawn = require("child_process").spawn;

// Vars
const hostname = "192.168.0.128"; // Enter your local IP address here
const port = 8001; // Enter a port number here

var files = {};
var lastImageId;


// Create HTTP server
const server = http.createServer((req, res) => {
	// Parse url
	const requrl = url.parse(req.url);
	var reqpath = requrl.pathname;
	if(reqpath == "/") reqpath = "/index.html";

	// Respond to requests
	try{
		// Return requested page
		res.statusCode = 200;
		res.write(fs.readFileSync("html"+reqpath));
		res.end();

	}catch(error){
		// 404 if page not found
		res.statusCode = 404;
		res.setHeader("Content-Type", "text/plain");
		res.end("404 - Page Not Found");
	}
});


// Create socket.io server
const io = require("socket.io")(server); // Can only be done when server is created

io.on("connection", (socket) => {
	console.log("New user connected!");

	// Give user unique ID
	var userId = sha256("USER"+rand(0,9999999)+"TIME"+Date.now()).substr(0, 10);
	console.log("userId = "+userId);

	// Init directory structure
	fs.mkdirSync("fawkes/files/"+userId);
	
	// Init socket listeners
	
	// Disconnect
	socket.on("disconnect", () => {
		console.log(userId+": disconnect");

		// Remove all files from this user
		fs.rmSync("fawkes/files/"+userId, {recursive: true});
	});

	// File upload
	socket.on("FILE_START", (data) => {
		console.log(userId+": file upload ("+data.name+", "+data.length+" bytes)");

		// Check length
		if(data.length > 20000000){
			console.log(userId+": file too large (data.length = "+data.length+")");
			socket.emit("FILE_ERROR", {name:data.name, error:"File too large. Max size = 20MB"});
			return;
		}

		// Check if exists
		if((userId+data.name) in files){
			console.log(userId+": file already exists (data.name = "+data.name+")");
			socket.emit("FILE_ERROR", {name:data.name, error:"File had already been uploaded."});
			return;
		}

		// Create file
		var name = data.name;
		var path = "fawkes/files/"+userId+"/"+name;
		var file = {
			length: data.length,
			name: data.name,
			path: path,
			numSegments: data.length/100000 + 1,
			nextSegment: 0
		};
		files[userId+data.name] = file;

		// Ask client to begin upload
		socket.emit("FILE_ACK", {name:file.name, nextSegment:file.nextSegment, numSegments:file.numSegments});
	});

	// File contents upload
	socket.on("FILE_DATA", (data) => {
		console.log(userId+": file data ("+data.name+", segment "+data.segment+", "+data.data.length+" bytes)");

		// Check if file exists
		if(!((userId+data.name) in files)){
			console.log(userId+": received segment for unknown file (data.name = "+data.name+")");
			socket.emit("FILE_ERROR", {name:data.name, error:"Received segment for unknown file."});
			return;
		}

		// Get file
		var file = files[userId+data.name];

		// Check size
		if(data.data.length > 100000){
			console.log(userId+": segment too large (data.name = "+data.name+", data.data.length = "+data.data.length+")");
			socket.emit("FILE_ERROR", {name:data.name, error:"Segment too large."});
			return;
		}

		// Check segment number
		if(data.segment != file.nextSegment || data.segment >= file.numSegments){
			console.log(userId+": unexpected segment (data.name = "+data.name+", data.segment = "+data.segment+")");
			socket.emit("FILE_ERROR", {name:data.name, error:"Unexpected segment."});
			return;
		}

		// Write data
		fs.appendFile(file.path, data.data, 'Binary', function(err){
			if(err){
				// Error
				console.log(userId+": couldn't write to file (error = "+err+")");
				socket.emit("FILE_ERROR", {name:data.name, error:"Internal server error. Couldn't write to file."});

			}else{
				file.nextSegment++;
				if(file.nextSegment < file.numSegments-1){
					// Ask for new segment
					socket.emit("FILE_ACK", {name:file.name, nextSegment:file.nextSegment, numSegments:file.numSegments});

				}else{
					// When file fully uploaded
					socket.emit("FILE_DONE", {name:file.name});
				}
			}
		});
	});

	// Fawkes start
	socket.on("FAWKES_START", () => {
		// Start binary
		var process = spawn("fawkes/protection", ["-d", "fawkes/files/"+userId, "--mode", "low"]);

		// Add listeners
		process.stdout.setEncoding("utf8");
		process.stdout.on("data", function(data){
			// Received output from process
			console.log("fawkes stdout: "+data);

			// Parse output
			data = data.replace("\n", " ");
			var splices = data.split(" ");
			if(data.startsWith("Find ")){
				var numFaces = splices[1];
				var imageId = splices[4];
				console.log("fawkes found "+numFaces+" faces in image "+imageId);

				// Update file info
				files[userId+imageId].numFaces = numFaces;
				lastImageId = imageId;

				// Update client states
				socket.emit("FAWKES_STATE", {name:imageId, face:1, numFaces:numFaces});

			}else if(splices[0] == "processing"){
				var face = splices[2];
				console.log("fawkes processing face "+face);

				// Update client states
				socket.emit("FAWKES_STATE", {name:lastImageId, face:face, numFaces:files[lastImageId].numFaces});

			}
			
		});
		process.stderr.setEncoding("utf8");
		process.stderr.on("data", function(data){
			// Received output from process
			console.log("fawkes stderr: "+data);
		});
		process.on("close", function(code){
			// Fawkes exited
			console.log("Fawkes exited with code "+code);
		});

		// Update file states
		socket.emit("FAWKES_STARTED");
	});
});


// Start HTTP server
server.listen(port, hostname, () => {
	console.log("Server running at http://"+hostname+":"+port);
});




// Helper methods
function rand(min, max){
	return Math.floor(Math.random()*(max-min) + min);
}
function sha256(message){
	return crypto.createHash("sha256").update(message).digest("hex");
}




























