// ========================================================================
//
// Copyright (C) 2016-2017 MARC CHEVALDONNE
//                         marc.chevaldonne.free.fr
//
// Module        : Program.cs
// Author        : Marc Chevaldonné
// Creation date : 2016-11-13
//
// ========================================================================

using static System.Console;
using System.Linq;
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace ex_042_020_EF_concurrency_customDbProp
{
    /// <summary>
    /// Cet exemple simule des mises à jours concurrentes de données dans la table.
    /// Contrairement à l'exemple 042_017, il montre comment il est possible de gérer facilement ces insertions concurrentes avec EntityFramework.
    /// Contrairement aux exemples 042_018 et 042_019, il ne laisse pas la priorité à la base de données ou au client, mais demande à l'utilisateur de choisir pour chaque propriété, laquelle il doit prendre.
    /// Cette méthode utilise les données originales, courantes et en base (cf. exemple 042_016).
    /// 
    /// Deux méthodes sont proposées pour cela :
    /// - (METHODE 1) : la première consiste à dédier une colonne pour les vérifications (update et delete) qu'on appelle généralement RowVersion et qu'on décore avec l'attribut Timestamp (cf. classe Nounours)
    /// - la seconde permet de ne pas rajouter de colonne spécifique mais de préciser quelles seront les colonnes à utiliser pour les tests de vérification
    ///     - (METHODE 2a) : soit en annotation de données ([ConcurrencyCheck()]) (cf. classe Nounours)
    ///     - (METHODE 2b) : soit en fluent API (modelBuilder.Entity<Nounours>().Property<int>(n => n.NbPoils).IsConcurrencyToken();) (cf. classe NounoursDBEntities)
    ///     
    /// Attention de bien noter que la méthode UpdateNounours a été modifiée pour catcher l'exception DbUpdateConcurrencyException et utiliser CurrentValues, OriginalValues et DatabaseValues en cas d'exception.
    /// 
    /// Commentez et décommentez les méthodes pour tester les différentes possibilités
    /// </summary>
    class Program
    {
        /// <summary>
        /// récupère un Nounours à partir de son id
        /// </summary>
        /// <param name="id">l'id du Nounours à récupérer</param>
        /// <returns>le Nounours récupéré (null, si l'id n'est pas connu dans la base)</returns>
        static Nounours GetNounours(Guid id)
        {
            Nounours nounours = null;

            using (NounoursDBEntities db = new NounoursDBEntities())
            {
                nounours = db.NounoursSet.Single(n => n.UniqueId == id);
            }

            return nounours;
        }

        /// <summary>
        /// mise à jour du Nounours dans la base
        /// </summary>
        /// <param name="nounours">nounours à mettre à jour</param>
        static void UpdateNounours(Nounours nounours)
        {
            using (NounoursDBEntities db = new NounoursDBEntities())
            {
                db.NounoursSet.Attach(nounours);
                db.Entry<Nounours>(nounours).State = EntityState.Modified;

                try
                {
                    db.SaveChanges();
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    //observez en mode debug de quelle manière la variable nounours est affectée
                    //récupération de l'entry
                    var entry = ex.Entries.Single();
                    // Using a NoTracking query means we get the entity but it is not tracked by the context
                    // and will not be merged with existing entities in the context.
                    var databaseEntity = db.NounoursSet.AsNoTracking().Single(n => n.UniqueId== (entry.Entity as Nounours).UniqueId);
                    var databaseEntry = db.Entry(databaseEntity);

                    //demande à l'utilisateur de choisir quelles valeurs utiliser : celles modifiées, ou celles actuellement en base
                    UserChoose(entry, databaseEntry);

                    //on applique les données courantes en base
                    db.SaveChanges();

                }
            }
        }

        /// <summary>
        /// demande à l'utilisateur de choisir entre les données courantes et les données en base de données à l'utilisateur
        /// </summary>
        /// <param name="currentValues">données courantes de l'entité en cours de modification</param>
        /// <param name="databaseValues">données en base de l'entité en cours de modification</param>
        /// <param name="resolvedValues">données résolues, i.e. choisies par l'utilisateur</param>
        private static void UserChoose(EntityEntry entry, EntityEntry databaseEntry)
        {
            WriteLine("Les données de l'entité que vous souhaitez modifier ont les valeurs suivantes en base (database values) :");
            foreach (var property in entry.Metadata.GetProperties())
            {
                WriteLine($"\t{property.Name}: {databaseEntry.Property(property.Name).CurrentValue}");
            }

            WriteLine("Les données de l'entité que vous voulez modifier sont les suivantes (current values) :");
            foreach (var property in entry.Metadata.GetProperties())
            {
                WriteLine($"\t{property.Name}: {entry.Property(property.Name).CurrentValue}");
            }

            foreach (var property in entry.Metadata.GetProperties())
            {
                var proposedValue = entry.Property(property.Name).CurrentValue;
                //var originalValue = entry.Property(property.Name).OriginalValue;
                var databaseValue = databaseEntry.Property(property.Name).CurrentValue;

                if (!proposedValue.Equals(databaseValue))
                {
                    WriteLine($"Que choisissez-vous de garder pour la propriété {property.Name} ? {proposedValue} (tapez c) ; {databaseValue} (tapez d)");
                    string réponse = ReadLine();
                    switch (réponse)
                    {
                        case "c":
                            WriteLine("Vous avez choisi de garder la valeur courante");
                            //met à jour les données courantes avec les données résolues
                            entry.Property(property.Name).CurrentValue = proposedValue;
                            break;
                        case "d":
                            WriteLine("Vous avez choisi de garder la valeur en base");
                            //met à jour les données courantes avec les données en base
                            entry.Property(property.Name).CurrentValue = databaseValue;
                            break;
                        default:
                            WriteLine("commande non reconnue, par défaut, on garde la donnée en base");
                            goto case "d";
                    }
                    //on change les données OriginalValues en les écrasant avec celles en base.
                    //Ainsi, EF ne voit pas de différences entre les données récupérées en base avant et après les modifications, et croit qu'on peut faire une modification avec CurrentValues
                    entry.Property(property.Name).OriginalValue = databaseEntry.Property(property.Name).CurrentValue;
                }
            }
        }

        static void Main(string[] args)
        {
            List<Guid> ids = new List<Guid>();

            //création du DbContext et injection de la dépendance à MyStubDataInitializationStrategy
            using (NounoursDBEntities db = new NounoursDBEntities())
            {
                DbContextInitializer.Seed(db);

                //récupère la liste des ids
                ids = db.NounoursSet.Select(n => n.UniqueId).ToList();
            }

            PrintTable("Au démarrage, la table contient :");

            WriteLine();

            //DEBUT DE LA SIMULATION DE MISES A JOUR CONCURRENTES

            //1. Le premier utilisateur récupère les données du premier nounours
            Nounours nounours1 = GetNounours(ids[0]);
            WriteLine("L'utilisateur 1 récupère le premier nounours");

            //2. Le deuxième utilisateur réupère les données du premier nounours
            Nounours nounours2 = GetNounours(ids[0]);
            WriteLine("L'utilisateur 2 récupère le premier nounours");

            //jusque-là, la table contient :
            // "Chewbacca", 27/05/1977, 1234567 poils
            // "Yoda", 21/05/1980, 3 poils
            // "Ewok", 25/05/1983, 3456789 poils

            //3. Le premier utilisateur tente de mettre à jour le nombre de poils, mais ne met pas tout de suite la table à jour
            nounours1.NbPoils = 2;

            WriteLine("Soudain, la table est modifiée par des facteurs extérieurs... (un recommandé peut-être...)");
            //4. La table est mise à jour par un autre biais
            using (NounoursDBEntities db = new NounoursDBEntities())
            {
                db.Database.ExecuteSqlCommand($"UPDATE dbo.TableNounours SET NbPoils = 5 WHERE UniqueId = {ids[0]}");
                db.SaveChanges();
            }
            PrintTable("Maintenant, la table contient :");
            //maintenant, la table contient :
            // "Chewbacca", 27/05/1977, 5 poils <==========================
            // "Yoda", 21/05/1980, 3 poils
            // "Ewok", 25/05/1983, 3456789 poils

            //5. L'utilisateur 1 tente de mettre à jour le nombre de poils
            UpdateNounours(nounours1);
            PrintTable("Après la tentative de modification du nombre de poils de Chewbacca à 2 par l'utilisateur 1 :");
            //une exception est lancée.
            //*A vous de choisir si vous voulez utiliser le nombre de poils en base, ou celui rentré par l'utilisateur 1.
            //Les autres propriétés n'ont pas changé

            //si vous choisissez de donner votre préférence au choix de l'utilisateur 1, la table contient :
            // "Chewbacca", 27/05/1977, 2 poils <==========================
            // "Yoda", 21/05/1980, 3 poils
            // "Ewok", 25/05/1983, 3456789 poils

            //6. Le deuxième utilisateur tente de mettre à jour la propriété Date De Naissance
            nounours2.DateDeNaissance = DateTime.Today;
            UpdateNounours(nounours2);
            PrintTable("Après la tentative de modification de la date de naissance de Chewbacca à aujourd'hui par l'utilisateur 2 :");
            //une exception est lancée.
            //deux propriétés sont différentes : la date de naissance car l'utilisateur 2 tente de la modifier, le nombre de poils car il a changé entre temps.
            //choisissez par exemple de garder la proposition de l'utilisateur 2 pour la date de naissance et la donnée en base pour le nombre de poils

            //si vous respectez ce choix, la table contiendra :
            // "Chewbacca", 10/11/2016, 2 poils <=========================
            // "Yoda", 21/05/1980, 3 poils
            // "Ewok", 25/05/1983, 3456789 poils
        }

        static void PrintTable(string intro)
        {
            WriteLine();
            WriteLine(intro);
            using (NounoursDBEntities db = new NounoursDBEntities())
            {
                //affiche les nounours après les modifications
                WriteLine("Après modifications");
                foreach (var n in db.NounoursSet)
                {
                    WriteLine($"\t{n}");
                }
            }
        }
    }
}
