using System;
using System.IO;
using System.Collections;
using System.Data;

using HtmlHelp.ChmDecoding;

namespace HtmlHelp
{
	/// <summary>
	/// The class <c>HtmlHelpSystem</c> implements the main object for reading chm files
	/// </summary>
	public sealed class HtmlHelpSystem
	{
		/// <summary>
		/// Private shared instance of current HtmlHelpSystem class
		/// </summary>
		private static HtmlHelpSystem _current=null;
		/// <summary>
		/// Internal member storing the attached files
		/// </summary>
		private ArrayList _chmFiles = new ArrayList();
		/// <summary>
		/// Internal member storing a merged table of contents
		/// </summary>
		private TableOfContents _toc = new TableOfContents();
		/// <summary>
		/// Internal member storing a merged index
		/// </summary>
		private Index _index = new Index();
		/// <summary>
		/// URL prefix for specifying a chm destination
		/// </summary>
		private static string _urlPrefix = "ms-its:";
		/// <summary>
		/// Internal flag specifying if the system should use the tree-images list
		/// from HtmlHelp2. If false the standard CHM-Viewer pics will be used.
		/// </summary>
		private static bool _useHH2TreePics = false;
		/// <summary>
		/// Internal member storing the read information types
		/// </summary>
		private ArrayList _informationTypes = new ArrayList();
		/// <summary>
		/// Internal member storing the read categories
		/// </summary>
		private ArrayList _categories = new ArrayList();

		/// <summary>
		/// Gets/Sets the url prefix for specifying a chm destination
		/// </summary>
		public static string UrlPrefix
		{
			get { return _urlPrefix; }
			set { _urlPrefix = value; }
		}

		public CHMStream.CHMStream BaseStream
		{
			get 
			{
				CHMFile chm=(CHMFile)_chmFiles[0];
				return chm.BaseStream; 
			}
		}

		/// <summary>
		/// Gets/Sets the flag specifying if the system should use the tree-images list
		/// from HtmlHelp2. If false the standard CHM-Viewer pics will be used.
		/// </summary>
		public static bool UseHH2TreePics
		{
			get { return _useHH2TreePics; }
			set { _useHH2TreePics = value; }
		}

		/// <summary>
		/// Gets the current HtmlHelpSystem instance
		/// </summary>
		public static HtmlHelpSystem Current
		{
			get 
			{
				return _current;
			}
		}

		/// <summary>
		/// Standard constructor
		/// </summary>
		public HtmlHelpSystem() : this("")
		{
		}

		/// <summary>
		/// Constructor of the reader class
		/// </summary>
		/// <param name="chmFile">chm file to attach with the reader</param>
		public HtmlHelpSystem(string chmFile)
		{
			_current = this;
			OpenFile(chmFile);
		}


		/// <summary>
		/// Opens a chm file and creates
		/// </summary>
		/// <param name="chmFile">full file path of the chm file to open</param>
		/// <remarks>If you call this method, all existing merged files will be cleared.</remarks>
		public void OpenFile(string chmFile)
		{
			OpenFile(chmFile, null);
		}

		/// <summary>
		/// Opens a chm file and creates
		/// </summary>
		/// <param name="chmFile">full file path of the chm file to open</param>
		/// <param name="dmpInfo">dumping info</param>
		/// <remarks>If you call this method, all existing merged files will be cleared.</remarks>
		public void OpenFile(string chmFile, DumpingInfo dmpInfo)
		{
			if( File.Exists(chmFile ) )
			{
				_chmFiles.Clear();
				_toc.Clear();
				_index.Clear();
				_informationTypes.Clear();
				_categories.Clear();

				CHMFile newFile = new CHMFile(this, chmFile, dmpInfo);

				_toc = new TableOfContents( newFile.TOC );
				_index = new Index( newFile.IndexKLinks, newFile.IndexALinks );

				_chmFiles.Add(newFile);
				// add all infotypes and categories of the read file to this system instance
				MergeFileInfoTypesCategories(newFile);

				// check if the file has a merged files list
				if( newFile.MergedFiles.Length > 0 )
				{
					// extract the path of the chm file (usually merged files are in the same path)
					FileInfo fi = new FileInfo(chmFile);
					string sPath = fi.DirectoryName;

					for(int i=0; i<newFile.MergedFiles.Length; i++)
					{
						string sFile = newFile.MergedFiles[i];

						if(sFile.Length > 0)
						{
							if(sFile[1] != ':') // no full path setting
							{
								sFile = Path.Combine(sPath, sFile);
							}

							MergeFile(sFile, dmpInfo, true);
						}
					}

					// if (newFile.MergLinks.Count>0)
					// 	RecalculateMergeLinks(newFile);

					RemoveMergeLinks(); // clear all merge-links which have no target !
				}
			}
		}

