using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Globalization;
using System.Reflection;
using System.Threading;
using HakInstaller.Utilities;
using NWN;
using NWN.FileTypes;
using NWN.FileTypes.Gff;

namespace HakInstaller
{
	/// <summary>
	/// This class stores the data for a hif conflict.
	/// </summary>
	public class HifConflict
	{
		#region public properties/methods
		/// <summary>
		/// Gets the name of the module in conflict
		/// </summary>
		public string Module { get { return module; } }

		/// <summary>
		/// Gets the name of the hif in conflict
		/// </summary>
		public string Hif { get { return hif; } }

		/// <summary>
		/// Gets the version of the hif installed in the module
		/// </summary>
		public float InstalledVersion { get { return installedVersion; } }

		/// <summary>
		/// Gets the version of the hif that is going to be installed
		/// </summary>
		public float CurrentVersion { get { return currentVersion; } }

		/// <summary>
		/// Class constructor.
		/// </summary>
		/// <param name="module">The module in conflict</param>
		/// <param name="hif">The hif in conflict</param>
		/// <param name="installedVersion">The version of the hif installed in the module</param>
		/// <param name="currentVersion">The version of the hif that is going to be installed</param>
		public HifConflict(string module, string hif, float installedVersion, float currentVersion)
		{
			this.module = module;
			this.hif = hif;
			this.installedVersion = installedVersion;
			this.currentVersion = currentVersion;
		}

		/// <summary>
		/// Override of ToString() that returns a nice formatted value.
		/// </summary>
		/// <returns></returns>
		public override string ToString()
		{
			System.Text.StringBuilder b = new System.Text.StringBuilder();
			b.AppendFormat("'{0}' contains '{1}'{2}{3}", module, hif,
				0 == installedVersion ? "" : " version ",
				0 == installedVersion ? "" : installedVersion.ToString("0.00"));
			return b.ToString();
		}
		#endregion

		#region private fields/properties/methods
		private string module;
		private string hif;
		private float installedVersion;
		private float currentVersion;
		#endregion
	}


	/// <summary>
	/// This class defines a type safe collection of HifConflict objects.
	/// </summary>
	public class HifConflictCollection: CollectionBase
	{
		#region public properties/methods
		/// <summary>
		/// Indexer to index into the collection to get FileConflict objects.
		/// </summary>
		public HifConflict this[int index] 
		{ get { return InnerList[index] as HifConflict; } }

		/// <summary>
		/// Default constructor
		/// </summary>
		public HifConflictCollection()
		{}

		/// <summary>
		/// Adds a conflict to the collection.
		/// </summary>
		/// <param name="conflict"></param>
		public void Add(HifConflict conflict)
		{
			InnerList.Add(conflict);
		}
		#endregion
	}


	/// <summary>
	/// This class stores the data for a file conflict, and how to resolve the
	/// conflict.
	/// </summary>
	public class FileConflict
	{
		#region public properties/methods
		public string ModuleFile { get { return moduleFile; } }

		public string HakFile { get { return hakFile; } }

		public string FileName { get { return Path.GetFileName(moduleFile); } }

		public bool ReplaceFile
		{
			get { return replaceFile; }
			set { replaceFile = value; }
		}

		/// <summary>
		/// Class constructor
		/// </summary>
		/// <param name="moduleFile">The module file in conflict</param>
		/// <param name="hakFile">The hak file in conflict</param>
		public FileConflict(string moduleFile, string hakFile)
		{
			this.moduleFile = moduleFile;
			this.hakFile = hakFile;
			this.replaceFile = true;
		}

		/// <summary>
		/// Replace ToString() with something more reasonable.
		/// </summary>
		/// <returns>The FileName property</returns>
		public override string ToString()
		{
			return FileName;
		}
		#endregion

		#region private fields/properties/methods
		private string moduleFile;
		private string hakFile;
		private bool replaceFile;
		#endregion
	}


	/// <summary>
	/// This class defines a type save collection of FileConflict objects.
	/// </summary>
	public class FileConflictCollection: CollectionBase
	{
		#region public properties/methods
		/// <summary>
		/// Indexer to index into the collection to get FileConflict objects.
		/// </summary>
		public FileConflict this[int index] 
		{ get { return InnerList[index] as FileConflict; } }

		/// <summary>
		/// Default constructor
		/// </summary>
		public FileConflictCollection()
		{}

		/// <summary>
		/// Adds a conflict to the collection.
		/// </summary>
		/// <param name="conflict"></param>
		public void Add(FileConflict conflict)
		{
			InnerList.Add(conflict);
		}

		/// <summary>
		/// Generates a copy of the FileConflictCollection
		/// </summary>
		/// <returns></returns>
		public FileConflictCollection Clone()
		{
			FileConflictCollection copy = new FileConflictCollection();
			foreach (object o in InnerList)
				copy.InnerList.Add(o);
			return copy;
		}

		/// <summary>
		/// Removes a conflict from the collection.
		/// </summary>
		/// <param name="conflict">The conflict to remove</param>
		public void Remove(FileConflict conflict)
		{
			InnerList.Remove(conflict);
		}
		#endregion
	}


	/// <summary>
	/// This class contains the information needed to describe an overwrite warning.
	/// This warns the user that one file is going to get overwritten by another,
	/// but they have no choice in which overwrites which (as opposed to a FileConflict
	/// where they do).
	/// </summary>
	public class OverwriteWarning
	{
		#region public properties/methods
		/// <summary>
		/// Gets the file being overwritten.
		/// </summary>
		public string File { get { return file; } }

		/// <summary>
		/// Gets the source who's file is being overwritten.
		/// </summary>
		public string Source { get { return source; } }

		/// <summary>
		/// gets the file that is overwriting the source.
		/// </summary>
		public string Replacer { get { return replacer; } }

		/// <summary>
		/// Class constructor.
		/// </summary>
		/// <param name="file">The file getting overwritten</param>
		/// <param name="source">The source hak/module/erf</param>
		/// <param name="replacer">The overwriting hak/module/erf</param>
		public OverwriteWarning(string file, string source, string replacer)
		{
			this.file = file;
			this.source = source;
			this.replacer = replacer;
		}

		/// <summary>
		/// Override of ToString() to give back a formatted value.
		/// </summary>
		/// <returns></returns>
		public override string ToString()
		{
			System.Text.StringBuilder b = new System.Text.StringBuilder();
			b.AppendFormat("{0} will overwrite {1} in {2}", replacer, file, source);
			return b.ToString();
		}
		#endregion

		#region private fields/properties/methods
		private string file;
		private string source;
		private string replacer;
		#endregion
	}


	/// <summary>
	/// This class defines a type safe collection of OverwriteWarning objects.
	/// </summary>
	public class OverwriteWarningCollection: CollectionBase
	{
		#region public properties/methods
		/// <summary>
		/// Indexer to get the index'th OverwriteWarning.
		/// </summary>
		public OverwriteWarning this[int index]
		{
			get { return InnerList[index] as OverwriteWarning; }
		}

		/// <summary>
		/// Adds an OverwriteWarning.
		/// </summary>
		/// <param name="warning"></param>
		public void Add(OverwriteWarning warning)
		{
			InnerList.Add(warning);
		}

