using System;
using System.Collections;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Threading;
using HakInstaller.Utilities;

namespace HakInstaller
{
	/// <summary>
	/// This class is a dictionary of hak properties.  Each property contains
	/// a string collection of values tied to the property.
	/// </summary>
	public class HakPropertyDictionary: DictionaryBase
	{
		#region public properties/methods
		/// <summary>
		/// Indexer to get the StringCollection for a given property
		/// </summary>
		public StringCollection this[string property]
		{
			get { return InnerHashtable[property] as StringCollection; }
		}

		/// <summary>
		/// Default Constructor
		/// </summary>
		public HakPropertyDictionary()
		{
		}

		/// <summary>
		/// Adds a new property to the collection, creating a blank StringCollection
		/// for it.
		/// </summary>
		/// <param name="property"></param>
		public void Add(string property)
		{
			InnerHashtable.Add(property, new StringCollection());
		}
		#endregion
	}


	/// <summary>
	/// This class represents a .hif file.  This file contains information about
	/// a 'hak' (hak in this case consisting of a collection of ERF, TLK, and HAK
	/// files, along with a list of items in the module to modify).  It provides
	/// functionality to read the file into memory and access the various pieces of
	/// the file.
	/// </summary>
	public class HakInfo
	{
		#region public properties/methods
		/// <summary>
		/// Gets the name of the HIF minus the extension.
		/// </summary>
		public string Name
		{ get { return Path.GetFileNameWithoutExtension(fileInfo.Name); } }

		/// <summary>
		/// Gets the title of the HIF.  This is the HIF's title property if
		/// it has one, or it's file name if it doesn't.
		/// </summary>
		public string Title
		{
			get
			{
				StringCollection title = GetStrings(TitleKey, string.Empty);
				return null == title || 0 == title.Count || string.Empty == title[0] ?
					Name : title[0];
			}
		}

		/// <summary>
		/// Gets the version number of the HIF.
		/// </summary>
		public float Version
		{
			get
			{
				try
				{
					StringCollection version = GetStrings(VersionKey, string.Empty);
					return (float) Convert.ToDouble(version[0], cultureUSA);
				}
				catch (Exception)
				{
					return 0;
				}
			}
		}

		/// <summary>
		/// Gets the version of the HIF as a text string.
		/// </summary>
		public string VersionText
		{
			get
			{
				try
				{
					return GetStrings(VersionKey, string.Empty)[0];
				}
				catch (Exception)
				{
					return string.Empty;
				}
			}
		}

		/// <summary>
		/// Gets the minimum version number of NWN required to install the HIF.
		/// </summary>
		public float RequiredNWNVersion
		{
			get
			{
				try
				{
					// Get the required string array and look for a string that starts with a 
					// digit, that would be the NWN version, return it if we find it.
					StringCollection required = GetStrings(RequiredNWNVersionKey, string.Empty);
					foreach (string s in required)
					{
						if (Char.IsDigit(s[0])) return (float) Convert.ToDouble(required[0], cultureUSA);
					}
					return 0;
				}
				catch (Exception)
				{
					return 0;
				}
			}
		}

		/// <summary>
		/// Returns true if XP1 is required.
		/// </summary>
		public bool IsXP1Required
		{
			get
			{
				try
				{
					// Get the required string array and look for "XP1" or "Undrentide".
					StringCollection required = GetStrings(RequiredNWNVersionKey, string.Empty);
					foreach (string s in required)
					{
						if (0 == string.Compare("XP1", s, true, CultureInfo.InvariantCulture) ||
							0 == string.Compare("Undrentide", s, true, CultureInfo.InvariantCulture))
							return true;
					}
				}
				catch (Exception)
				{}

				return false;
			}
		}

		/// <summary>
		/// Returns true if XP2 is required.
		/// </summary>
		public bool IsXP2Required
		{
			get
			{
				try
				{
					// Get the required string array and look for "XP1" or "Undrentide".
					StringCollection required = GetStrings(RequiredNWNVersionKey, string.Empty);
					foreach (string s in required)
					{
						if (0 == string.Compare("XP2", s, true, CultureInfo.InvariantCulture) ||
							0 == string.Compare("Underdark", s, true, CultureInfo.InvariantCulture))
							return true;
					}
				}
				catch (Exception)
				{}

				return false;
			}
		}