		/// <summary>
		/// Merges a chm file to the current help contents
		/// </summary>
		/// <param name="chmFile">full file path of the chm file to merge</param>
		public void MergeFile(string chmFile)
		{
			MergeFile(chmFile, null);
		}

		/// <summary>
		/// Merges a chm file to the current help contents
		/// </summary>
		/// <param name="chmFile">full file path of the chm file to merge</param>
		/// <param name="dmpInfo">dumping info</param>
		public void MergeFile(string chmFile, DumpingInfo dmpInfo)
		{
			MergeFile(chmFile, dmpInfo, false);
		}

		/// <summary>
		/// Merges a chm file to the current help contents
		/// </summary>
		/// <param name="chmFile">full file path of the chm file to merge</param>
		/// <param name="dmpInfo">dumping info</param>
		/// <param name="mergedFileList">true if the merge is done because a merged file list 
		/// was found in the previously loaded CHM.</param>
		internal void MergeFile(string chmFile, DumpingInfo dmpInfo, bool mergedFileList)
		{
			if( File.Exists(chmFile ) )
			{
				if( _chmFiles.Count == 1)
				{
					// if we open the first file, we directly point into the toc and index of this file.
					// So that we don't merge the new toc's indexe's into the first file, we have to 
					// clone the internal arraylists first to a new instance of the toc/index holder classes.
					ArrayList atoc = _toc.TOC;
					ArrayList alinks = _index.ALinks;
					ArrayList klinks = _index.KLinks;

					_toc = new TableOfContents();
					_index = new Index();

					_toc.MergeToC( atoc );
					_index.MergeIndex( alinks, IndexType.AssiciativeLinks );
					_index.MergeIndex( klinks, IndexType.KeywordLinks );
				}

				CHMFile newFile = new CHMFile(this, chmFile, dmpInfo);

				if(mergedFileList) // if we've called this method due to a merged file list merge
				{
					RecalculateMergeLinks(newFile);

					_toc.MergeToC( newFile.TOC, _chmFiles );
					_index.MergeIndex( newFile.IndexALinks, IndexType.AssiciativeLinks );
					_index.MergeIndex( newFile.IndexKLinks, IndexType.KeywordLinks );

					_chmFiles.Add(newFile);

					// add all infotypes and categories of the read file to this system instance
					MergeFileInfoTypesCategories(newFile);
				}  
				else 
				{
					_toc.MergeToC( newFile.TOC, _chmFiles );
					_index.MergeIndex( newFile.IndexALinks, IndexType.AssiciativeLinks );
					_index.MergeIndex( newFile.IndexKLinks, IndexType.KeywordLinks );

					_chmFiles.Add(newFile);

					// add all infotypes and categories of the read file to this system instance
					MergeFileInfoTypesCategories(newFile);

					// check if the file has a merged files list
					if( newFile.MergedFiles.Length > 0 )
					{
						// extract the path of the chm file (usually merged files are in the same path)
						FileInfo fi = new FileInfo(chmFile);
						string sPath = fi.DirectoryName;

						for(int i=0; i<newFile.MergedFiles.Length; i++)
						{
							string sFile = newFile.MergedFiles[i];

							if(sFile.Length > 0)
							{
								if(sFile[1] != ':') // no full path setting
								{
									sFile = Path.Combine(sPath, sFile);
								}

								MergeFile(sFile, dmpInfo, true);
							}
						}

						RemoveMergeLinks(); // clear all merge-links which have no target !
					}
				}
			}
		}

