using System;
using System.Collections.Generic;
using System.Text;
using System.Data.SQLite;
using System.Data;

namespace BookShop.Model
{
    /// <summary>
    /// Data Access Object minta: olyan osztály ami a felsőbb rétegek felé az adatok elérését
    /// biztosítja. Alapvető adatmanipulációs műveletek megvalósítása: CRUD.
    /// 
    /// C = Create (új adat létrehozása)
    /// R = Read (adatok lekérdezése feltétel(ek) alapján)
    /// U = Update (meglévő adatok módosítása)
    /// D = Delete (adatok törlése)
    /// </summary>
    class BookShopDao
    {
        /// <summary>
        /// SQLite specifikus connection stringünk, relatív útmegadással.
        /// A Webes alkalmazás a windows/system32-ben fut, ezért ott absz. útvonal kell
        /// </summary>
        private static readonly String connectionString = @"Data Source=F:\alkfejl\kulso\db\bookshop.db;";

        /// <summary>
        /// ebben tároljuk az ügyfeleket, memóriában: név / ügyfél párként
        /// </summary>
        private Dictionary<String, Customer> customers = new Dictionary<string, Customer>();
        /// <summary>
        /// ebben meg a könyveket
        /// </summary>
        private Dictionary<String, Book> books = new Dictionary<string, Book>();

        /// <summary>
        /// Új ügyfél hozzáadása.
        /// </summary>
        /// <param name="customer">az ügyfél példány amit eltárolunk</param>
        /// <returns>true, ha sikerült a hozzáadás, false, ha már létezik azonos nevű ügyfél</returns>
        public bool AddCustomer(Customer customer)
        {
            if (DoesCustomerExist(customer))
            {
                return false;
            }
            else
            {
                return InsertCustomer(customer);
            }
        }


        /// <summary>
        /// Ellenőrzi, hogy egy vásárló esetleg létezik-e már.
        /// Az egyszerűség kedvéért a vásárló nevét tekintjük 
        /// azonosítónak.
        /// </summary>
        /// <returns>true, ha létezik már a felhasználó,
        /// egyébként false</returns>
        private bool DoesCustomerExist(Customer customer)
        {
            // true-val kezdünk, mivel ez nagyobb biztonságot jelent
            // az adatbázis logikára nézve, ha false maradna egy esetleg 
            // hiba miatt, akkor a felhasználó kód megpróbálhatna már egy 
            // létező felhasználót felvenni
            bool rvExists = true;

            using (SQLiteConnection conn = new SQLiteConnection(connectionString))
            using (SQLiteCommand cmd = conn.CreateCommand())
            {
                // előkészítjük a parancsot, egy egyszerű paraméteres lekérdezés
                // a customer nevét felhasználva
                cmd.CommandText = "SELECT id From Customer WHERE name = @name";

                // regisztráljuk a paramétert a név számára
                SQLiteParameter nameParam =
                    cmd.Parameters.Add("@name", DbType.String);
                nameParam.Value = customer.Name;

                conn.Open();
                object result = cmd.ExecuteScalar();

                rvExists = result != null;
            }

            return rvExists;
        }


        /// <summary>
        /// Egy új vásárlót vesz fel az adatbázisba.
        /// </summary>
        /// <param name="customer"></param>
        /// <returns>true, ha a művelet sikeres, egyébként false</returns>
        private bool InsertCustomer(Customer customer)
        {
            bool rvSuccess = false;

            // kapcsolati objektumok létrehozása
            using (SQLiteConnection conn = new SQLiteConnection(connectionString))
            using (SQLiteCommand cmd = conn.CreateCommand())
            {
                // literálokkal végezhetünk műveleteket, mert a C# fordító végez constant folding-ot,
                // azaz a fordítási időben elvégezhető műveleteket elvégzi.
                cmd.CommandText = "INSERT INTO Customer (name, age, female, rented, student, grantee, qualif) " +
                                  "VALUES (@name, @age, @female, @rented, @student, @grantee, @qualif)";


                //regisztráljuk és egyúttal be is állítjuk a paramétereket
                // A Convert osztály a boolean -> int32 átalakítást
                // igényeinknek megfelelően végzi el:
                // true -> 1, false -> 0
                cmd.Parameters.Add("@name", DbType.String).Value = customer.Name;
                cmd.Parameters.Add("@age", DbType.Int32).Value = customer.Age;
                cmd.Parameters.Add("@female", DbType.Int32).Value = Convert.ToInt32(customer.Female);
                cmd.Parameters.Add("@rented", DbType.Int32).Value = Convert.ToInt32(customer.Rented);
                cmd.Parameters.Add("@student", DbType.Int32).Value = Convert.ToInt32(customer.Student);
                cmd.Parameters.Add("@grantee", DbType.Int32).Value = Convert.ToInt32(customer.Grantee);
                cmd.Parameters.Add("@qualif", DbType.String).Value = customer.Qualification;

                conn.Open();
                int rowCount = cmd.ExecuteNonQuery();

                rvSuccess = rowCount == 1;
            }

            return rvSuccess;
        }


