using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.IO;

namespace Helion
{    
    /// <summary>
    /// Klasa umożliwiająca wczytywanie tabeli z pliku tekstowego uwzględniając możliwość sortowania i filtrowania, a także zapisu zmodyfikowanych danych.
    /// </summary>
    public class PlikTekstowy
    {
        #region Pola
        int iloscRekordow = 0;
        int iloscKolumn = 0;
        List<List<string>> tabela = null;
        List<string> nazwyKolumn = null;
        List<Type> typyKolumn = null;
        string sciezka = string.Empty;
        string znakiSeparacji = "\t;";
        string znakKomentarza = "#";
        List<int> numeryWierszy;
        #endregion

        #region Konstruktor

        public PlikTekstowy(string sciezka, string znakKomentarza = "#", string znakiSeparacji = "\t;")
        {
            WczytajDaneZPliku(sciezka, znakKomentarza, znakiSeparacji);
        }

        #endregion

        #region Wlasnosci (tylko do odczytu)

        /// <summary>
        /// Zwraca całą tabele odczytaną z pliku
        /// </summary>
        public List<List<string>> Tabela { get { return tabela; } }

        /// <summary>
        /// Ilość wierszy w tabeli
        /// </summary>
        public int IloscRekordow { get { return iloscRekordow; } }

        /// <summary>
        /// Ilość kolumn w tabeli
        /// </summary>
        public int IloscKolumn { get { return iloscKolumn; } }

        /// <summary>
        /// Nagłówek tabeli, nazwy kolumn po znaku komentarza, 
        /// w przypadku wielu komentarzy brany jest jest ostatni komentarz jako definicja nagłówka
        /// </summary>
        public List<string> NazwyKolumn { get { return nazwyKolumn; } }

        public List<Type> TypyKolumn { get { return typyKolumn; } }

        public List<int> NumeryWierszy { get { return numeryWierszy; } }

        #endregion


        #region Metody

        #region Publiczne

        /// <summary>
        /// Wczytywanie danych z pliku.
        /// </summary>
        /// <param name="sciezka">Pełna ścieżka dostępu do pliku .txt.</param>
        /// <param name="znak">Znak komentarza (domyślnie jest to '#').</param>
        /// <param name="rozdzielniki">Tablica znaków rodzielejących dane (domyślnie są to '/t' i ';').</param>
        /// <returns>Stan operacji.</returns>
        public void WczytajDaneZPliku(string sciezka, string znakKomentarza = "#", string znakiSeparacji = "\t;")
        {
            if (!File.Exists(sciezka)) throw new FileNotFoundException("Plik " + sciezka + " nie istnieje.");
            
            this.sciezka = sciezka;
            this.znakiSeparacji = znakiSeparacji;
            this.znakKomentarza = znakKomentarza;
            
            tabela = new List<List<string>>();
            nazwyKolumn = new List<string>();
            typyKolumn = new List<Type>();

            FileStream plik = new FileStream(sciezka, FileMode.Open);
            StreamReader strumien = new StreamReader(plik);
            int numerWiersza = -1;
            while (!strumien.EndOfStream)
            {
                string wiersz = strumien.ReadLine();
                //wiersz = wiersz.Replace('.', ','); //wymuszenie polskiego znaku separacji dziesietnej
                string[] tablica = wiersz.Split(znakiSeparacji.ToCharArray());
                int dlugosc = tablica.Length;
                if (tablica[dlugosc - 1] == String.Empty) dlugosc--;
                string[] tab = new string[dlugosc];
                int licznik = 0;
                if (!tablica[0].StartsWith(znakKomentarza))                
                {
                    //odczytywanie danych
                    numerWiersza++;
                    for (int i = 0; i < dlugosc; i++)
                        if (tablica[i] != string.Empty)
                        {
                            tab[licznik] = tablica[i];
                            licznik++;
                        }
                    string[] nowa = new string[licznik];
                    nowa = Przepisz(tab, licznik);
                    this.DodajWiersz(nowa, numerWiersza);
                }
                else
                {
                    //odczytywanie nazw kolumn
                    if (tablica[0].StartsWith(znakKomentarza + "nazwa"))
                    {
                        nazwyKolumn.Clear();
                        for (int i = 1; i < dlugosc; i++)
                            if (tablica[i] != string.Empty)
                                nazwyKolumn.Add(tablica[i]);
                    }
                    //odczytywanie typow kolumn
                    if (tablica[0].StartsWith(znakKomentarza + "typ"))
                    {
                        typyKolumn.Clear();
                        for (int i = 1; i < dlugosc; i++)
                            switch (tablica[i])
                            {
                                case "Int32":
                                case "int": typyKolumn.Add(Type.GetType("System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"));
                                    break;
                                case "Double":
                                case "double": typyKolumn.Add(Type.GetType("System.Double, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"));
                                    break;
                                default: typyKolumn.Add(Type.GetType("System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"));
                                    break;
                            }
                    }
                }
            }
            strumien.Close();
            plik.Close();
        }

