using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Globalization;
using System.Reflection;
using HakInstaller.Utilities;
using NWN;
using NWN.FileTypes;
namespace HakInstaller
{
	/// 
	/// This class impelments logic to resolve file conflicts between content
	/// being added to the module from HIFs and content that already exists
	/// in the module.  Support is provided for the following:
	/// 
	/// TLK: If there are multiple tlk files between all content the class
	/// will attempt to generate a single tlk file by merging all non-empty
	/// rows into one tlk.  If multiple tlk files use the same row then
	/// the merge will fail.
	/// 
	/// 2DA: If HIFs and the module both have copies of the same 2da file
	/// then the class will attempt to generate a merge hak containing
	/// merged copies of all of the conflicting 2da's.  The merged 2da's
	/// will be built from the topmost version of the 2da from each HIF and
	/// the module.  If two content sources change the same row in a 2da
	/// and the rows are not exactly the same then the merge for that 2da
	/// will fail, but it will still attempt to merge other conflicting 2das.
	/// 
	public class ConflictResolver
	{
		#region public properties/methods
		/// 
		/// Class constructor
		/// 
		/// The interface used to provide progress information
		/// to the user
		public ConflictResolver(IHakInstallProgress progress)
		{
			this.progress = progress;
			conflictHak = string.Empty;
			conflictHakDir = string.Empty;
			conflictHakMessageShown = false;
		}
		/// 
		/// Attempts to resolve tlk file conflicts between the module tlk and any
		/// tlk's defined in the hifs.  It does this by attempting to build a
		/// new tlk file containing all of the tlk entries from all tlks.  If there
		/// are no overlapping entries in the tlks's then this will succeed and
		/// the name of the new tlk will be returned, if there are overlaps then
		/// this will fail and string.Empty will be returned.
		/// 
		/// The module for which we are resolving conflicts
		/// The list of tlk files from the HIFs being
		/// installed.
		/// The name of the merge tlk file, or string.Empty if a merge tlk
		/// could not be generated.
		public string ResolveTlkConflict(Erf module, string[] hifTlks)
		{
			try
			{
				// Let the user know we are building a merge tlk.
				progress.SetMessage("Building merge tlk for module\n'{0}'.", 
					Path.GetFileNameWithoutExtension(module.FileName));
                
				// Create an array to hold all of the tlk objects.
				Tlk[] tlks = new Tlk[hifTlks.Length];
				// Load all of the tlk's.
				for (int i = 0; i < hifTlks.Length; i++)
					tlks[i] = Tlk.LoadTlk(NWNInfo.GetFullFilePath(hifTlks[i]));
				// Generate the name of the new tlk file.
				string newTlkFileName = GetFileName(module, "tlk");
				if (null == newTlkFileName) 
					throw new NWNException("Cannot create new tlk file for module {0}", module.FileName);
				// Get the largest entry count in all of the tlk files, we cannot move any of the tlk
				// entries from where they are so the new tlk file will have as many entries as the
				// largest source tlk file.
				int count = 0;
				foreach (Tlk tlk in tlks)
					if (tlk.Count > count) count = tlk.Count;
				// Create a new tlk file and add all of the entries from all of the tlk files
				// to it.
				Tlk newTlk = new Tlk(count);
				for (int i = 0; i < count; i++)
				{
					// Check to see which tlk file contains this entry.  If multiple tlk
					// files contain this entry we cannot merge the tlk's
					Tlk.TlkEntry entry = null;
					foreach (Tlk tlk in tlks)
					{
						// Ignore empty entries.
						if (i >= tlk.Count || tlk.IsEmpty(i)) continue;
						// If we haven't gotten an entry for this row yet
						// then save this entry.  If we have then we cannot
						// do the merge.
						if (null == entry)
							entry = tlk[i];
						else
						{
							// Check to see if the data in two entries is the same.
							// If it is then both tlk files have the same string
							// data in the entry and we can still do the merge.  This
							// is most likely to happen at index 0 where many tlk
							// files place "Bad Strref".
							if (0 == string.Compare(entry.Text, tlk[i].Text, true, CultureInfo.InvariantCulture))
								continue;
							throw new InvalidOperationException();
						}
					}
					// Save the entry in our new tlk file.
					if (null != entry) newTlk[i] = entry;
				}
				// Save the new tlk file and return it's file name.
				newTlk.SaveAs(NWN.NWNInfo.GetFullFilePath(newTlkFileName));
				return newTlkFileName;
			}
			catch (InvalidOperationException)
			{
				// If an error occurs return string.Empty to indicate we couldn't generate
				// a merge tlk.
				return string.Empty;
			}
		}
		/// 
		/// Attempts to resolve tlk file conflicts between the module tlk and any
		/// tlk's defined in the hifs.  It does this by attempting to build a
		/// new tlk file containing all of the tlk entries from all tlks.  If there
		/// are no overlapping entries in the tlks's then this will succeed and
		/// the name of the new tlk will be returned, if there are overlaps then
		/// this will fail and string.Empty will be returned.
		/// 
		/// The HIFs being added to the module
		/// The module for which we are resolving conflicts
		/// The module info for the module
		/// The list of files in conflict
		/// The name of the merge hak file, or string.Empty if a merge tlk
		/// could not be generated.
		public string ResolveConflicts(HakInfo[] hakInfos, Erf module, ModuleInfo moduleInfo,
			OverwriteWarningCollection conflicts)
		{
			try
			{
				// Reset the message shown flag so we show the message once.
				conflictHakMessageShown = false;
				// Generate the name of the conflict resolution hak and the directory
				// in which to place the files that will be added to the hak.
				conflictHak = GetFileName(module, "hak");
				conflictHakDir = NWN.NWNInfo.GetFullFilePath(conflictHak) + ".temp";
				OverwriteWarningCollection copy = conflicts.Clone();
				foreach (OverwriteWarning conflict in copy)
				{
					// Check to see if we can attempt to resolve the conflict, if
					// we can then attempt to resolve it, and if the resolution is
					// successful then remove the conflict from the collection.
					switch (Path.GetExtension(conflict.File).ToLower())
					{
						case ".2da":
							DisplayConflictHakMessage(module);
							if (Resolve2daConflict(hakInfos, module, moduleInfo, conflict))
								conflicts.Remove(conflict);
							break;
					}
				}
				// Get all of the files in the conflict hak directory, if there are none
				// then there is no conflict hak.
				if (!Directory.Exists(conflictHakDir)) return string.Empty;
				string[] files = Directory.GetFiles(conflictHakDir);
				if (0 == files.Length) return string.Empty;
				// We have some resolved conflicts make the merge hak.
				Erf hak = Erf.New(Erf.ErfType.HAK, "Auto-generated merge hak");
				foreach (string file in files)
					hak.AddFile(file, true);
				hak.SaveAs(NWN.NWNInfo.GetFullFilePath(conflictHak));
				return conflictHak;
			}
			finally
			{
				if (Directory.Exists(conflictHakDir)) Directory.Delete(conflictHakDir, true);
			}
		}
		#endregion
		#region private fields/properties/methods
		private bool conflictHakMessageShown;
		private IHakInstallProgress progress;
		private string conflictHak;
		private string conflictHakDir;
		/// 
		/// Displays the building merge hak message once.
		/// 
		/// The module that we are resolving conflicts for
		private void DisplayConflictHakMessage(Erf module)
		{
			if (conflictHakMessageShown) return;
			// Let the user know we are building a merge tlk.
			progress.SetMessage("Building merge hak for module\n'{0}'.", 
				Path.GetFileNameWithoutExtension(module.FileName));
			conflictHakMessageShown = true;
		}
		/// 
		/// Generates a name for a conflict resolution file (hak or tlk).  It generates
		/// a name that is currently unused on disk.
		/// 
		/// The module for which to create a new tlk file
		/// The extension of the file to get a name for
		/// The tlk file name, or null if the name could not be created
		private string GetFileName(Erf module, string extension)
		{
			// Use the first 12 characters of the module name as the base.  Tlk
			// files can only have 16 character names max, and we want to save 4
			// characters for the index.
			string namePrefix = Path.GetFileNameWithoutExtension(module.FileName);
			if (namePrefix.Length > 12) namePrefix = namePrefix.Substring(0, 12);
			namePrefix = namePrefix.Replace(" ", "_");
			for (int i = 1; i <= 9999; i ++)
			{
				// Build the name using the name prefix and i, if the file name
				// does not exist in the tlk directory then return it.
				string name = string.Format("{0}{1:0000}.{2}", namePrefix, i, extension);
				if (!File.Exists(NWNInfo.GetFullFilePath(name))) return name;
			}
			return null;
		}
		/// 
		/// Extracts a 2da file from the specified hak file, returning a
		/// 2da object containing the 2da data.
		/// 
		/// The hak from which to extract the 2da
		/// The file name of the 2da
		/// A 2da object for the 2da file or null if the hak does not
		/// contain the 2da file
		private _2DA Extract2da(string hak, string fileName)
		{
			// Extract the 2da file from the hak and create an in memory copy.
			MemoryStream stream = Erf.GetFile(NWN.NWNInfo.GetFullFilePath(hak), fileName);
			return null == stream ? null : _2DA.Load2da(stream);
		}
		/// 
		/// Gets the 2da file from the module by looking at all of the haks in the
		/// module info and checking each hak for the file.
		/// 
		/// The module info for the module
		/// The name of the 2da to get
		/// A 2da object for the 2da or null if the 2da is not in any
		/// of the module's haks
		private _2DA Get2da(ModuleInfo moduleInfo, string fileName)
		{
			// Get the list of haks in the module and loop through all of them
			// trying to load the 2da, as soon as we get it return it.
			StringCollection haks = moduleInfo.Haks;
			if (null == haks) return null;
			foreach (string hak in haks)
			{
				_2DA twoDA = Extract2da(hak + ".hak", fileName);
				if (null != twoDA) return twoDA;
			}
			return null;
		}
		/// 
		/// Gets the 2da file from the HIF by looking at all of the haks in the
		/// HIF and checking each hak for the file.
		/// 
		/// The HIF
		/// The name of the 2da to get
		/// A 2da object for the 2da or null if the 2da is not in any
		/// of the HIF's haks
		private _2DA Get2da(HakInfo hakInfo, string fileName)
		{
			// Get the list of haks in the HIF and loop through all of them
			// trying to load the 2da, as soon as we get it return it.
			StringCollection haks = hakInfo.ModuleProperties["hak"];
			if (null == haks) return null;
			foreach (string hak in haks)
			{
				_2DA twoDA = Extract2da(hak, fileName);
				if (null != twoDA) return twoDA;
			}
			return null;
		}
		/// 
		/// Attempts to resolve conflicts for a 2da file.  It does this be attempting to
		/// merge all duplicate copies of the 2da file into one merge 2da file.
		/// 
		/// The HIFs being added to the module
		/// The module
		/// The module info for the module
		/// The 2da file in conflict
		private bool Resolve2daConflict(HakInfo[] hakInfos, Erf module, ModuleInfo moduleInfo,
			OverwriteWarning conflict)
		{
			try
			{
				// Create an array list and get the 2da from the module,
				// adding it to the list if we get it.
				ArrayList list = new ArrayList();
				_2DA twoDA = Get2da(moduleInfo, conflict.File);
				if (null != twoDA) list.Add(twoDA);
				// Now get all of the copies of the 2da from the various HIFs and
				// add them as well.
				foreach (HakInfo hakInfo in hakInfos)
				{
					twoDA = Get2da(hakInfo, conflict.File);
					if (null != twoDA) list.Add(twoDA);
				}
				// Load the BioWare version of the the 2da to use as a baseline, if the
				// file isn't in the bioware directory then we will have to make due w/o
				// it just make a blank 2da with the correct schema.
				_2DA bioware = LoadBioWare2da(conflict.File);
				// At this point we have all relevent copies of the conflicting 2da loaded into
				// memory, we now need to generate a merge 2da if possible.
				_2DA merge = Merge2das(bioware, list);
				if (null == merge) return false;
				// We have successfully merged all of the 2das, save the merge 2da and
				// return true.
				if (!Directory.Exists(conflictHakDir)) Directory.CreateDirectory(conflictHakDir);
				merge.SaveAs(Path.Combine(conflictHakDir, conflict.File));
				return true;
			}
			catch (Exception)
			{
				return false;
			}
		}
		/// 
		/// Loads a bioware 2da file.
		/// 
		/// The name of the 2da to load
		/// A _2DA object representing the file.
		private _2DA LoadBioWare2da(string name)
		{
			Stream stream = NWN.FileTypes.BIF.KeyCollection.GetFile(name);
			_2DA bioware = _2DA.Load2da(stream);
			return bioware;
		}
		/// 
		/// Attempts to merge all of the 2da's in the passed array list into 1 merge
		/// 2da, by combining all of the non-empty rows in each 2da.  If 2 2da's have
		/// changes the same row then the merge will fail.
		/// 
		/// The bioware baseline version of the 2da
		/// The list of 2da's to merge
		/// The merged 2da or null if the 2da's cannot be merged
		private _2DA Merge2das(_2DA baseline, ArrayList list)
		{
			// Create a flat list to have a strongly typed list of 2da's.
			_2DA[] merges = new _2DA[list.Count];
			list.CopyTo(merges);
			// Figure out the maximum number of rows we have to deal with
			int rows = baseline.Rows;
			foreach (_2DA merge in merges)
				rows = System.Math.Max(rows, merge.Rows);
			// Create the output 2da.
			_2DA output = new _2DA(baseline.Schema);
			output.Pad(rows);
			// Loop through all rows attempting to merge each row into the
			// output 2da.
			for (int i = 0; i < rows; i++)
			{
				StringCollection mergedRow = null;
				_2DA useForOutput = null;
				foreach (_2DA merge in merges)
				{
					// Make an attempt to filter out junk rows with things
					// such as "reserved", "deleted", etc in their labels.
					// These often conflict but are really empty rows.
					if (IsJunkRow(merge, i)) continue;
					// If we have gone past the end of this 2da or the
					// row is an empty row then ignore it.
					if (i >= merge.Rows || merge.IsEmpty(i)) continue;
					
					// If this is a row from the bioware version of the 2da
					// and the data is the same as the bioware 2da then
					// ignore this row in the 2da.
					if (i < baseline.Rows &&
						_2DA.CompareRow(baseline, i, merge, i, true)) continue;
					// If we get here we have a non-empty row that differs from
					// one of the bioware rows.  Only 1 2da file per row can
					// get past this point for use to be able to do a successful
					// merge, if 2 2da's get here then 2 have changed the same
					// row and we cannot merge.
					// If we don't have any proposed row data yet then
					// save this 2da's row data.
					if (null == useForOutput)
					{
						useForOutput = merge;
						continue;
					}
					// If we get here we have 2 2da's that want to change the same row.
					// Our only hope for a successful merge is that the data in the
					// 2 2da's is identical.
					if (_2DA.CompareRow(useForOutput, i, merge, i, true)) continue;
					// We already have an output 2da, which means that 2 2da's have
					// changed the same row, attempt to glue all of the merge changes
					// together.  If we cannot generate a merged row then return null.
					mergedRow = GenerateMergeRow(baseline, list, i);
					if (null == mergedRow) return null;
					// If we get here we have generated a merge row for all 2da's
					// so we don't need to look at the data in this row any further
					// break out of the loopo and use the mergedRow.
					break;
				}
				// If we have merge 2da to copy from then copy the
				// cell data.  If we don't have a merge 2da but the row is
				// withing the baseline 2da then copy the baseline data. 
				// Otherwise don't copy any data.
				if (null != mergedRow)
					output.CopyRow(mergedRow, i);
				else if (null != useForOutput)
					output.CopyRow(useForOutput, i, i);
				else if (i < baseline.Rows)
					output.CopyRow(baseline, i, i);
			}
			return output;
		}
		/// 
		/// Attempts to generate a merge row by taking all of the alterations made
		/// to the bioware row from the merge 2da's and incorporating them into 1
		/// row.  This will work unless 2 different 2da's change the same column
		/// in the row, which will make the merge fail.
		/// 
		/// The bioware baseline 2da
		/// The list of 2da's being merged
		/// The row for which to generate a merge row
		/// The merged row, or null if a merge row could not be
		/// generated.
		private StringCollection GenerateMergeRow(_2DA baseline, ArrayList list, int row)
		{
			try
			{
				// We cannot merge if the row is not in the baseline.
				if (row > baseline.Rows) return null;
				// Create a copy of the merge row in the baseline 2da.
				StringCollection resultRow = new StringCollection();
				StringCollection baselineRow = baseline.GetRowData(row);
				foreach (string s in baselineRow)
					resultRow.Add(s);
			
				// Create a bool array to keep track of which columns
				// we modify.
				bool[] writtenTo = new bool[resultRow.Count];
				for (int i = 0; i < writtenTo.Length; i++)
					writtenTo[i] = false;
				foreach (_2DA merge in list)
				{
					// Get the row from the merge 2da.
					StringCollection mergeRow = merge.GetRowData(row);
					// If the collections do not have the same length then
					// fail the merge, the added column may not be at the end.
					if (mergeRow.Count != resultRow.Count) return null;
					// Loop through all of the columns.
					for (int i = 1; i < resultRow.Count; i++)
					{
						// Ignore empty data cells in the merge row.
						if (_2DA.Empty == mergeRow[i]) continue;
						// Compare the cell value against the baseline.  If it is the
						// same then ignore it. (the result row starts out as the baseline
						// so we do not need to set these values, and we need to ignore
						// them to detect double writes to the same cell)
						if (_2DA.CompareCell(baselineRow[i], mergeRow[i], true))
							continue;
						// Compare the cells from the result row and the merge row,
						// if they are different then we need to copy the merge
						// row's value into the result row.  However, if a previous
						// merge 2da has modified this column then we have 2 different
						// 2da's wanting non-bioware default values in the same
						// column, if that happens there is no way to merge.
						if (!_2DA.CompareCell(mergeRow[i], resultRow[i], true))
						{
							// If we've already changed the bioware default for this
							// column we cannot merge return null.
							if (writtenTo[i])
								return null;
							else
							{
								// Overwrite the bioware default for this column and
								// save the fact that we have changed this column
								resultRow[i] = mergeRow[i];
								writtenTo[i] = true;
							}
						}
					}
				}
				// If we get here we were able to take all of the various 2da
				// modifications to the bioware row and make 1 merge row with all
				// of the changes, return it.
				return resultRow;
			}
			catch (Exception)
			{
				return null;
			}
		}
		/// 
		/// Returns true if the row is a junk row.
		/// 
		/// The 2da to test
		/// The row to test
		/// True if the row is a junk row.
		private bool IsJunkRow(_2DA file, int row)
		{
			if (row >= file.Rows) return false;
			int index = file.GetIndex("LABEL");
			if (-1 == index) index = file.GetIndex("NAME");
			if (-1 == index) return false;
			// Check for common labels indicating that it is a junk row.
			string value = file[row, index].ToLower();
			if (-1 != value.IndexOf("deleted") ||
				-1 != value.IndexOf("reserved") ||
				-1 != value.IndexOf("user")) return true;
			
			return false;
		}
		#endregion
	}
}