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

namespace Orciid.Core
{
	/// <summary>
	/// Properties interface
	/// </summary>
	/// <remarks>
	/// This interface must be implemented by all classes that will have properties assigned
	/// to their objects.
	/// </remarks>
	public interface IHasProperties
	{
		/// <summary>
		/// Unique identifier
		/// </summary>
		/// <remarks>
		/// Every object must be uniquely identifiable by an integer.
		/// </remarks>
		/// <returns>The unique identifier for this object</returns>
		int GetID();

		/// <summary>
		/// Unique class identifier
		/// </summary>
		/// <remarks>
		/// Every class implementing this interface must be uniquely identifiable by a character.
		/// </remarks>
		/// <returns>The unique identifier for this class</returns>
		char GetObjectTypeIdentifier();
	}

	/// <summary>
	/// Properties
	/// </summary>
	/// <remarks>
	/// This class allows storage of properties for objects in a more flexible way than defining
	/// those properties directly in the respective classes.  New properties can be added at any time
	/// without having to extend the class.  Only properties that are set are being stored in the
	/// database.
	/// </remarks>
	public class Properties:
		CachableObject	
	{
		private Hashtable properties = new Hashtable();
		private IHasProperties obj;

		private class SystemPlaceHolder:
			IHasProperties
		{
			public int GetID()
			{
				return 0;
			}

			public char GetObjectTypeIdentifier()
			{
				return 'O';
			}
		}

		/// <summary>
		/// Private constructor, use <see cref="GetProperties"/> method to get an instance
		/// </summary>
		private Properties():
			base(false)
		{
		}

		/// <summary>
		/// Get properties for specified object
		/// </summary>
		/// <remarks>
		/// The Properties class does not have a regular constructor, use this method
		/// to get an instance containing the properties for the specified object
		/// </remarks>
		/// <param name="o">Properties will be for this object</param>
		/// <returns>A Properties object, or <c>null</c> if the parameter is <c>null</c></returns>
		public static Properties GetProperties(IHasProperties o)
		{
			if (o == null)
				return null;
			Properties p = (Properties)GetFromCache(String.Format("Orciid.Core.Properties_{0}", o.GetObjectTypeIdentifier()), o.GetID());
			if (p == null)
			{
				p = new Properties();
				p.obj = o;
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"SELECT Property,PropertyValue FROM Properties 
						WHERE ObjectType={type} AND ObjectID={id}");
					query.AddParam("type", p.obj.GetObjectTypeIdentifier());
					query.AddParam("id", p.obj.GetID());
					DataTable table = conn.SelectQuery(query);
					foreach (DataRow row in table.Rows)
					{
						string key = conn.DataToString(row["Property"]);
						if (!p.properties.ContainsKey(key))
							p.properties.Add(key, conn.DataToString(row["PropertyValue"]));
						else
							TransactionLog.Instance.Add("Duplicate property",
								String.Format("Warning: Duplicate property found: {0}\n" +
									"Value to add (will be ignored): {1}\n" +
									"Existing value: {2}\n", 
									key, 
									conn.DataToString(row["PropertyValue"]),
									p.properties[key]));									
					}
				}
				AddToCache(p);
			}
			return p;
		}

		/// <summary>
		/// System properties
		/// </summary>
		/// <remarks>
		/// The is only one set of properties for the system.
		/// </remarks>
		/// <returns>The properties object for the system itself</returns>
		public static Properties GetSystemProperties()
		{
			return GetProperties(new SystemPlaceHolder());
		}

