// WideMargin. Simple fast bible software.
// Copyright (C) 2011  Daniel Hughes
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using Mono.Data.Sqlite;
using WideMargin.MVCInterfaces;

namespace WideMargin.Database
{
	/// <summary>
	/// Class for creating, updating and querying the bible database.
	/// </summary>
	public class BibleDatabase : IDisposable
	{
		private IDbConnection _connection;
		private IDbCommand _searchCommand;
		private IDbCommand _searchFilteredCommand;
		private IDbCommand _chapterCommand;
		private IDbCommand _searchCount;
		private IDbCommand _getDaylyReadings;
		private IDbCommand _searchCountFiltered;
		
		/// <summary>
		/// Default constructor to enable unit testing.
		/// </summary>
		public BibleDatabase()
		{
		}
		
		/// <summary>
		/// Create a bible database.
		/// </summary>
		/// <param name='file'>
		/// Path to database file, creates one if file doesn't exist.
		/// </param>
		public BibleDatabase(string file)
		{
			if(!File.Exists(file))
			{
				CreateDatabase(file);
				return;
			}
			
			_connection = new SqliteConnection (string.Format("DbLinqProvider=Sqlite;Data Source={0}",file));
			_connection.Open();
			
			Upgrade(file);
		}
		
		/// <summary>
		/// Upgrade the specified file.
		/// </summary>
		/// <param name='file'>
		/// File.
		/// </param>
		private void Upgrade(string file)
		{
			//check if the table exists
			if(!SettingsTableExist())
			{
				RebuildDatabase(file);
				return;
			}			
		}
		
		/// <summary>
		/// Rebuilds the database from scratch
		/// </summary>
		/// <param name='file'>
		/// database file to rebuild.
		/// </param>
		private void RebuildDatabase(string file)
		{
			DisconnectAndCleanup();
			CreateDatabase(file);
		}
		
		/// <summary>
		/// Checks if the setting table exists,
		/// (the first version (version 0)did not have a settings table.
		/// </summary>
		/// <returns>true if the settings table exists</returns>
		private bool SettingsTableExist()
		{
			using(IDbCommand command = _connection.CreateCommand())
			{
				command.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name='Settings'";
				using(IDataReader reader = command.ExecuteReader())
				{
					return reader.Read();
				}
			}
		}
		
		/// <summary>
		/// Creates the database and imports the reading data and kjv text.
		/// </summary>
		/// <param name='file'>
		/// File.
		/// </param>
		private void CreateDatabase(string file)
		{
			if(File.Exists(file))
			{
				File.Delete(file);	
			}
			
			SqliteConnection.CreateFile(file);
			
			_connection = new SqliteConnection (string.Format("DbLinqProvider=Sqlite;Data Source={0}",file));
				
			_connection.Open();
			using(IDbCommand command = _connection.CreateCommand())
			{
				string sqlFileName = GetSqlFile();
				command.CommandText = File.ReadAllText(sqlFileName);
				command.ExecuteNonQuery();
			}
			
			ImportData();
		}
		
		/// <summary>
		/// Gets the sql file.
		/// </summary>
		/// <returns>
		/// The sql file.
		/// </returns>
		private static string GetSqlFile()
		{
			return WideMarginPaths.GetApplicationDataFile("BibleDatabase.sql");
		}
		
		/// <summary>
		/// Imports the database data (kjv text and reading planner)
		/// </summary>
		private void ImportData()
		{
			string versePerLinefile = WideMarginPaths.GetApplicationDataFile(@"kjv.rawtxt");
			string readingPlannerFile = WideMarginPaths.GetApplicationDataFile(@"ReadingPlanner.txt");
			
			var lookup = NameAbbreviationLookup.Get();
			
			//clean out anyting allready in the verse table

			var verses = 
				from line in File.ReadAllLines(versePerLinefile).Skip(1)
				let spaceIndex = line.IndexOf(' ')
				let content = line.Substring(spaceIndex, line.Length - spaceIndex)
				let colonIndex = line.IndexOf(":")
				let verse = line.Substring(colonIndex + 1, spaceIndex - colonIndex - 1)
				let bookLength = line.Length - line.SkipWhile( val => char.IsNumber(val)).SkipWhile( val => !char.IsNumber(val)).Count()
				let book = lookup.Lookup(line.Substring(0, bookLength))
				let chapter = line.Substring(bookLength, colonIndex - bookLength)
				select new Verse(book, int.Parse(chapter), int.Parse(verse), content);
			AddVerses(verses);
			
			AddReadingPlannerData(readingPlannerFile);
		}
		
