app/src/main/java/ch/epfl/sdp/blindly/user/User.kt
package ch.epfl.sdp.blindly.user
import android.util.Log
import ch.epfl.sdp.blindly.utils.Date
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.Exclude
import com.google.firebase.firestore.ktx.getField
import kotlinx.serialization.Serializable
private const val SIZE_OF_LOCATION_LIST = 2
private const val SIZE_OF_AGE_RANGE_LIST = 2
const val USERNAME = "username"
const val LOCATION = "location"
const val BIRTHDAY = "birthday"
const val GENDER = "gender"
const val SEXUAL_ORIENTATIONS = "sexualOrientations"
const val SHOW_ME = "showMe"
const val PASSIONS = "passions"
const val RADIUS = "radius"
const val MATCHES = "matches"
const val LIKES = "likes"
const val DISLIKES = "dislikes"
const val RECORDING_PATH = "recordingPath"
const val AGE_RANGE = "ageRange"
const val DELETED = "deleted"
/**
* A class to represent a User
*/
@Serializable
data class User private constructor(
@Exclude var uid: String?,
var username: String?,
var location: List<Double>?,
var birthday: String?,
var gender: String?,
var sexualOrientations: List<String>?,
var showMe: String?,
var passions: List<String>?,
var radius: Int?,
var matches: List<String>?,
var likes: List<String>?,
var dislikes: List<String>?,
var recordingPath: String?,
var ageRange: List<Int>?,
var deleted: Boolean
) {
/**
* A builder used to partially initialize a user during the profile_setup activities
* Made serializable so that it can be put in a bundle and passed as extra
*
* @property uid the uid of the User
* @property username the username of the User
* @property location the location of the User
* @property birthday the birthday of the User
* @property gender the gender of the User
* @property sexualOrientations a List<String> containing the
* sexual orientations of the User
* @property showMe the show_me of the User
* @property passions a List<String> containing the
* passions of the User
* @property radius the radius in which the User want the matching algorithm to look in
* @property matches a List<User> containing the Users the User has a match with
* @property recordingPath the path to the recording of the user
* @property ageRange the ageRange of the User
*/
@Serializable
data class Builder(
var uid: String? = null,
var username: String? = null,
var location: List<Double>? = null,
var birthday: String? = null,
var gender: String? = null,
var sexualOrientations: List<String> = listOf(),
var showMe: String? = null,
var passions: List<String> = listOf(),
var radius: Int? = null,
var matches: List<String> = listOf(),
var likes: List<String> = listOf(),
var dislikes: List<String>? = listOf(),
var recordingPath: String? = null,
var ageRange: List<Int> = listOf()
) {
/**
* Set the uid in the UserBuilder
*
* @param uid the username of the User
*/
fun setUid(uid: String) = apply {
this.uid = uid
}
/**
* Set the username in the UserBuilder
*
* @param username the username of the User
*/
fun setUsername(username: String) = apply {
this.username = username
}
/**
* Set the location in the UserBuilder
*
* @param location the location of the User
*/
fun setLocation(location: List<Double>) = apply {
if (location.size == SIZE_OF_LOCATION_LIST)
this.location = location
else
throw IllegalArgumentException(
"Expected location.size to be " +
"$SIZE_OF_LOCATION_LIST but got: ${location.size} instead"
)
}
/**
* Set the birthday in the UserBuidler
*
* @param birthday the birthday of the User
*/
fun setBirthday(birthday: String) = apply {
this.birthday = birthday
}
/**
* Set the gender in the UserBuilder
*
* @param gender the gender of the User
*/
fun setGender(gender: String) = apply {
this.gender = gender
}
/**
* Set the sexual orientations in the UserBuilder
*
* @param sexualOrientations the sexual orientations of the User
*/
fun setSexualOrientations(sexualOrientations: List<String>) = apply {
this.sexualOrientations = sexualOrientations
}
/**
* Set the show me in the UserBuidler
*
* @param showMe the show me of the User
*/
fun setShowMe(showMe: String) = apply {
this.showMe = showMe
}
/**
* Set the passions in the UserBuilder
*
* @param passions the passions of the User
*/
fun setPassions(passions: List<String>) = apply {
this.passions = passions
}
/**
* Set the radius in the UserBuilder
*
* @param radius the radius of the User
*/
fun setRadius(radius: Int) = apply {
this.radius = radius
}
/**
* Set the matches in the UserBuilder. Other users are represented by their UID.
*
* @param matches the matches of the User
*/
fun setMatches(matches: List<String>) = apply {
this.matches = matches
}
/**
* Set the likes in the UserBuilder
*
* @param likes the likes of the User. Other users are represented by their UID.
*/
fun setLikes(likes: List<String>) = apply {
this.likes = likes
}
/**
* Set the dislikes in the UserBuilder
*
* @param dislikes the dislikes of the User. Other users are represented by their UID.
*/
fun setDislikes(dislikes: List<String>) = apply {
this.dislikes = dislikes
}
/**
* Set the recording path in the UserBuilder
*
* @param recordingPath the path to the recording
*/
fun setRecordingPath(recordingPath: String) = apply {
this.recordingPath = recordingPath
}
/**
* Set the age range in the UserBuilder
*
* @param ageRange a list containing the age range :
* ageRange[0] = minAge,
* ageRange[1] = maxAge
*/
fun setAgeRange(ageRange: List<Int>) = apply {
if (ageRange.size == SIZE_OF_AGE_RANGE_LIST)
this.ageRange = ageRange
else
throw IllegalArgumentException(
"Expected ageRange.size to be " +
"$SIZE_OF_AGE_RANGE_LIST but got: ${ageRange.size} instead"
)
}
/**
* Build a User from the UserBuilder parameters, by default a User cannot be deleted at
* creation and the reportingUsers list is empty
*
* @return a User
*/
fun build(): User {
return User(
uid,
username,
location,
birthday,
gender,
sexualOrientations,
showMe,
passions,
radius,
matches,
likes,
dislikes,
recordingPath,
ageRange,
false
)
}
}
companion object {
private const val TAG = "User"
/**
* Converts the document received from firestore back into a User
*
* @return a User
*/
fun DocumentSnapshot.toUser(): User? {
try {
val uid = id
val username = getString(USERNAME)!!
val location = get(LOCATION) as? List<Double>
val birthday = getString(BIRTHDAY)!!
val gender = getString(GENDER)!!
val sexualOrientations = get(SEXUAL_ORIENTATIONS) as? List<String>
val showMe = getString(SHOW_ME)!!
val passions = get(PASSIONS) as? List<String>
val radius = getField<Int>(RADIUS)!!
val matches = get(MATCHES) as? List<String>
val likes = get(LIKES) as? List<String>
val dislikes = get(DISLIKES) as? List<String>
val ageRange = get(AGE_RANGE) as? List<Long>
val recordingPath = getString(RECORDING_PATH)!!
val deleted = getField<Boolean>(DELETED)!!
return User(
uid,
username,
location,
birthday,
gender,
sexualOrientations,
showMe,
passions,
radius,
matches,
likes,
dislikes,
recordingPath,
listOf(
ageRange!![0].toInt(),
ageRange[1].toInt()
), //Numbers on Firestore are Long, so we need to cast back to Int
deleted
)
} catch (e: Exception) {
Log.e(TAG, "Error converting user profile for id $id", e)
return null
}
}
/**
* Compute the age of the user
*
* @param user the user whose age we want to compute
* @return an Int containing the age of the User
*/
fun getUserAge(user: User?): Int? {
val birthday = user?.birthday
val date = Date.getDate(birthday)
if (date != null)
return date.getAge()
return null
}
/**
* Update the field of a User
*
* @param T either a String, an Int or a List<*>
* @param user the user to update
* @param field the field to update
* @param newValue the new value
* @return the updated user
*/
fun <T> updateUser(user: User, field: String, newValue: T): User {
when (field) {
USERNAME -> {
assertIsString(newValue)
user.username = newValue as String
}
LOCATION -> {
assertIsLocation(newValue)
user.location = newValue as List<Double>
}
BIRTHDAY -> {
assertIsString(newValue)
user.birthday = newValue as String
}
GENDER -> {
assertIsString(newValue)
user.gender = newValue as String
}
SEXUAL_ORIENTATIONS -> {
assertIsListOfString(newValue)
user.sexualOrientations = newValue as List<String>
}
SHOW_ME -> {
assertIsString(newValue)
user.showMe = newValue as String
}
PASSIONS -> {
assertIsListOfString(newValue)
user.passions = newValue as List<String>
}
RADIUS -> {
assertIsInt(newValue)
user.radius = newValue as Int
}
MATCHES -> {
val newMatches = newValue as List<String>
if (newMatches.isNotEmpty()) { //newMatches is a non empty list, the type must be a string
assertIsListOfString(newValue)
}
user.matches = newMatches
}
LIKES -> {
val newLikes = newValue as List<String>
if (newLikes.isNotEmpty()) { // newLikes is a non empty list, the type must be a string
assertIsListOfString(newValue)
}
user.likes = newLikes
}
DISLIKES -> {
val newDislikes = newValue as List<String>
if (newDislikes.isNotEmpty()) { // newLikes is a non empty list, the type must be a string
assertIsListOfString(newValue)
}
user.dislikes = newDislikes
}
RECORDING_PATH -> {
assertIsString(newValue)
user.recordingPath = newValue as String
}
AGE_RANGE -> {
assertIsAgeRange(newValue)
user.ageRange = newValue as List<Int>
}
DELETED -> {
assertIsBoolean(newValue)
user.deleted = newValue as Boolean
}
else -> {
throw java.lang.IllegalArgumentException("Incorrect field")
}
}
return user
}
private fun <T> assertIsListOfString(newValue: T) {
if (newValue !is List<*>)
throw java.lang.IllegalArgumentException("Expected newValue to be a List<String>")
if (newValue[0] !is String)
throw java.lang.IllegalArgumentException("Expected newValue to be a List<String>")
}
private fun <T> assertIsAgeRange(newValue: T) {
if (newValue !is List<*>)
throw java.lang.IllegalArgumentException("Expected newValue to be a List<Int>")
if (newValue[0] !is Int)
throw java.lang.IllegalArgumentException("Expected newValue to be a List<Int>")
if (newValue.size != SIZE_OF_AGE_RANGE_LIST)
throw IllegalArgumentException(
"Expected ageRange.size to be " +
"$SIZE_OF_AGE_RANGE_LIST but got: ${newValue.size} instead"
)
}
private fun <T> assertIsLocation(newValue: T) {
if (newValue !is List<*>)
throw java.lang.IllegalArgumentException("Expected newValue to be a List<Double>")
if (newValue[0] !is Double)
throw java.lang.IllegalArgumentException("Expected newValue to be a List<Double>")
if (newValue.size != SIZE_OF_LOCATION_LIST)
throw IllegalArgumentException(
"Expected location.size to be " +
"$SIZE_OF_LOCATION_LIST but got: ${newValue.size} instead"
)
}
private fun <T> assertIsString(newValue: T) {
if (newValue !is String)
throw java.lang.IllegalArgumentException("Expected newValue to be a String")
}
private fun <T> assertIsInt(newValue: T) {
if (newValue !is Int)
throw java.lang.IllegalArgumentException("Expected newValue to be an Int")
}
private fun <T> assertIsBoolean(newValue: T) {
if (newValue !is Boolean)
throw java.lang.IllegalArgumentException("Expected newValue to be a Boolean")
}
}
/* This is for debugging in tests, you're free to modify it if you need to (but don't forget
to modify the test results in UserUnitTest too) */
override fun toString(): String {
return "$username"
}
}