        /// <summary>
        /// Sortuje tabele metodą bąbelkową
        /// </summary>
        /// <param name="sortownik">Określa jak dany string jest ważny i daje to wyniku,
        /// czym większa wartość tym element będzie wyżej na liście</param>
        /// <param name="numerKolumny">Kolumna po której ma zostać wykonane sortowanie</param>
        /// <returns>Posortowane wiersze</returns>
        public List<List<string>> Sortuj(Func<string, int> sortownik, int numerKolumny)
        {
            List<PSortowanie> pomocniczaTabela = new List<PSortowanie>();
            List<List<string>> wynik = new List<List<string>>();
            for (int i = 0; i < iloscRekordow; i++)
                pomocniczaTabela.Add(new PSortowanie(tabela[i], sortownik(tabela[i][numerKolumny])));
            for (int i = 0; i < pomocniczaTabela.Count; i++)
            {
                for (int j = i + 1; j < pomocniczaTabela.Count; j++)
                {
                    if (pomocniczaTabela[i].wartosc > pomocniczaTabela[j].wartosc)
                    {
                        PSortowanie temp = new PSortowanie(pomocniczaTabela[i]);
                        pomocniczaTabela[i] = pomocniczaTabela[j];
                        pomocniczaTabela[j] = temp;
                    }
                }
            }
            for (int i = 0; i < pomocniczaTabela.Count; i++)
                wynik.Add(pomocniczaTabela[i].dana);
            return wynik;
        }

        /// <summary>
        /// Sortuje tabele (daną w klasie) metodą bąbelkową.
        /// </summary>
        /// <param name="sortownik">Określa który z dwóch elementów jest większy,
        /// 1 - pierrwszy element był większy,
        /// -1 - drugi element był większy,
        /// 0 - elementy byłe równe.</param>
        /// <param name="numerKolumny">Kolumna po której ma zostać wykonane sortowanie.</param>
        /// <returns>Posortowane wiersze.</returns>
        public List<List<string>> Sortuj(Func<string, string, int> sortownik, int numerKolumny)
        {
            return PlikTekstowy.Sortuj(sortownik, numerKolumny, this.tabela);
        }

        /// <summary>
        /// Sortuje tabele metodą bąbelkową.
        /// </summary>
        /// <param name="sortownik">Określa który z dwóch elementów jest większy,
        /// 1 - pierrwszy element był większy,
        /// -1 - drugi element był większy,
        /// 0 - elementy byłe równe.</param>
        /// <param name="numerKolumny">Kolumna po której ma zostać wykonane sortowanie.</param>
        /// <param name="tabelaDanych">Tabela na której ma zostać wykonane sortowanie.</param>
        /// <returns>Posortowane wiersze.</returns>
        public static List<List<string>> Sortuj(Func<string, string, int> sortownik, int numerKolumny, List<List<string>> tabelaDanych)
        {
            List<List<string>> pomocniczaTabela = new List<List<string>>();
            for (int i = 0; i < tabelaDanych.Count; i++)
                pomocniczaTabela.Add(tabelaDanych[i]);
            for (int i = 0; i < pomocniczaTabela.Count; i++)
            {
                for (int j = i + 1; j < pomocniczaTabela.Count; j++)
                {
                    if (sortownik(pomocniczaTabela[i][numerKolumny], pomocniczaTabela[j][numerKolumny]) > 0)
                    {
                        List<string> temp = new List<string>(pomocniczaTabela[i]);
                        pomocniczaTabela[i] = pomocniczaTabela[j];
                        pomocniczaTabela[j] = temp;
                    }
                }
            }
            return pomocniczaTabela;
        }