		/// <summary>
		/// Create a bible database using an existing open connection
		/// </summary>
		/// <param name="connection">open connection to use</param>
		public BibleDatabase(IDbConnection connection)
		{
			_connection = connection;
		}
		
		/// <summary>
		/// Get the SearchPageResult for the first page.
		/// </summary>
		/// <param name='searchText'>
		/// Text to search for.
		/// </param>
		/// <returns>
		/// The first page.
		/// </returns>
		public SearchPageResult SearchFirstPage(string searchText)
		{
			var count = SearchCount(searchText);
			return Search(searchText, count, 10, 0);
		}
		
		/// <summary>
		/// Searches for the search term in the specified book.
		/// </summary>
		/// <returns>first page of results.</returns>
		/// <param name='searchTerm'>what to search for.</param>
		/// <param name='book'>book to search in.</param>
		public SearchPageResult SearchFirstPageFiltered(string searchTerm, string book)
		{
			var count = SearchCount(searchTerm, book);
			return Search(searchTerm, book, count, 10, 0);
		}
		
		/// <summary>
		/// Searches for searchText in the bible.
		/// </summary>
		/// <param name="searchText">Text to search for</param>
		/// <param name="pageSize">Number of results to show</param>
		/// <param name="pageNumber">Page number to get (Zero based)</param>
		/// <returns>search results</returns>
		public SearchPageResult Search(string searchText, IDictionary<string, int> resultCount, int pageSize, int pageNumber)
		{
			if(_searchCommand == null)
			{
				_searchCommand = _connection.CreateCommand();
				
				_searchCommand.CommandText = "SELECT * FROM Verse WHERE Contents MATCH @searchTerm LIMIT @limit OFFSET @offset";
				
				//searchTerm parameter
				IDbDataParameter searchTerm =_searchCommand.CreateParameter();
				searchTerm.ParameterName = "@searchTerm";
				searchTerm.DbType = DbType.String;
  				_searchCommand.Parameters.Add(searchTerm);
				
				//limit paramter
				IDbDataParameter limit = _searchCommand.CreateParameter();
				limit.ParameterName = "@limit";
				limit.DbType = DbType.Int32;
				_searchCommand.Parameters.Add(limit);
				
				//offset paramter
				IDbDataParameter offset = _searchCommand.CreateParameter();
				offset.ParameterName = "@offset";
				offset.DbType = DbType.Int32;
				_searchCommand.Parameters.Add(offset);
			}
			
			((IDataParameter)_searchCommand.Parameters["@searchTerm"]).Value = searchText;			
			((IDataParameter)_searchCommand.Parameters["@limit"]).Value = pageSize;
			((IDataParameter)_searchCommand.Parameters["@offset"]).Value = pageNumber * pageSize;
			
			List<IVerse> verses = new List<IVerse>();
			using(IDataReader reader = _searchCommand.ExecuteReader())
			{
				while (reader.Read())
		        {
		            verses.Add(new Verse(reader));
		        }	
			}
			return new SearchPageResult(verses, resultCount, pageSize, pageNumber, searchText);
		}
		