        /// <summary>
        /// Ügyfelek listájának lekérése.
        /// </summary>
        /// <returns>ügyfelek kollekciója</returns>
        public IEnumerable<Customer> GetAllCustomers()
        {
            customers.Clear();

            // kapcsolati objektumok létrehozása
            using (SQLiteConnection conn = new SQLiteConnection(connectionString))
            using (SQLiteCommand cmd = conn.CreateCommand())
            {
                // a teljes tábla tartalmát le kívánjuk kérdezni
                // lehetne így is, de nem támogatott (kivételt kapunk)
                //cmd.CommandType = CommandType.TableDirect;
                //cmd.CommandText = "Customer";
                cmd.CommandText = "SELECT * FROM Customer";

                conn.Open();
                // végrehajtjuk a lekérdezést, visszakapva az eredményhalmazt
                // DataReader is IDisposable, ezért using-ot használunk
                using (SQLiteDataReader reader = cmd.ExecuteReader())
                {
                    // amíg van új eredménysor, addih olvasunk
                    while (reader.Read())
                    {
                        // minden egyes sorbon létehozunk egy objektumot
                        Customer c = new Customer();
                        c.Id = reader.GetInt32(reader.GetOrdinal("id"));
                        c.Name = reader.GetString(reader.GetOrdinal("name"));
                        c.Age = reader.GetInt32(reader.GetOrdinal("age"));
                        // !!! a GetBoolean automatikusan konvertálta az értéket
                        // az adatbázisban int-ként tároljuk
                        c.Female = reader.GetBoolean(reader.GetOrdinal("female"));
                        c.Rented = reader.GetBoolean(reader.GetOrdinal("rented"));
                        c.Student = reader.GetBoolean(reader.GetOrdinal("student"));
                        c.Grantee = reader.GetBoolean(reader.GetOrdinal("grantee"));
                        c.Qualification = reader.GetString(reader.GetOrdinal("qualif"));
                        customers.Add(c.Name, c);
                    }
                }
            }

            return customers.Values;
        }

        /// <summary>
        /// Megnézi hogy az adott könyv szerepel-e már az adatbázisban
        /// (szerző, cím és kiadási év alapján). Ha már létezik visszaadja
        /// az Id-ját, egyébként meg null értéket.
        /// 
        /// Az int? típus megegyezik a sima int típussal de lehet neki null
        /// értéket is adni. Ez remekül használható arra hogy jelezzünk egy
        /// nemlétező értéket. (Hasonlóan működik bármely másik érték alapú
        /// típusnál, pl. bool? egy 3-állapotú érték, ami lehet true, false
        /// vagy null.)
        /// </summary>
        /// <param name="book">egy könyv példány</param>
        /// <returns>a könyv id-je, vagy null ha nincs ilyen az adatbázisban</returns>
        public int? GetBookId(Book book)
        {
            int? id = null;

            // A using kulcsszó a nem menedzselt erőforrások kezelését segíti
            // a blokkból kilépve garantálja hogy a paraméternek megadott erőforrás
            // (itt a 'conn') le lesz zárva (conn.Close();) akár sikeresen lefutottak
            // a műveletek, akár hiba (exception) miatt lép ki a blokkból.
            //
            // Nem menedzselt erőforrások .Net alatt (és Javaban is!):
            // adatbáziskapcsolat, írás/olvasás, fájlelérés, hálózati műveletek...
            using (SQLiteConnection conn = new SQLiteConnection(connectionString))
            {
                // létrehozunk egy parancs objektumot, ami SQL parancsok végrehajtására
                // lesz alkalmas, ezt mindig egy adott connection-höz kötjük
                SQLiteCommand command = conn.CreateCommand();

                // CommandText property tartalmazza magát a nyers SQL parancsot.
                // Itt most paraméteres SQL parancsot adunk, a paramétereket névvel ellátva
                // definiálhatjuk .Net-ben.
                command.CommandText = "select id from Book where author=@author and title=@title and year=@year";

                // Értéket adunk a paramétereknek. Ehhez előbb lérehozunk egy új paramétert
                // (Add) és megadjuk a nevét + SQL szerinti típusát. A név azonos az előbb definiált
                // SQL parancsban írt nevekkel. Ezután a keletkezett (SQLite)Parameter objektum
                // Value property-nek értékül adhatjuk a paraméter értékét.
                command.Parameters.Add("author", System.Data.DbType.String).Value = book.Author;
                command.Parameters.Add("title", System.Data.DbType.String).Value = book.Title;
                command.Parameters.Add("year", System.Data.DbType.Int32).Value = book.Year;

                // Ellentétben Java-val a connection létrehozása nem jelent automatikus csatlakozást
                // az adatbázishoz. A tényleges műveletvégzés előtt meg kell nyitni a kapcsolatot.
                // Megj.: a végén le kéne zárni is, de ezt a 'using' blokk használata elintézi.
                conn.Open();

                // A parancsot lefuttatjuk. Az ExecuteScalar() metódust akkor használhatjuk, 
                // ha csak egyetlen eredményt várunk. Itt csak egyetlen 'id' mező értékét
                // kértük le. Arról persze már nekünk kell gondoskodni, hogy azonos cím+szerző+év
                // adattal ne lehessen két különböző könyv az adatbázisban. A metódus object-et
                // ad vissza, amit type castolni kell a várt eredménytípusra. Ha nincs eredmény
                // akkor null értéket kapunk.
                object result = command.ExecuteScalar();
                if (result != null)
                {
                    id = Convert.ToInt32(result);
                }
            }

            return id;
        }