		/// <summary>
		/// Makes a clone of the collection
		/// </summary>
		/// <returns>The clone</returns>
		public OverwriteWarningCollection Clone()
		{
			OverwriteWarningCollection copy = new OverwriteWarningCollection();
			foreach (object o in InnerList)
				copy.InnerList.Add(o);
			return copy;
		}

		/// <summary>
		/// Removes an overwrite warning from the collection
		/// </summary>
		/// <param name="warning">The warning to remove</param>
		public void Remove(OverwriteWarning warning)
		{
			InnerList.Remove(warning);
		}
		#endregion
	}


	/// <summary>
	/// This enum defines the different types of warnings that
	/// </summary>
	public enum OverwriteWarningType
	{
		HifsOverwritesModule,
		ModuleOverwritesHifs
	}


	/// <summary>
	/// Interface used to display progress information for the install.
	/// </summary>
	public interface IHakInstallProgress
	{
		#region properties/methods
		/// <summary>
		/// Gets whether the user cancelled the install.
		/// </summary>
		bool IsCancelled { get; }

		/// <summary>
		/// Gets/sets the number of steps for the progress bar.
		/// </summary>
		int ProgressSteps { get; set; }

		/// <summary>
		/// Advances the progress bar 1 step.
		/// </summary>
		void Step();

		/// <summary>
		/// Sets the currently displayed progress message.
		/// </summary>
		/// <param name="format">Format string</param>
		/// <param name="args">Message arguments</param>
		void SetMessage(string format, params object[] args);

		/// <summary>
		/// This methods should ask the user for confirmation of replacing
		/// the listed files in the module with files from sources in the
		/// hif files, as this operation may break the module.
		/// </summary>
		/// <param name="replacedFiles">The list of file conflicts.  The method
		/// may alter the list to indicate what the resolution of the conflict
		/// should be on a file by file basis.</param>
		/// <returns>true if the files should be replaced, false if adding
		/// the hak(s) to the module should be aborted</returns>
		bool ShouldReplaceFiles(FileConflictCollection conflicts);

		/// <summary>
		/// This method should ask the user for confirmation of overwriting
		/// the listed files.  If fatal is true then there is no confirmation,
		/// it is just an informational message that the operation must be aborted.
		/// </summary>
		/// <param name="warnings">The list of warnings</param>
		/// <param name="fatal">True if the warnings are fatal</param>
		/// <param name="type">The type of overwrite being confirmed</param>
		/// <returns>True if the operation should proceed</returns>
		bool ShouldOverwrite(OverwriteWarningCollection warnings, bool fatal, 
			OverwriteWarningType type);

		/// <summary>
		/// Displays an error message to the user.
		/// </summary>
		/// <param name="error">The error message to display</param>
		void DisplayErrorMessage(string error);

		/// <summary>
		/// Displays a message to the user.
		/// </summary>
		/// <param name="error">The message to display</param>
		void DisplayMessage(string message);
		#endregion
	}


	/// <summary>
	/// Exception used by the hak installer to cancel the install, when
	/// the IHakInstallProgress.IsCancelled property returns true.
	/// </summary>
	internal class InstallCancelledException: Exception
	{
		#region public properties/methods
		public InstallCancelledException() {}
		#endregion
	}


	/// <summary>
	/// Delegate for methods that handle the setting of properties.
	/// </summary>
	internal delegate void PropertyHandler(Erf module, object source, string property, 
		StringCollection values);


	/// <summary>
	/// This attribute is used to tag methods that handle module properties
	/// in the HakInstaller.
	/// </summary>
	[AttributeUsage(AttributeTargets.Method,
		 AllowMultiple = true,
		 Inherited = false)]
	internal class PropertyHandlerAttribute: Attribute
	{
		#region public properties/methods
		/// <summary>
		/// Gets the name of the object containing the property.
		/// </summary>
		public string Object { get { return obj; } }

		/// <summary>
		/// Gets the name of the property.
		/// </summary>
		public string Property { get { return property; } }

		/// <summary>
		/// Class constructor
		/// </summary>
		/// <param name="obj">The object containing the property</param>
		/// <param name="property">The name of the property</param>
		public PropertyHandlerAttribute(string obj, string property)
		{
			this.obj = obj;
			this.property = property;
		}
		#endregion

		#region public static methods
		/// <summary>
		/// Gets the handler collection for the specified object.  This collection
		/// contains all of the methods tagged with the [PropertyHandler] attribute.
		/// </summary>
		/// <param name="sourceObject">The source object for which to get the
		/// collection</param>
		/// <returns>The property handler collection</returns>
		public static ObjectProperyHandlerDictionary GetHandlerCollection(object sourceObject)
		{
			ObjectProperyHandlerDictionary objects = new ObjectProperyHandlerDictionary();

			// Get all of the methods of the type and look for ones that have the [DBQuery] attached.
			Type sourceType = sourceObject.GetType();
			MethodInfo[] methods = sourceType.GetMethods (
				BindingFlags.Public | BindingFlags.NonPublic | 
				BindingFlags.Static | BindingFlags.Instance);
			foreach (MethodInfo method in methods)
			{
				// Try to get the method's [PropertyHandler] attribute.  If it 
				// doesn't have one then ignore it.
				Attribute[] atts = method.GetCustomAttributes (
					typeof (PropertyHandlerAttribute), false) as Attribute[];
				if (null == atts || 0 == atts.Length) continue;

				// Create a delegate for the method.
				PropertyHandler handler = (PropertyHandler) (method.IsStatic ?
					PropertyHandler.CreateDelegate (typeof (PropertyHandler), method) :
					PropertyHandler.CreateDelegate(typeof (PropertyHandler), sourceObject, method.Name));

				// Add the handler once for each property it handles.  Handlers may
				// handle more than one property thus the attribute array may have
				// multiple entries.
				foreach (Attribute att in atts)
				{
					PropertyHandlerAttribute handlerAtt = (PropertyHandlerAttribute) att;

					// Look up the handler dictionary for the object, if there isn't one
					// yet then create on.
					PropertyHandlerDictionary handlers = objects[handlerAtt.Object];
					if (null == handlers)
					{
						handlers = new PropertyHandlerDictionary(handlerAtt.Object);
						objects.Add(handlerAtt.Object, handlers);
					}

					handlers.Add(handlerAtt.Property, handler);
				}
			}

			return objects;
		}
		#endregion

		#region private fields/properties/methods
		private string obj;
		private string property;
		#endregion
	}


	/// <summary>
	/// This class defines a dictionary of PropertyHandler objects,
	/// allowing for the handler for a particular property to be looked up.
	/// </summary>
	internal class PropertyHandlerDictionary: DictionaryBase
	{
		#region public properties/methods
		/// <summary>
		/// Gets the object for which this collection contains property handlers.
		/// </summary>
		public string Object { get { return obj; } }

		/// <summary>
		/// Indexer to look up a handler for a property.
		/// </summary>
		public PropertyHandler this[string property]
		{
			get { return InnerHashtable[property.ToLower()] as PropertyHandler; }
		}

		/// <summary>
		/// Default constructor
		/// </summary>
		/// <param name="obj">The type of object that this dictionary
		/// contains handlers for</param>
		public PropertyHandlerDictionary(string obj)
		{
			this.obj = obj;
		}