		/// <summary>
		/// Gets the list of ERF files to add to the collection.
		/// </summary>
		public StringCollection Erfs  { get { return components[ErfKey] as StringCollection; } }

		/// <summary>
		/// Gets the dictionary of module properties that must be added/modified.
		/// </summary>
		public HakPropertyDictionary ModuleProperties
		{ get { return components[ModuleKey] as HakPropertyDictionary; } }

		/// <summary>
		/// Class constructor to load a .hif file from disk.
		/// </summary>
		/// <param name="fileName">The name of the hif file, hif files should
		/// all live in the hak directory.</param>
		public HakInfo(string fileName)
		{
			// 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
			{
				fileInfo = new FileInfo(fileName);

				InitializeComponents();
				using(StreamReader reader = new StreamReader(fileName))
				{
					// Loop through all of the lines in the file.
					for (int i = 1; reader.Peek() > -1; i++)
					{
						// Read the references line and split off the 2da file name and the references.
						string line = reader.ReadLine();
						line = line.Trim();

						// If the line is blank or begins with a '#' ignore it.
						if (0 == line.Length || '#' == line[0]) continue;

						// Split the line into the type and references.  If we don't get both
						// parts the the line has a syntax error.
						string[] strings = line.Split(':');
						if (2 != strings.Length)
							ThrowException("{0}: line {1}: syntax error", fileName, i.ToString());

						// Save the component type and data.
						string componentType = strings[0].Trim().ToLower();
						string data = strings[1].Trim();

						// Check to see if the component has a property.  If there is
						// a '.' in the name then it has a sub type, we need to split
						// the component type into the type and property.
						string componentProperty = string.Empty;
						if (-1 != componentType.IndexOf('.'))
						{
							strings = componentType.Split('.');
							componentType = strings[0];
							componentProperty = strings[1];
						}

						// Split the various values, in case this can contain multiple
						// values, and add each to the collection.
						strings = data.Split(',');
						StringCollection coll = GetStrings(componentType, componentProperty);
						foreach (string s in strings)
							coll.Add(s.Trim());
					}
				}

				// The hak may or may not have a version number in it, if it doesn't add 0 so
				// we always have a version number for lookup.
				StringCollection version = GetStrings(VersionKey, string.Empty);
				if (0 == version.Count) version.Add("0");
			}
			finally
			{
				Thread.CurrentThread.CurrentCulture = currentCulture;
			}
		}

		/// <summary>
		/// Validates the HIF to make sure that it can be installed, returning an error
		/// message if it cannot.
		/// </summary>
		/// <param name="error">The error message if the HIF cannot be installed, or 
		/// string.Empty if the HIF can be installed</param>
		/// <returns>True if the HIF can be installed, false if it cannot.</returns>
		public bool Validate(out string error)
		{
			error = string.Empty;

			// If the HIF has a minimum required version and the current NWN install isn't
			// high enough then error out right away.
			if (RequiredNWNVersion > 0 && 
				(float) Convert.ToDouble(NWN.NWNInfo.Version, cultureUSA) < RequiredNWNVersion)
			{
				error = StringResources.GetString("ValidateNWNVersionError",
					Title, RequiredNWNVersion, NWN.NWNInfo.Version);
				return false;
			}

			// If the content requires XP1 then validate it.
			if (IsXP1Required && !NWN.NWNInfo.IsXP1Installed)
			{
				error = StringResources.GetString("ValidateNWNXP1Error", Title);
				return false;
			}

			// If the content requies XP2 then validate it.
			if (IsXP2Required && !NWN.NWNInfo.IsXP2Installed)
			{
				error = StringResources.GetString("ValidateNWNXP2Error", Title);
				return false;
			}

			// Build a list of ALL of the files referenced by the HIF.
			StringCollection files = new StringCollection();
			StringCollection strings = this.GetStrings(ErfKey, string.Empty);
			foreach (string s in strings) files.Add(NWN.NWNInfo.GetFullFilePath(s));
			strings = GetStrings(ModuleKey, "hak");
			foreach (string s in strings) files.Add(NWN.NWNInfo.GetFullFilePath(s));
			strings = GetStrings(ModuleKey, "tlk");
			foreach (string s in strings) files.Add(NWN.NWNInfo.GetFullFilePath(s));

			// Loop through all of the files checking to see which, if any, are missing.
			string missingFiles = string.Empty;
			foreach (string file in files)
			{
				// If the file is missing add it to our missing files string.
				if (!File.Exists(file))
				{
					if (0 == missingFiles.Length) missingFiles += "\r\n";
					missingFiles += "\r\n\t";
					missingFiles += NWN.NWNInfo.GetPartialFilePath(Path.GetFileName(file));
				}
			}

			// If there are missing files then format the error message and return it.
			if (missingFiles.Length > 0)
				error = StringResources.GetString("ValidateMissingFilesError", Title, missingFiles);

			return 0 == error.Length;
		}