		/// <summary>
		/// Remove all properties
		/// </summary>
		/// <remarks>
		/// This method should be called if an object is permanently removed from the system.
		/// </remarks>
		/// <param name="o">All properties will be removed for this object</param>
		public static void ClearProperties(IHasProperties o)
		{
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"DELETE FROM Properties WHERE ObjectType={type} AND ObjectID={id}");
				query.AddParam("type", o.GetObjectTypeIdentifier());
				query.AddParam("id", o.GetID());
				conn.ExecQuery(query);
			}
		}

		/// <summary>
		/// Retrieve property value
		/// </summary>
		/// <remarks>
		/// All properties are stored in the database as strings.
		/// </remarks>
		/// <param name="key">The property name</param>
		/// <param name="def">The default value to return in case the property is not set</param>
		/// <returns>The property value or the specified default value if the property value is not set</returns>
		public string Get(string key, string def)
		{
			if (properties.ContainsKey(key))
				return (string)properties[key];
			else
				return def;
		}

		/// <summary>
		/// Set property value
		/// </summary>
		/// <remarks>
		/// All properties are stored in the database as strings.  If the specified property
		/// value is <c>null</c>, the property is removed from the database.  There is no
		/// distinction between properties being <c>null</c> or not being set.
		/// </remarks>
		/// <param name="key">The property name</param>
		/// <param name="val">The property value</param>
		public void Set(string key, string val)
		{
			if (key == null || key.Length == 0 || key.Length > 16)
				throw new CoreException("Property key must be between 1 and 16 characters.");
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"DELETE FROM Properties 
					WHERE ObjectType={type} AND ObjectID={id} AND Property={property}");
				query.AddParam("type", obj.GetObjectTypeIdentifier());
				query.AddParam("id", obj.GetID());
				query.AddParam("property", key);
				conn.ExecQuery(query);
				if (val == null)
				{
					properties.Remove(key);
				}
				else
				{
					properties[key] = val;
					query = new Query(conn,
						@"INSERT INTO Properties 
						(ObjectType,ObjectID,Property,PropertyValue)
						VALUES ({type},{id},{property},{value})");
					query.AddParam("type", obj.GetObjectTypeIdentifier());
					query.AddParam("id", obj.GetID());
					query.AddParam("property", key);
					query.AddParam("value", val);
					conn.ExecQuery(query);
				}
			}		
		}

		/// <summary>
		/// Remove property value
		/// </summary>
		/// <remarks>
		/// This method sets the specified property to <c>null</c>, effectively removing it
		/// </remarks>
		/// <param name="key">The property name</param>
		public void Remove(string key)
		{
			Set(key, null);
		}

		/// <summary>
		/// Remove all property values
		/// </summary>
		/// <remarks>
		/// This method removes all property values
		/// </remarks>
		public void RemoveAll()
		{
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"DELETE FROM Properties 
					WHERE ObjectType={type} AND ObjectID={id}");
				query.AddParam("type", obj.GetObjectTypeIdentifier());
				query.AddParam("id", obj.GetID());
				conn.ExecQuery(query);
			}
			properties.Clear();
		}

		/// <summary>
		/// Get property value
		/// </summary>
		/// <remarks>
		/// All property values are stored in the database as strings.  This method provides
		/// an easy way to retrieve an integer value.
		/// </remarks>
		/// <param name="key">The property name</param>
		/// <param name="def">The default value that is returned if the property is not set
		/// or is set to a value that cannot be interpreted as an integer</param>
		/// <returns>The property value, or the specified default value if the property is not set
		/// or is set to a value that cannot be interpreted as an integer</returns>
		public int GetAsInt(string key, int def)
		{
			try
			{
				return Int32.Parse(Get(key, def.ToString()));
			}
			catch
			{
				return def;
			}
		}

		/// <summary>
		/// Set property value
		/// </summary>
		/// <remarks>
		/// All property values are stored in the database as strings.  This method provides
		/// an easy way to set an integer value.
		/// </remarks>
		/// <param name="key">The property name</param>
		/// <param name="val">The property value</param>
		public void SetAsInt(string key, int val)
		{
			Set(key, val.ToString());
		}

		/// <summary>
		/// Class identifier
		/// </summary>
		/// <remarks>
		/// This method is required to allow caching.  Since the Properties class can
		/// be used together with several different classes, its class identifier depends
		/// on the type of object it is connected to.
		/// </remarks>
		/// <returns>The class identifier</returns>
		protected override string GetClassIdentifier()
		{
			return String.Format("Orciid.Core.Properties_{0}", obj.GetObjectTypeIdentifier());
		}

		/// <summary>
		/// Modification check
		/// </summary>
		/// <remarks>
		/// Instances of this class can always be modified
		/// </remarks>
		/// <param name="user">The user to check the modification privilege for</param>
		/// <returns><c>true</c></returns>
		public override bool CanModify(User user)
		{
			return true;
		}

		/// <summary>
		/// Creation check
		/// </summary>
		/// <remarks>
		/// Instances of this class can always be created
		/// </remarks>
		/// <param name="user">The user to check the creation privilege for</param>
		/// <returns><c>true</c></returns>
		public override bool CanCreate(User user)
		{
			return true;
		}

		/// <summary>
		/// Commit changes
		/// </summary>
		/// <remarks>
		/// This method does not do anything
		/// </remarks>
		protected override void CommitChanges()
		{
		}

		/// <summary>
		/// Internal identifier
		/// </summary>
		/// <remarks>
		/// Since the Properties class on its own provides no data, the internal identifier
		/// of an instance is always the internal identifier of the object that the instance 
		/// of Properties is connected to
		/// </remarks>
		/// <returns>The internal identifier of the object this instance of Properties is
		/// connected to</returns>
		public override int GetID()
		{
			return obj.GetID();
		}

		/// <summary>
		/// Available properties
		/// </summary>
		/// <value>
		/// A list of names of available properties
		/// </value>
		/// <remarks>
		/// The returned collection gives access to all available properties, so it
		/// is possible to loop through them
		/// </remarks>
		public ICollection Keys
		{
			get
			{
				return properties.Keys;
			}
		}
	}
}