		/// <summary>
		/// Searches for searchText in the bible.
		/// </summary>
		/// <param name="searchText">Text to search for</param>
		/// <param name="pageSize">Number of results to show</param>
		/// <param name="pageNumber">Page number to get (Zero based)</param>
		/// <returns>search results</returns>
		public SearchPageResult Search(string searchText, string book, IDictionary<string, int> resultCount, int pageSize, int pageNumber)
		{
			if(_searchFilteredCommand == null)
			{
				_searchFilteredCommand = _connection.CreateCommand();
				
				_searchFilteredCommand.CommandText = "SELECT * FROM Verse WHERE Contents MATCH @searchTerm AND Book = @book LIMIT @limit OFFSET @offset";
				
				//searchTerm parameter
				IDbDataParameter searchTerm =_searchFilteredCommand.CreateParameter();
				searchTerm.ParameterName = "@searchTerm";
				searchTerm.DbType = DbType.String;
  				_searchFilteredCommand.Parameters.Add(searchTerm);
				
				//book parameter
				IDbDataParameter bookParameter =_searchFilteredCommand.CreateParameter();
				bookParameter.ParameterName = "@book";
				bookParameter.DbType = DbType.String;
  				_searchFilteredCommand.Parameters.Add(bookParameter);
				
				//limit paramter
				IDbDataParameter limit = _searchFilteredCommand.CreateParameter();
				limit.ParameterName = "@limit";
				limit.DbType = DbType.Int32;
				_searchFilteredCommand.Parameters.Add(limit);
				
				//offset paramter
				IDbDataParameter offset = _searchFilteredCommand.CreateParameter();
				offset.ParameterName = "@offset";
				offset.DbType = DbType.Int32;
				_searchFilteredCommand.Parameters.Add(offset);
			}
			
			((IDataParameter)_searchFilteredCommand.Parameters["@searchTerm"]).Value = searchText;
			((IDataParameter)_searchFilteredCommand.Parameters["@book"]).Value = book;	
			((IDataParameter)_searchFilteredCommand.Parameters["@limit"]).Value = pageSize;
			((IDataParameter)_searchFilteredCommand.Parameters["@offset"]).Value = pageNumber * pageSize;
			
			List<IVerse> verses = new List<IVerse>();
			using(IDataReader reader = _searchFilteredCommand.ExecuteReader())
			{
				while (reader.Read())
		        {
		            verses.Add(new Verse(reader));
		        }	
			}
			return new SearchPageResultFiltered(verses, resultCount, pageSize, pageNumber, searchText, book);
		}
		
		/// <summary>
		/// Finds the number of hits for a search term.
		/// </summary>
		/// <param name='searchText'>
		/// text to search for.
		/// </param>
		/// <returns>The number of hits</returns>
		private IDictionary<string, int> SearchCount(string searchText)
		{
			if(_searchCount == null)
			{
				_searchCount = _connection.CreateCommand();
				_searchCount.CommandText = "SELECT Book, Count(*) From Verse WHERE Contents MATCH @searchTerm GROUP BY Book";
				var searchTerm =_searchCount.CreateParameter();
				searchTerm.ParameterName = "@searchTerm";
				searchTerm.DbType = DbType.String;
  				_searchCount.Parameters.Add(searchTerm);
			}
			((IDataParameter)_searchCount.Parameters["@searchTerm"]).Value = searchText;

			var resultsPerBook = new Dictionary<string, int>();
			using(IDataReader reader = _searchCount.ExecuteReader())
			{
				while (reader.Read())
		        {
		            int bookIndex = reader.GetOrdinal("Book");
					string book = reader.GetString(bookIndex);
					
					int countIndex = reader.GetOrdinal("Count(*)");
					int count = reader.GetInt32(countIndex);
					resultsPerBook.Add (book, count);
		        }	
			}
			return resultsPerBook;
		}
		