        public List<List<string>> Sortuj(Func<string, string, int> sortownik, string nazwaKolumny, List<List<string>> tabelaDanych)
        {
            return Sortuj(sortownik, nazwyKolumn.FindIndex(c => c == nazwaKolumny ? true : false), tabelaDanych);
        }

        /// <summary>
        /// Filtruję dane
        /// </summary>
        /// <param name="filtr">Funckja określająca czy dany element ma zostać wyświetlony.</param>
        /// <param name="numerKolumny">Po której kolumnie filtrujemy.</param>
        /// <returns>Wiersze które przeszły filtrowanie.</returns>
        public List<List<string>> Filtruj(Func<string, bool> filtr, int numerKolumny)
        {
            numeryWierszy = new List<int>();
            List<List<string>> pomocniczaTabela = new List<List<string>>();
            for (int i = 0; i < tabela.Count; i++)
                if (filtr(tabela[i][numerKolumny]))
                {
                    pomocniczaTabela.Add(tabela[i]);
                    numeryWierszy.Add(i);
                }
            return pomocniczaTabela;
        }

        public List<List<string>> Filtruj(Func<string, bool> filtr, string nazwaKolumny)
        {
            return Filtruj(filtr, nazwyKolumn.FindIndex(c => c == nazwaKolumny ? true : false));
        }

        public List<List<string>> Filtruj(Func<string, int, bool> filtr, int numerKolumny, int zmienna)
        {
            numeryWierszy = new List<int>();
            List<List<string>> pomocniczaTabela = new List<List<string>>();
            for (int i = 0; i < tabela.Count; i++)
                if (tabela[i][numerKolumny] != "NULL" && filtr(tabela[i][numerKolumny], zmienna))
                {
                    pomocniczaTabela.Add(tabela[i]);
                    numeryWierszy.Add(i);
                }
            return pomocniczaTabela;
        }

        public List<List<string>> Filtruj(Func<string, int, bool> filtr, string nazwaKolumny, int zmienna)
        {
            return Filtruj(filtr, nazwyKolumn.FindIndex(c => c == nazwaKolumny ? true : false), zmienna);

        }

        public List<List<string>> Filtruj(Func<string, double, bool> filtr, int numerKolumny, double zmienna)
        {
            numeryWierszy = new List<int>();
            List<List<string>> pomocniczaTabela = new List<List<string>>();
            for (int i = 0; i < tabela.Count; i++)
                if (tabela[i][numerKolumny] != "NULL" && filtr(tabela[i][numerKolumny], zmienna))
                {
                    pomocniczaTabela.Add(tabela[i]);
                    numeryWierszy.Add(i);
                }
            return pomocniczaTabela;
        }

        public List<List<string>> Filtruj(Func<string, double, bool> filtr, string nazwaKolumny, double zmienna)
        {
            return Filtruj(filtr, nazwyKolumn.FindIndex(c => c == nazwaKolumny ? true : false), zmienna);

        }

        /// <summary>
        /// Dodaje wiersz do danych i zapisuję do w pliku
        /// </summary>
        /// <param name="wiersz">Wiersz z danymi do zapisu</param>
        /// <returns>Stan zapisu</returns>
        public void DodajWiersz(List<string> wiersz)
        {
            FileStream plik = new FileStream(sciezka, FileMode.Append);
            StreamWriter pisarz = new StreamWriter(plik);
            tabela.Add(wiersz);
            StringBuilder wierszDoDodania = new StringBuilder();
            for (int j = 0; j < iloscKolumn; j++)
                wierszDoDodania.Append(wiersz[j] + znakiSeparacji[0]);
            pisarz.WriteLine(wierszDoDodania.ToString());
            pisarz.Close();
            plik.Close();            
        }

