using Models;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text;
using System.Text.Json;
namespace LocalServices.Data
{
///
/// Database implementation with catastrophic performances.
/// This database implementation persists data in json and will save all the data in their files for each mutable requests.
///
public class CatastrophicPerformancesDatabase : IDatabase
{
private static readonly string RECIPES_FILENAME = "recipes_data.xml";
private static readonly string USERS_FILENAME = "users_data.xml";
private static readonly string ACCOUNTS_FILENAME = "accounts_data.xml";
private readonly Dictionary recipesData;
private readonly Dictionary usersData;
private readonly Dictionary accountsData;
private readonly string dbPath;
public CatastrophicPerformancesDatabase(string folderPath)
{
dbPath = folderPath;
if (!Directory.Exists(folderPath))
Directory.CreateDirectory(folderPath);
usersData = Load(USERS_FILENAME);
recipesData = Load(RECIPES_FILENAME);
accountsData = Load(ACCOUNTS_FILENAME);
}
public bool IsEmpty()
{
return recipesData.Count == 0 && usersData.Count == 0 && accountsData.Count == 0;
}
public Account? GetAccount(string email, string passwordHash)
{
if (!accountsData.TryGetValue(email, out AccountData? data))
return null;
if (data.PasswordHash != passwordHash) return null;
return new Account(usersData[data.UserId].User, data.Email);
}
public void InsertAccount(Account account, string passwordHash)
{
accountsData[account.Email] = new AccountData(account.User.Id, account.Email, passwordHash);
Save(ACCOUNTS_FILENAME, accountsData);
InsertUser(account.User);
}
public Recipe? GetRecipe(Guid id)
{
if (recipesData.TryGetValue(id, out RecipeData? data))
return ConvertRecipeDataToRecipe(data);
return null;
}
public RecipeRate GetRecipeRate(Guid user, Guid recipe)
{
return usersData[user].Rates[recipe];
}
public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount)
{
usersData[userId].RecipesList[recipeId] = persAmount;
Save(USERS_FILENAME, usersData);
}
public void RemoveFromUserList(Guid userId, Guid recipeId)
{
usersData[userId].RecipesList.Remove(recipeId);
Save(USERS_FILENAME, usersData);
}
public void InsertRecipe(Recipe recipe)
{
recipesData[recipe.Info.Id] = new RecipeData(recipe.Info, recipe.Owner.Id, recipe.Ingredients, recipe.Steps);
Save(RECIPES_FILENAME, recipesData);
}
public void InsertUser(User user)
{
usersData[user.Id] = new UserData(user, new Dictionary(), new Dictionary());
Save(USERS_FILENAME, usersData);
}
public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate)
{
usersData[userId].Rates[recipeId] = rate;
Save(USERS_FILENAME, usersData);
}
public void RemoveRecipe(Guid id)
{
recipesData.Remove(id);
Save(RECIPES_FILENAME, recipesData);
}
public ImmutableList ListAllRecipes()
{
return recipesData.Values.ToImmutableList().ConvertAll(ConvertRecipeDataToRecipe);
}
public ImmutableDictionary ListRatesOf(Guid user)
{
return usersData[user].Rates.ToImmutableDictionary();
}
public ImmutableDictionary GetRecipeListOf(Guid user)
{
return usersData[user].RecipesList.ToImmutableDictionary();
}
private Recipe ConvertRecipeDataToRecipe(RecipeData rd)
{
var owner = usersData[rd.OwnerID].User;
return new Recipe(rd.Info, owner, rd.Ingredients, rd.Steps);
}
private Dictionary Load(string fileName)
{
var file = dbPath + "/" + fileName;
var fileInfo = new FileInfo(file);
if (!fileInfo.Exists)
fileInfo.Create();
if (fileInfo.Length == 0)
return new Dictionary(); //file is empty thus there is nothing to deserialize
string text = File.ReadAllText(file);
return JsonSerializer.Deserialize>(text);
}
private async void Save(string fileName, Dictionary dict)
{
string json = JsonSerializer.Serialize(dict);
using (var stream = WaitForFile(dbPath + "/" + fileName, FileMode.Open, FileAccess.Write, FileShare.Write))
{
var bytes = Encoding.ASCII.GetBytes(json);
await stream.WriteAsync(bytes, 0, bytes.Length);
}
}
// This is a workaround function to wait for a file to be released before opening it.
// This was to fix the Save method that used to throw sometimes as the file were oftenly being scanned by the androids' antivirus.
// Simply wait until the file is released and return it. This function will never return until
private static FileStream WaitForFile(string fullPath, FileMode mode, FileAccess access, FileShare share)
{
for (int attempt = 0 ; attempt < 40; attempt++)
{
FileStream? fs = null;
try
{
fs = new FileStream(fullPath, mode, access, share);
return fs;
}
catch (FileNotFoundException e)
{
throw e;
}
catch (IOException e)
{
Debug.WriteLine(e.Message + " in thread " + Thread.CurrentThread.Name + " " + Thread.CurrentThread.ManagedThreadId);
if (fs != null)
fs.Dispose();
Thread.Sleep(200);
}
}
throw new TimeoutException("Could not access file '" + fullPath + "', maximum attempts reached.");
}
}
}