		/// <summary>
		/// Finds the number of hits in the specified book
		/// </summary>
		/// <param name='searchText'>text to search for.</param>
		/// <param name='bookFilter'>book to search in</para>
		/// <returns>The number of hits</returns>
		private IDictionary<string, int> SearchCount(string searchText, string bookFilter)
		{
			if(_searchCountFiltered == null)
			{
				_searchCountFiltered = _connection.CreateCommand();
				_searchCountFiltered.CommandText = "SELECT Book, Count(*) From Verse WHERE Contents MATCH @searchTerm AND Book = @book GROUP BY Book";
				//search parameter
				var searchTerm =_searchCount.CreateParameter();
				searchTerm.ParameterName = "@searchTerm";
				searchTerm.DbType = DbType.String;
  				_searchCountFiltered.Parameters.Add(searchTerm);
				//book parameter
				var bookParameter =_searchCount.CreateParameter();
				bookParameter.ParameterName = "@book";
				bookParameter.DbType = DbType.String;
  				_searchCountFiltered.Parameters.Add(bookParameter);
			}
			((IDataParameter)_searchCountFiltered.Parameters["@searchTerm"]).Value = searchText;
			((IDataParameter)_searchCountFiltered.Parameters["@book"]).Value = bookFilter;
			
			var resultsPerBook = new Dictionary<string, int>();
			using(IDataReader reader = _searchCountFiltered.ExecuteReader())
			{
				while (reader.Read())
		        {
		            int bookIndex = reader.GetOrdinal("Book");
					string book = reader.GetString(bookIndex);
					
					int countIndex = reader.GetOrdinal("Count(*)");
					int count = reader.GetInt32(countIndex);
					resultsPerBook.Add (book, count);
		        }	
			}
			return resultsPerBook;
		}
		
		/// <summary>
		/// Gets the verses in a chapter.
		/// </summary>
		/// <param name='book'>Book the chapter is in</param>
		/// <param name='chapter'>Chapter to get the verses for.</param>
		/// <returns>The verses from teh chapter</returns>
		public IEnumerable<IVerse> GetChapter(string book, int chapter)
		{
			if(_chapterCommand == null)
			{
				_chapterCommand = _connection.CreateCommand();
				
				_chapterCommand.CommandText = "SELECT * FROM Verse WHERE Book=@book AND Chapter=@chapter";
				
				IDataParameter bookTerm = _chapterCommand.CreateParameter();
				bookTerm.ParameterName = "@book";
				bookTerm.DbType = DbType.String;
  				_chapterCommand.Parameters.Add(bookTerm);
				
				IDataParameter chapterTerm = _chapterCommand.CreateParameter();
				chapterTerm.ParameterName = "@chapter";
				chapterTerm.DbType = DbType.Int32;
  				_chapterCommand.Parameters.Add(chapterTerm);
			}

			((IDataParameter)_chapterCommand.Parameters["@book"]).Value = book;
			((IDataParameter)_chapterCommand.Parameters["@chapter"]).Value = chapter;
			
			List<IVerse> verses = new List<IVerse>();
			using(IDataReader reader = _chapterCommand.ExecuteReader())
			{
				while (reader.Read())
		        {
		            verses.Add(new Verse(reader));
		        }	
			}
			return verses;
		}
		
		/// <summary>
		/// Adds the verses to the database
		/// </summary>
		/// <param name='verses'>Verses to add to the database.</param>
		public void AddVerses(IEnumerable<Verse> verses)
		{
			using(var command = _connection.CreateCommand())
			{
				command.CommandText = "INSERT INTO Verse VALUES (@book, @chapter, @verse, @contents)";
				
				var bookParam = command.CreateParameter();
				bookParam.ParameterName = "@book";
				bookParam.DbType = DbType.String;
				command.Parameters.Add(bookParam);
				
				var chapterParam = command.CreateParameter();
				chapterParam.ParameterName = "@chapter";
				chapterParam.DbType = DbType.Int32;
				command.Parameters.Add(chapterParam);
				
				var verseParam = command.CreateParameter();
				verseParam.ParameterName = "@verse";
				verseParam.DbType = DbType.Int32;
				command.Parameters.Add(verseParam);
				
				var contentsParam = command.CreateParameter();
				contentsParam.ParameterName = "@contents";
				contentsParam.DbType = DbType.String;
				command.Parameters.Add(contentsParam);
				
				using(IDbTransaction transaction = _connection.BeginTransaction())
				{
					foreach(var verse in verses)
					{
						bookParam.Value = verse.Book;
						chapterParam.Value = verse.Chapter;	
						verseParam.Value = verse.VerseNumber;
						contentsParam.Value = verse.Contents;
						command.Transaction = transaction;
						command.ExecuteNonQuery();
					}
					transaction.Commit();
				}
			}
		}
		