        /// <summary>
        /// Aktualizuję wiersze według filtru
        /// </summary>
        /// <param name="wiersz">Wiersz którym będą aktualizowane wiersze. 
        /// string.Empty oznacza brak aktualizacji danego pola,
        /// null oznacza wymazanie danego pola</param>
        /// <param name="filtr">Funckja określająca czy dany element ma zostać zaktualizowany</param>
        /// <param name="numerKolumny">Po której kolumnie filtrujemy</param>
        /// <returns>Stan aktualizacji</returns>
        public int AktualizujWiersz(List<string> wiersz, Func<string, bool> filtr, int numerKolumny)
        {
            List<Update> wierszeAktualizowane = FiltrujWewnętrzny(filtr, numerKolumny);
            foreach (Update wierszPrzetwarzany in wierszeAktualizowane)
                this.AktualizacjaWiersza(wierszPrzetwarzany.numer, wiersz);
            this.ZapiszTabeleDoPliku(sciezka); //niepowinno być będziez metoda Update.
            return wierszeAktualizowane.Count;
        }

        public int AktualizujWiersz(List<string> wiersz, List<int> numeryWierszy)
        {
            for (int i = 0; i < numeryWierszy.Count; i++)
            {
                this.AktualizacjaWiersza(numeryWierszy[i], wiersz);
            }
            return numeryWierszy.Count;
        }

        /// <summary>
        /// Zwraca jeden konkretny wiersz.
        /// </summary>
        /// <param name="numer">Numer wiersza.</param>
        /// <returns>Wiersz.</returns>
        public List<string> PobierzWiersz(int numer) { return tabela[numer]; }

        public void ZapiszZmianyWTabeli()
        {
            this.ZapiszTabeleDoPliku(this.sciezka);
        }

        public Dictionary<string, List<int>> PobierzHashDanych()
        {
            Dictionary<string, List<int>> wynik = new Dictionary<string, List<int>>(this.tabela.Count);
            for (int i = 0; i < this.tabela.Count; i++)
            {
                string temp = string.Empty;
                for (int j = 0; j < this.tabela[i].Count; j++)
                {
                    temp += this.tabela[i][j];
                }
                if (wynik.ContainsKey(temp))
                    wynik[temp].Add(i);
                else
                {
                    List<int> kolumny = new List<int>();
                    kolumny.Add(i);
                    wynik.Add(temp, kolumny);
                }
            }
            return wynik;
        }

        public int UsunZTabeli(List<int> numeryWierszy)
        {
            numeryWierszy.Sort();
            for (int i = 0; i < numeryWierszy.Count; i++)
                tabela.RemoveAt(numeryWierszy[i] - i);
            iloscRekordow -= numeryWierszy.Count;
            return numeryWierszy.Count;
        }

        #endregion

        #region Prywatne

        /// <summary>
        /// Dodaje wiersz do tabeli
        /// </summary>
        /// <param name="tablica">Nowy wiersz</param>
        private void DodajWiersz(string[] tablica, int numerWiersza)
        {
            if (iloscKolumn == 0) iloscKolumn = tablica.Length;
            if (tablica.Length != iloscKolumn) return;
            List<string> temp = new List<string>(tablica.Length);
            for (int i = 0; i < tablica.Length; i++)
                if (String.IsNullOrEmpty(tablica[i]))
                    temp.Add("NULL");
                else temp.Add(tablica[i]);
            tabela.Add(temp);
            iloscRekordow = tabela.Count;
            return;
        }

