using System;
using System.Data;
using System.Collections;
using System.Text;
using System.Text.RegularExpressions;

namespace Orciid.Core
{
	/// <summary>
	/// User Group
	/// </summary>
	/// <remarks>
	/// A user group is an entity that can be granted or denied privileges.  Individual users
	/// can be members of any number of user groups, either through explicit membership
	/// (<see cref="MemberUserGroup"/>) or other identifying properties, e.g. client IP addresses
	/// (<see cref="IPBasedUserGroup"/>), authentication status etc.
	/// </remarks>
	public abstract class UserGroup:
		CachableObject, IComparable, IPrivilegeAssignee
	{
		/// <summary>
		/// The class identifier
		/// </summary>
		protected const string classid = "Orciid.Core.UserGroup";
		/// <summary>
		/// The internal identifier
		/// </summary>
		protected int id;
		/// <summary>
		/// Group title
		/// </summary>
		protected string title;

		/// <summary>
		/// Default constructor
		/// </summary>
		/// <remarks>
		/// This constructor creates a new UserGroup object if the current user's privileges allow it.
		/// </remarks>
		public UserGroup():
			base()
		{
		}

		/// <summary>
		/// Internal constructor
		/// </summary>
		/// <remarks>
		/// This constructor needs to be called if a UserGroup object is constructed with data from the
		/// database. No privilege check or other initialization is performed.
		/// </remarks>
		/// <param name="init">Dummy parameter, should always be <c>false</c></param>
		protected UserGroup(bool init):
			base(init)
		{
		}

		/// <summary>
		/// Check creation privileges
		/// </summary>
		/// <remarks>
		/// This method checks if a user has sufficient privileges to create a user group.
		/// </remarks>
		/// <param name="user">The <see cref="User"/> creating the user group.</param>
		/// <returns><c>true</c> if the user has sufficient privileges, <c>false otherwise</c></returns>
		public override bool CanCreate(User user)
		{
			return User.HasPrivilege(Privilege.ManageUsers, user);
		}

		/// <summary>
		/// Check modification privileges
		/// </summary>
		/// <remarks>
		/// This method checks if a user has sufficient privileges to modify a user group.
		/// </remarks>
		/// <param name="user">The <see cref="User"/> modifying the user group.</param>
		/// <returns><c>true</c> if the user has sufficient privileges, <c>false otherwise</c></returns>
		public override bool CanModify(User user)
		{
			return User.HasPrivilege(Privilege.ManageUsers, user);
		}

		/// <summary>
		/// Compares the titles of two UserGroup objects
		/// </summary>
		/// <remarks>
		/// This method is provided to easily sort a list of UserGroups by their title.
		/// </remarks>
		/// <param name="c">Another UserGroup object</param>
		/// <returns>The result of the string comparison of the two UserGroups' titles.</returns>
		public int CompareTo(object c)
		{
			return title.CompareTo(((UserGroup)c).title);
		}

		/// <summary>
		/// Delete user group
		/// </summary>
		/// <remarks>
		/// This method removes a user group from the system.
		/// </remarks>
		public virtual void Delete()
		{
			User.RequirePrivilege(Privilege.ManageUsers);
			if (id > 0)
			{
				using (DBConnection conn = DBConnector.GetConnection())
				{
					// remove access control entries
					Query query = new Query(conn,
						@"DELETE FROM AccessControl WHERE GroupID={groupid}");
					query.AddParam("groupid", id);
					conn.ExecQuery(query);

					// remove user group entry
					query = new Query(conn,
						@"DELETE FROM UserGroups WHERE ID={groupid}");
					query.AddParam("groupid", id);
					conn.ExecQuery(query);
				}
				RemoveFromCache(this, true);
				id = 0;
			}
			ResetModified();
		}

		/// <summary>
		/// Internal identifier
		/// </summary>
		/// <remarks>
		/// The internal identifier is the primary key used in the database.
		/// </remarks>
		/// <value>
		/// The internal identifier of this user group.
		/// </value>
		public int ID
		{
			get
			{
				return id;
			}
		}

		/// <summary>
		/// Internal identifier
		/// </summary>
		/// <remarks>
		/// The internal identifier is the primary key used in the database.
		/// </remarks>
		/// <returns>
		/// The internal identifier of this user group.
		/// </returns>
		public override int GetID()
		{
			return id;
		}

		/// <summary>
		/// Return the class identifier
		/// </summary>
		/// <remarks>
		/// Every class derived from <see cref="CachableObject"/> must implement this method. 
		/// It must return a string that is unique in the system. 
		/// The suggested format is <c>Namespace.Class</c>.
		/// </remarks>
		/// <returns>A unique string identifier for this class</returns>
		protected override string GetClassIdentifier()
		{
			return classid;
		}

		/// <summary>
		/// Title
		/// </summary>
		/// <remarks>
		/// The title cannot be <c>null</c> or empty.
		/// </remarks>
		/// <value>
		/// The title of this user group
		/// </value>
		/// <exception cref="CoreException">Thrown if the specified title is <c>null</c> or empty.</exception>
		public string Title
		{
			get
			{
				return title;
			}
			set
			{
				if (value != null && value.Length > 0) 
				{
					MarkModified();
					title = (value.Length > 255 ? value.Substring(0, 255) : value);
				}
				else
					throw new CoreException("Title must not be null or empty.");
			}
		}

		/// <summary>
		/// Write changes to the database
		/// </summary>
		/// <remarks>
		/// The CommitChanges method is called by the <see cref="ModifiableObject.Update"/> method
		/// whenever the object has been modified and needs to be committed to the database.
		/// </remarks>
		/// <exception cref="CoreException">Thrown if no title has been set for this user group.</exception>
		/// <exception cref="CoreException">Thrown if the user group could not be written to the database.</exception>
		protected override void CommitChanges()
		{
			if (title == null || title.Length == 0)
				throw new CoreException("Title has to be set for a user group");
			int result;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query;
				if (id == 0) 
				{
					query = new Query(conn,
						@"INSERT INTO UserGroups (Title,Type) 
						VALUES ({title},{type})");
					query.AddParam("title", title);
					query.AddParam("type", GetGroupType());
					result = conn.ExecQuery(query);
					if (result == 1)
					{
						id = conn.LastIdentity("UserGroups");
						AddToCache(this, true);
					}
					else
						throw new CoreException("Could not write new user group object to database");
				}
				else
				{
					query = new Query(conn,
						@"UPDATE UserGroups SET Title={title},Type={type} WHERE ID={id}");
					query.AddParam("title", title);
					query.AddParam("type", GetGroupType());
					query.AddParam("id", id);
					result = conn.ExecQuery(query);
				}
			}
			if (result != 1) 
				throw new CoreException("Could not write modified user group object to database");
		}

		/// <summary>
		/// Initialize user group
		/// </summary>
		/// <remarks>
		/// This method is called after a user group object has been created and its
		/// basic properties (like its title) have been retrieved from the database.  At this point
		/// classes inheriting from UserGroup should load other properties, user membership lists,
		/// etc.
		/// </remarks>
		public abstract void Initialize();

		/// <summary>
		/// Extended properties
		/// </summary>
		/// <remarks>
		/// This method indicates if a user group class provides extended properties.
		/// </remarks>
		/// <returns><c>true</c> if this instance is from a class that has extended properties,
		/// <c>false</c> otherwise.</returns>
		public abstract bool HasProperties();

		/// <summary>
		/// User deletion handler 
		/// </summary>
		/// <remarks>
		/// This method is called when a user account is deleted.  It calls 
		/// <see cref="DeletingUser(int)"/>, which must be overridden if
		/// user related data needs to be removed from the database.
		/// </remarks>
		/// <param name="user">The user that is being deleted.</param>
		public void DeletingUser(User user)
		{
			User.RequirePrivilege(Privilege.ManageUsers);
			if (user != null)
				DeletingUser(user.ID);
		}

		/// <summary>
		/// User deletion handler 
		/// </summary>
		/// <remarks>
		/// This method is called when a user account is deleted.  It must be overridden if
		/// user related data needs to be removed from the database.
		/// </remarks>
		/// <param name="userid">The internal identifier of the user that is being deleted.</param>
		public virtual void DeletingUser(int userid)
		{
			User.RequirePrivilege(Privilege.ManageUsers);
		}

		/// <summary>
		/// Get starting patterns of user group titles
		/// </summary>
		/// <remarks>
		/// This is a utility method that allows a user group management tool to create
		/// a user group selection menu, e.g. show all groups that start with a certain letter.
		/// </remarks>
		/// <param name="length">The length of the patterns to return, usually 1</param>
		/// <returns>An sorted array of patterns</returns>
		public static string[] GetTitlePatterns(int length)
		{
			if (length < 1)
				return null;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					"SELECT DISTINCT UPPER(LEFT(Title,{length})) AS P FROM UserGroups ORDER BY P");
				query.AddParam("length", length);
				DataTable table = conn.SelectQuery(query);
				string[] result = new string[table.Rows.Count];
				for (int i = 0; i < table.Rows.Count; i++)
					result[i] = conn.DataToString(table.Rows[i]["P"]);
				return result;
			}
		}

		/// <summary>
		/// Get user groups by their title
		/// </summary>
		/// <remarks>
		/// This is a utility method that allows a user group management tool to create
		/// a user group selection menu, e.g. show all groups that start with a certain letter.
		/// </remarks>
		/// <param name="beginswith">The pattern that all returned user groups' titles need to
		/// start with</param>
		/// <returns>A sorted array of user groups, or an empty array if no matching user groups
		/// are found</returns>
		public static UserGroup[] GetByTitlePattern(string beginswith)
		{
			if (beginswith == null || beginswith.Length == 0)
				return new UserGroup[0];
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					"SELECT ID FROM UserGroups WHERE Title LIKE '{beginswith:lq}%' ORDER BY Title");
				query.AddParam("beginswith", beginswith);
				DataTable table = conn.SelectQuery(query);
				UserGroup[] usergroups = new UserGroup[table.Rows.Count];
				for (int row = 0; row < table.Rows.Count; row++)
					usergroups[row] = GetByID(conn.DataToInt(table.Rows[row]["ID"], 0));
				return usergroups;
			}
		}

		/// <summary>
		/// Find group memberships
		/// </summary>
		/// <remarks>
		/// This method searches for a user specified by the internal identifier in all user groups. 
		/// </remarks>
		/// <param name="user">The <see cref="User"/> to find</param>
		/// <returns>An array of identifiers of the user groups containing the user.</returns>
		public static UserGroup[] FindUser(User user) 
		{
			// get a list of all usergroups that don't have explicit membership plus
			// all usergroups with explicit membership that the requested user is a member of
			if (user == null || user.ID == 0)
				return null;
			int[] ids = (int[])GetObjectItemFromCache(
				String.Format("finduser_{0}", user.ID));
			if (ids == null)
			{
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"SELECT DISTINCT ID FROM UserGroups WHERE Type<>'M' OR 
						ID IN (SELECT GroupID FROM UserGroupMembers WHERE UserID={userid})");
					query.AddParam("userid", user.ID);
					ids = conn.TableToArray(conn.SelectQuery(query));
				}
				if (ids == null)
					return null;
				AddObjectItemToCache(String.Format("finduser_{0}", user.ID), ids);
			}
			ArrayList a = new ArrayList();
			foreach (UserGroup group in UserGroup.GetByID(ids))
				if (group.HasMember(user))
					a.Add(group);
			if (a.Count == 0)
				return null;
			else
				return (UserGroup[])a.ToArray(typeof(UserGroup));
		}

		/// <summary>
		/// Available user groups
		/// </summary>
		/// <remarks>
		/// This method returns a complete list of all user groups in the system
		/// </remarks>
		/// <returns>An array of all available user groups</returns>
		public static UserGroup[] GetUserGroups()
		{
			ArrayList ids = GetUserGroupIDs();
			ArrayList result = new ArrayList(ids.Count);
			ArrayList missing = new ArrayList();
			foreach (int id in ids)
			{
				UserGroup g = (UserGroup)GetFromCache(classid, id);
				if (g == null)
					missing.Add(id);
				else
					result.Add(g);
			}
			if (missing.Count > 0)
			{
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"SELECT ID,Title,Type FROM UserGroups WHERE ID IN {ids}");
					query.AddParam("ids", missing);
					DataTable table = conn.SelectQuery(query);
					foreach (DataRow row in table.Rows)
					{
						UserGroup group = CreateUserGroupByType(conn.DataToChar(row["type"], '\0'), false);
						group.id = conn.DataToInt(row["ID"], 0);
						group.title = conn.DataToString(row["Title"]);
						group.Initialize();
						AddToCache(group);
						result.Add(group);
					}
				}
			}
			return (UserGroup[])result.ToArray(typeof(UserGroup));
		}
		
		/// <summary>
		/// Retrieve a user group
		/// </summary>
		/// <remarks>
		/// This method returns a user group by its internal identifier.
		/// </remarks>
		/// <param name="id">The internal identifier of the user group</param>
		/// <returns>The UserGroup object or <c>null</c> if the user group could not be found.</returns>
		public static UserGroup GetByID(int id)
		{
			UserGroup group = (UserGroup)GetFromCache(classid, id);
			if (group == null)
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"SELECT ID,Title,Type FROM UserGroups WHERE ID={id}");
					query.AddParam("id", id);
					DataTable table = conn.SelectQuery(query);
					if (table.Rows.Count > 0)
					{
						DataRow row = table.Rows[0];
						group = CreateUserGroupByType(conn.DataToChar(row["type"], '\0'), false);
						group.id = conn.DataToInt(row["ID"], 0);
						group.title = conn.DataToString(row["Title"]);
						group.Initialize();
						AddToCache(group);
					}
				}
			return group;
		}

		private static UserGroup CreateUserGroupByType(char type, bool checkpriv)
		{
			switch (type)
			{
				case 'M':
					return new MemberUserGroup(checkpriv);
				case 'E':
					return new EverybodyUserGroup(checkpriv);
				case 'A':
					return new AuthenticatedUserGroup(checkpriv);
				case 'I':
					return new IPBasedUserGroup(checkpriv);
				case 'P':
					return new AttributeBasedUserGroup(checkpriv);
				default:
					throw new CoreException("Invalid user group type");
			}
		}

		/// <summary>
		/// Create new user group
		/// </summary>
		/// <remarks>
		/// This utility method creates a new user group object of the specified type.
		/// </remarks>
		/// <param name="type">The type of user group to create</param>
		/// <returns>A new instance of the specified user group class</returns>
		public static UserGroup CreateUserGroupByType(char type)
		{
			switch (type)
			{
				case 'M':
					return new MemberUserGroup();
				case 'E':
					return new EverybodyUserGroup();
				case 'A':
					return new AuthenticatedUserGroup();
				case 'I':
					return new IPBasedUserGroup();
				case 'P':
					return new AttributeBasedUserGroup();
				default:
					throw new CoreException("Invalid user group type");
			}
		}

		/// <summary>
		/// Retrieve user groups
		/// </summary>
		/// <remarks>
		/// This method returns user groups by their internal identifiers.
		/// </remarks>
		/// <param name="id">An array of internal identifiers of the user groups</param>
		/// <returns>An array with the same length as the parameter array. Individual
		/// array elements may be <c>null</c> if not all user groups could be retrieved.</returns>
		public static UserGroup[] GetByID(int[] id)
		{
			UserGroup[] groups = new UserGroup[id.Length];
			for (int i = 0; i < id.Length; i++)
				groups[i] = GetByID(id[i]);
			return groups;
		}

		/// <summary>
		/// Retrieve list of user group identifiers
		/// </summary>
		/// <remarks>
		/// This method retrieves a complete list of all internal identifiers of all
		/// user groups in the system.
		/// </remarks>
		/// <returns>An ArrayList holding the internal identifiers of all
		/// user groups in the system.</returns>
		private static ArrayList GetUserGroupIDs()
		{
			ArrayList ids = GetIDsFromCache(classid);
			if (ids == null)
			{
				ids = new ArrayList();
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						"SELECT ID FROM UserGroups");
					DataTable table = conn.SelectQuery(query);
					foreach (DataRow row in table.Rows)
						ids.Add(conn.DataToInt(row["ID"], 0));
				}
				AddIDsToCache(classid, ids);
			}
			return ids;
		}

		/// <summary>
		/// The title of the user group
		/// </summary>
		/// <remarks>
		/// This method returns the title of the user group
		/// </remarks>
		/// <returns>The title of the user group</returns>
		public string GetName()
		{
			return title;
		}

		/// <summary>
		/// Check for user membership
		/// </summary>
		/// <remarks>
		/// This method checks if a user is a member of this user group.
		/// </remarks>
		/// <param name="user">The user to check</param>
		/// <returns><c>true</c> if the user is a member, <c>false</c> otherwise.</returns>
		public abstract bool HasMember(User user);

		/// <summary>
		/// Group type identifier
		/// </summary>
		/// <remarks>
		/// Each type of user group must have a unique identifier.
		/// </remarks>
		/// <returns>A character representing the user group type.</returns>
		public abstract char GetGroupType();
	}

	/// <summary>
	/// Group of Users
	/// </summary>
	/// <remarks>
	/// User groups are simple collection of <see cref="User"/> objects. They are used to group similar users
	/// together to collectively assign them <see cref="Privilege"/>s.
	/// </remarks>
	public class MemberUserGroup:
		UserGroup
	{
		private ArrayList users = new ArrayList();

		/// <summary>
		/// Constructor
		/// </summary>
		/// <remarks>
		/// Default constructor
		/// </remarks>
		public MemberUserGroup():
			base()
		{
		}

		/// <summary>
		/// Internal constructor
		/// </summary>
		/// <remarks>
		/// This constructor needs to be called if a MemberUserGroup object is constructed 
		/// with data from the database. No privilege check or other initialization is performed.
		/// </remarks>
		/// <param name="init">Dummy parameter, should always be <c>false</c></param>
		internal MemberUserGroup(bool init):
			base(init)
		{
		}

		/// <summary>
		/// Group type identifier
		/// </summary>
		/// <remarks>
		/// Each type of user group must have a unique identifier.
		/// </remarks>
		/// <returns>A character representing the user group type.</returns>
		public override char GetGroupType()
		{
			return 'M';
		}

		/// <summary>
		/// Initialize user group
		/// </summary>
		/// <remarks>
		/// This method is called after a user group object has been created and its
		/// basic properties (like its title) have been retrieved from the database.  This
		/// method then retrieves the list of users who are members of the group.
		/// </remarks>
		public override void Initialize()
		{
			users = new ArrayList();
			int[] ids;
			using (DBConnection conn = DBConnector.GetConnection())
			{
				// only return existing user IDs, just in case there are invalid rows
				Query query = new Query(conn,
					@"SELECT UserID FROM UserGroupMembers WHERE GroupID={groupid}");
				query.AddParam("groupid", id);
				ids = conn.TableToArray(conn.SelectQuery(query));
			}
			if (ids != null)
				users.AddRange(ids);
		}

		/// <summary>
		/// Extended properties
		/// </summary>
		/// <remarks>
		/// This method indicates if a user group class provides extended properties.
		/// </remarks>
		/// <returns><c>true</c></returns>
		public override bool HasProperties()
		{
			return true;
		}

		/// <summary>
		/// Delete user group
		/// </summary>
		/// <remarks>
		/// This method removes a user group from the system.
		/// </remarks>
		public override void Delete()
		{
			User.RequirePrivilege(Privilege.ManageUsers);
			if (id > 0)
				RemoveAll();
			base.Delete();
		}

		/// <summary>
		/// Add user to this user group
		/// </summary>
		/// <remarks>
		/// This method adds the user specified by the internal identifier
		/// </remarks>
		/// <param name="user">The user to add</param>
		public virtual void Add(User user)
		{			
			if (user != null)
				Add(user.ID);
		}

		/// <summary>
		/// Add user to this user group
		/// </summary>
		/// <remarks>
		/// This method adds the user specified by the internal identifier
		/// </remarks>
		/// <param name="userid">The internal identifier of the user to add</param>
		/// <exception cref="CoreException">Thrown if the group has been newly created
		/// and not written to the database yet.</exception>
		/// <exception cref="CoreException">Thrown if the user has been newly created
		/// and not written to the database yet.</exception>
		/// <exception cref="CoreException">Thrown if the user could not be added
		/// to the user group.</exception>
		public virtual void Add(int userid)
		{
			User.RequirePrivilege(Privilege.ManageUsers);
			if (id == 0)
				throw new CoreException("Group must be written to database first");
			if (userid == 0)
				throw new CoreException("User must be written to database first");
			if (!users.Contains(userid))
			{
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"INSERT INTO UserGroupMembers (UserID,GroupID) 
						VALUES ({userid},{groupid})");
					query.AddParam("userid", userid);
					query.AddParam("groupid", id);
					int result = conn.ExecQuery(query);
					if (result == 1)
						users.Add(userid);
					else
						throw new CoreException("Could not add user to group");
				}
			}
		}


		/// <summary>
		/// Remove user from user group
		/// </summary>
		/// <remarks>
		/// This method removes a user from this group.
		/// </remarks>
		/// <param name="userid">The internal identifier of the user to remove</param>
		/// <exception cref="CoreException">Thrown if the group has been newly created and not
		/// been written to the database yet.</exception>
		/// <exception cref="CoreException">Thrown if the user could not be removed from
		/// the user group.</exception>
		public override void DeletingUser(int userid)
		{
			base.DeletingUser(userid);
			if (id == 0)
				throw new CoreException("Group must be written to database first");
			if (users.Contains(userid))
			{
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						"DELETE FROM UserGroupMembers WHERE UserID={userid} AND GroupID={groupid}");
					query.AddParam("userid", userid);
					query.AddParam("groupid", id);

					int result = conn.ExecQuery(query);
					if (result == 1)
						users.Remove(userid);
					else
						throw new CoreException("Could not remove user from group");
				}
			}
		}

		/// <summary>
		/// Remove user from user group
		/// </summary>
		/// <remarks>
		/// This method removes a user from this group by calling <see cref="DeletingUser"/>.
		/// </remarks>
		/// <param name="user">The user to remove</param>
		public void Remove(User user)
		{
			DeletingUser(user);
		}

		/// <summary>
		/// Remove user from user group
		/// </summary>
		/// <remarks>
		/// This method removes a user from this group by calling <see cref="DeletingUser"/>.
		/// </remarks>
		/// <param name="userid">The internal identifier of the user to remove</param>
		public void Remove(int userid)
		{
			DeletingUser(userid);
		}

		/// <summary>
		/// Remove all users from user group
		/// </summary>
		/// <remarks>
		/// This method removes all users from this user group.
		/// </remarks>
		/// <exception cref="CoreException">Thrown if the user group has been newly created
		/// and not been written to the database yet.</exception>
		public virtual void RemoveAll()
		{
			User.RequirePrivilege(Privilege.ManageUsers);
			if (id == 0)
				throw new CoreException("Group must be written to database first");
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"DELETE FROM UserGroupMembers WHERE GroupID={groupid}");
				query.AddParam("groupid", id);
				conn.ExecQuery(query);
				users.Clear();
			}
		}

		/// <summary>
		/// Return user group members
		/// </summary>
		/// <remarks>
		/// This method returns <see cref="User"/> objects for all members of this group.
		/// </remarks>
		/// <returns>An array of <see cref="User"/> objects.</returns>
		public virtual User[] GetUsers()
		{
			ArrayList u = new ArrayList(User.GetByID((int[])users.ToArray(typeof(int))));
			while (u.Contains(null))
				u.Remove(null);
			return (User[])u.ToArray(typeof(User));
		}

		/// <summary>
		/// Return user group members
		/// </summary>
		/// <remarks>
		/// This method returns the internal identifiers of all users that are members
		/// of this group.
		/// </remarks>
		/// <returns>An array of internal identifiers.</returns>
		public virtual int[] GetUserIDs()
		{
			return (int[])users.ToArray(typeof(int));
		}

		/// <summary>
		/// Return user count
		/// </summary>
		/// <remarks>
		/// This method returns the number of users in this group
		/// </remarks>
		/// <returns>The number of users in this group</returns>
		public virtual int GetUserCount()
		{
			return users.Count;
		}

		/// <summary>
		/// Check user membership
		/// </summary>
		/// <remarks>
		/// This method determines if a given user is member of this group
		/// </remarks>
		/// <param name="user">The user to check for</param>
		/// <returns><c>true</c> if the user is a member of this group, <c>false</c> otherwise</returns>
		public override bool HasMember(User user)
		{
			return (user != null && users.Contains(user.ID));
		}
	}

	/// <summary>
	/// Authenticated Users Group
	/// </summary>
	/// <remarks>
	/// All authenticated users are automatically members of this user group.
	/// </remarks>
	public class AuthenticatedUserGroup:
		UserGroup
	{
		/// <summary>
		/// Default constructor
		/// </summary>
		/// <remarks>
		/// This constructor creates a new object if the current user's privileges allow it.
		/// </remarks>
		public AuthenticatedUserGroup():
			base()
		{
		}

		/// <summary>
		/// Internal constructor
		/// </summary>
		/// <remarks>
		/// This constructor needs to be called if a UserGroup object is constructed with data from the
		/// database. No privilege check or other initialization is performed.
		/// </remarks>
		/// <param name="init">Dummy parameter, should always be <c>false</c></param>
		internal AuthenticatedUserGroup(bool init):
			base(init)
		{
		}

		/// <summary>
		/// Group type identifier
		/// </summary>
		/// <remarks>
		/// Each type of user group must have a unique identifier.
		/// </remarks>
		/// <returns>A character representing the user group type.</returns>
		public override char GetGroupType()
		{
			return 'A';
		}

		/// <summary>
		/// Check for user membership
		/// </summary>
		/// <remarks>
		/// This method checks if a user is a member of this user group.
		/// </remarks>
		/// <param name="user">The user to check</param>
		/// <returns><c>true</c> if the user is a member, <c>false</c> otherwise.</returns>
		public override bool HasMember(User user)
		{
			return (user != null && user.IsAuthenticated && user.ID > 0);
		}

		/// <summary>
		/// Initialize user group
		/// </summary>
		/// <remarks>
		/// This method is called after a user group object has been created and its
		/// basic properties (like its title) have been retrieved from the database.  
		/// This type of user group requires no further action.
		/// </remarks>
		public override void Initialize()
		{
		}

		/// <summary>
		/// Extended properties
		/// </summary>
		/// <remarks>
		/// This method indicates if a user group class provides extended properties.
		/// </remarks>
		/// <returns><c>false</c></returns>
		public override bool HasProperties()
		{
			return false;
		}
	}

	/// <summary>
	/// User group containing everybody
	/// </summary>
	/// <remarks>
	/// Every user is automatically a member of this group
	/// </remarks>
	public class EverybodyUserGroup:
		UserGroup
	{
		/// <summary>
		/// Default constructor
		/// </summary>
		/// <remarks>
		/// This constructor creates a new object if the current user's privileges allow it.
		/// </remarks>
		public EverybodyUserGroup():
			base()
		{
		}

		/// <summary>
		/// Internal constructor
		/// </summary>
		/// <remarks>
		/// This constructor needs to be called if a UserGroup object is constructed with data from the
		/// database. No privilege check or other initialization is performed.
		/// </remarks>
		/// <param name="init">Dummy parameter, should always be <c>false</c></param>
		internal EverybodyUserGroup(bool init):
			base(init)
		{
		}

		/// <summary>
		/// Group type identifier
		/// </summary>
		/// <remarks>
		/// Each type of user group must have a unique identifier.
		/// </remarks>
		/// <returns>A character representing the user group type.</returns>
		public override char GetGroupType()
		{
			return 'E';
		}

		/// <summary>
		/// Check for user membership
		/// </summary>
		/// <remarks>
		/// This method checks if a user is a member of this user group.
		/// </remarks>
		/// <param name="user">The user to check</param>
		/// <returns><c>true</c> if the user is a member, <c>false</c> otherwise.</returns>
		public override bool HasMember(User user)
		{
			return (user != null);
		}

		/// <summary>
		/// Initialize user group
		/// </summary>
		/// <remarks>
		/// This method is called after a user group object has been created and its
		/// basic properties (like its title) have been retrieved from the database.  
		/// This type of user group requires no further action.
		/// </remarks>
		public override void Initialize()
		{
		}

		/// <summary>
		/// Extended properties
		/// </summary>
		/// <remarks>
		/// This method indicates if a user group class provides extended properties.
		/// </remarks>
		/// <returns><c>false</c></returns>
		public override bool HasProperties()
		{
			return false;
		}
	}

	/// <summary>
	/// IP address based user group
	/// </summary>
	/// <remarks>
	/// Users membership for this type of user group is determined by the user's IP address
	/// </remarks>
	public class IPBasedUserGroup:
		UserGroup
	{
		/// <summary>
		/// IP address range
		/// </summary>
		/// <remarks>
		/// Membership in an <see cref="IPBasedUserGroup"/> is determined by the user's IP address.
		/// It has to be within a range of addresses specified by a subnet and subnet mask.
		/// </remarks>
		public struct RangeEntry
		{
			internal string subnet;
			internal string mask;

			/// <summary>
			/// Subnet part of the IP address range specification
			/// </summary>
			/// <remarks>
			/// See <see cref="IPAddress.IsInSubnet"/> for examples.
			/// </remarks>
			/// <value>
			/// Subnet part of the IP address range specification
			/// </value>
			public string Subnet
			{
				get
				{
					return subnet;
				}
			}

			/// <summary>
			/// Mask part of the IP address range specification
			/// </summary>
			/// <remarks>
			/// See <see cref="IPAddress.IsInSubnet"/> for examples.
			/// </remarks>
			/// <value>
			/// Mask part of the IP address range specification
			/// </value>
			public string Mask
			{
				get
				{
					return mask;
				}
			}
		}

		private ArrayList entries;

		/// <summary>
		/// Default constructor
		/// </summary>
		/// <remarks>
		/// This constructor creates a new object if the current user's privileges allow it.
		/// </remarks>
		public IPBasedUserGroup():
			base()
		{
		}

		/// <summary>
		/// Internal constructor
		/// </summary>
		/// <remarks>
		/// This constructor needs to be called if a UserGroup object is constructed with data from the
		/// database. No privilege check or other initialization is performed.
		/// </remarks>
		/// <param name="init">Dummy parameter, should always be <c>false</c></param>
		internal IPBasedUserGroup(bool init):
			base(init)
		{
		}

		/// <summary>
		/// Group type identifier
		/// </summary>
		/// <remarks>
		/// Each type of user group must have a unique identifier.
		/// </remarks>
		/// <returns>A character representing the user group type.</returns>
		public override char GetGroupType()
		{
			return 'I';
		}

		/// <summary>
		/// Check for user membership
		/// </summary>
		/// <remarks>
		/// This method checks if a user is a member of this user group.
		/// </remarks>
		/// <param name="user">The user to check</param>
		/// <returns><c>true</c> if the user is a member, <c>false</c> otherwise.</returns>
		public override bool HasMember(User user)
		{
			string a = user.IPAddress;
			if (a != null)
				foreach (RangeEntry entry in entries)
					if (IPAddress.IsInSubnet(a, entry.subnet, entry.mask))
						return true;
			return false;
		}

		/// <summary>
		/// Initialize user group
		/// </summary>
		/// <remarks>
		/// This method is called after a user group object has been created and its
		/// basic properties (like its title) have been retrieved from the database.  
		/// This method then loads the IP address settings required to determine group
		/// membership.
		/// </remarks>
		public override void Initialize()
		{
			entries = new ArrayList();
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT Subnet,Mask FROM UserGroupIPRanges WHERE GroupID={groupid}");
				query.AddParam("groupid", id);
				DataTable table = conn.SelectQuery(query);
				foreach (DataRow r in table.Rows)
				{
					RangeEntry e = new RangeEntry();
					e.subnet = conn.DataToString(r["Subnet"]);
					e.mask = conn.DataToString(r["Mask"]);
					entries.Add(e);
				}
			}
		}

		/// <summary>
		/// Delete user group
		/// </summary>
		/// <remarks>
		/// This method removes a user group from the system.
		/// </remarks>
		public override void Delete()
		{
			User.RequirePrivilege(Privilege.ManageUsers);
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"DELETE FROM UserGroupIPRanges WHERE GroupID={groupid}");
				query.AddParam("groupid", id);
				conn.ExecQuery(query);
			}
			base.Delete();
		}

		/// <summary>
		/// Extended properties
		/// </summary>
		/// <remarks>
		/// This method indicates if a user group class provides extended properties.
		/// </remarks>
		/// <returns><c>true</c></returns>
		public override bool HasProperties()
		{
			return true;
		}

		/// <summary>
		/// Add new IP address range
		/// </summary>
		/// <remarks>
		/// This method adds a new IP address range to this user group, thereby changing
		/// which users will be members of this group.
		/// </remarks>
		/// <param name="subnet">Subnet part of the IP address range specification</param>
		/// <param name="mask">Mask part of the IP address range specification</param>
		public void Add(string subnet, string mask)
		{
			User.RequirePrivilege(Privilege.ManageUsers);
			if (!IPAddress.IsValid(subnet) || !IPAddress.IsValid(mask))
				throw new CoreException("IP address is not valid");
			RangeEntry e = new RangeEntry();
			e.subnet = subnet;
			e.mask = mask;
			if (!entries.Contains(e))
			{
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"INSERT INTO UserGroupIPRanges (GroupID,Subnet,Mask) 
						VALUES ({groupid},{subnet},{mask})");
					query.AddParam("groupid", id);
					query.AddParam("subnet", subnet);
					query.AddParam("mask", mask);
					conn.ExecQuery(query);
				}
				entries.Add(e);
			}
		}

		/// <summary>
		/// Remove IP address range
		/// </summary>
		/// <remarks>
		/// This method removes an IP address range from this user group, thereby changing
		/// which users will be members of this group.
		/// </remarks>
		/// <param name="subnet">Subnet part of the IP address range specification</param>
		/// <param name="mask">Mask part of the IP address range specification</param>
		public void Remove(string subnet, string mask)
		{
			User.RequirePrivilege(Privilege.ManageUsers);
			RangeEntry e = new RangeEntry();
			e.subnet = subnet;
			e.mask = mask;
			if (entries.Contains(e))
			{
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn,
						@"DELETE FROM UserGroupIPRanges 
						WHERE GroupID={groupid} AND Subnet={subnet} AND Mask={mask}");
					query.AddParam("groupid", id);
					query.AddParam("subnet", subnet);
					query.AddParam("mask", mask);
					conn.ExecQuery(query);
				}
				entries.Remove(e);
			}
		}

		/// <summary>
		/// Retrieve current entries
		/// </summary>
		/// <remarks>
		/// This method returns the currently configured IP address ranges for this user group.
		/// </remarks>
		/// <returns>An array of current IP address ranges</returns>
		public RangeEntry[] GetEntries()
		{
			return (RangeEntry[])entries.ToArray(typeof(RangeEntry));
		}
	}

	/// <summary>
	/// Attribute based user group
	/// </summary>
	/// <remarks>
	/// Users membership for this type of user group is determined by the user's attributes.
	/// <seealso cref="User.GetAttribute"/>
	/// </remarks>
	public class AttributeBasedUserGroup:
		UserGroup
	{
		/// <summary>
		/// Required attribute entry
		/// </summary>
		/// <remarks>
		/// An attribute entry consists of an attribute name plus a set of possible values.
		/// The requirement is fulfilled if the specified attribute has one of the specified
		/// values.
		/// </remarks>
		public class AttributeEntry
		{
			internal string attribute;
			internal ArrayList values = new ArrayList();

			/// <summary>
			/// Attribute
			/// </summary>
			/// <remarks>
			/// The name of the attribute.
			/// </remarks>
			public string Attribute
			{
				get
				{
					return attribute;
				}
			}

			/// <summary>
			/// Attribute values
			/// </summary>
			/// <remarks>
			/// The list of attribute values of which one has to match the user's attribute value.
			/// </remarks>
			public string[] Values
			{
				get
				{
					return (string[])values.ToArray(typeof(string));
				}
			}
		}

		private SortedList entries;

		/// <summary>
		/// Default constructor
		/// </summary>
		/// <remarks>
		/// This constructor creates a new object if the current user's privileges allow it.
		/// </remarks>
		public AttributeBasedUserGroup():
			base()
		{
		}

		internal AttributeBasedUserGroup(bool init):
			base(init)
		{
		}

		/// <summary>
		/// Group type identifier
		/// </summary>
		/// <remarks>
		/// Each type of user group must have a unique identifier.
		/// </remarks>
		/// <returns>A character representing the user group type.</returns>
		public override char GetGroupType()
		{
			return 'P';
		}

		/// <summary>
		/// Check for user membership
		/// </summary>
		/// <remarks>
		/// This method checks if a user is a member of this user group.
		/// </remarks>
		/// <param name="user">The user to check</param>
		/// <returns><c>true</c> if the user is a member, <c>false</c> otherwise.</returns>
		public override bool HasMember(User user)
		{
			if (entries.Count == 0)
				return false;
			foreach (AttributeEntry a in entries.Values)
			{
				string[] values = user.GetMultiValueAttribute(a.Attribute);
				if (values == null || values.Length == 0)
					return false;
				string[] required = a.Values;
				bool found = false;
				for (int i = 0; i < values.Length && !found; i++)
					for (int j = 0; j < required.Length && !found; j++)
						if (values[i] == required[j])
							found = true;
				if (!found)
					return false;
			}
			return true;
		}

		/// <summary>
		/// Initialize user group
		/// </summary>
		/// <remarks>
		/// This method is called after a user group object has been created and its
		/// basic properties (like its title) have been retrieved from the database.  
		/// This method then loads the IP address settings required to determine group
		/// membership.
		/// </remarks>
		public override void Initialize()
		{
			entries = new SortedList();
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"SELECT Attribute,AttributeValue 
					FROM UserGroupAttributes WHERE GroupID={groupid} 
					ORDER BY Attribute,AttributeValueInstance");
				query.AddParam("groupid", id);
				DataTable table = conn.SelectQuery(query);
				AttributeEntry e = null;
				foreach (DataRow r in table.Rows)
				{
					string a = conn.DataToString(r["Attribute"]);
					if (e == null || a != e.attribute)
					{
						e = new AttributeEntry();
						e.attribute = a;
						entries.Add(a, e);
					}
					e.values.Add(conn.DataToString(r["AttributeValue"]));
				}
			}
		}

		/// <summary>
		/// Delete user group
		/// </summary>
		/// <remarks>
		/// This method removes a user group from the system.
		/// </remarks>
		public override void Delete()
		{
			User.RequirePrivilege(Privilege.ManageUsers);
			using (DBConnection conn = DBConnector.GetConnection())
			{
				Query query = new Query(conn,
					@"DELETE FROM UserGroupAttributes WHERE GroupID={groupid}");
				query.AddParam("groupid", id);
				conn.ExecQuery(query);
			}
			base.Delete();
		}

		/// <summary>
		/// Extended properties
		/// </summary>
		/// <remarks>
		/// This method indicates if a user group class provides extended properties.
		/// </remarks>
		/// <returns><c>true</c></returns>
		public override bool HasProperties()
		{
			return true;
		}

		/// <summary>
		/// Add attribute requirement
		/// </summary>
		/// <remarks>
		/// This method adds another attribute requirement.  A user is only considered
		/// to be a member of the group if all requirements are fulfilled.
		/// </remarks>
		/// <param name="attribute">The name of the attribute</param>
		/// <param name="values">An array of values of which one has to match the
		/// user's attribute value to make the user a member of the group.</param>
		public void Add(string attribute, string[] values)
		{
			User.RequirePrivilege(Privilege.ManageUsers);
			if (attribute == null || attribute.Length == 0 ||
				values == null || values.Length == 0)
				throw new CoreException("Incomplete parameters for attribute entry");
			if (entries.ContainsKey(attribute))
				throw new CoreException("This attribute is already defined");
			AttributeEntry e = new AttributeEntry();
			e.attribute = attribute;
			using (DBConnection conn = DBConnector.GetConnection())
				for (int i = 0; i < values.Length; i++)
					if (values[i] != null && values[i].Length > 0)
					{
						e.values.Add(values[i]);
						Query query = new Query(conn,
							@"INSERT INTO UserGroupAttributes 
							(GroupID,Attribute,AttributeValueInstance,AttributeValue) 
							VALUES ({groupid},{attribute},{instance},{value})");
						query.AddParam("groupid", id);
						query.AddParam("attribute", e.attribute);
						query.AddParam("instance", i);
						query.AddParam("value", values[i]);
						conn.ExecQuery(query);
					}
			if (e.values.Count == 0)
				throw new CoreException("No attribute values specified");
			entries.Add(attribute, e);
		}

		/// <summary>
		/// Remove attribute requirement
		/// </summary>
		/// <remarks>
		/// This method removes an attribute requirement.
		/// </remarks>
		/// <param name="attribute">The name of the attribute of the requirement to remove.</param>
		public void Remove(string attribute)
		{
			User.RequirePrivilege(Privilege.ManageUsers);
			if (entries.ContainsKey(attribute))
			{
				entries.Remove(attribute);
				using (DBConnection conn = DBConnector.GetConnection())
				{
					Query query = new Query(conn, 
						@"DELETE FROM UserGroupAttributes
						WHERE GroupID={groupid} AND Attribute={attribute}");
					query.AddParam("groupid", id);
					query.AddParam("attribute", attribute);
					conn.ExecQuery(query);
				}
			}
		}

		/// <summary>
		/// Retrieve current entries
		/// </summary>
		/// <remarks>
		/// This method returns the currently configured attribute requirements for this user group.
		/// </remarks>
		/// <returns>An array of current attribute requirements</returns>
		public AttributeEntry[] GetEntries()
		{
			AttributeEntry[] a = new AttributeEntry[entries.Count];
			entries.Values.CopyTo(a, 0);
			return a;
		}
	}

}