		/// <summary>
		/// Adds a handler to the collection
		/// </summary>
		/// <param name="property">The property the handler handles</param>
		/// <param name="handler">The handler</param>
		public void Add(string property, PropertyHandler handler)
		{
			InnerHashtable.Add(property.ToLower(), handler);
		}
		#endregion

		#region private fields/properties/methods
		private string obj;
		#endregion
	}


	/// <summary>
	/// This class defines a dictionary collectoin of PropertyHandlerDictionary
	/// objects, each of those handling properties for a particular object.
	/// </summary>
	internal class ObjectProperyHandlerDictionary: DictionaryBase
	{
		#region public properties/methods
		/// <summary>
		/// Indexer to look up a handler dictionary for an object.
		/// </summary>
		public PropertyHandlerDictionary this[string obj]
		{
			get { return InnerHashtable[obj.ToLower()] as PropertyHandlerDictionary; }
		}

		/// <summary>
		/// Default constructor
		/// </summary>
		public ObjectProperyHandlerDictionary()
		{
		}

		/// <summary>
		/// Adds a handler to the collection
		/// </summary>
		/// <param name="obj">The object that the handlers handle properties for</param>
		/// <param name="handlers">The handler collection</param>
		public void Add(string obj, PropertyHandlerDictionary handlers)
		{
			InnerHashtable.Add(obj.ToLower(), handlers);
		}
		#endregion
	}


	/// <summary>
	/// Summary description for HakInstaller.
	/// </summary>
	public class HakInstaller
	{
		#region public static methods
		/// <summary>
		/// This method checks to see if any of the proposed hifs are installed on any of the
		/// given modules, returning a collection of conflicts that can be displayed to the user.
		/// </summary>
		/// <param name="proposedHifs">The list of hifs that the user wants to add to the modules</param>
		/// <param name="modules">The list of modules</param>
		/// <returns>A collection containing the list of conflicts, or null if there are no
		/// conflicts</returns>
		public static HifConflictCollection CheckInstalledHifs(HakInfo[] proposedHifs, string[] modules)
		{
			// Force the thread to use the invariant culture to make the install
			// code work on foreign language versions of windows.
			CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
			Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
			try
			{
				HifConflictCollection conflicts = null;
				foreach (string module in modules)
				{
					// Load the module info data and create an object for it.
					MemoryStream stream = Erf.GetFile(NWNInfo.GetFullFilePath(module), ModuleInfo.FileName);
					if (null == stream) NWN.FileTypes.Tools.NWNLogger.Log(10, "HakInstaller.CheckInstalledHifs, Erf.GetFile() returned null!!!");
					ModuleInfo info = new ModuleInfo(stream);

					// Load the installed hifs in module if any.
					string[] installedHifs;
					float[] installedVersions;
					info.GetInstalledHakInfos(out installedHifs, out installedVersions);

					// Create a StringCollection of the proposed hifs so we can use IndexOf(),
					// then check to see if there are any hif conflicts, if there are then
					// add them to the conflict list.
					StringCollection proposedHifsColl = new StringCollection();
					foreach (HakInfo hif in proposedHifs)
						proposedHifsColl.Add(Path.GetFileNameWithoutExtension(hif.Name).ToLower());
					for (int i = 0; i < installedHifs.Length; i++)
					{
						if (proposedHifsColl.Contains(installedHifs[i].ToLower()))
						{
							HifConflict conflict = new HifConflict(module, 
								installedHifs[i], installedVersions[i], 0);
							if (null == conflicts) conflicts = new HifConflictCollection();
							conflicts.Add(conflict);
						}
					}
				}

				return conflicts;
			}
			finally
			{
				Thread.CurrentThread.CurrentCulture = currentCulture;
			}
		}

		/// <summary>
		/// Installs the listed haks (defined by hif files) on the listed
		/// modules.  
		/// </summary>
		/// <param name="hifs">The list of haks to add</param>
		/// <param name="modules">The list of modules to add the haks to</param>
		/// <param name="progress">An interface used to an object used to display
		/// progress information, or null if no progress information is desired</param>
		public static void InstallHaks(HakInfo[] hifs, string[] modules,
			IHakInstallProgress progress)
		{
			// Force the thread to use the invariant culture to make the install
			// code work on foreign language versions of windows.
			CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
			Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
			try
			{
				// If no progress was given then use a dummy one which does nothing.
				if (null == progress) progress = new DummyProgress();

				// Invoke the private method on the singleton to do all the real work.
				Singleton.DoInstall(hifs, modules, progress);
			}
			finally
			{
				Thread.CurrentThread.CurrentCulture = currentCulture;
			}
		}

		/// <summary>
		/// Installs the listed haks (defined by hif files) on the module.
		/// </summary>
		/// <param name="hifs">The list of haks to add</param>
		/// <param name="moduleFile">The module to add the haks to</param>
		/// <param name="progress">An interface used to an object used to display
		/// progress information, or null if no progress information is desired</param>
		public static void InstallHaks(HakInfo[] hifs, string moduleFile,
			IHakInstallProgress progress)
		{
			// Force the thread to use the invariant culture to make the install
			// code work on foreign language versions of windows.
			CultureInfo currentCulture = Thread.CurrentThread.CurrentCulture;
			Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
			try
			{
				// If no progress was given then use a dummy one which does nothing.
				if (null == progress) progress = new DummyProgress();

				// Invoke the private method on the singleton to do all the real work.
				Singleton.DoInstall(hifs, new string[] { moduleFile }, progress);
			}
			finally
			{
				Thread.CurrentThread.CurrentCulture = currentCulture;
			}
		}
		#endregion

		#region module property handlers
		/// <summary>
		/// Property handler for all of the module event properties.  It sets
		/// the specified events on the appropriate event handler, creating
		/// another script to execute all scripts in the chain if there are
		/// multiple scripts on the event.
		/// </summary>
		/// <param name="module">The module being modified</param>
		/// <param name="source">The source object, a ModuleInfo object in this case</param>
		/// <param name="property">The property to set</param>
		/// <param name="values">The collection containing the property values</param>
		[PropertyHandler("Module", "OnAcquireItem")]
		[PropertyHandler("Module", "OnActivateItem")]
		[PropertyHandler("Module", "OnClientEnter")]
		[PropertyHandler("Module", "OnClientLeave")]
		[PropertyHandler("Module", "OnCutsceneAbort")]
		[PropertyHandler("Module", "OnHeartbeat")]
		[PropertyHandler("Module", "OnModuleLoad")]
		[PropertyHandler("Module", "OnModuleStart")]
        [PropertyHandler("Module", "OnPlayerChat")]
		[PropertyHandler("Module", "OnPlayerDeath")]
		[PropertyHandler("Module", "OnPlayerDying")]
		[PropertyHandler("Module", "OnPlayerEquipItem")]
		[PropertyHandler("Module", "OnPlayerLevelUp")]
		[PropertyHandler("Module", "OnPlayerRest")]
		[PropertyHandler("Module", "OnPlayerUnEquipItem")]
		[PropertyHandler("Module", "OnPlayerRespawn")]
		[PropertyHandler("Module", "OnUnaquireItem")]
		[PropertyHandler("Module", "OnUserDefined")]
		private void Module_SetEvent(Erf module, object source, string property, 
			StringCollection values)
		{
			ModuleInfo moduleInfo = (ModuleInfo) source;

			// Get the current event handler on the module, if any.
			string currentEvent = moduleInfo[property];

			// Get the total number of event handlers.
			int count = values.Count + (string.Empty == currentEvent ? 0 : 1);

			// If there is only 1 handler then this is easy, just set the handler.
			if (1 == count)
			{
				moduleInfo[property] = values[0];
				return;
			}

			// There are multiple events, we must build a new script that
			// invokes the events.
			string newScript = CreateExecuteScript(module, property, currentEvent, values);
			moduleInfo[property] = newScript;
		}

