//
// System.Data.ConstraintCollection.cs
//
// Author:
//   Franklin Wise <gracenote@earthlink.net>
//   Daniel Morgan
//   
// (C) Ximian, Inc. 2002
// (C) 2002 Franklin Wise
// (C) 2002 Daniel Morgan
//

//
// Copyright (C) 2004 Novell, Inc (http://www.novell.com)
//
// 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;
using System.ComponentModel;

namespace System.Data {
	[Editor]
	[Serializable]
	internal delegate void DelegateValidateRemoveConstraint(ConstraintCollection sender, Constraint constraintToRemove, ref bool fail,ref string failReason);
	
	/// <summary>
	/// hold collection of constraints for data table
	/// </summary>
	[DefaultEvent ("CollectionChanged")]
	[EditorAttribute("Microsoft.VSDesigner.Data.Design.ConstraintsCollectionEditor, "+Consts.AssemblyMicrosoft_VSDesigner, "System.Drawing.Design.UITypeEditor, "+Consts.AssemblySystem_Drawing )]
	[Serializable]
	public class ConstraintCollection : InternalDataCollectionBase 
	{
		//private bool beginInit = false;
		
		public event CollectionChangeEventHandler CollectionChanged;
		internal event DelegateValidateRemoveConstraint ValidateRemoveConstraint;
		private DataTable table;
		
		// Call this to set the "table" property of the UniqueConstraint class
                // intialized with UniqueConstraint( string, string[], bool );
                // And also validate that the named columns exist in the "table"
                private delegate void PostAddRange( DataTable table );
		
		// Keep reference to most recent constraints passed to AddRange()
                // so that they can be added when EndInit() is called.
                private Constraint [] _mostRecentConstraints;

		//Don't allow public instantiation
		//Will be instantianted from DataTable
		internal ConstraintCollection(DataTable table){
			this.table = table;
		} 

		internal DataTable Table{
			get{
				return this.table;
			}
		}

		public virtual Constraint this[string name] {
			get {
				//If the name is not found we just return null
				int index = IndexOf(name); //case insensitive
				if (-1 == index) return null;
				return this[index];
			}
		}
		
		public virtual Constraint this[int index] {
			get {
				if (index < 0 || index >= List.Count)
					throw new IndexOutOfRangeException();
				return (Constraint)List[index];
			}
		}

		private void _handleBeforeConstraintNameChange(object sender, string newName)
		{
			//null or empty
			if (newName == null || newName == "") 
				throw new ArgumentException("ConstraintName cannot be set to null or empty " +
					" after it has been added to a ConstraintCollection.");

			if (_isDuplicateConstraintName(newName,(Constraint)sender))
				throw new DuplicateNameException("Constraint name already exists.");
		}

		private bool _isDuplicateConstraintName(string constraintName, Constraint excludeFromComparison) 
		{
			foreach (Constraint cst in List) {
				if (String.Compare (constraintName, cst.ConstraintName, !Table.CaseSensitive) == 0  && cst != excludeFromComparison) 
					return true;
			}

			return false;
		}
		
		//finds an open name slot of ConstraintXX
		//where XX is a number
		private string _createNewConstraintName() 
		{
			bool loopAgain = false;
			int index = 1;

			do
			{	
				loopAgain = false;
				foreach (Constraint cst in List) 
				{
					//Case insensitive
					if (String.Compare (cst.ConstraintName,
						"Constraint" + index,
						!Table.CaseSensitive,
						Table.Locale)
						== 0)
					{
						loopAgain = true;
						index++;
						break;
					}
				}
			} while (loopAgain);

			return "Constraint" + index.ToString(); 	
			
		}
		
		
		// Overloaded Add method (5 of them)
		// to add Constraint object to the collection

