
//
// 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;

namespace System.Data
{
	/// <summary>
	/// Summary description for MergeManager.
	/// </summary>
	internal class MergeManager
	{
		internal static void Merge(DataSet targetSet, DataSet sourceSet, bool preserveChanges, MissingSchemaAction missingSchemaAction)
		{
			if(targetSet == null)
				throw new ArgumentNullException("targetSet");
			if(sourceSet == null)
				throw new ArgumentNullException("sourceSet");

			bool prevEC = targetSet.EnforceConstraints;
			targetSet.EnforceConstraints = false;

			foreach (DataTable t in sourceSet.Tables)
				MergeManager.Merge(targetSet, t, preserveChanges, missingSchemaAction);

			AdjustSchemaRelations (targetSet, sourceSet, missingSchemaAction);
			targetSet.EnforceConstraints = prevEC;
		}

		internal static void Merge(DataSet targetSet, DataTable sourceTable, bool preserveChanges, MissingSchemaAction missingSchemaAction)
		{
			if(targetSet == null)
				throw new ArgumentNullException("targetSet");
			if(sourceTable == null)
				throw new ArgumentNullException("sourceTable");

			bool savedEnfoceConstraints = targetSet.EnforceConstraints;
			targetSet.EnforceConstraints = false;

			DataTable targetTable = null;
			if (!AdjustSchema(targetSet, sourceTable, missingSchemaAction,ref targetTable))
				return;
			if (targetTable != null)
				fillData(targetTable, sourceTable, preserveChanges);
			targetSet.EnforceConstraints = savedEnfoceConstraints;
		}

		internal static void Merge (DataTable targetTable, 
					    DataTable sourceTable, 
					    bool preserveChanges, 
					    MissingSchemaAction missingSchemaAction)
		{
			if(targetTable== null)
				throw new ArgumentNullException("targetTable");
			if(sourceTable == null)
				throw new ArgumentNullException("sourceTable");

			bool savedEnforceConstraints = targetTable.EnforceConstraints;
			targetTable.EnforceConstraints = false;

			if (!AdjustSchema(targetTable, sourceTable, missingSchemaAction))
				return;

			fillData(targetTable, sourceTable, preserveChanges);
			targetTable.EnforceConstraints = savedEnforceConstraints;
		}

		internal static void Merge(DataSet targetSet, DataRow[] sourceRows, bool preserveChanges, MissingSchemaAction missingSchemaAction)
		{
			if(targetSet == null)
				throw new ArgumentNullException("targetSet");
			if(sourceRows == null)
				throw new ArgumentNullException("sourceRows");

			bool savedEnfoceConstraints = targetSet.EnforceConstraints;
			targetSet.EnforceConstraints = false;

			ArrayList targetTables = new ArrayList();
			for (int i = 0; i < sourceRows.Length; i++) {
				DataRow row = sourceRows[i];
				DataTable sourceTable = row.Table;
				DataTable targetTable = null;
				if (!AdjustSchema(targetSet, sourceTable, missingSchemaAction,ref targetTable))
					return;
				if (targetTable != null) {
					MergeRow(targetTable, row, preserveChanges);
					if (!(targetTables.IndexOf(targetTable) >= 0))
						targetTables.Add(targetTable);
				}
			}

			targetSet.EnforceConstraints = savedEnfoceConstraints;
		}

		// merge a row into a target table.
		private static void MergeRow(DataTable targetTable, DataRow row, bool preserveChanges)
		{
			DataColumnCollection columns = row.Table.Columns;
			DataColumn[] primaryKeys = targetTable.PrimaryKey;
			DataRow targetRow = null;
			DataRowVersion version = DataRowVersion.Default;
			if (row.RowState == DataRowState.Deleted)
				version = DataRowVersion.Original;

			if (primaryKeys != null && primaryKeys.Length > 0) // if there are any primary key.
			{
				// initiate an array that has the values of the primary keys.
				object[] keyValues = new object[primaryKeys.Length];
				
				for (int j = 0; j < keyValues.Length; j++)
				{
					keyValues[j] = row[primaryKeys[j].ColumnName, version];
				}
			
				// find the row in the target table.
				targetRow = targetTable.Rows.Find(keyValues, DataViewRowState.OriginalRows);
				if (targetRow == null)
					targetRow = targetTable.Rows.Find(keyValues);
			}
			// row doesn't exist in target table, or there are no primary keys.
			// create new row and copy values from source row to the new row.
			if (targetRow == null)
			{ 
				DataRow newRow = targetTable.NewNotInitializedRow();
				row.CopyValuesToRow(newRow);
				targetTable.Rows.AddInternal (newRow);
			}
			// row exists in target table, and presere changes is false - 
			// change the values of the target row to the values of the source row.
			else
			{
				row.MergeValuesToRow(targetRow, preserveChanges);
			}
		}
			