		/// <summary>
		/// Property handler for the cache property, it adds all of the scripts to
		/// the module's cache list.
		/// </summary>
		/// <param name="module">The module being modified</param>
		/// <param name="source">The source object, a ModuleInfo object in this case</param>
		/// <param name="property">The property to set</param>
		/// <param name="values">The collection containing the property values</param>
		[PropertyHandler("Module", "Cache")]
		private void Module_ScriptCache(Erf module, object source, string property,
			StringCollection values)
		{
			ModuleInfo moduleInfo = (ModuleInfo) source;

			// Copy the haks to a flat array and add them to the module.
			string[] scripts = new string[values.Count];
			for (int i = 0; i < scripts.Length; i++)
				scripts[i] = (values[i].ToLower());
			moduleInfo.AddToCache(scripts);
		}

		/// <summary>
		/// Property handler for the module's custom tlk property.  It sets
		/// the first string in the collection to be the module's custom tlk.
		/// </summary>
		/// <param name="module">The module being modified</param>
		/// <param name="source">The source object, a ModuleInfo object in this case</param>
		/// <param name="property">The property to set</param>
		/// <param name="values">The collection containing the property values</param>
		[PropertyHandler("Module", "CustomTlk")]
		private void Module_CustomTlk(Erf module, object source, string property,
			StringCollection values)
		{
			// If we have a merge tlk then do not set the custom tlk, the
			// conflict resolution code for tlk's did that already.
			if (string.Empty != mergeTlk) return;

			string tlk = Path.GetFileNameWithoutExtension(values[0].ToLower());

			// If the tlk is being set to the same tlk then do nothing.
			ModuleInfo moduleInfo = (ModuleInfo) source;
			if (0 == string.Compare(moduleInfo.CustomTlk, tlk, true, CultureInfo.InvariantCulture)) return;

			// Check to see if the module already has a custom tlk file, if it does
			// then we are dead; we cannot change it w/o breaking the module but the
			// hak won't run w/o it's custom tlk either.
			if (string.Empty != moduleInfo.CustomTlk)
				throw new NWNException(
					"The module {0} already contains a custom tlk {1}, hak cannot be added",
					moduleInfo.Name, values[0]);

			moduleInfo.CustomTlk = tlk;
		}

		/// <summary>
		/// Property handler for the module's hak property.  It adds all of the haks
		/// to the module.
		/// </summary>
		/// <param name="module">The module being modified</param>
		/// <param name="source">The source object, a ModuleInfo object in this case</param>
		/// <param name="property">The property to set</param>
		/// <param name="values">The collection containing the property values</param>
		[PropertyHandler("Module", "Hak")]
		private void Module_Hak(Erf module, object source, string property,
			StringCollection values)
		{
			ModuleInfo moduleInfo = (ModuleInfo) source;

			// Copy the haks to a flat array and add them to the module.
			string[] haks = new string[values.Count];
			for (int i = 0; i < haks.Length; i++)
				haks[i] = Path.GetFileNameWithoutExtension(values[i].ToLower());
			moduleInfo.AddHaks(haks);
		}

		/// <summary>
		/// Property handler for the module's hak property.  It adds all of the haks
		/// to the module.
		/// </summary>
		/// <param name="module">The module being modified</param>
		/// <param name="source">The source object, a ModuleInfo object in this case</param>
		/// <param name="property">The property to set</param>
		/// <param name="values">The collection containing the property values</param>
		[PropertyHandler("Module", "Areas")]
		private void Module_Areas(Erf module, object source, string property,
			StringCollection values)
		{
			ModuleInfo moduleInfo = (ModuleInfo) source;

			// Copy the haks to a flat array and add them to the module.
			string[] areas = new string[values.Count];
			for (int i = 0; i < areas.Length; i++)
				areas[i] = values[i].ToLower();
			moduleInfo.AddAreas(areas);
		}
		#endregion

		#region private nested classes
		/// <summary>
		/// This class implements a null IHakInstallProgress implementation, i.e.
		/// all of the properties and methods on the interface are no-ops.
		/// </summary>
		private class DummyProgress: IHakInstallProgress
		{
			#region implementation
			/// <summary>
			/// Gets whether the user cancelled the install.
			/// </summary>
			bool IHakInstallProgress.IsCancelled { get { return false;} }

			/// <summary>
			/// Gets/sets the number of steps for the progress bar.
			/// </summary>
			int IHakInstallProgress.ProgressSteps { get { return steps; } set { steps = value;} }

			/// <summary>
			/// Advances the progress bar 1 step.
			/// </summary>
			void IHakInstallProgress.Step() {}

			/// <summary>
			/// Sets the currently displayed progress message.
			/// </summary>
			/// <param name="format">Format string</param>
			/// <param name="args">Message arguments</param>
			void IHakInstallProgress.SetMessage(string format, params object[] args) {}

			/// <summary>
			/// This methods should ask the user for confirmation of replacing
			/// the listed files in the module with files from sources in the
			/// hif files, as this operation may break the module.
			/// </summary>
			/// <param name="replacedFiles">The list of replaced files</param>
			/// <returns>true if the files should be replaced, false if adding
			/// the hak(s) to the module should be aborted</returns>
			bool IHakInstallProgress.ShouldReplaceFiles(FileConflictCollection conflicts)
			{
				return true;
			}

			/// <summary>
			/// This method should ask the user for confirmation of overwriting
			/// the listed files.  If fatal is true then there is no confirmation,
			/// it is just an informational message that the operation must be aborted.
			/// </summary>
			/// <param name="warnings">The list of warnings</param>
			/// <param name="fatal">True if the warnings are fatal</param>
			/// <param name="type">The type of overwrite being confirmed</param>
			/// <returns>True if the operation should proceed</returns>
			bool IHakInstallProgress.ShouldOverwrite(OverwriteWarningCollection warnings, 
				bool fatal, OverwriteWarningType type)
			{
				return true;
			}

			/// <summary>
			/// Displays an error message to the user.
			/// </summary>
			/// <param name="error">The error message to display</param>
			void IHakInstallProgress.DisplayErrorMessage(string error) {}

			/// <summary>
			/// Displays a message to the user.
			/// </summary>
			/// <param name="error">The message to display</param>
			void IHakInstallProgress.DisplayMessage(string message) {}

			private int steps = 0;
			#endregion
		}
		#endregion

		#region private fields/properties/methods
		private string currentTempDir;
		private string mergeTlk;
		private string mergeHak;
		private ObjectProperyHandlerDictionary objects;

