/* Copyright (c) 2007 Ben Howell
 * This software is licensed under the MIT License
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a 
 * copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the 
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in 
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;

namespace GoArrow.RouteFinding
{
	public class LocationChangedEventArgs : EventArgs
	{
		private Location mLoc;
		public Location Loc { get { return mLoc; } }
		internal LocationChangedEventArgs(Location loc)
		{
			mLoc = loc;
		}
	}

	[Flags]
	public enum SearchField
	{
		Name = 0x1,
		Description = 0x2,
		Both = Name | Description,
	}

	[Flags]
	public enum DatabaseType
	{
		CrossroadsOfDereth = 0x1,
		ACSpedia = 0x2,
		Both = CrossroadsOfDereth | ACSpedia,
		Neither = 0x0,
	}

	public class LocationDatabase : IDisposable
	{
		private class CharComparerIgnoreCase : IComparer<char>, IEqualityComparer<char>
		{
			public int Compare(char x, char y) { return char.ToLower(x) - char.ToLower(y); }
			public bool Equals(char x, char y) { return char.ToLower(x) == char.ToLower(y); }
			public int GetHashCode(char c) { return char.ToLower(c); }
		}

		private Dictionary<int, Location> mLocations;
		private SortedList<char, List<Location>> mAlphaIndex;
		private List<Location> mFavorites;
		private List<Location> mPortalLocations;

		private string mSavePath;

		private bool mNeedsSave = false;
		private bool mDisposed = false;

		private DatabaseType mDatabaseType;
		private DateTime mLastUpdate = DateTime.MinValue;

		public event EventHandler<LocationChangedEventArgs> FavoritesListChanged;
		public event EventHandler<LocationChangedEventArgs> LocationAdded;
		public event EventHandler<LocationChangedEventArgs> LocationRemoved;

		public Location this[int id]
		{
			get { return mLocations[id]; }
		}

		public Dictionary<int, Location> Locations
		{
			get { return mLocations; }
		}

		public SortedList<char, List<Location>> AlphaIndex
		{
			get { return mAlphaIndex; }
		}

		public List<Location> Favorites
		{
			get { return mFavorites; }
		}

		public List<Location> PortalLocations
		{
			get { return mPortalLocations; }
		}

		public string SavePath
		{
			get { return mSavePath; }
			set { mSavePath = value; }
		}

		public bool NeedsSave
		{
			get { return mNeedsSave; }
			set { mNeedsSave = value; }
		}

		public bool Disposed
		{
			get { return mDisposed; }
		}

		public DatabaseType DatabaseType
		{
			get { return mDatabaseType; }
		}

		public DateTime LastUpdate
		{
			get { return mLastUpdate; }
		}

		public string LastUpdateString
		{
			get
			{
				if (mLastUpdate == DateTime.MinValue)
				{
					return "Unknown";
				}
				else
				{
					DateTime localTime = mLastUpdate.ToLocalTime();
					return localTime.ToShortDateString() + ", " + localTime.ToShortTimeString();
				}
			}
		}

		public LocationDatabase()
		{
			mLocations = new Dictionary<int, Location>();

			mAlphaIndex = new SortedList<char, List<Location>>(27, new CharComparerIgnoreCase());
			mAlphaIndex['#'] = new List<Location>();
			for (char i = 'A'; i <= 'Z'; i++)
				mAlphaIndex[i] = new List<Location>();

			mPortalLocations = new List<Location>();

			mFavorites = new List<Location>();

			mSavePath = Util.FullPath("locations.xml");
		}

		public LocationDatabase(DatabaseType type)
			: this()
		{
			mDatabaseType = type;
		}

		public LocationDatabase(XmlDocument locDoc)
			: this()
		{
			LoadLocationsXml(locDoc);
		}

		public LocationDatabase(string databaseFilePath)
			: this()
		{
			mSavePath = databaseFilePath;
			XmlDocument locDoc = new XmlDocument();
			locDoc.Load(databaseFilePath);
			LoadLocationsXml(locDoc);
		}

		public void Dispose()
		{
			if (mDisposed)
				return;

			if (NeedsSave)
				Save(SavePath);

			mDisposed = true;
		}

		private void LoadLocationsXml(XmlDocument locDoc)
		{
			mDatabaseType = DatabaseType.CrossroadsOfDereth;
			if (locDoc.DocumentElement.HasAttribute("type"))
			{
				try
				{
					mDatabaseType = (DatabaseType)Enum.Parse(typeof(DatabaseType), locDoc.DocumentElement.GetAttribute("type"));
				}
				catch { /* Ignore */ }
			}

			mLastUpdate = DateTime.MinValue;
			if (locDoc.DocumentElement.HasAttribute("updated"))
			{
				try
				{
					mLastUpdate = new DateTime(long.Parse(locDoc.DocumentElement.GetAttribute("updated")), DateTimeKind.Utc);
				}
				catch { /* Ignore */ }
			}

			foreach (XmlElement locNode in locDoc.DocumentElement.ChildNodes)
			{
				Location loc = Location.FromXml(locNode);

				if (!mLocations.ContainsKey(loc.Id))
				{
					mLocations[loc.Id] = loc;

					if (loc.HasExitCoords)
					{
						mPortalLocations.Add(loc);
					}

					GetAlphaIndex(loc.Name[0]).Add(loc);

					if (loc.IsFavorite)
					{
						mFavorites.Add(loc);
					}

					loc.IsFavoriteChanged -= Location_IsFavoriteChanged;
					loc.IsFavoriteChanged += new EventHandler(Location_IsFavoriteChanged);
				}
			}

			mPortalLocations.TrimExcess();

			foreach (List<Location> locList in mAlphaIndex.Values)
				locList.Sort();

			mFavorites.Sort();
		}

		public void Add(Location loc)
		{
			if (!mLocations.ContainsKey(loc.Id))
			{
				mLocations[loc.Id] = loc;

				List<Location> addTo = GetAlphaIndex(loc.Name[0]);
				int idx = addTo.BinarySearch(loc);
				if (idx < 0)
					addTo.Insert(~idx, loc);
				else
					addTo[idx] = loc;

				if (loc.IsFavorite)
				{
					idx = mFavorites.BinarySearch(loc);
					if (idx < 0)
						mFavorites.Insert(~idx, loc);
					else
						mFavorites[idx] = loc;
				}

				if (loc.HasExitCoords)
				{
					mPortalLocations.Remove(loc);
					mPortalLocations.Add(loc);
				}

				loc.IsFavoriteChanged -= Location_IsFavoriteChanged;
				loc.IsFavoriteChanged += new EventHandler(Location_IsFavoriteChanged);

				if (LocationAdded != null)
				{
					LocationAdded(this, new LocationChangedEventArgs(loc));
				}
			}
			else
			{
				throw new ArgumentException("Location already exists: " + loc, "loc");
			}
		}

		public void UpdatedNow()
		{
			mLastUpdate = DateTime.UtcNow;
			NeedsSave = true;
		}

		public bool Remove(int id)
		{
			Location loc;
			if (!mLocations.TryGetValue(id, out loc))
				return false;

			loc.IsFavoriteChanged -= Location_IsFavoriteChanged;

			mLocations.Remove(id);
			GetAlphaIndex(loc.Name[0]).Remove(loc);
			mFavorites.Remove(loc);
			mPortalLocations.Remove(loc);

			if (LocationRemoved != null)
			{
				LocationRemoved(this, new LocationChangedEventArgs(loc));
			}

			return true;
		}

		public bool Remove(Location loc)
		{
			loc.IsFavoriteChanged -= Location_IsFavoriteChanged;
			GetAlphaIndex(loc.Name[0]).Remove(loc);
			mFavorites.Remove(loc);
			mPortalLocations.Remove(loc);
			if (mLocations.Remove(loc.Id))
			{
				if (LocationRemoved != null)
				{
					LocationRemoved(this, new LocationChangedEventArgs(loc));
				}
				return true;
			}
			return false;
		}

		public bool Contains(int id)
		{
			return mLocations.ContainsKey(id);
		}

		public bool TryGet(int id, out Location loc)
		{
			if (!mLocations.TryGetValue(id, out loc))
			{
				loc = Location.NO_LOCATION;
				return false;
			}
			return true;
		}

		public bool TryGet(string exactName, out Location loc)
		{
			List<Location> lst = GetAlphaIndex(exactName[0]);
			int idx = BinarySearch(lst, exactName);
			loc = (idx >= 0) ? lst[idx] : Location.NO_LOCATION;
			return (idx >= 0);
		}

		public List<Location> GetAlphaIndex(char nameFirstLetter)
		{
			if (char.IsLetter(nameFirstLetter))
				return mAlphaIndex[nameFirstLetter];
			return mAlphaIndex['#'];
		}

		public List<Location> SearchNameBeginsWith(string nameBeginsWith)
		{
			return SearchNameBeginsWith(nameBeginsWith, LocationType.Any);
		}

		public List<Location> SearchNameBeginsWith(string nameBeginsWith, LocationType limitToType)
		{
			List<Location> foundLocations = new List<Location>();

			List<Location> lst = GetAlphaIndex(nameBeginsWith[0]);
			int i = BinarySearch(lst, nameBeginsWith);
			if (i < 0) { i = ~i; }

			for (; i < lst.Count; i++)
			{
				if (lst[i].Name.StartsWith(nameBeginsWith, StringComparison.OrdinalIgnoreCase))
				{
					if (lst[i].TypeMatches(limitToType))
						foundLocations.Add(lst[i]);
				}
				else
				{
					break;
				}
			}

			return foundLocations;
		}

		public int BinarySearch(List<Location> lst, string name)
		{
			int left = 0, right = lst.Count - 1, mid = 0;
			while (left <= right)
			{
				mid = (left + right) / 2;
				int cmp = StringComparer.OrdinalIgnoreCase.Compare(name, lst[mid].Name);
				if (cmp == 0)
					return mid;
				if (cmp < 0)
					right = mid - 1;
				else
					left = mid + 1;
			}

			if (mid < lst.Count && StringComparer.OrdinalIgnoreCase.Compare(name, lst[mid].Name) > 0)
				mid++;

			return ~mid;
		}

		public List<Location> Search(string fieldContains, SearchField field)
		{
			return Search(fieldContains, field, LocationType.Any);
		}

		public List<Location> Search(string fieldContains, SearchField field, LocationType limitToType)
		{
			List<Location> foundLocations = new List<Location>();

			fieldContains = fieldContains.ToLower();
			foreach (List<Location> locList in AlphaIndex.Values)
			{
				foreach (Location loc in locList)
				{
					if (!loc.TypeMatches(limitToType))
						continue;

					string searchField;
					switch (field)
					{
						case SearchField.Name:
							searchField = loc.Name.ToLower();
							break;
						case SearchField.Description:
							searchField = loc.Notes.ToLower();
							break;
						case SearchField.Both:
							searchField = (loc.Name + loc.Notes).ToLower();
							break;
						default:
							searchField = loc.Name.ToLower();
							break;
					}
					if (searchField.Contains(fieldContains))
					{
						foundLocations.Add(loc);
					}
				}
			}

			return foundLocations;
		}

		public List<Location> Search(Coordinates nearThis, double maxDistance)
		{
			return Search(nearThis, maxDistance, LocationType.Any);
		}

		public List<Location> Search(Coordinates nearThis, double maxDistance, LocationType limitToType)
		{
			List<Location> foundLocations = new List<Location>();

			foreach (List<Location> locList in AlphaIndex.Values)
			{
				foreach (Location loc in locList)
				{
					if (loc.TypeMatches(limitToType) && loc.Coords.DistanceTo(nearThis) <= maxDistance)
					{
						foundLocations.Add(loc);
					}
				}
			}

			return foundLocations;
		}

		public List<Location> Search(string fieldContains, SearchField field,
				Coordinates nearThis, double maxDistance)
		{
			return Search(fieldContains, field, nearThis, maxDistance, LocationType.Any);
		}

		public List<Location> Search(string fieldContains, SearchField field,
				Coordinates nearThis, double maxDistance, LocationType limitToType)
		{
			List<Location> foundLocations = new List<Location>();

			fieldContains = fieldContains.ToLower();
			foreach (List<Location> locList in AlphaIndex.Values)
			{
				foreach (Location loc in locList)
				{
					if (!loc.TypeMatches(limitToType) || loc.Coords.DistanceTo(nearThis) > maxDistance)
						continue;

					string searchField;
					switch (field)
					{
						case SearchField.Name:
							searchField = loc.Name.ToLower();
							break;
						case SearchField.Description:
							searchField = loc.Notes.ToLower();
							break;
						case SearchField.Both:
							searchField = (loc.Name + loc.Notes).ToLower();
							break;
						default:
							searchField = loc.Name.ToLower();
							break;
					}
					if (searchField.Contains(fieldContains))
					{
						foundLocations.Add(loc);
					}
				}
			}

			return foundLocations;
		}

		public Location GetLocationAt(Coordinates coords)
		{
			List<Location> results = Search(coords, 0.049);
			if (results.Count == 1)
			{
				return results[0];
			}
			else if (results.Count > 0)
			{
				foreach (Location loc in results)
				{
					if (loc.Type == LocationType.PortalHub)
						return loc;
				}
				return results[0];
			}
			return null;
		}

		public void Save(string path)
		{
			XmlDocument locDoc = new XmlDocument();
			locDoc.AppendChild(locDoc.CreateElement("locations"));
			locDoc.DocumentElement.SetAttribute("type", DatabaseType.ToString());
			locDoc.DocumentElement.SetAttribute("updated", mLastUpdate.Ticks.ToString());

			foreach (Location loc in Locations.Values)
			{
				locDoc.DocumentElement.AppendChild(loc.ToXml(locDoc));
			}

			Util.SaveXml(locDoc, path);
			NeedsSave = false;
		}

		private void Location_IsCustomizedChanged(object sender, EventArgs e)
		{
			NeedsSave = true;
		}

		private void Location_IsFavoriteChanged(object sender, EventArgs e)
		{
			Location loc = (Location)sender;
			if (loc.IsFavorite)
			{
				if (!mFavorites.Contains(loc))
				{
					int idx = mFavorites.BinarySearch(loc);
					if (idx < 0)
						mFavorites.Insert(~idx, loc);
					else
						mFavorites[idx] = loc;
					NeedsSave = true;
					if (FavoritesListChanged != null)
						FavoritesListChanged(this, new LocationChangedEventArgs(loc));
				}
			}
			else
			{
				if (mFavorites.Remove(loc))
				{
					NeedsSave = true;
					if (FavoritesListChanged != null)
						FavoritesListChanged(this, new LocationChangedEventArgs(loc));
				}
			}
		}
	}
}