		/// <summary>
		/// Checks all Merg-links read till now. Checks if the merg-link points to the 
		/// file <c>currentFile</c>. If yes the link will be replaced by the contents of the 
		/// merged file.
		/// </summary>
		/// <param name="currentFile">Current CHMFile instance</param>
		internal void RecalculateMergeLinks(CHMFile currentFile)
		{
			foreach(CHMFile curFile in _chmFiles)
			{
				if( curFile.MergLinks.Count > 0)
				{
					for(int i=0; i<curFile.MergLinks.Count; i++)
					{
						TOCItem curItem = curFile.MergLinks[i] as TOCItem;

						string sMerge = curItem.MergeLink;
						string [] sSplit = sMerge.Split( new char[]{':'} );

						string sFName = "";
						string sTarget = "";

						if( sSplit.Length > 3) // merge info contains path name
						{
							sFName = sSplit[0] + ":" + sSplit[1];
							sTarget = sSplit[3];
						} 
						else if( sSplit.Length == 3)// merge info contains only file name
						{
							FileInfo fi = new FileInfo(currentFile.ChmFilePath);
							string sPath = fi.DirectoryName;

							string sFile = sSplit[0];

							if(sFile.Length > 0)
							{
								if(sFile[1] != ':') // no full path setting
								{
									sFile = Path.Combine(sPath, sFile);
								}
							}

							sFName = sFile;
							sTarget = sSplit[2];
						}

						ArrayList arrToc = null;
						if( (sFName.Length>0) && (sTarget.Length>0) )
						{
						// if this link points into the current file
							if( sFName.ToLower() == currentFile.ChmFilePath.ToLower() )
							{
								if(sTarget.ToLower().IndexOf(".hhc") >= 0)
								{
									string sfCheck = sTarget;

									// remove prefixing ./
									while( (sfCheck[0]=='.') || (sfCheck[0]=='/') )
									{
										sfCheck = sfCheck.Substring(1);
									}

									if( currentFile.ContentsFile.ToLower() != sfCheck )
									{
										arrToc = currentFile.ParseHHC( sTarget );

										if( arrToc.Count > 0)
										{
										}
									} 
									else 
									{
										arrToc = currentFile.TOC;
									}

									// target points to a complete TOC
									int nCnt = 0;

									foreach(TOCItem chkItem in arrToc)
									{
										if(nCnt == 0)
										{
											curItem.AssociatedFile = currentFile;
											curItem.Children = chkItem.Children;
											curItem.ChmFile = currentFile.ChmFilePath;
											curItem.ImageIndex = chkItem.ImageIndex;
											curItem.Local = chkItem.Local;
											curItem.MergeLink = chkItem.MergeLink;
											curItem.Name = chkItem.Name;
											curItem.TocMode = chkItem.TocMode;
											curItem.TopicOffset = chkItem.TopicOffset;

											MarkChildrenAdded(chkItem.Children, curFile.MergLinks);
										} 
										else 
										{
											ArrayList checkList = null;

											if(curItem.Parent != null)
												checkList = curItem.Parent.Children;
											else
												checkList = curFile.TOC;
											
											int nIdx = checkList.IndexOf(curItem);
											if((nIdx+nCnt)>checkList.Count)
												checkList.Add(chkItem);
											else
												checkList.Insert(nIdx+nCnt, chkItem);
											
											curFile.MergLinks.Add(chkItem);
											MarkChildrenAdded(chkItem.Children, curFile.MergLinks);
										}

										nCnt++;
									}
								} 
								else 
								{
								
									// target points to a single topic
									TOCItem chkItem = currentFile.GetTOCItemByLocal(sTarget);
									if(chkItem != null)
									{
										curItem.AssociatedFile = currentFile;
										curItem.Children = chkItem.Children;
										curItem.ChmFile = currentFile.ChmFilePath;
										curItem.ImageIndex = chkItem.ImageIndex;
										curItem.Local = chkItem.Local;
										curItem.MergeLink = chkItem.MergeLink;
										curItem.Name = chkItem.Name;
										curItem.TocMode = chkItem.TocMode;
										curItem.TopicOffset = chkItem.TopicOffset;

										curFile.MergLinks.Add(chkItem);
										MarkChildrenAdded(chkItem.Children, curFile.MergLinks);
									}
								}
							}
						}
					}
				}
			}
		}