		private static bool AdjustSchemaRelations (DataSet targetSet, DataSet sourceSet, MissingSchemaAction missingSchemaAction)
		{
			if (missingSchemaAction == MissingSchemaAction.Ignore)
				return true;

			foreach(DataTable sourceTable in sourceSet.Tables) {

				DataTable targetTable = targetSet.Tables[sourceTable.TableName];
				if (targetTable == null)
					continue;

				foreach (Constraint constraint in sourceTable.Constraints) {

					Constraint targetConstraint = null;

					string constraintName = constraint.ConstraintName;
					if (targetTable.Constraints.Contains (constraintName))
						constraintName = "";

					UniqueConstraint uc = constraint as UniqueConstraint;
					// PrimaryKey is already taken care of while merging the table
					// ForeignKey constraint takes care of Parent Unique Constraints
					if (uc != null) {
						if (uc.IsPrimaryKey || uc.ChildConstraint != null)
							continue;
						DataColumn[] columns = ResolveColumns (targetTable, uc.Columns);
						targetConstraint = new UniqueConstraint (constraintName, columns, false);
					}

					ForeignKeyConstraint fc = constraint as ForeignKeyConstraint;
					if (fc != null) {
						DataColumn[] columns = ResolveColumns (targetTable, fc.Columns);
						DataColumn[] relatedColumns = ResolveColumns (targetSet.Tables [fc.RelatedTable.TableName],
											fc.RelatedColumns);
						targetConstraint = new ForeignKeyConstraint (constraintName, relatedColumns, columns);
					}

					bool dupConstraintFound = false;
					foreach (Constraint cons in targetTable.Constraints) {
					if (!targetConstraint.Equals (cons))
						continue;
					dupConstraintFound = true;
					break;
					}

					// If equivalent-constraint already exists, then just do nothing
					if (dupConstraintFound)
						continue;

					if (missingSchemaAction == MissingSchemaAction.Error)
						throw new DataException ("Target DataSet missing " + targetConstraint.GetType () +
								targetConstraint.ConstraintName);
					else
						targetTable.Constraints.Add (targetConstraint);
				}
			}

			foreach (DataRelation relation in sourceSet.Relations) {
				DataRelation targetRelation = targetSet.Relations [relation.RelationName];
				if (targetRelation == null) {
					if (missingSchemaAction == MissingSchemaAction.Error)
						throw new ArgumentException ("Target DataSet mising definition for " +
								relation.RelationName);

					DataColumn[] parentColumns = ResolveColumns (targetSet.Tables [relation.ParentTable.TableName],
							relation.ParentColumns);
					DataColumn[] childColumns = ResolveColumns (targetSet.Tables [relation.ChildTable.TableName],
							relation.ChildColumns);
					targetRelation = targetSet.Relations.Add (relation.RelationName, parentColumns,
							childColumns, relation.createConstraints);
					targetRelation.Nested = relation.Nested;
				} else if (!CompareColumnArrays (relation.ParentColumns, targetRelation.ParentColumns) ||
						!CompareColumnArrays (relation.ChildColumns, targetRelation.ChildColumns)) {
					RaiseMergeFailedEvent (null, "Relation " + relation.RelationName +
						" cannot be merged, because keys have mismatch columns.");
				}
			}

			return true;
		}

		private static DataColumn[] ResolveColumns(DataTable targetTable, DataColumn[] sourceColumns)
		{
			if (sourceColumns != null && sourceColumns.Length > 0) {
				// lets just assume that all columns are from the Same table
				if (targetTable != null) {
					int i=0;
					DataColumn[] targetColumns = new DataColumn[sourceColumns.Length];

					DataColumn tmpCol ;
					foreach(DataColumn sourceColumn in sourceColumns) {
						tmpCol = targetTable.Columns[sourceColumn.ColumnName];
						if (tmpCol == null)
							throw new DataException ("Column " + sourceColumn.ColumnName  + 
									" does not belong to table " + targetTable.TableName);
						targetColumns [i++] = tmpCol;
					}
					return targetColumns;
				}
			}
			return null;
		}


		// adjust the table schema according to the missingschemaaction param.
		// return false if adjusting fails.
		private static bool AdjustSchema(DataSet targetSet, DataTable sourceTable, MissingSchemaAction missingSchemaAction, ref DataTable newTable)
		{
			// if the source table not exists in the target dataset
			// we act according to the missingschemaaction param.
			DataTable targetTable = targetSet.Tables [sourceTable.TableName];
			if (targetTable == null) {
				if (missingSchemaAction == MissingSchemaAction.Ignore)
					return true;

				if (missingSchemaAction == MissingSchemaAction.Error)
					throw new ArgumentException ("Target DataSet missing definition for " +
							sourceTable.TableName + ".");

				targetTable = (DataTable)sourceTable.Clone();
				targetSet.Tables.Add(targetTable);
			}

			AdjustSchema (targetTable, sourceTable, missingSchemaAction);

			newTable = targetTable;
			return true;
		}