		/// <summary>
		/// Gets the dayly readings for the specified date.
		/// </summary>
		/// <param name="dateTime">date to get readings for</param>
		/// <returns>dayly readings or empty if none found for given date</returns>
		public IEnumerable<DaylyReading> GetDaylyReading(DateTime dateTime)
		{
			if(_getDaylyReadings == null)
			{
				_getDaylyReadings = _connection.CreateCommand();
				_getDaylyReadings.CommandText = "SELECT FirstPortion, SecondPortion, ThirdPortion FROM ReadingPlanner WHERE Day=@day AND Month=@month";
				
				var dayParam = _getDaylyReadings.CreateParameter();
				dayParam.ParameterName = "@day";
				dayParam.DbType = DbType.Int32;
				_getDaylyReadings.Parameters.Add(dayParam);
				
				var monthParam = _getDaylyReadings.CreateParameter();
				monthParam.ParameterName = "@month";
				monthParam.DbType = DbType.Int32;
				_getDaylyReadings.Parameters.Add(monthParam);
			}
			
			((IDataParameter)_getDaylyReadings.Parameters["@day"]).Value = dateTime.Day;
			((IDataParameter)_getDaylyReadings.Parameters["@month"]).Value = dateTime.Month;
			
			using(IDataReader reader = _getDaylyReadings.ExecuteReader())
			{
				
				if(!reader.Read())
				{
					return new DaylyReading[]{};//return empty
				}
				
				List<DaylyReading> readings = new List<DaylyReading>();
				
				
				int firstIndex = reader.GetOrdinal("FirstPortion");
		    	string firstPortion = reader.GetString(firstIndex);
				readings.Add(DecodeDaylyReading(firstPortion));
				
				int secondIndex = reader.GetOrdinal("SecondPortion");
		    	string secondPortion = reader.GetString(secondIndex);
				readings.Add(DecodeDaylyReading(secondPortion));
				
				int thirdIndex = reader.GetOrdinal("ThirdPortion");
		    	string thirdPortion = reader.GetString(thirdIndex);
				readings.Add(DecodeDaylyReading(thirdPortion));
				
				return readings;
			}
		}
		
		/// <summary>
		/// Converts the dayly reading string into a DaylyReading
		/// </summary>
		/// <param name="reading">string in format Gen 2-4</param>
		/// <returns>Object representing the reading</returns>
		public DaylyReading DecodeDaylyReading(string reading)
		{
			if(reading == "2-3John")
			{
				//special 2 book case (only one)
				return new DaylyReading(new VerseIdentifier("2 John", 1, 1), "2-3 John");
			}
			int verse = 1;
			var parts = reading.Split(' ');
			string abbreviatedBookName = parts[0];
			string chapters = parts[1];
			int chapter;
			if(chapters.Contains(':'))
			{
				//verse range (ie psa 119:1-56)
				var chapterVerses = chapters.Split(':');
				chapter = int.Parse(chapterVerses[0]);
				verse = int.Parse(chapterVerses[1].Split('-')[0]);
			}
			else if(chapters.Contains("-"))
			{
				chapter = int.Parse(chapters.Split('-')[0]);	
			}
			else
			{
				chapter = int.Parse(chapters);	
			}

			string book = NameAbbreviationLookup.Get().Lookup(abbreviatedBookName);
			var verseId = new VerseIdentifier(book, chapter, verse);
			string fullReading = string.Format("{0} {1}", book, chapters);
			return new DaylyReading(verseId, fullReading);		                                  
		}
		