		public void Add(Constraint constraint) 
		{		
			//not null
			if (null == constraint) throw new ArgumentNullException("Can not add null.");
			
			//check constraint membership 
			//can't already exist in this collection or any other
			if (this == constraint.ConstraintCollection) 
				throw new ArgumentException("Constraint already belongs to this collection.");
			if (null != constraint.ConstraintCollection) 
				throw new ArgumentException("Constraint already belongs to another collection.");
			
			//check for duplicate name
#if !NET_1_1
			if (_isDuplicateConstraintName(constraint.ConstraintName,null)  )
				throw new DuplicateNameException("Constraint name already exists.");
#endif
	
			// Check whether Constraint is UniqueConstraint and initailized with the special
                        // constructor - UniqueConstraint( string, string[], bool );
                        // If yes, It must be added via AddRange() only
                        // Environment.StackTrace can help us 
			// FIXME: Is a different mechanism to do this?
                        if (constraint is UniqueConstraint){
                                if ((constraint as UniqueConstraint).DataColsNotValidated == true){
                                        if ( Environment.StackTrace.IndexOf( "AddRange" ) == -1 ){
                                                throw new ArgumentException(" Some DataColumns are invalid - They may not belong to the table associated with this Constraint Collection" );
                                        }
                                }
                        }

			  if (constraint is ForeignKeyConstraint){
                                if ((constraint as ForeignKeyConstraint).DataColsNotValidated == true){
                                        if ( Environment.StackTrace.IndexOf( "AddRange" ) == -1 ){
                                                throw new ArgumentException(" Some DataColumns are invalid - They may not belong to the table associated with this Constraint Collection" );
                                        }
                                }
                        }
 	
			//Allow constraint to run validation rules and setup 
			constraint.AddToConstraintCollectionSetup(this); //may throw if it can't setup

			//Run Constraint to check existing data in table
			// this is redundant, since AddToConstraintCollectionSetup 
			// calls AssertConstraint right before this call
			//constraint.AssertConstraint();

			//if name is null or empty give it a name
			if (constraint.ConstraintName == null || 
				constraint.ConstraintName == "" ) 
			{ 
				constraint.ConstraintName = _createNewConstraintName();
			}

			//Add event handler for ConstraintName change
			constraint.BeforeConstraintNameChange += new DelegateConstraintNameChange(
				_handleBeforeConstraintNameChange);
			
			constraint.ConstraintCollection = this;
			List.Add(constraint);

			if (constraint is UniqueConstraint) 
				((UniqueConstraint)constraint).UpdatePrimaryKey();

			OnCollectionChanged( new CollectionChangeEventArgs( CollectionChangeAction.Add, this) );
		}

	

		public virtual Constraint Add(string name, DataColumn column, bool primaryKey) 
		{

			UniqueConstraint uc = new UniqueConstraint(name, column, primaryKey);
			Add(uc);
			 
			return uc;
		}

		public virtual Constraint Add(string name, DataColumn primaryKeyColumn,
			DataColumn foreignKeyColumn) 
		{
			ForeignKeyConstraint fc = new ForeignKeyConstraint(name, primaryKeyColumn, 
					foreignKeyColumn);
			Add(fc);

			return fc;
		}

		public virtual Constraint Add(string name, DataColumn[] columns, bool primaryKey) 
		{
			UniqueConstraint uc = new UniqueConstraint(name, columns, primaryKey);
			Add(uc);

			return uc;
		}

		public virtual Constraint Add(string name, DataColumn[] primaryKeyColumns,
			DataColumn[] foreignKeyColumns) 
		{
			ForeignKeyConstraint fc = new ForeignKeyConstraint(name, primaryKeyColumns, 
					foreignKeyColumns);
			Add(fc);

			return fc;
		}

		public void AddRange(Constraint[] constraints) {

			//When AddRange() occurs after BeginInit,
                        //it does not add any elements to the collection until EndInit is called.
			if (this.table.fInitInProgress) {
				// Keep reference so that they can be added when EndInit() is called.
                                _mostRecentConstraints = constraints;
                                return;
                        }
                        
                        // Check whether the constraint is UniqueConstraint
                        // And whether it was initialized with the special ctor
                        // i.e UniqueConstraint( string, string[], bool );
                        for (int i = 0; i < constraints.Length; i++){
                                if (constraints[i] is UniqueConstraint){
                                        if (( constraints[i] as UniqueConstraint).DataColsNotValidated == true){
                                                PostAddRange _postAddRange= new PostAddRange ((constraints[i] as UniqueConstraint).PostAddRange);
                                                // UniqueConstraint.PostAddRange() validates whether all named
                                                // columns exist in the table associated with this instance of
                                                // ConstraintCollection.
                                                _postAddRange (this.table);
                                                                                                    
                                        }
                                }
				else if (constraints [i] is ForeignKeyConstraint){
                                        if (( constraints [i] as ForeignKeyConstraint).DataColsNotValidated == true){
                                                (constraints [i] as ForeignKeyConstraint).postAddRange (this.table);
                                        }
                                }
	
                        }
                                                                                                    
                        if ( (constraints == null) || (constraints.Length == 0))
                                throw new ArgumentNullException ("Cannot add null");

			 else {
                                foreach (Constraint constraint in constraints)
                                         Add (constraint);
                                       
                               }

				
		}

		// Helper AddRange() - Call this function when EndInit is called
                internal void PostEndInit()
                {
                        AddRange (_mostRecentConstraints);
                }