		/// <summary>
		/// Adds sub-items of an TOC-entry to the merg-linked list. 
		/// This will mark this item as "added" during the extra merge run 
		/// of the HtmlHelpSystem class.
		/// </summary>
		/// <param name="tocs">TOCItem list</param>
		/// <param name="merged">Arraylist which holds the merged-items</param>
		internal void MarkChildrenAdded(ArrayList tocs, ArrayList merged)
		{
			foreach(TOCItem curItem in tocs)
			{
				if(!merged.Contains(curItem))
				{
					merged.Add(curItem);

					MarkChildrenAdded(curItem.Children, merged);
				}
			}
		}

		/// <summary>
		/// Removes merge-links from the toc of files which were not loaded
		/// </summary>
		internal void RemoveMergeLinks()
		{
			foreach(CHMFile curFile in _chmFiles)
			{
				if( curFile.MergLinks.Count > 0)
				{
					while(curFile.MergLinks.Count > 0)
					{
						TOCItem curItem = curFile.MergLinks[0] as TOCItem;
						if(curItem.MergeLink.Length > 0)
							curFile.RemoveTOCItem(curItem);

						curFile.MergLinks.RemoveAt(0);
					}
				}
			}
		}

		/// <summary>
		/// Merges the information types and categories read by the CHMFile instance 
		/// into the system instance
		/// </summary>
		/// <param name="chmFile">file instance</param>
		private void MergeFileInfoTypesCategories(CHMFile chmFile)
		{
			if(chmFile.HasInformationTypes)
			{
				for(int i=0; i<chmFile.InformationTypes.Count;i++)
				{
					InformationType curType = chmFile.InformationTypes[i] as InformationType;
					InformationType sysType = GetInformationType( curType.Name );

					if( sysType == null)
						_informationTypes.Add(curType);
					else
						curType.ReferenceCount++;
				}
			}

			if(chmFile.HasCategories)
			{
				for(int i=0; i<chmFile.Categories.Count;i++)
				{
					Category curCat = chmFile.Categories[i] as Category;
					Category sysCat = GetCategory( curCat.Name );

					if(sysCat == null)
						_categories.Add(curCat);
					else
						curCat.ReferenceCount++;
				}
			}
		}

		/// <summary>
		/// Removes the information types and categories read by the CHMFile instance 
		/// </summary>
		/// <param name="chmFile">file instance</param>
		private void RemoveFileInfoTypesCategories(CHMFile chmFile)
		{
			if(chmFile.HasInformationTypes)
			{
				for(int i=0; i<chmFile.InformationTypes.Count;i++)
				{
					InformationType curType = chmFile.InformationTypes[i] as InformationType;
					InformationType sysType = GetInformationType( curType.Name );

					if(sysType != null)
					{
						sysType.ReferenceCount--;

						if(sysType.ReferenceCount<=0)
							_informationTypes.Remove(sysType);
					}
				}
			}

			if(chmFile.HasCategories)
			{
				for(int i=0; i<chmFile.Categories.Count;i++)
				{
					Category curCat = chmFile.Categories[i] as Category;
					Category sysCat = GetCategory( curCat.Name );

					if(sysCat != null)
					{
						sysCat.ReferenceCount--;

						if(sysCat.ReferenceCount<=0)
							_categories.Remove(sysCat);
					}
				}
			}
		}

		/// <summary>
		/// Removes a chm file from the internal file collection
		/// </summary>
		/// <param name="chmFile">full file path of the chm file to remove</param>
		public void RemoveFile(string chmFile)
		{
			int nIdx = -1;
			CHMFile removeInstance=null;

			foreach(CHMFile curFile in _chmFiles)
			{
				nIdx++;

				if( curFile.ChmFilePath.ToLower() == chmFile.ToLower() )
				{
					removeInstance = curFile;
					break;
				}
			}

			if(nIdx >= 0)
			{
				_toc.Clear(); // forces a rebuild of the merged toc
				_index.Clear(); // force a rebuild of the merged index
				
				RemoveFileInfoTypesCategories(removeInstance);
				_chmFiles.RemoveAt(nIdx);
			}
		}