		/// <summary>
		/// Default constructor, private to force singleton implementation.
		/// </summary>
		private HakInstaller()
		{
			objects = PropertyHandlerAttribute.GetHandlerCollection(this);
		}

		/// <summary>
		/// Gets the number of progress steps required given a source list of
		/// hifs and modules.
		/// </summary>
		/// <param name="hifs">The list of hifs</param>
		/// <param name="modules">The list of modules</param>
		/// <returns>The number of progress steps required.</returns>
		private int GetProgressCount(HakInfo[] hifs, string[] modules)
		{
			// Count the number of erfs and files in the erfs.
			int fileCount, erfCount;
			CountAddedFiles(hifs, out fileCount, out erfCount);

			// Start with 3 steps per module, to load, decompress, and save.
			int count = modules.Length * 3;

			// Add one step per hif to load them.
			count += hifs.Length;

			// Add 2 steps per erf, one to load one to decompress.
			count += erfCount * 2;

			// Add a number of steps to add each file to each module.
			count += modules.Length * fileCount;

			// Add 1 steps for each (module, hif) pair to wire up events. 
			count += modules.Length * hifs.Length;

			return count;
		}

		/// <summary>
		/// Counts the number of files in the hifs that will be added to a
		/// module, to allow for progress bar movement.
		/// </summary>
		/// <param name="hifs">The list of hifs</param>
		/// <param name="fileCount">Returns the number of files in the erfs.</param>
		/// <param name="erfCount">Returns the number of erf files.</param>
		private void CountAddedFiles(HakInfo[] hifs, out int fileCount, out int erfCount)
		{
			fileCount = 0;
			erfCount = 0;

			// Now loop through all of the haks and add them to the module.
			foreach (HakInfo hif in hifs)
			{
				erfCount += hif.Erfs.Count;
				foreach (string erf in hif.Erfs)
					fileCount += Erf.GetFileCount(Path.Combine(NWNInfo.GetPathForFile(erf), erf));
			}
		}

		/// <summary>
		/// Decompresses an ERF file, returning the temp director that the
		/// file is decompressed to.
		/// </summary>
		/// <param name="erf">The erf to decompress</param>
		/// <param name="tempDirs">The string collection in which to place
		/// the temp directory.</param>
		/// <returns>The temp directory that the ERF was decompressed to, this
		/// is also added to the StringCollection</returns>
		private string Decompress (Erf erf, StringCollection tempDirs)
		{
			string tempDir = erf.FileName + ".Temp";
			tempDirs.Add(tempDir);
			erf.Decompress(tempDir);
			return tempDir;
		}

		/// <summary>
		/// Loads a collection of hifs into memory.
		/// </summary>
		/// <param name="hifs">The hifs to load</param>
		/// <param name="progress">An interface used to an object used to display
		/// progress information, or null if no progress information is desired</param>
		/// <returns>An array of HakInfo objects representing the hifs</returns>
		private HakInfo[] LoadHifs(string[] hifs, IHakInstallProgress progress)
		{
			// Loop through the array of hif files loading them into HakInfo objects.
			HakInfo[] hakInfos = new HakInfo[hifs.Length];
			for (int i = 0; i < hifs.Length; i++)
			{
				// Load the hif file, stepping the progress bar.
				Progress(progress, true, "Reading {0}", hifs[i]);
				if (progress.IsCancelled) throw new InstallCancelledException();
				hakInfos[i] = new HakInfo(Path.Combine(NWNInfo.HakInfoPath, hifs[i]));
			}

			return hakInfos;
		}

		/// <summary>
		/// Decompresses all of the ERF files in the hak info objects, returning
		/// a collection of the temp directories they are in.
		/// </summary>
		/// <param name="hakInfos">The array of hak infos for which to decompress
		/// the erfs</param>
		/// <param name="progress">An interface used to an object used to display
		/// progress information, or null if no progress information is desired</param>
		/// <returns>A string collection of all of the directories in which the
		/// ERF's have been decompressed.</returns>
		private StringCollection DecompressHifErfs(HakInfo[] hakInfos, 
			IHakInstallProgress progress)
		{
			StringCollection tempDirs = new StringCollection();
			foreach (HakInfo hakInfo in hakInfos)
			{
				// Add any erf files to the module.
				foreach (string erf in hakInfo.Erfs)
				{
					// Load the erf.
					Progress(progress, true, "Loading {0}", erf);
					if (progress.IsCancelled) throw new InstallCancelledException();
					Erf hakErf = Erf.Load(Path.Combine(NWNInfo.GetPathForFile(erf), erf));

					// Decompress the erf into it's own temporary directory, saving the
					// directory for later cleanup.
					Progress(progress, true, "Decompressing {0}", erf);
					Decompress(hakErf, tempDirs);
				}
			}

			return tempDirs;
		}

		/// <summary>
		/// Method to display a progress message.  It displays the message,
		/// steps the progress bar, and checks to see if the operation has
		/// been cancelled throwing a InstallCancelledException if it has.
		/// </summary>
		/// <param name="progress">The progress object</param>
		/// <param name="step">If true steps the progress bar</param>
		/// <param name="format">Message format string</param>
		/// <param name="args">Message arguments</param>
		private void Progress(IHakInstallProgress progress, bool step,
			string format, params object[] args)
		{
			// If the operation has been cancelled then abort.
			if (progress.IsCancelled) throw new InstallCancelledException();

			// Display the message and step the progress bar.
			progress.SetMessage(format, args);
			if (step) progress.Step();
		}
		#endregion

		#region private methods to deal with conflict resolution
		/// <summary>
		/// This method creates a conflict collection from the module.
		/// </summary>
		/// <param name="module"></param>
		/// <returns></returns>
		private FileConflictCollection CreateConflictCollection(Erf module)
		{
			FileConflictCollection conflicts = new FileConflictCollection();
			StringCollection replacedFiles = module.ReplacedFiles;
			foreach (string file in replacedFiles)
			{
				// Generate the full path of the module file, which has the same name
				// but is decompressed to the current temp directory.
				string moduleFile = Path.Combine(currentTempDir, Path.GetFileName(file));

				// Create the conflict object and add it to the collection.
				FileConflict conflict = new FileConflict(moduleFile, file);
				conflicts.Add(conflict);
			}
			return conflicts;
		}