        /// <summary>
        /// Filtruję dane i zmienia wiersze na elementy klasy Update
        /// </summary>
        /// <param name="filtr">Funckja określająca czy dany element ma przejść fitrowanie</param>
        /// <param name="numerKolumny">Po której kolumnie filtrujemy</param>
        /// <returns>Wiersze które przeszły filtrowanie</returns>
        private List<Update> FiltrujWewnętrzny(Func<string, bool> filtr, int numerKolumny)
        {
            List<Update> pomocniczaTabela = new List<Update>();
            for (int i = 0; i < tabela.Count; i++)
                if (filtr(tabela[i][numerKolumny]))
                    pomocniczaTabela.Add(new Update(tabela[i], i));
            return pomocniczaTabela;
        }

        /// <summary>
        /// Aktualizuję wiersz nowymi danymi, wrazie gdy dane są string.empty dane pole nie zostaję aktualizowane
        /// aby usuną dane z jakiegoś pola należy w danych wpisać null
        /// </summary>
        /// <param name="numerWiersza">Numer wiersza który aktualizujemy</param>
        /// <param name="dane">Dane którymi aktualizujemy wiersz</param>
        private void AktualizacjaWiersza(int numerWiersza, List<string> dane)
        {
            for (int i = 0; i < dane.Count; i++)
            {
                if (dane[i] != string.Empty)
                    tabela[numerWiersza][i] = dane[i];
            }
            return;
        }

        /// <summary>
        /// Zapisuje dane z tabeli do wskazanego pliku nadpisując go
        /// </summary>
        /// <param name="sciezka">Pełna ścieżka do pliku</param>
        /// <returns>Stan zapisu</returns>
        private void ZapiszTabeleDoPliku(string sciezka)
        {
            FileStream plik = new FileStream(sciezka, FileMode.Create);
            StreamWriter pisarz = new StreamWriter(plik);
            StringBuilder nagłówek = new StringBuilder(znakKomentarza);
            StringBuilder typy = new StringBuilder(znakKomentarza);
            StringBuilder wiersz = new StringBuilder();

            nagłówek.Append("nazwa" + znakiSeparacji[0]);
            for (int i = 0; i < iloscKolumn; i++)
                nagłówek.Append(nazwyKolumn[i] + znakiSeparacji[0]);
            pisarz.WriteLine(nagłówek.ToString());

            if (this.typyKolumn[0] != null)
            {
                typy.Append("typ" + znakiSeparacji[0]);
                for (int i = 0; i < iloscKolumn; i++)
                    typy.Append(this.typyKolumn[i].Name + znakiSeparacji[0]);
                pisarz.WriteLine(typy.ToString());
            }

            for (int i = 0; i < iloscRekordow; i++)
            {
                wiersz = new StringBuilder();
                for (int j = 0; j < iloscKolumn; j++)
                    wiersz.Append(tabela[i][j] + znakiSeparacji[0]);
                pisarz.WriteLine(wiersz.ToString());
            }
            pisarz.Close();
            plik.Close();            
        }


        /// <summary>
        /// Przepisuje ograniczoną ilość pół z jednej tabeli do drugiej
        /// </summary>
        /// <param name="tab">Tabla źródłowa</param>
        /// <param name="ile">Ilość pół do przepisania</param>
        /// <returns>Tabela wynikowa</returns>
        private string[] Przepisz(string[] tab, int ile)
        {
            if (tab.Length > ile) ile = tab.Length;
            string[] wynik = new string[ile];
            for (int i = 0; i < ile; i++) wynik[i] = tab[i];
            return wynik;
        }

        #endregion

        #endregion
    }

    class PSortowanie
    {
        public List<string> dana;
        public int wartosc;

        public PSortowanie(List<string> zrodlo, int waznosc)
        {
            dana = new List<string>(zrodlo);
            wartosc = waznosc;
        }

        public PSortowanie(PSortowanie oryginal)
        {
            dana = new List<string>(oryginal.dana);
            wartosc = oryginal.wartosc;
        }
    }

    class Update
    {
        public List<string> dana;
        public int numer;

        public Update(List<string> zrodlo, int numerWiersza)
        {
            dana = new List<string>(zrodlo);
            numer = numerWiersza;
        }
    }
}
