using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Firebase;
using Firebase.Auth;
using Firebase.Database;
using Firebase.Extensions;
using UnityEngine;
///
/// the general managing class for handling communication with the firebase backend
/// (to be initialised by GameManager)
///
public class Backend
{
///
/// enum for the result of the authentication process
///
public enum AuthenticationResult
{
Ok,
AlreadyAuthenticated,
NonExistentUser,
AlreadyExistingUser,
InvalidEmail,
InvalidCredentials,
GenericError
}
///
/// generic enum for the result of a database transaction
///
public enum DatabaseTransactionResult
{
Ok,
Unauthenticated,
Error
}
///
/// enum for the connection status of the firebase backend
///
public enum FirebaseConnectionStatus
{
NotConnected,
Connected,
UpdateRequired, // "a required system component is out of date"
Updating, // "a required system component is updating, retrying in a bit..."
ExternalError, // "a system component is disabled, invalid, missing, or permissions are insufficient"
InternalError // "an unknown error occurred"
}
///
/// callback functions to be invoked when the user signs in
///
private readonly List> _onSignInCallback = new();
///
/// callback functions to be invoked when the user signs out
///
private readonly List> _onSignOutCallback = new();
///
/// the firebase authentication object
///
private FirebaseAuth _auth;
///
/// the firebase database reference
///
private DatabaseReference _db;
///
/// the current user object, if authenticated
///
private FirebaseUser _user;
///
/// the current user's username, if authenticated
///
private string _username;
///
/// whether the backend is connected to the firebase backend
///
public FirebaseConnectionStatus Status = FirebaseConnectionStatus.NotConnected;
///
/// variable initialisation function
///
public void Initialise()
{
FirebaseApp.CheckAndFixDependenciesAsync().ContinueWith(task =>
{
// cher is this robust enough
switch (task.Result)
{
case DependencyStatus.Available:
_auth = FirebaseAuth.GetAuth(FirebaseApp.DefaultInstance);
_auth.StateChanged += AuthStateChanged;
_db = FirebaseDatabase.DefaultInstance.RootReference;
Status = FirebaseConnectionStatus.Connected;
break;
case DependencyStatus.UnavailableDisabled:
case DependencyStatus.UnavailableInvalid:
case DependencyStatus.UnavilableMissing:
case DependencyStatus.UnavailablePermission:
Status = FirebaseConnectionStatus.ExternalError;
break;
case DependencyStatus.UnavailableUpdating:
Status = FirebaseConnectionStatus.Updating;
RetryInitialiseAfterDelay();
break;
case DependencyStatus.UnavailableUpdaterequired:
Status = FirebaseConnectionStatus.UpdateRequired;
break;
case DependencyStatus.UnavailableOther:
default:
Status = FirebaseConnectionStatus.InternalError;
Debug.LogError("firebase ??? blew up or something," + task.Result);
break;
}
Debug.Log("firebase status is" + Status);
});
}
///
/// async function to retry initialisation after a delay
///
private async void RetryInitialiseAfterDelay()
{
await Task.Delay(TimeSpan.FromSeconds(10));
Initialise();
}
///
/// cleanup function
///
public void Cleanup()
{
SignOutUser();
_auth.StateChanged -= AuthStateChanged;
_auth = null;
}
///
/// function to handle the authentication state change event
///
/// the object that triggered the event
/// the event arguments
private void AuthStateChanged(object sender, EventArgs eventArgs)
{
// if the user hasn't changed, do nothing
if (_auth.CurrentUser == _user) return;
// if the user has changed, check if they've signed in or out
var signedIn = _user != _auth.CurrentUser && _auth.CurrentUser != null;
// if we're not signed in, but we still hold _user locally, we've signed out
if (!signedIn && _user != null)
{
Debug.Log($"signed out successfully as {_user.UserId}");
foreach (var callback in _onSignOutCallback)
callback(_user);
}
// they have signed in, update _user
_user = _auth.CurrentUser;
if (signedIn)
{
Debug.Log($"signed in successfully as {_user.UserId}");
RetrieveUsername();
foreach (var callback in _onSignInCallback) callback(_user);
}
}
///
/// function to register a callback for when the user signs in
///
/// callback function that takes in a FirebaseUser argument
public void RegisterOnSignInCallback(Action callback)
{
_onSignInCallback.Add(callback);
}
///
/// function to register a callback for when the user signs out
///
/// callback function that takes in a FirebaseUser argument
public void RegisterOnSignOutCallback(Action callback)
{
_onSignOutCallback.Add(callback);
}
///
/// abstraction function to authenticate the user
///
/// email string
/// user raw password string
/// callback function that takes in an AuthenticationResult argument
/// whether to treat authentication as registration
/// username string if registering
public void AuthenticateUser(
string email,
string password,
Action callback,
bool registerUser = false,
string registeringUsername = "")
{
if (GameManager.Instance.Backend.GetUser() != null)
{
callback(AuthenticationResult.AlreadyAuthenticated);
return;
}
if (registerUser)
{
// register user
_auth.CreateUserWithEmailAndPasswordAsync(email, password)
.ContinueWithOnMainThread(createTask =>
{
if (createTask.IsCompletedSuccessfully)
{
// store username
_db.Child("users")
.Child(_user.UserId)
.Child("username")
.SetValueAsync(registeringUsername)
.ContinueWithOnMainThread(setUsernameTask =>
{
if (setUsernameTask.IsCompletedSuccessfully)
{
_username = registeringUsername;
callback(AuthenticationResult.Ok);
}
else
{
Debug.LogError(setUsernameTask.Exception);
callback(AuthenticationResult.GenericError);
}
});
return;
}
if (createTask.Exception?.InnerException == null)
{
callback(AuthenticationResult.GenericError);
return;
}
var error = (AuthError)((FirebaseException)createTask.Exception.InnerException).ErrorCode;
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (error)
{
case AuthError.UserNotFound:
callback(AuthenticationResult.NonExistentUser);
return;
case AuthError.InvalidEmail:
callback(AuthenticationResult.InvalidEmail);
return;
case AuthError.WeakPassword:
case AuthError.InvalidCredential:
callback(AuthenticationResult.InvalidCredentials);
return;
case AuthError.AccountExistsWithDifferentCredentials:
case AuthError.EmailAlreadyInUse:
callback(AuthenticationResult.AlreadyExistingUser);
return;
case AuthError.Failure:
default:
Debug.LogError(error);
Debug.LogError(createTask.Exception);
callback(AuthenticationResult.GenericError);
break;
}
});
return;
}
_auth.SignInWithEmailAndPasswordAsync(email, password)
.ContinueWithOnMainThread(signInTask =>
{
if (signInTask.IsCompletedSuccessfully)
{
callback(AuthenticationResult.Ok);
return;
}
if (signInTask.Exception?.InnerException == null)
{
callback(AuthenticationResult.GenericError);
return;
}
var error = (AuthError)((FirebaseException)signInTask.Exception.InnerException).ErrorCode;
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (error)
{
case AuthError.UserNotFound:
callback(AuthenticationResult.NonExistentUser);
return;
case AuthError.InvalidEmail:
callback(AuthenticationResult.InvalidEmail);
return;
case AuthError.WeakPassword:
case AuthError.InvalidCredential:
callback(AuthenticationResult.InvalidCredentials);
return;
case AuthError.AccountExistsWithDifferentCredentials:
case AuthError.EmailAlreadyInUse:
callback(AuthenticationResult.AlreadyExistingUser);
return;
case AuthError.Failure:
default:
Debug.LogError(error);
Debug.LogError(signInTask.Exception);
callback(AuthenticationResult.GenericError);
break;
}
});
}
///
/// function to retrieve the user's username from the database
///
private void RetrieveUsername()
{
if (!Status.Equals(FirebaseConnectionStatus.Connected)) return;
_db.Child("users").Child(_user.UserId).Child("username").GetValueAsync().ContinueWith(task =>
{
if (task.IsCompletedSuccessfully)
{
_username = task.Result.Value.ToString();
Debug.Log($"our username is {_username}");
}
else
{
_username = "???";
Debug.LogError("failed to get username");
}
});
}
///
/// abstraction function to retrieve the user
///
/// the firebase user object
public FirebaseUser GetUser()
{
return _user;
}
public string GetUsername()
{
return _username;
}
///
/// abstraction function to sign out the user
///
public void SignOutUser()
{
_auth.SignOut();
}
///
/// abstraction function to submit a play to the database
///
/// play data
/// callback function that takes in one DatabaseTransactionResult argument
public void SubmitPlay(
PlayData playData,
Action callback)
{
throw new NotImplementedException();
}
///
/// abstraction function to get and calculate the user's rating from the database
/// calculation is done locally, call UpdateUserRating to update the user's rating in the database
///
/// callback function that takes in a DatabaseTransactionResult and float (user rating) argument
public void CalculateUserRating(
Action callback)
{
throw new NotImplementedException();
}
///
/// abstraction function to update the user's rating in the database
///
/// new user rating value as a float
/// callback function that takes in one DatabaseTransactionResult argument
public void UpdateUserRating(
float newRating,
Action callback)
{
throw new NotImplementedException();
}
///
/// abstraction function to get the leaderboard from the database
///
///
/// callback function that takes in a DatabaseTransactionResult and LeaderboardEntry[] argument
///
public void GetLeaderboard(
Action callback)
{
throw new NotImplementedException();
}
// private void Playground()
// {
// // update username
// _db.Child("users").Child(_user.UserId).Child("username").SetValueAsync("newusername").ContinueWith(task => { });
//
// // update email
// _user.SendEmailVerificationBeforeUpdatingEmailAsync("name@example.com").ContinueWith(task => { });
//
// // update password
// _user.UpdatePasswordAsync("password").ContinueWith(task => { });
//
// // reset password
// _auth.SendPasswordResetEmailAsync("name@example.com").ContinueWith(task => { });
// }
///
/// struct for a leaderboard entry
///
public struct LeaderboardEntry
{
public string Username;
public float Rating;
public int PlayCount;
public LeaderboardEntry(string username, float rating, int playCount)
{
Username = username;
Rating = rating;
PlayCount = playCount;
}
}
///
/// struct for play data
///
public struct PlayData
{
public int RoundsPlayed;
public float AverageOverallAccuracy;
public float AverageLightnessAccuracy;
public float AverageChromaAccuracy;
public float AverageHueAccuracy;
}
}