		/// <summary>
		/// This function checks for tlk conflicts, checking to see if the module
		/// and hifs have tlk files.  If there are multiple tlk files it will attempt
		/// to generate a merge tlk file, if this cannot be done it will display an
		/// error message and throw an InstallCancelledException to cancel the install.
		/// </summary>
		/// <param name="hakInfos">The hak infos being added to the module</param>
		/// <param name="module">The module</param>
		/// <param name="moduleInfo">The module info</param>
		/// <param name="progress">The object implemening the progress interface</param>
		private void CheckForTlkConflicts(HakInfo[] hakInfos,
			Erf module, ModuleInfo moduleInfo,
			IHakInstallProgress progress)
		{
			// Create a tlk string collection and add the module's tlk if it has one.
			StringCollection tlks = new StringCollection();
			if (string.Empty != moduleInfo.CustomTlk) 
				tlks.Add(moduleInfo.CustomTlk.ToLower() + ".tlk");

			// Add all of the tlk's from all of the HIFs.
			foreach (HakInfo hif in hakInfos)
			{
				StringCollection hifTlks = hif.ModuleProperties["customtlk"];
				if (null != hifTlks && hifTlks.Count > 0)
				{
					// Loop through the tlk's individually to exclude duplicates.
					foreach (string hifTlk in hifTlks)
					{
						string lower = hifTlk.ToLower();
						if (!tlks.Contains(lower)) tlks.Add(lower);
					}
				}
			}

			// If we have less than 2 tlks there is no conflict to resolve.
			if (tlks.Count < 2) return;

			// We have 2 or more tlk files, create a conflict resolver to
			// build a merge tlk file.
			ConflictResolver resolver = new ConflictResolver(progress);
			string[] tlkStrings = new string[tlks.Count];
			tlks.CopyTo(tlkStrings, 0);
			mergeTlk = resolver.ResolveTlkConflict(module, tlkStrings);

			// If we don't get a merge tlk back from the conflict resolver then we couldn't
			// resolve the conflict.  This is a fatal error so display an error message and
			// cancel the install.
			if (string.Empty == mergeTlk)
			{
				progress.DisplayErrorMessage("The module and custom content contain tlk files " +
					"that cannot be merged.  The module update will be aborted.");
				throw new InstallCancelledException();
			}

			// Save the merge tlk as the module's custom tlk.
			moduleInfo.CustomTlk = Path.GetFileNameWithoutExtension(mergeTlk.ToLower());
		}

		/// <summary>
		/// This function checks for hak conflicts, checking to see if any files
		/// in the hifs will overwrite files in the module or vica versa.  If 
		/// overwrites will happen, it prompts the user to see if we should continue,
		/// throwing an InstallCancelledException() if the user chooses to cancel.
		/// </summary>
		/// <param name="hakInfos">The hak infos being added to the module</param>
		/// <param name="decompressedErfs">The decompressed erfs</param>
		/// <param name="module">The module</param>
		/// <param name="moduleInfo">The module info</param>
		/// <param name="progress">The object implemening the progress interface</param>
		private void CheckForHakConflicts(HakInfo[] hakInfos, 
			StringCollection decompressedErfs, Erf module, ModuleInfo moduleInfo,
			IHakInstallProgress progress)
		{
			// Create a hashtable for fast lookup and add all of the files in all
			// of the decompressed erf's to it.
			Hashtable hifErfHash = new Hashtable(10000);
			foreach(string directory in decompressedErfs)
			{
				// Strip the ".temp" off the end of the name.
				string erf = Path.GetFileNameWithoutExtension(directory);
				string[] files = Directory.GetFiles(directory);
				foreach (string file in files)
				{
					// Only add the ERF file if it's not already there.  We assume that
					// the ERF's in the HIF play well together so we ignore duplicates.
					string key = Path.GetFileName(file).ToLower();
					if ("exportinfo.gff" != key && !hifErfHash.Contains(key)) hifErfHash.Add(key, erf.ToLower());
				}
			}

			// Build a list of all of the added haks.
			StringCollection hakInfoHaks = new StringCollection();
			foreach (HakInfo hakInfo in hakInfos)
			{
				StringCollection temp = hakInfo.ModuleProperties["hak"] as StringCollection;
				if (null != temp)
				{
					foreach (string tempString in temp)
						hakInfoHaks.Add(tempString.ToLower());
				}
			}

			// Add all of the files in all of the haks to the hash table.
			Hashtable hifHakHash = new Hashtable(10000);
			foreach (string hakName in hakInfoHaks)
			{
				Erf hak = Erf.Load(NWNInfo.GetFullFilePath(hakName));
				StringCollection files = hak.Files;
				foreach (string file in files)
					try
					{
						string key = file.ToLower();
						string hakNameLower = hakName.ToLower();
						hifHakHash.Add(key, hakNameLower);
					}
					catch (ArgumentException)
					{}
			}

			// At this point we have built a lookup hash table that contains every
			// file going into the module (either directly in an erf or indirectly
			// in a hak).  Now we need to loop through all of the files in the
			// module (and all of it's haks) and check to see if any of them are
			// going to get overwritten.  At this point we have several cases.
			// 1. Module content is going to get replaced by erf content.  We 
			//    do not handle that case now, we wait until the end and allow
			//    the user to selectivly overwrite whatever they wish.
			// 2. Module content is going to get replaced by hak content.  We must
			//    warn the user that module files will not be used and the module
			//    may not work.
			// 3. Module hak content is going to get replaced by hak content.  Same
			//    as above.
			// 4. Module hak content is going to overwrite erf content from the hif.
			//    In this case the hif's content is the content that is going to be
			//    ignored, again the user has to be warned.
			OverwriteWarningCollection hakWarnings = new OverwriteWarningCollection();
			OverwriteWarningCollection erfWarnings = new OverwriteWarningCollection();

			string moduleFileName = Path.GetFileName(module.FileName);

			// Loop through all of the files in the module checking to see if files in
			// added haks will overwrite them.
			StringCollection moduleFiles = module.Files;
			foreach (string file in moduleFiles)
			{
				string source = hifHakHash[file.ToLower()] as string;
				if (null != source)
					hakWarnings.Add(new OverwriteWarning(file.ToLower(), moduleFileName, source));
			}

			// Loop through all of the files in the module's haks checking to see if
			// files in the added haks will overwrite them or if they will overwrite
			// files in added erf's.
			StringCollection moduleHaks = moduleInfo.Haks;
			foreach (string moduleHak in moduleHaks)
			{
				// Check to see if the hak in the module is one of the haks being added (this is
				// a no-op condition which will result in 100% duplicates, no need to check it).
				string hak = moduleHak + ".hak";
				if (hakInfoHaks.Contains(hak.ToLower())) continue;

				Erf erf = Erf.Load(NWNInfo.GetFullFilePath(hak));
				StringCollection hakFiles = erf.Files;
				foreach (string file in hakFiles)
				{
					// If the file is in the hak hash then it is going to be
					// overwritten by the hif's haks.
					string key = file.ToLower();
					string source = hifHakHash[key] as string;
					if (null != source)
						hakWarnings.Add(new OverwriteWarning(key, 
							Path.GetFileName(erf.FileName.ToLower()), source));

					// If the file is in the erf hash then it will overwrite the
					// hif's erf.
					source = hifErfHash[key] as string;
					if (null != source)
						erfWarnings.Add(new OverwriteWarning(key, source, 
							Path.GetFileName(erf.FileName.ToLower())));
				}
			}

			// We have built the list of conflicts, before asking the user try to resolve the
			// conflicts as we may be able to generate a merge hak to resolve some of them.
			if (hakWarnings.Count > 0)
			{
				ConflictResolver resolver = new ConflictResolver(progress);
				mergeHak = resolver.ResolveConflicts(hakInfos, module, moduleInfo, hakWarnings);
			}

			// We have finished checking for files that are going to get overwritten.
			// If we have any warnings to issue to the user then do so now.
			if (hakWarnings.Count > 0 && 
				!progress.ShouldOverwrite(hakWarnings, false, OverwriteWarningType.HifsOverwritesModule))
				throw new InstallCancelledException();

			if (erfWarnings.Count > 0 && 
				!progress.ShouldOverwrite(erfWarnings, false, OverwriteWarningType.ModuleOverwritesHifs))
				throw new InstallCancelledException();
		}
		#endregion