		/// <summary>
		/// Closes all files and destroys TOC/index
		/// </summary>
		public void CloseAllFiles()
		{
			for(int i=0; i < _chmFiles.Count; i++)
			{
				CHMFile curFile = _chmFiles[i] as CHMFile;

				_chmFiles.RemoveAt(i);
				curFile.Dispose();
				i--;
			}

			_chmFiles.Clear();
			_toc.Clear();
			_index.Clear();
			_informationTypes.Clear();
			_categories.Clear();
		}

		/// <summary>
		/// Gets an array of loaded chm files. 
		/// </summary>
		public CHMFile[] FileList
		{
			get
			{
				CHMFile[] ret = new CHMFile[ _chmFiles.Count ];
				for(int i=0;i<_chmFiles.Count;i++)
					ret[i] = (CHMFile)_chmFiles[i];

				return ret;
			}
		}

		/// <summary>
		/// Returns true if the HtmlHelpSystem instance contains 1 or more information types
		/// </summary>
		public bool HasInformationTypes
		{
			get { return (_informationTypes.Count>0); }
		}

		/// <summary>
		/// Returns true if the HtmlHelpSystem instance contains 1 or more categories
		/// </summary>
		public bool HasCategories
		{
			get { return (_categories.Count>0); }
		}

		/// <summary>
		/// Gets an ArrayList of <see cref="InformationType">InformationType</see> items
		/// </summary>
		public ArrayList InformationTypes
		{
			get { return _informationTypes; }
		}

		/// <summary>
		/// Gets an ArrayList of <see cref="Category">Category</see> items
		/// </summary>
		public ArrayList Categories
		{
			get { return _categories; }
		}

		/// <summary>
		/// Gets the information type specified by its name
		/// </summary>
		/// <param name="name">name of the information type to receive</param>
		/// <returns>Returns the Instance for the name or null if not found</returns>
		public InformationType GetInformationType(string name)
		{
			if(HasInformationTypes)
			{
				for(int i=0; i<_informationTypes.Count;i++)
				{
					InformationType iT = _informationTypes[i] as InformationType;

					if(iT.Name == name)
						return iT;
				}
			}

			return null;
		}

		/// <summary>
		/// Gets the category specifiyd by its name
		/// </summary>
		/// <param name="name">name of the category</param>
		/// <returns>Returns the Instance for the name or null if not found</returns>
		public Category GetCategory(string name)
		{
			if(HasCategories)
			{
				for(int i=0; i<_categories.Count;i++)
				{
					Category cat = _categories[i] as Category;

					if(cat.Name == name)
						return cat;
				}
			}

			return null;
		}

		/// <summary>
		/// Gets the default topic
		/// </summary>
		public string DefaultTopic
		{
			get
			{
				if( _chmFiles.Count > 0 )
				{
					foreach(CHMFile curFile in _chmFiles)
					{
						if( curFile.DefaultTopic.Length > 0)
						{
							return curFile.FormURL( curFile.DefaultTopic );
						}
					}
				}

				return "about:blank";
			}
		}

		/// <summary>
		/// Gets a merged table of contents of all opened chm files
		/// </summary>
		public TableOfContents TableOfContents
		{
			get
			{
				if( _chmFiles.Count > 0 )
				{
					if( _toc.Count() <= 0)
					{
						// merge toc of files
						foreach(CHMFile curFile in _chmFiles)
						{
							_toc.MergeToC( curFile.TOC );
						}
					}
				}

				return _toc;
			}
		}

		/// <summary>
		/// Gets a merged index  of all opened chm files
		/// </summary>
		public Index Index
		{
			get
			{
				if( _chmFiles.Count > 0 )
				{
					if( (_index.Count(IndexType.KeywordLinks)+_index.Count(IndexType.AssiciativeLinks)) <= 0)
					{
						// merge index files
						foreach(CHMFile curFile in _chmFiles)
						{
							_index.MergeIndex( curFile.IndexKLinks, IndexType.KeywordLinks);
							_index.MergeIndex( curFile.IndexALinks, IndexType.AssiciativeLinks);
						}
					}
				}

				return _index;
			}
		}