		/// <summary>
		/// Override of ToString() to return the name of the HIF.
		/// </summary>
		/// <returns></returns>
		public override string ToString()
		{
			return Name;
		}

		#endregion

		#region private static fields/properties/methods
		// Create a CultureInfo for US English to do proper number conversion.
		private static CultureInfo cultureUSA = new CultureInfo(0x0409);
		#endregion

		#region private fields/properties/methods
		/// <summary>
		/// Gets the string collection for the specified (type, property) from the
		/// hash table.
		/// </summary>
		/// <param name="componentType">The component type</param>
		/// <param name="componentProperty">The property, or string.Empty if
		/// the type does not support properties</param>
		/// <returns>The string collection for the (type, property).</returns>
		private StringCollection GetStrings(string componentType, string componentProperty)
		{
			// Get the value for the given component if we can't find it
			// then throw an exception.
			object o = components[componentType];
			if (null == o) ThrowException("Unknown type {0}", componentType);

			if (string.Empty == componentProperty)
			{
				// No sub-type, the value for this component type should be
				// a string collection.
				if (!(o is StringCollection)) ThrowException("Type {0} requires a property", componentType);
				return (StringCollection) o;
			}
			else
			{
				// Get the hashtable for the type, if there is no hashtable
				// then throw an exception.
				HakPropertyDictionary hash = o as HakPropertyDictionary;
				if (null == hash) ThrowException("Type {0} cannot have properties", componentType);

				// Get the string collection for the property, if there is no
				// collection for the property yet then create one.
				StringCollection coll = hash[componentProperty];
				if (null == coll)
				{
					hash.Add(componentProperty);
					coll = hash[componentProperty];
				}
				return coll;
			}
		}

		/// <summary>
		/// Initializes the components hash table.
		/// </summary>
		private void InitializeComponents()
		{
			// Create the hashtable then walk the key/value arrays, creating
			// the proper value objects for the keys.
			components = new Hashtable();
			for (int i = 0; i < Keys.Length; i++)
			{
				System.Reflection.ConstructorInfo ci = Types[i].GetConstructor(new Type[0]);
				object val = ci.Invoke(new object[0]);
				components.Add(Keys[i], val);
			}
		}

		/// <summary>
		/// Throws an exception with the specified message.
		/// </summary>
		/// <param name="format">Format string</param>
		/// <param name="args">Format arguments</param>
		private void ThrowException(string format, params object[] args)
		{
			System.Text.StringBuilder b = new System.Text.StringBuilder();
			b.AppendFormat(format, args);
			throw new Exception(b.ToString());
		}

		// Define constants for the various component types.
		private const string ErfKey = "erf";
		private const string ModuleKey = "module";
		private const string VersionKey = "version";
		private const string RequiredNWNVersionKey = "minnwnversion";
		private const string TitleKey = "title";

		// Arrays that define the supported component types.  Keys is an array of
		// key values which match what is on the left of the ':' in the hif file.
		// Types is the type of object that is placed in the hash table for each
		// key.
		private string[] Keys = new string[]
		{ 
			VersionKey,
			RequiredNWNVersionKey,
			ErfKey, 
			TitleKey,
			ModuleKey
		};
		private Type[] Types = new Type[]
		{ 
			typeof(StringCollection),
			typeof(StringCollection),
			typeof(StringCollection), 
			typeof(StringCollection),
			typeof(HakPropertyDictionary)
		};

		private Hashtable components;
		private FileInfo fileInfo;
		#endregion
	}
}