		#region private methods to do the install work.
		/// <summary>
		/// Installs the listed haks (defined by hif files) on the module.
		/// </summary>
		/// <param name="hakInfos">The array of loaded hif files to install.</param>
		/// <param name="decompressedErfs">The string collection of decompressed
		/// erf files from the hifs.  The values in here should be the
		/// temp directories in which the erfs have been decompressed to.  If this
		/// collection is empty then the erfs will be decompressed to temp directories
		/// and the directories returned in this collection.  It will be the caller's
		/// responsibility to delete them.</param>
		/// <param name="moduleFile">The module to add the haks to</param>
		/// <param name="progress">An interface used to an object used to display
		/// progress information, or null if no progress information is desired</param>
		private void DoInstall(HakInfo[] hakInfos, StringCollection decompressedErfs,
			string moduleFile, IHakInstallProgress progress)
		{
			mergeHak = string.Empty;
			mergeTlk = string.Empty;

			StringCollection tempDirs = new StringCollection();
			try
			{
				// Load the module file.
				Progress(progress, true, "Loading {0}", moduleFile);
				if (progress.IsCancelled) return;
				Erf module = Erf.Load(NWNInfo.GetFullFilePath(moduleFile));

				// Decompress the module to a temp directory.
				Progress(progress, true, "Decompressing {0}", moduleFile);
				currentTempDir = Decompress(module, tempDirs);

				// Load the moduleInfo for the module.
				ModuleInfo moduleInfo = new ModuleInfo(currentTempDir);

				// Check for any tlk file conflicts and abort the install if they
				// cannot be resolved.
				CheckForTlkConflicts(hakInfos, module, moduleInfo, progress);

				// Check for any file conflicts and make sure it is OK with the user.
				Progress(progress, false, "Checking for overwrites");
				CheckForHakConflicts(hakInfos, decompressedErfs, module, moduleInfo,
					progress);

				// Add any erf files to the module.
				foreach (string erfDir in decompressedErfs)
				{
					// Add each of the erf files to the module.
					string[] files = Directory.GetFiles(erfDir);
					foreach (string file in files)
					{
						// Add the file to the module.
						Progress(progress, true, "Adding {1} to\n{0}", moduleFile, Path.GetFileName(file));
						module.AddFile(Path.Combine(erfDir, file), true);

						// If the file is an area file then add it to the module's area list.
						if (0 == string.Compare(".are", Path.GetExtension(file), true, CultureInfo.InvariantCulture))
							moduleInfo.AddAreas(new string[] { Path.GetFileNameWithoutExtension(file) });
					}
				}

				// If we are overwriting files in the module then warn the user that
				// they are doing so and give them a chance to abort.
				if (module.ReplacedFiles.Count > 0)
				{
					// Create a conflict collection and ask the user what to do.  If
					// they cancel then trow a cancel exception to abort adding the
					// hak(s).
					FileConflictCollection conflicts = CreateConflictCollection(module);
					if (!progress.ShouldReplaceFiles(conflicts))
						throw new InstallCancelledException();

					// The user may have chosen to keep some of the original module
					// files, un-add any file that they chose not to replace.
					foreach (FileConflict conflict in conflicts)
						if (!conflict.ReplaceFile)
							module.RemoveFileFromAddedList(conflict.HakFile);
				}

				// Now loop through all of the haks and make any module changes
				// required.
				foreach (HakInfo hakInfo in hakInfos)
				{
					// Set all of the module properties.
					Progress(progress, true, "Setting module properties for {0}", moduleFile);
					PropertyHandlerDictionary handlers = objects["Module"];
					foreach (DictionaryEntry entry in hakInfo.ModuleProperties)
					{
						if (progress.IsCancelled) throw new InstallCancelledException();

						// Resolve the DictionaryEntry to native data.
						string property = (string) entry.Key;
						StringCollection values = (StringCollection) entry.Value;
						if (0 == values.Count) continue;

						// Look up the handler for the property, throwing an exception
						// if we don't find one, then invoke it.
						PropertyHandler handler = handlers[property];
						if (null == handler) 
							throw new InvalidOperationException("Unknown module property " + property);
						handler(module, moduleInfo, property, values);
					}
				}

				// If we have a merge hak then add it now so it goes to the top of
				// the hak list.
				if (string.Empty != mergeHak)
				{
					StringCollection mergeHakCollection = new StringCollection();
					mergeHakCollection.Add(mergeHak);
					Module_Hak(module, moduleInfo, "hak", mergeHakCollection);
				}

				// Build string arrays of the hif names and versions of all of the HakInfo
				// objects we added to the module, then update the module's installed
				// HakInfo property.  This is a custom property used by this tool to
				// keep track of what is installed on a module.
				string[] hifs = new string[hakInfos.Length];
				float[] versions = new float[hakInfos.Length];
				for (int i = 0; i < hakInfos.Length; i++)
				{
					hifs[i] = hakInfos[i].Name;
					versions[i] = hakInfos[i].Version;
				}
				moduleInfo.AddInstalledHakInfos(hifs, versions);

				// Save the changes to the module info file.
				moduleInfo.Save();

				// Backup the old module file before saving.
                // changed to not use the same extension as the toolset
				string backupName = Path.Combine(NWNInfo.GetPathForFile(moduleFile), 
					Path.GetFileNameWithoutExtension(moduleFile) + ".prc.BackupMod");
                // if a backup already exists, this means the module installer has been
                // used before. We don't want to overwrite it, otherwise the original is lost
                if(!File.Exists(backupName))
				    File.Copy(NWNInfo.GetFullFilePath(moduleFile), backupName, false);

				// Recreate the module file with our changed files.
				Progress(progress, true, "Saving {0}", moduleFile);
				module.RecreateFile();

				// If we created merge files then display a message to the user
				// telling them what we did.
				if (string.Empty != mergeHak || string.Empty != mergeTlk)
				{
					string files = "\r\n\r\n\t";
					if (string.Empty != mergeHak) files += NWN.NWNInfo.GetPartialFilePath(mergeHak);
					if (string.Empty != mergeTlk)
					{
						if (string.Empty != files) files += "\r\n\t";
						files += NWN.NWNInfo.GetPartialFilePath(mergeTlk);
					}

					progress.DisplayMessage(string.Format(
						"There were conflicts between the custom content you are trying to add and " +
						"the files already used by the module '{0}'.  Merge files were created to resolve " +
						"these conflicts, you should delete these files when you are finished " +
						"with your module." + files, Path.GetFileNameWithoutExtension(module.FileName)));
				}
			}
			catch (Exception e)
			{
				// Delete any merge files we created, the install is failing.s
				if (string.Empty != mergeTlk) File.Delete(NWN.NWNInfo.GetFullFilePath(mergeTlk));
				if (string.Empty != mergeHak) File.Delete(NWN.NWNInfo.GetFullFilePath(mergeHak));

				// If the exception isn't an InstallCancelledException then throw it.
				// InstallCancelledExceptions are thrown to abort the install, we want to eat those.
				if (!(e is InstallCancelledException)) throw;
			}
			finally
			{
				// Always clean up temp dirs no matter what.
				foreach (string dir in tempDirs)
					try
					{
						if (Directory.Exists(dir)) Directory.Delete(dir, true);
					}
					catch{}
			}
		}

