1 var express = require('express');
  2 var moment = require('moment')
  3 var database = require('./database');
  4 var router = express.Router();
  5 var verify = require('./verify');
  6 
  7 
  8 /* GET users listing. */
  9 
 10 /**
 11 	* Middleware function to get matches for a user.
 12 	* API to access this function: GET /match/
 13 	* @param {Object} req The express routing HTTP client request object.
 14 	* @param {Object} res The express routing HTTP client response object.
 15 	* @param {callback} next The express routing callback function to invoke next middleware in the stack.
 16 	* @return {Void}
 17 */
 18 var matchGet = function(req, res, next) {
 19     var db = req.app.locals.db; //get instance of db
 20 	var userId = parseInt(req.params.userid);
 21 
 22 	db.collection('Users')
 23 			.find({'userId': userId})
 24 			.toArray(function(err, results) {
 25 				if (results.length == 0) {
 26 					res.status(404).send("404: userId not found");
 27 				} else {
 28 					user = results[0]; // should only be one match
 29 
 30 					if (!verify.checkLogin(req.cookies.jwt, user.email)) {
 31 						res.status(401).redirect('/login');
 32 						return;
 33 					}
 34 
 35 					var matchedResults = [];
 36 					matchUsers(req, res, next, userId, matchedResults);
 37 
 38 					//db call to insertEvent here:
 39 					database.insertEvent(database.routerProperties(req, res, next), matchedResults["user_id"], matchedResults["match_id"], matchedResults["event"], matchedResults["unix_time"], matchedResults["end_time"], matchedResults["location"]);
 40 
 41 				}
 42 			});
 43 }
 44 
 45 router.get('/:userid', matchGet);
 46 
 47 const MAX_AVAILABILITY_SCORE = 6;
 48 const MAX_INTEREST_SCORE = 7.5;
 49 const MAX_SKILL_SCORE = 5;
 50 const NORMALIZED_BASE = 10.0;
 51 const DAYS = 7;
 52 const TIME_SLOTS = 48;
 53 
 54 /**
 55 	* Key function to generate matches for a single user, given the user id.
 56 	* @param {Object} req The express routing HTTP client request object.
 57 	* @param {Object} res The express routing HTTP client response object.
 58 	* @param {callback} next The express routing callback function to invoke next middleware in the stack.
 59 	* @param {number} userId An integer that represents a user id.
 60 	* @return {!Array} A sorted array of matching users, from highest score match
 61 	* to lowest score match.
 62 */
 63 function matchUsers(req, res, next, userId, matches) {
 64 	// Pass in objects of data for two users into match user function to get score match.
 65 	// Keep mapping of each user to score match (i.e. dictionary). Make sure that
 66 	// match is not attmepted for user with himself/herself.
 67 	let performMatch = function(curr_user, matchResults) {
 68 		currUser = curr_user[0];
 69 		database.searchUsers(database.routerProperties(req, res, next), {"userId": {$ne: userId}}, matchResults, function(users, matchResultsArray) {
 70 			for (let i = 0; i < users.length; i ++) {
 71 				let potentialMatchUser = users[i];
 72 
 73 				var info = matchUser(currUser, potentialMatchUser);
 74 
 75 				// info["event"] has the name, look up in database
 76 
 77 				// //searchActivities to get location for activity
 78 				// database.searchActivities(database.routerProperties(req, res, next), {"name": info["event"]}, function(activities) {
 79 				// 	console.log(activities[0]["locations"][0]);
 80 				// 	info["location"] = activities[0]["locations"][0];
 81 				// });
 82 
 83 				matchResultsArray.push(info);
 84 				// do something with result - or not, just keep appending to results and return
 85 
 86 			}
 87 			//console.log(JSON.stringify(matchResultsArray));
 88 			//sorting matchResultsArray
 89 			matchResultsArray.sort(function(a, b){
 90 			    return b.score-a.score;
 91 			});
 92 
 93 			//convert time to unix time
 94 
 95 			console.log(matchResultsArray);
 96 			//res.status(200).json(matchResultsArray);
 97 		});
 98 	};
 99 	database.searchUsers(database.routerProperties(req, res, next), {"userId": userId}, matches, performMatch);
100 }
101 
102 /**
103 	* Compute a match score for two users.
104 	* @param {Object} curr_user An object containing the user profile information for the logged
105 	* in user.
106 	* @param {Object} potential_match An object containing the user profile information for the the
107 	* match candidate for the logged in user.
108 	* @return {Object} An object conaining matched activity, event time, and score.
109 	* A score representing how good the match is between the logged in user and the potential match.
110 	* This match is based on availability, interest level, and skill level.
111 */
112 function matchUser(curr_user, potential_match) {
113 	var availability_match_score = getAvailabilityMatchScore(curr_user["availability"], potential_match["availability"]);
114 	var activity_match = getBestActivityMatch(curr_user["activities"], potential_match["activities"]);
115 	var event_time = getEventStartTime(curr_user["availability"], potential_match["availability"]);
116 	var total_score = 0;
117 	var match = {};
118 	const MILLISECONDS_IN_HOUR = 3600000;
119 	if (activity_match["interest_score"] != 0 && availability_match_score != 0) {
120 		total_score = activity_match["interest_score"] + activity_match["skill_score"] + availability_match_score;
121 	}
122 
123 	var unix_time = getEventDate(event_time[0], event_time[1]);
124 	match["event"] = activity_match["name"];
125 	match["user_id"] = curr_user["userId"];
126 	match["score"] = total_score;
127 	match["time"] = event_time;
128 	match["unix_time"] = unix_time; //startTime in db
129 	match["end_time"] = (unix_time + MILLISECONDS_IN_HOUR);  //endTime in db
130 	match["match_name"] = potential_match["name"];
131 	match["match_id"] = potential_match["userId"];
132 	match["match_email"] = potential_match["email"];
133 	match["location"] = getActivityLocations()[activity_match["name"]][0];
134 
135 	return match;
136 }
137 
138 /**
139 	* Returns the best time to meet up for two users.
140 	* This is done by computing the largest overlapping block of time between the two users, and
141 	* finding the start time of this largest overlapping block.
142 	* @param {Array<Array<<boolean>>} curr_user_availability A list of true/false availabilities
143 	* for the current user for each thirty-minute time slot on each day of the week.
144 	* @param {Array<Array<<boolean>>} potential_match_availability A list of true/false availabilities
145 	* for the potentialMatch for each thirty-minute time slot on each day of the week.
146 	* @return {Array<number>} A day and time slot for the start of the event.
147 */
148 function getEventStartTime(curr_user_availability, potential_match_availability) {
149 	var availability_match = getAvailabilityMatch(curr_user_availability, potential_match_availability);
150 	var day = availability_match["day"];
151 	var start_time_slot = availability_match["slot"];
152 	return [day, start_time_slot];
153 }
154 /**
155 	* Returns a score for the availability match between two users.
156 	* This is done by computing the largest overlapping block of time between the two users.
157 	* This score is capped at a maximum defined by a constant MAX_AVAILABILITY_SCORE.
158 	* @param {Array} curr_user_availability A list of true/false availabilities
159 	* for the current user for each thirty-minute time slot on each day of the week.
160 	* @param {Array} potential_match_availability A list of true/false availabilities
161 	* for the potentialMatch for each thirty-minute time slot on each day of the week.
162 	* @return {number} A score representing how good the time availability match is, where 0 represents
163 	* a failed match and MAX_AVAILABILITY_SCORE is the highest possible match score for this category.
164 */
165 function getAvailabilityMatchScore(curr_user_availability, potential_match_availability) {
166 	var availability_match = getAvailabilityMatch(curr_user_availability, potential_match_availability);
167 	var num_overlapping_periods = availability_match["periods"];
168 
169 	// Thirty minutes is two short for an activity, so a match requires at least an hour of matched times.
170 	// Furthermore, activity matches are not expected to have an activity for over three hours,
171 	// so a maximum cutoff is set for highest possible match.
172 	if (num_overlapping_periods <= 1) {
173 		num_overlapping_periods = 0;
174 	} else if (num_overlapping_periods > MAX_AVAILABILITY_SCORE) {
175 		num_overlapping_periods = MAX_AVAILABILITY_SCORE;
176 	}
177 	return computeNormalizedScore(num_overlapping_periods, MAX_AVAILABILITY_SCORE);
178 }
179 
180 
181 /**
182 	* Returns maximum number of consecutive overlapping half-hours for two users. This could
183 	* span over multiple days of the week, as long as it is a consecutive chunk of hours (i.e.
184 	* 11:00 PM on Monday until 1 AM on Tuesday).
185 	* @param {Array} curr_user_availability A list of true/false availabilities
186 	* for the current user for each thirty-minute time slot on each day of the week.
187 	* @param {Array} potential_match_availability A list of true/false availabilities
188 	* for the potentialMatch for each thirty-minute time slot on each day of the week.
189 	* @return {Object} The day, start time slot, and maximum number of overlapping consecutive half-hours
190 	* between the two users.
191 */
192 function getAvailabilityMatch(curr_user_availability, potential_match_availability) {
193 	var max_sequence = 0;
194 	var curr_sequence = 0;
195 
196 	var curr_start_day = 0;
197 	var curr_start_time_slot = 0;
198 	var max_start_day = 0;
199 	var max_start_time_slot = 0;
200 	var already_started = false;
201 	var availability_match = {};
202 
203 	for (var i = 0; i < curr_user_availability.length; i++) {
204 		for (var j = 0; j < curr_user_availability[i].length; j++) {
205 			if (curr_user_availability[i][j] && potential_match_availability[i][j]) {
206 				if (!already_started) {
207 					curr_start_time_slot = j;
208 					curr_start_day = i;
209 					already_started = true;
210 				}
211 				curr_sequence++;
212 				if (curr_sequence > max_sequence) {
213 					max_start_day = curr_start_day;
214 					max_start_time_slot = curr_start_time_slot;
215 					max_sequence = curr_sequence;
216 				}
217 			}
218 		 	else {
219 				curr_sequence = 0;
220 				already_started	= false;
221 			}
222 		}
223 	}
224 	availability_match["day"] = max_start_day;
225 	availability_match["slot"] = max_start_time_slot;
226 	availability_match["periods"] = max_sequence;
227 	return availability_match;
228 }
229 
230 /**
231 	* Returns an object containing the best activity match between two users and the
232 	* interest and skill level scores for that activity.
233 	* @param {Array} curr_user_activities A list of activities for the current user with
234 	* interest and skill level scores.
235 	* @param {Array} potential_match_activities A list of activities for the potential
236 	* match user with interest and skill level scores.
237 	* @return {Object} The matched activity yielding the highest score for the current user.
238 	* The object contains a field for the activity name, a field for the interest match score, and
239 	* a field for the skill match score.
240 */
241 function getBestActivityMatch(curr_user_activities, potential_match_activities) {
242 	return getBestActivityMatchFromList(generateActivityMatches(curr_user_activities, potential_match_activities));
243 }
244 
245 /**
246 	* Generates a list of activity matches with an interest match and a skill match for each activity.
247 	* @param {Array} curr_user_activities A list of activities for the current user with
248 	* interest and skill level scores.
249 	* @param {Array} potential_match_activities A list of activities for the potential
250 	* match user with interest and skill level scores.
251 	* @return {Array} List of objects with the potential match activities. Each object has
252 	* the name of the potential match activity, a skill score, and an interest score.
253 */
254 function generateActivityMatches(curr_user_activities, potential_match_activities) {
255 	var activity_matches = [];
256 
257 	// Create a list with all activities with match score temporarily as 0.
258 	for (var i = 0; i < curr_user_activities.length; i++) {
259 		var activity_match = {};
260 		activity_match["name"] = curr_user_activities[i]["name"];
261 		activity_match["skill_score"] = 0;
262 		activity_match["interest_score"] = 0;
263 		activity_matches.push(activity_match);
264 	}
265 
266 	// Fill array of matched activities.
267 	for (var i = 0; i < curr_user_activities.length; i++) {
268 		for (var j = 0; j < potential_match_activities.length; j++) {
269 			if (curr_user_activities[i]["name"] !== potential_match_activities[j]["name"]) {
270 				continue;
271 			}
272 			activity_matches[i]["skill_score"] = computeSkillMatch(curr_user_activities[i]["skill"], potential_match_activities[j]["skill"]);
273 			activity_matches[i]["interest_score"] = computeInterestMatch(curr_user_activities[i]["interest"], potential_match_activities[j]["interest"]);
274 		}
275 	}
276 	return activity_matches;
277 }
278 
279 /**
280 	* Returns an object containing the best activity match between two users and the
281 	* interest and skill level scores for that activity, given a list of activities and scores.
282 	* The best match is defined as having the highest skill score and interest score combined,
283 	* disregarding activities with an interest match of 0.
284 	* @param {Array} activity_matches Current activity matches with name, skill score, and interest
285 	* score for each activity.
286 	* @return {Object} The matched activity yielding the highest score for the current user.
287 	* The object contains a field for the activity name, a field for the interest match score, and
288 	* a field for the skill match score.
289 */
290 function getBestActivityMatchFromList(activity_matches) {
291 	var best_activity_match = {};
292 	// Default initialization so that keys are present in the object. When using the score in other functions,
293 	// handle exceptional case of having 0 interest match score (meaning no match).
294 	best_activity_match["name"] = "Lifting";
295 	best_activity_match["skill_score"] = 0;
296 	best_activity_match["interest_score"] = 0;
297 	var max_score = 0;
298 
299 	for (var i = 0; i < activity_matches.length; i++) {
300 		if (activity_matches[i]["interest_score"] == 0) {
301 			continue;
302 		} else {
303 			var curr_score = activity_matches[i]["interest_score"] + activity_matches[i]["skill_score"];
304 			if (curr_score > max_score) {
305 				max_score = curr_score;
306 				best_activity_match["name"] = activity_matches[i]["name"];
307 				best_activity_match["skill_score"] = activity_matches[i]["skill_score"];
308 				best_activity_match["interest_score"] = activity_matches[i]["interest_score"];
309 			}
310 		}
311 	}
312 	return best_activity_match;
313 }
314 
315 /**
316 	* Returns a score for interest match. The computation most highly weights the current
317 	* user's interests, and then weights the other user's interest level with secondary importance.
318 	* Note that an interest level of 1 is the lowest possible interest for an activity, so this
319 	* automatically results in a score of 0 for the interest match.
320 	* @param {number} curr_user_interest Interest level for current user
321 	* @param {number} potential_match_interest Interest level for other user
322 	* @return {number} A normalized score representing the interest match between two users.
323 */
324 function computeInterestMatch(curr_user_interest, potential_match_interest) {
325 	var unnormalized_score = curr_user_interest + 0.5*(potential_match_interest);
326 	if (curr_user_interest == 1 || potential_match_interest == 1) {
327 		unnormalized_score = 0;
328 	}
329 	return computeNormalizedScore(unnormalized_score, MAX_INTEREST_SCORE);
330 }
331 
332 /**
333 	* Returns a score for skill match. The score is high if the two users have similar skill levels.
334 	* @param {number} curr_user_skill Skill level of current user
335 	* @param {number} potential_match_skill Skill level of potential match
336 	* @return {number} A normalized score representing the skill level match between two users.
337 */
338 function computeSkillMatch(curr_user_skill, potential_match_skill) {
339 	var unnormalized_score = MAX_SKILL_SCORE - Math.abs(curr_user_skill-potential_match_skill)
340 	return computeNormalizedScore(unnormalized_score, MAX_SKILL_SCORE);
341 }
342 
343 /**
344 	* Normalizes the score for the three categories, so all three types of scores
345 	* have the same maximum and minimum values.
346 	* @param {number} curr_score Current score for that category
347 	* @param {number} curr_max Maximum score for that cateogry
348 	* @return {number} A normalized score representing the match score between two users for one category.
349 */
350 function computeNormalizedScore(curr_score, curr_max) {
351 	return curr_score*1.0/curr_max*NORMALIZED_BASE;
352 }
353 
354 /**
355 	* Return the time of the event in Unix Epoch time format.
356 	* @param {number} day Day in database for the event, where 0 is
357 	* Monday and 6 is Sunday
358 	* @param {number} time_slot The timeslot in the database for the
359 	* event, where 0 means midnight, one means 12:30 AM, etc.
360 */
361 function getEventDate(day, time_slot) {
362 	// Convert days in database to match with day of the week in moment.js,
363 	// which has Sunday as 0 and Saturday as 6. In the database, the index
364 	// of 0 corresponds to Monday instead, which is why we need this line.
365 	if (day === 6) {
366 		day = 0;
367 	} else {
368 		day = day + 1;
369 	}
370 
371 	var event_date = moment().day(day);
372 	event_date = event_date.toDate();
373 	event_date.setHours(time_slot/2);
374 	event_date.setMinutes((time_slot%2===0)?0:30);
375 	event_date.setSeconds(0);
376 	event_date.setMilliseconds(0);
377 	return Date.parse(event_date);
378 }
379 
380 /**
381 	* Return the locations of activities.
382 	* @return {Object}
383 */
384 function getActivityLocations() {
385 	var activity_locations = {"Lifting": ["Bfit", "Wooden"], "Running": ["Drake Stadium", "Perimeter run"],
386 						"Swimming":["Sunset Rec", "SAC", "North Pool"], "Basketball": ["Hitch Courts", "Wooden"],
387 						"Soccer": ["IM Field"], "Tennis": ["LA Tennis Courts"], "Volleyball": ["Sunset Rec"],
388 						"Climbing": ["Wooden"], "Squash": ["Wooden"], "Frisbee": ["IM Field"]};
389 
390 	return activity_locations;
391 }
392 
393 module.exports = router;
394 
395 //UNCOMMENT BELOW FOR MATCHING FUNCTION TESTING PURPOSES (and comment out above 'module.exports = router')
396 // module.exports = {
397 // 	MAX_AVAILABILITY_SCORE:MAX_AVAILABILITY_SCORE,
398 // 	MAX_INTEREST_SCORE:MAX_INTEREST_SCORE,
399 // 	MAX_SKILL_SCORE:MAX_SKILL_SCORE,
400 //  	NORMALIZED_BASE:NORMALIZED_BASE,
401 // 	DAYS:DAYS,
402 // 	TIME_SLOTS:TIME_SLOTS,
403 // 	matchUsers:matchUsers,
404 // 	matchUser:matchUser,
405 // 	getAvailabilityMatchScore:getAvailabilityMatchScore,
406 // 	getAvailabilityMatch:getAvailabilityMatch,
407 // 	getBestActivityMatch:getBestActivityMatch,
408 // 	router
409 // };
410