		public bool CanRemove(Constraint constraint) 
		{

			//Rule A UniqueConstraint can't be removed if there is
			//a foreign key relationship to that column

			//not null 
			//LAMESPEC: MSFT implementation throws and exception here
			//spec says nothing about this
			if (null == constraint) throw new ArgumentNullException("Constraint can't be null.");
			
			//LAMESPEC: spec says return false (which makes sense) and throw exception for False case (?).
			//TODO: I may want to change how this is done
			//maybe put a CanRemove on the Constraint class
			//and have the Constraint fire this event

			//discover if there is a related ForeignKey
			string failReason ="";
			return _canRemoveConstraint(constraint, ref failReason);
			
		}

		public void Clear() 
		{
			
			//CanRemove? See Lamespec below.

			//the Constraints have a reference to us
			//and we listen to name change events 
			//we should remove these before clearing
			foreach (Constraint con in List)
			{
				con.ConstraintCollection = null;
				con.BeforeConstraintNameChange -= new DelegateConstraintNameChange(
				_handleBeforeConstraintNameChange);
			}

			//LAMESPEC: MSFT implementation allows this
			//even when a ForeignKeyConstraint exist for a UniqueConstraint
			//thus violating the CanRemove logic
			//CanRemove will throws Exception incase of the above
			List.Clear(); //Will violate CanRemove rule
			OnCollectionChanged( new CollectionChangeEventArgs(CollectionChangeAction.Refresh, this) );
		}

		public bool Contains(string name) 
		{
			return (-1 != IndexOf(name));
		}

		public int IndexOf(Constraint constraint) 
		{
			return List.IndexOf(constraint);
		}

		public virtual int IndexOf(string constraintName) 
		{
			//LAMESPEC: Spec doesn't say case insensitive
			//it should to be consistant with the other 
			//case insensitive comparisons in this class

			int index = 0;
			foreach (Constraint con in List)
			{
				if (String.Compare (constraintName, con.ConstraintName, !Table.CaseSensitive, Table.Locale) == 0)
				{
					return index;
				}

				index++;
			}
			return -1; //not found
		}

		public void Remove(Constraint constraint) {
			//LAMESPEC: spec doesn't document the ArgumentException the
			//will be thrown if the CanRemove rule is violated
			
			//LAMESPEC: spec says an exception will be thrown
			//if the element is not in the collection. The implementation
			//doesn't throw an exception. ArrayList.Remove doesn't throw if the
			//element doesn't exist
			//ALSO the overloaded remove in the spec doesn't say it throws any exceptions

			//not null
			if (null == constraint) throw new ArgumentNullException();

			string failReason = "";
			if (! _canRemoveConstraint(constraint, ref failReason) )
			{
				if (failReason != null || failReason != "")	
					throw new ArgumentException(failReason);
				else
					throw new ArgumentException("Can't remove constraint.");		
			}
				
			constraint.RemoveFromConstraintCollectionCleanup(this);
			List.Remove(constraint);
			OnCollectionChanged( new CollectionChangeEventArgs(CollectionChangeAction.Remove,this));
		}

		public void Remove(string name) 
		{
			//if doesn't exist fail quietly
			int index = IndexOf(name);
			if (-1 == index) return;

			Remove(this[index]);
		}

		public void RemoveAt(int index) 
		{
			Remove(this[index]);
		}

		protected override ArrayList List {
			get{
				return base.List;
			}
		}

		protected virtual void OnCollectionChanged( CollectionChangeEventArgs ccevent) 
		{
			if (null != CollectionChanged)
			{
				CollectionChanged(this, ccevent);
			}
		}

		private bool _canRemoveConstraint(Constraint constraint, ref string failReason )
		{
			bool cancel = false;
			string tmp = "";
			if (null != ValidateRemoveConstraint)
			{
				ValidateRemoveConstraint(this, constraint, ref cancel, ref tmp);
			}
			failReason = tmp;
			return !cancel;
		}

		internal ICollection UniqueConstraints
		{
			get
			{ 
				return GetConstraintsCollection(typeof(UniqueConstraint));
			}
		}

		internal ICollection ForeignKeyConstraints
		{
			get
			{ 
				return GetConstraintsCollection(typeof(ForeignKeyConstraint));
			}
		}

		private ICollection GetConstraintsCollection (Type constraintType)
		{
			ArrayList cCollection = new ArrayList();
			foreach (Constraint c in List) 
			{
				if (c.GetType() == constraintType)
					cCollection.Add(c);
			}
			return cCollection;
		}

	}
}