		private static bool AdjustSchema(DataTable targetTable, DataTable sourceTable, MissingSchemaAction missingSchemaAction)
		{
			if (missingSchemaAction == MissingSchemaAction.Ignore)
				return true;

			for (int i = 0; i < sourceTable.Columns.Count; i++) {
				DataColumn sourceColumn = sourceTable.Columns[i];
				// if a column from the source table doesn't exists in the target table
				// we act according to the missingschemaaction param.
				DataColumn targetColumn = targetTable.Columns [sourceColumn.ColumnName];
				if(targetColumn == null) {
					if (missingSchemaAction == MissingSchemaAction.Error)
						throw new DataException ("Target table " + targetTable.TableName +
								" missing definition for column " + sourceColumn.ColumnName);
					
					targetColumn = new DataColumn(sourceColumn.ColumnName, sourceColumn.DataType, 
								sourceColumn.Expression, sourceColumn.ColumnMapping);
					targetTable.Columns.Add(targetColumn);
				}

				if(sourceColumn.AutoIncrement) {
					targetColumn.AutoIncrement = sourceColumn.AutoIncrement;
					targetColumn.AutoIncrementSeed = sourceColumn.AutoIncrementSeed;
					targetColumn.AutoIncrementStep = sourceColumn.AutoIncrementStep;
				}
			}

			if (!AdjustPrimaryKeys(targetTable, sourceTable))
				return false;

			checkColumnTypes (targetTable, sourceTable);

			return true;
		}
	
	
		// find if there is a valid matching between the targetTable PrimaryKey and the
		// sourceTable primatyKey.
		// return true if there is a match, else return false and raise a MergeFailedEvent.
		private static bool AdjustPrimaryKeys(DataTable targetTable, DataTable sourceTable)
		{
			if (sourceTable.PrimaryKey.Length == 0)
				return true;

			// If targetTable does not have a PrimaryKey, just import the sourceTable PrimaryKey
			if (targetTable.PrimaryKey.Length == 0) {
				DataColumn[] targetColumns = ResolveColumns (targetTable, sourceTable.PrimaryKey);
				targetTable.PrimaryKey = targetColumns;
				return true;
			}
			
			// If both the tables have a primary key, verify that they are equivalent.
			// raise a MergeFailedEvent if the keys are not equivalent
			if (targetTable.PrimaryKey.Length != sourceTable.PrimaryKey.Length) {
				RaiseMergeFailedEvent (targetTable, "<target>.PrimaryKey and <source>.PrimaryKey have different Length.");
				return false;
			}

			for (int i=0; i < targetTable.PrimaryKey.Length; ++i) {
				if (targetTable.PrimaryKey [i].ColumnName.Equals (sourceTable.PrimaryKey [i].ColumnName))
					continue;
				RaiseMergeFailedEvent (targetTable, "Mismatch columns in the PrimaryKey : <target>." + 
					targetTable.PrimaryKey [i].ColumnName + " versus <source>." + sourceTable.PrimaryKey [i].ColumnName);
				return false;
			}
			return true;
		}

		// fill the data from the source table to the target table
		private static void fillData(DataTable targetTable, DataTable sourceTable, bool preserveChanges)
		{
			for (int i = 0; i < sourceTable.Rows.Count; i++)
			{
				DataRow row = sourceTable.Rows[i];
				MergeRow(targetTable, row, preserveChanges);
			}
		}

		// check tha column from 2 tables that has the same name also has the same datatype.
		private static void checkColumnTypes(DataTable targetTable, DataTable sourceTable)
		{
			for (int i = 0; i < sourceTable.Columns.Count; i++)
			{
				DataColumn fromCol = sourceTable.Columns[i];
				DataColumn toCol = targetTable.Columns[fromCol.ColumnName];
				if (toCol == null)
					continue;
				if (toCol.DataTypeMatches (fromCol))
					continue;
				throw new DataException("<target>." + fromCol.ColumnName + " and <source>." + 
						fromCol.ColumnName + " have conflicting properties: DataType " + 
						" property mismatch.");
			}
		}

		private static bool CompareColumnArrays (DataColumn[] arr1, DataColumn[] arr2)
		{
			if (arr1.Length != arr2.Length)
				return false;

			for (int i=0; i < arr1.Length; ++i)
				if (!arr1 [i].ColumnName.Equals (arr2 [i].ColumnName))
					return false;
			return true;
		}

		private static void RaiseMergeFailedEvent (DataTable targetTable, string errMsg)
		{
			MergeFailedEventArgs args = new MergeFailedEventArgs (targetTable, errMsg);
			if (targetTable.DataSet != null)
				targetTable.DataSet.OnMergeFailed (args);
		}
	}
}
