using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;

namespace EF.BazaDanych
{
    #region Klasy encji    
    public class Osoba
    {
        [Key] public int Id { get; set; } //primary key
        public string Imię { get; set; }
        public string Nazwisko { get; set; }
        public int? NumerTelefonu { get; set; }
        public Adres Adres { get; set; }

        public override bool Equals(object? obj)
        {
            if (obj is not Osoba) return false;
            Osoba innaOsoba = obj as Osoba;
            if (innaOsoba == null) return false;
            return Id == innaOsoba.Id && Imię.Equals(innaOsoba.Imię) && Nazwisko.Equals(innaOsoba.Nazwisko);
            //nie sprawdzamy adresu
        }

        public override string ToString()
        {
            return $"({Id}) {Imię} {Nazwisko} ({(NumerTelefonu.HasValue ? NumerTelefonu.Value.ToString() : "---")}), {Adres}";
        }

        public override int GetHashCode()
        {
            return Id ^ Imię.GetHashCode() ^ Nazwisko.GetHashCode() ^ NumerTelefonu.GetHashCode();
        }
    }

    public class Adres
    {
        [Key] public int Id { get; set; }
        public string Miasto { get; set; }
        public string Ulica { get; set; }
        public int NumerDomu { get; set; }
        public int? NumerMieszkania { get; set; }

        public override bool Equals(object? obj)
        {
            if (obj is not Adres) return false;
            Adres innyAdres = obj as Adres;
            if (innyAdres == null) return false;
            return 
                Miasto.Equals(innyAdres.Miasto) && 
                Ulica.Equals(innyAdres.Ulica) && 
                NumerDomu == innyAdres.NumerDomu && 
                NumerMieszkania == innyAdres.NumerMieszkania;            
        }

        public override int GetHashCode()
        {
            return Miasto.GetHashCode() ^ Ulica.GetHashCode() ^ NumerDomu ^ NumerMieszkania.GetHashCode();
        }

        public override string ToString()
        {
            return $"({Id}) {Miasto}, {Ulica} {NumerDomu}/{NumerMieszkania}".TrimEnd('/');
        }
    }
    #endregion

    class BazaDanychOsóbDbContext : DbContext
    {
        public DbSet<Osoba> Osoby { get; set; }
        public DbSet<Adres> Adresy { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            //base.OnConfiguring(optionsBuilder);
            optionsBuilder.UseSqlite("Data Source=osoby.db");
        }
    }

    public class BazaDanychOsób : IDisposable
    {
        private BazaDanychOsóbDbContext dbc = new BazaDanychOsóbDbContext();

        public BazaDanychOsób()
        {
            dbc.Database.EnsureCreated();
            dbc.SavedChanges += dbc_SavedChanges;
            dbc.SaveChangesFailed += dbc_SaveChangesFailed;
        }

        private void dbc_SavedChanges(object? sender, SavedChangesEventArgs e)
        {
            Console.WriteLine("DBC: Zmiany zapisane do bazy danych");
        }

        private void dbc_SaveChangesFailed(object? sender, SaveChangesFailedEventArgs e)
        {
            Console.WriteLine("DBC: Zapisane do bazy danych nie powiódł się");
        }

        public void Dispose()
        {
            dbc.SavedChanges -= dbc_SavedChanges;
            dbc.SaveChangesFailed -= dbc_SaveChangesFailed;
            dbc.Dispose();
        }

        #region Debugowanie
#if DEBUG
        public Osoba[] Osoby { get { return dbc.Osoby.ToArray(); } }
        public Adres[] Adresy { get { return dbc.Adresy.ToArray(); } }
#endif
        #endregion

        public Osoba PobierzOsobę(int idOsoby)
        {
            return dbc.Osoby.FirstOrDefault(o => o.Id == idOsoby);
        }

        //CRUD

        public Osoba this[int idOsoby]
        {
            get => PobierzOsobę(idOsoby);
        }

        public int[] IdentyfikatoryOsób
        {
            get
            {
                return dbc.Osoby.Select(o => o.Id).ToArray();
            }
        }

        public int DodajOsobę(Osoba osoba) //zwraca Id
        {
            if (osoba == null)
                throw new ArgumentNullException("Podano pustą referencję obiektu osoby");
            if (osoba.Adres == null) throw new ArgumentException("Brak adresu zameldowania uniemożliwia dodanie osoby");

            if (dbc.Osoby.ToArray().Any(o => o.Equals(osoba)))
                return osoba.Id;
            else
            {
                if (dbc.Osoby.Any(o => o.Id == osoba.Id))
                    osoba.Id = dbc.Osoby.Max(o => o.Id) + 1;
            }

            //unikanie dublowania adresów
            Adres adres = dbc.Adresy.ToArray().FirstOrDefault(a => a.Equals(osoba.Adres));
            if (adres != null) osoba.Adres = adres; //adres już jest, używam istnijący obiekt

            dbc.Osoby.Add(osoba);
            dbc.SaveChanges();

            return osoba.Id;
        }

        private Adres[] pobierzNieużywaneAdresy()
        {
            int[] używaneIdentyfikatoryAdresów = dbc.Osoby.Select(o => o.Adres.Id).Distinct().ToArray();
            List<Adres> nieużywaneAdresy = new List<Adres>();
            foreach(Adres adres in dbc.Adresy)
            {
                if (!używaneIdentyfikatoryAdresów.Contains(adres.Id))
                    nieużywaneAdresy.Add(adres);
            }
            return nieużywaneAdresy.ToArray();
        }

        private void usuńNieużywaneAdresy()
        {
            dbc.Adresy.RemoveRange(pobierzNieużywaneAdresy());
            dbc.SaveChanges();
        }

        public void UsuńOsobę(int idOsoby)
        {
            Osoba osoba = PobierzOsobę(idOsoby);
            if(osoba != null)
            {
                dbc.Osoby.Remove(osoba);
                dbc.SaveChanges();
                //sprzątamy adresy
                usuńNieużywaneAdresy();
            }
        }

        public void AktualizujOsobę(int idOsoby, Osoba noweDane)
        {
            Osoba osoba = PobierzOsobę(idOsoby);
            if (osoba == null) throw new ArgumentException("Nie ma osoby o podanym identyfikatorze");
            osoba.Imię = noweDane.Imię;
            osoba.Nazwisko = noweDane.Nazwisko;
            osoba.NumerTelefonu = noweDane.NumerTelefonu;
            osoba.Adres = noweDane.Adres;
            dbc.SaveChanges();
        }
    }
}