		/// <summary>
		/// Adds the reading planner data.
		/// </summary>
		/// <param name='readingPlannerFile'>Reading planner file.</param>
		internal void AddReadingPlannerData(string readingPlannerFile)
		{
			string[] days = File.ReadAllLines(readingPlannerFile);
			var dayRows = 
				from day in days
				let parts = day.Split(',')
				let dateParts = parts[0].Split('/')
				let dayOfMonth = int.Parse(dateParts[0])
				let month = int.Parse(dateParts[1])
				let firstPortion = parts[1]
				let secondPortion = parts[2]
				let thirdPortion = parts[3]
				select new
				{
					Day = dayOfMonth,
					Month = month,
					FirstPortion = firstPortion,
					SecondPortion = secondPortion,
					ThirdPortion = thirdPortion
				};
			
			using(var command = _connection.CreateCommand())
			{
				command.CommandText = "INSERT INTO ReadingPlanner VALUES (@day, @month, @firstPortion, @secondPortion, @thirdPortion)";
				
				var dayParam = command.CreateParameter();
				dayParam.ParameterName = "@day";
				dayParam.DbType = DbType.Int32;
				command.Parameters.Add(dayParam);
				
				var monthParam = command.CreateParameter();
				monthParam.ParameterName = "@month";
				monthParam.DbType = DbType.Int32;
				command.Parameters.Add(monthParam);
				
				var firstPortionParam = command.CreateParameter();
				firstPortionParam.ParameterName = "@firstPortion";
				firstPortionParam.DbType = DbType.String;
				command.Parameters.Add(firstPortionParam);
				
				var secondPortionParam = command.CreateParameter();
				secondPortionParam.ParameterName = "@secondPortion";
				secondPortionParam.DbType = DbType.String;
				command.Parameters.Add(secondPortionParam);
						
				var thirdPortionParam = command.CreateParameter();
				thirdPortionParam.ParameterName = "@thirdPortion";
				thirdPortionParam.DbType = DbType.String;
				command.Parameters.Add(thirdPortionParam);
				
				using(IDbTransaction transaction = _connection.BeginTransaction())
				{
					foreach(var day in dayRows)
					{
						dayParam.Value = day.Day;
						monthParam.Value = day.Month;
						firstPortionParam.Value = day.FirstPortion;
						secondPortionParam.Value = day.SecondPortion;
						thirdPortionParam.Value = day.ThirdPortion;
						command.Transaction = transaction;
						command.ExecuteNonQuery();
					}
					transaction.Commit();
				}
			}
				
		}
		
		/// <summary>
		/// Disconnects the and cleanup.
		/// </summary>
		private void DisconnectAndCleanup()
		{
			if(_chapterCommand != null)
			{
				_chapterCommand.Dispose();	
				_chapterCommand = null;
			}
			if(_connection != null)
			{
				_connection.Dispose();
				_connection = null;
			}
			if(_searchCommand != null)
			{
				_searchCommand.Dispose();	
				_searchCommand = null;
			}
			if(_searchCount != null)
			{
				_searchCount.Dispose();
				_searchCount = null;
			}
			if(_getDaylyReadings != null)
			{
				_getDaylyReadings.Dispose();
				_getDaylyReadings = null;
			}
			if(_searchCountFiltered != null)
			{
				_searchCountFiltered.Dispose();
				_searchCountFiltered = null;
			}
			if(_searchFilteredCommand != null)
			{
				_searchFilteredCommand.Dispose();
				_searchFilteredCommand = null;
			}
		}
		
		/// <summary>
		/// Dispose
		/// </summary>
		/// <param name='disposing'>True if disposing managed resources.</param>
		protected virtual void Dispose(bool disposing)
		{
			if(disposing)
			{
				DisconnectAndCleanup();
			}			
		}
		
		/// <summary>
		/// Releases all resource used by the <see cref="WideMargin.Database.BibleDatabase"/> object.
		/// </summary>
		/// <remarks>
		/// Call <see cref="Dispose"/> when you are finished using the <see cref="WideMargin.Database.BibleDatabase"/>. The
		/// <see cref="Dispose"/> method leaves the <see cref="WideMargin.Database.BibleDatabase"/> in an unusable state.
		/// After calling <see cref="Dispose"/>, you must release all references to the
		/// <see cref="WideMargin.Database.BibleDatabase"/> so the garbage collector can reclaim the memory that the
		/// <see cref="WideMargin.Database.BibleDatabase"/> was occupying.
		/// </remarks>
		public void Dispose()
		{
			GC.SuppressFinalize(this);
			Dispose(true);
		}
	}
}