		/// <summary>
		/// Gets a flag if the current instance offers a table of contents
		/// </summary>
		public bool HasTableOfContents
		{
			get 
			{
				return (TableOfContents.Count() > 0);
			}
		}

		/// <summary>
		/// Gets a flag if the current instance offers an index
		/// </summary>
		public bool HasIndex
		{
			get 
			{
				return (HasALinks || HasKLinks);
			}
		}

		/// <summary>
		/// Gets a flag if the index holds klinks
		/// </summary>
		public bool HasKLinks
		{
			get
			{
				return (_index.Count(IndexType.KeywordLinks) > 0);
			}
		}

		/// <summary>
		/// Gets a flag if the index holds alinks
		/// </summary>
		public bool HasALinks
		{
			get
			{
				return (_index.Count(IndexType.AssiciativeLinks) > 0);
			}
		}

		/// <summary>
		/// Gets a flag if the current instance supports fulltext searching
		/// </summary>
		public bool FullTextSearch
		{
			get 
			{
				bool bRet = false;

				foreach(CHMFile curFile in _chmFiles)
				{
					bRet |= curFile.FullTextSearch;
				}

				return bRet;
			}
		}

		/// <summary>
		/// Performs a full-text search over the chm files
		/// </summary>
		/// <param name="words">words to search</param>
		/// <param name="partialMatches">true if partial word should be matched also 
		/// ( if this is true a search of 'support' will match 'supports', otherwise not )</param>
		/// <param name="titleOnly">true if titles only</param>
		/// <returns>A DataTable containing the search hits</returns>
		public DataTable PerformSearch(string words, bool partialMatches, bool titleOnly)
		{
			return PerformSearch(words, -1, partialMatches, titleOnly);
		}

		/// <summary>
		/// Performs a full-text search over the chm files
		/// </summary>
		/// <param name="words">words to search</param>
		/// <param name="MaxResults">maximal number of hits to return</param>
		/// <param name="partialMatches">true if partial word should be matched also 
		/// ( if this is true a search of 'support' will match 'supports', otherwise not )</param>
		/// <param name="titleOnly">true if titles only</param>
		/// <returns>A DataTable containing the search hits</returns>
		public DataTable PerformSearch(string words, int MaxResults, bool partialMatches, bool titleOnly)
		{
			if( ! FullTextSearch )
				return null;

			DataTable dtResult = null;

			int nCnt = 0;

			foreach(CHMFile curFile in _chmFiles)
			{
				if(nCnt == 0)
				{
					if(curFile.FullTextSearchEngine.CanSearch)
					{
						if(curFile.FullTextSearchEngine.Search(words, MaxResults, partialMatches, titleOnly))
						{
							dtResult = curFile.FullTextSearchEngine.Hits;
							dtResult.DefaultView.Sort = "Rating DESC";
						}
					}
				}
				else
				{
					if(curFile.FullTextSearchEngine.CanSearch)
					{
						if(curFile.FullTextSearchEngine.Search(words, MaxResults, partialMatches, titleOnly))
						{
							DataTable table = curFile.FullTextSearchEngine.Hits;
							
							// append rows from 2nd file
							foreach(DataRow curRow in table.Rows)
							{
								dtResult.ImportRow( curRow );
							}

							dtResult.DefaultView.Sort = "Rating DESC";

							// adjust max hits
							if(MaxResults >= 0)
							{
								if(dtResult.DefaultView.Count > MaxResults)
								{
									for(int i=MaxResults-1; i<dtResult.DefaultView.Count-1;i++)
									{
										if(dtResult.DefaultView[i].Row.RowState != DataRowState.Deleted)
										{
											dtResult.DefaultView[i].Row.Delete();
											dtResult.DefaultView[i].Row.AcceptChanges();
											i--;
										}
									}

									dtResult.AcceptChanges();
									dtResult.DefaultView.Sort = "Rating DESC";
								}
							}
						}
					}
				}

				nCnt++;
			}

			return dtResult;
		}
	}
}