        /// <summary>
        /// Egy könyv hozzáadása az adatbázishoz. Ha már létezik a könyv, akkor a meglévő készletet
        /// növeli csak, egyébként hozzáad egy újat az Book táblához.
        /// </summary>
        /// <param name="book"></param>
        /// <returns></returns>
        public bool AddOrUpdateBook(Book book)
        {
            int? id = GetBookId(book);

            using (SQLiteConnection conn = new SQLiteConnection(connectionString))
            {
                SQLiteCommand command = conn.CreateCommand();

                if (id != null)
                {
                    // a könyv már létezik update sql parancsot kell használni
                    command.CommandText = "update Book set pieces=pieces+@pieces where id=@id";
                    command.Parameters.Add("id", System.Data.DbType.Int32).Value = id;
                    command.Parameters.Add("pieces", System.Data.DbType.Int32).Value = book.Piece;
                }
                else
                {
                    // a könyv még nincs az adatbázisban, insert sql parancs kell
                    command.CommandText = "insert into Book (author, title, year, category, price, pieces, ancient) "
                        + " values (@author, @title, @year, @category, @price, @pieces, @ancient)";
                    command.Parameters.Add("author", System.Data.DbType.String).Value = book.Author;
                    command.Parameters.Add("title", System.Data.DbType.String).Value = book.Title;
                    command.Parameters.Add("year", System.Data.DbType.Int32).Value = book.Year;
                    command.Parameters.Add("category", System.Data.DbType.String).Value = book.Category;
                    command.Parameters.Add("price", System.Data.DbType.Int32).Value = book.Price;
                    command.Parameters.Add("pieces", System.Data.DbType.Int32).Value = book.Piece;
                    command.Parameters.Add("ancient", System.Data.DbType.Int32).Value = book.Ancient ? 1 : 0;
                }

                conn.Open();

                // az ExecuteNonQuery() metódus végrehajtja a parancsot, olyan parancsok esetén
                // használjuk ahol nem várunk vissza eredményt (nem Select). A visszatérési értéke
                // a parancs által érintett sorok száma. Ha ez itt nem 1 akkor valami gond van, mivel
                // mi pont egyetlen könyvet szeretnénk beszúrni/módosítani.
                if (command.ExecuteNonQuery() != 1)
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// A készleten lévő könyvek lekérése. Csak azokat adja vissza ahol a készet (Piece)
        /// legalább 1.
        /// </summary>
        /// <returns>könyvek listája</returns>
        public IEnumerable<Book> GetAllBooks()
        {
            // töröljük a memóriában lévő értékeket
            books.Clear();

            using (SQLiteConnection conn = new SQLiteConnection(connectionString))
            {
                SQLiteCommand command = conn.CreateCommand();
                command.CommandText = "select * from Book";

                conn.Open();

                // ExecuteReader()-t használunk ha Select parancsot küldünk és a teljes
                // eredményre (táblázat) kíváncsiak vagyunk. Az eredmény mindig egy
                // valamilyen DataReader objektum.
                SQLiteDataReader reader = command.ExecuteReader();

                while (reader.Read())
                {
                    Book book = new Book();
                    book.Author = reader["author"].ToString();
                    book.Title = reader["title"].ToString();
                    book.Year = Convert.ToInt32(reader["year"]);
                    book.Category = reader["category"].ToString();
                    book.Price = Convert.ToInt32(reader["price"]);
                    book.Piece = Convert.ToInt32(reader["pieces"]);
                    book.Ancient = (Convert.ToInt32(reader["ancient"]) != 0);
                    book.Id = Convert.ToInt32(reader["id"]);

                    books.Add(book.Title, book);
                }
            }

            return books.Values;
        }


        public bool SellBook(Book book, Customer customer)
        {
            bool rvSuccess = false;

            using (SQLiteConnection conn = new SQLiteConnection(connectionString))
            using (SQLiteCommand cmd = conn.CreateCommand())
            {
                cmd.CommandText = "INSERT INTO SoldBookInstances (bookid, customerid, sellDate) " +
                                  "VALUES (@bookid, @customerid, @sellDate)";

                cmd.Parameters.Add("@bookid", DbType.Int32).Value = book.Id;
                cmd.Parameters.Add("@customerid", DbType.Int32).Value = customer.Id;
                cmd.Parameters.Add("@sellDate", DbType.Date).Value = DateTime.Now;

                conn.Open();
                int rowCount = cmd.ExecuteNonQuery();

                rvSuccess = rowCount == 1;
            }

            return rvSuccess;
        }


        public IEnumerable<SoldBook> GetSoldBooks()
        {
            List<SoldBook> soldbooks = new List<SoldBook>();

            using (SQLiteConnection conn = new SQLiteConnection(connectionString))
            using (SQLiteCommand cmd = conn.CreateCommand())
            {
                cmd.CommandText = "SELECT * FROM SoldBookInstances";

                conn.Open();
                using (SQLiteDataReader reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        SoldBook sb = new SoldBook();
                        sb.SellDate = reader.GetDateTime(reader.GetOrdinal("sellDate"));
                        sb.Customer = GetCustomer(reader.GetInt32(reader.GetOrdinal("customerid")));
                        sb.Book = GetBook(reader.GetInt32(reader.GetOrdinal("bookid")));
                        soldbooks.Add(sb);
                    }
                }
            }

            return soldbooks;
        }


        public Customer GetCustomer(int id)
        {
            Customer c = null;

            using (SQLiteConnection conn = new SQLiteConnection(connectionString))
            using (SQLiteCommand cmd = conn.CreateCommand())
            {
                cmd.CommandText = "SELECT * FROM Customer WHERE id = @id";

                cmd.Parameters.Add("@id", DbType.Int32).Value = id;

                conn.Open();
                using (SQLiteDataReader reader = cmd.ExecuteReader())
                {
                    if (reader.Read())
                    {
                        c = new Customer();
                        c.Id = reader.GetInt32(reader.GetOrdinal("id"));
                        c.Name = reader.GetString(reader.GetOrdinal("name"));
                        c.Age = reader.GetInt32(reader.GetOrdinal("age"));
                        c.Female = reader.GetBoolean(reader.GetOrdinal("female"));
                        c.Rented = reader.GetBoolean(reader.GetOrdinal("rented"));
                        c.Student = reader.GetBoolean(reader.GetOrdinal("student"));
                        c.Grantee = reader.GetBoolean(reader.GetOrdinal("grantee"));
                        c.Qualification = reader.GetString(reader.GetOrdinal("qualif"));
                    }
                }
            }

            return c;
        }

        public Book GetBook(int id)
        {
            Book b = null;

            using (SQLiteConnection conn = new SQLiteConnection(connectionString))
            using (SQLiteCommand cmd = conn.CreateCommand())
            {
                cmd.CommandText = "SELECT * FROM Book WHERE id = @id";

                cmd.Parameters.Add("@id", DbType.Int32).Value = id;

                conn.Open();
                using (SQLiteDataReader reader = cmd.ExecuteReader())
                {
                    if (reader.Read())
                    {
                        b = new Book();
                        b.Author = reader["author"].ToString();
                        b.Title = reader["title"].ToString();
                        b.Year = Convert.ToInt32(reader["year"]);
                        b.Category = reader["category"].ToString();
                        b.Price = Convert.ToInt32(reader["price"]);
                        b.Piece = Convert.ToInt32(reader["pieces"]);
                        b.Ancient = (Convert.ToInt32(reader["ancient"]) != 0);
                        b.Id = Convert.ToInt32(reader["id"]);
                    }
                }
            }

            return b;
        }
    }
}