		/// <summary>
		/// Installs the listed haks (defined by hif files) on the listed
		/// modules.  
		/// </summary>
		/// <param name="hifs">The list of haks to add</param>
		/// <param name="modules">The list of modules to add the haks to</param>
		/// <param name="progress">An interface used to an object used to display
		/// progress information, or null if no progress information is desired</param>
		private void DoInstall(HakInfo[] hifs, string[] modules,
			IHakInstallProgress progress)
		{
			StringCollection tempDirs = null;
			try
			{
				// Calcualte the number of steps needed for the progress bar.  The
				// hard coded numbers are based on the number of step calls in
				// DoInstall().
				progress.ProgressSteps = GetProgressCount(hifs, modules);

				// Load the hif files and decompress them to temp directories.
				tempDirs = DecompressHifErfs(hifs, progress);

				// Now apply the hifs to each module in turn.
				foreach (string module in modules)
					DoInstall(hifs, tempDirs, module, progress);
			}
			finally
			{
				// Always clean up temp dirs no matter what.
				if (null != tempDirs)
				{
					foreach (string dir in tempDirs)
						try
						{
							if (Directory.Exists(dir)) Directory.Delete(dir, true);
						}
						catch{}
				}
			}
		}
		#endregion

		#region private methods to generate merge scripts
		/// <summary>
		/// This method creates a script that calls ExecuteScript() to invoke
		/// multiple scripts.  It is designed to allow multiple scripts to wire up
		/// to an event handler.  The script is created and compiled and both
		/// the NSS and NCS files are added to the module.
		/// </summary>
		/// <param name="module">The module to modify</param>
		/// <param name="property">The property to which the scripts are being attached</param>
		/// <param name="originalScript">The original script on the property, or
		/// string.Empty if none</param>
		/// <param name="otherScripts">A list of other scripts to execute</param>
		/// <returns>The ResRef of the newly created script</returns>
		private string CreateExecuteScript(Erf module, string property, 
			string originalScript, StringCollection otherScripts)
		{
			// If the original script and the otherScripts are the same then
			// there is nothing to do, just return originalScript as the RefRef
			if (1 == otherScripts.Count && 
				0 == string.Compare(otherScripts[0], originalScript, true, CultureInfo.InvariantCulture))
				return originalScript.ToLower();

			// Build the file name and full name of the script file.
			string substring = property.Length > 12 ? property.Substring(0, 12) : property;
			string sourceName = "hif_" + substring + ".nss";
			string fullSourceName = Path.Combine(currentTempDir, sourceName);
			System.Text.StringBuilder b = new System.Text.StringBuilder();

			// Check to see if the original script is one of our generated scripts
			// (the name will start with "hif_" if it is).  If so then we need to
			// open the file and read the list of scripts currently being called
			// and add them to the list of scripts to call.
			StringCollection scriptsToExecute = new StringCollection();
			bool createScript = 
				0 != string.Compare(originalScript, Path.GetFileNameWithoutExtension(sourceName), true, CultureInfo.InvariantCulture);
			if (!createScript)
			{
				// Read the list of scripts currently being executed from the hif_
				// script file.
				string[] scripts = null;
				using (StreamReader reader = new StreamReader(fullSourceName))
				{
					// Read the first line, strip the comment prefix off, and
					// split the line into all of the scripts that the script
					// executes.
					string line = reader.ReadLine();
					line = line.Trim();
					line = line.Substring(3, line.Length - 3);
					scripts = line.Split(',');
				}

				// Add all of the scripts currently in the file, and then add
				// all of the scripts in the otherScripts collection if they aren't
				// already there.
				scriptsToExecute.AddRange(scripts);
				foreach (string script in otherScripts)
					if (!scriptsToExecute.Contains(script)) scriptsToExecute.Insert(0, script);
			}
			else
			{
                // Add all of the other scripts to our execute list, then add the
                // original script if there was one.
                // modified by fluffyamoeba 2008-06-16 
                // swapped so original script is executed last
				foreach (string script in otherScripts)
					scriptsToExecute.Add(script);
                if (string.Empty != originalScript) scriptsToExecute.Add(originalScript);
			}

			// Create the script file.
			using (StreamWriter writer = new StreamWriter(fullSourceName, false, 
					   System.Text.Encoding.ASCII))
			{
				// Make the first line be a list of the scripts being executed
				// so we can do updates to the file later.
				b.Length = 0;
				foreach (string script in scriptsToExecute)
				{
					if (b.Length > 0) b.Append(",");
					b.Append(script);
				}
				writer.WriteLine("// {0}", b.ToString());

				// Write out a comment header.
				writer.WriteLine("/////////////////////////////////////////////////////////////////////");
				writer.WriteLine("//");
				writer.WriteLine("// This script has been auto-generated by HakInstaller to call");
				writer.WriteLine("// multiple handlers for the {0} event.", property);
				writer.WriteLine("//");
				writer.WriteLine("/////////////////////////////////////////////////////////////////////");
				writer.WriteLine("");

				writer.WriteLine("void main()");
				writer.WriteLine("{");

				// Add an execute line for each script in the collection.
				foreach (string script in scriptsToExecute)
					writer.WriteLine("    ExecuteScript(\"{0}\", OBJECT_SELF);", script);

				writer.WriteLine("}");
				writer.Flush();
				writer.Close();
			}
			
			// Build the name of the obj file.
			string objName = Path.GetFileNameWithoutExtension(sourceName) + ".ncs";
			string fullObjName = Path.Combine(currentTempDir, objName);

			// Generate the compiler command line.
			string compiler = Path.Combine(NWNInfo.ToolsPath, "clcompile.exe");
			b.Length = 0;
			b.AppendFormat("\"{0}\" \"{1}\"", fullSourceName, currentTempDir);

			// Start the compiler process and wait for it.
			ProcessStartInfo info = new ProcessStartInfo();
			info.FileName = compiler;
			info.Arguments = b.ToString();
			info.CreateNoWindow = true;
			info.WindowStyle = ProcessWindowStyle.Hidden;
			Process process = Process.Start(info);
			process.WaitForExit();

			// If the compiler didn't work then we have a problem.
			if (0 != process.ExitCode)
				throw new NWNException("Could not run the NWN script compiler");

			// Add the source and object files to the module, if we have created new
			// files.  If the original script was a hif_ script that we just changed
			// then the file is already part of the module, no need to add it.
			if (createScript)
			{
				module.AddFile(fullSourceName, true);
				module.AddFile(fullObjName, true);
			}

			// Return the ResRef for the new script file.
			return Path.GetFileNameWithoutExtension(sourceName).ToLower();
		}
		#endregion

		#region private static fields/properties/methods
		private static HakInstaller singleton;

		/// <summary>
		/// Gets the singleton HakInstaller object.
		/// </summary>
		private static HakInstaller Singleton
		{
			get
			{
				// Create the singleton if we haven't already, and return it.
				if (null == singleton) singleton = new HakInstaller();
				return singleton;
			}
		}
		#endregion
	}
}