#region Copyright and GPL
/*************************************************************************
 * dataMagus - The open source database learning software
 * Copyright (C) 2007 by all authors
 * Authors: Monika Schwitter (monsy@gmx.ch)
 *          Jürg Zgraggen (j.zgraggen@ejaz.net)
 *          HSLU (Hochschule Luzern - Technik+Architektur)
 * 
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 * 
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *************************************************************************/
#endregion
#region Using
using System;
using System.IO;
using System.ComponentModel;
using System.Collections.Generic;
using System.Collections;
using System.Runtime.Serialization.Formatters.Binary;
using dataMagus.Common.Logical;
#endregion

namespace dataMagus.Common.Physical.Relational
{
	/// <summary>
	/// This class represents a physical relational schema.
	/// </summary>
    [Serializable]
	public sealed class PhysicalRelationalSchema : IPhysicalSchema
	{
		#region Constants
		/// <summary>
		/// Default name.
		/// </summary>
        private static readonly string DefaultName = global::dataMagus.Common.OtherResources.DefaultPhysicalRelationalSchemaName;
        
        /// <summary>
        /// Name of the Name property
        /// </summary>
        private const string NamePropertyName = "Name";
        
        /// <summary>
        /// Name of the ExtendedProperties property
        /// </summary>
        private const string ExtendedPropertiesPropertyName = "ExtendedProperties";
		#endregion
		
		#region Members
		/// <summary>
		/// Id of the schema.
		/// </summary>
		private string _id;
		
		/// <summary>
		/// Name of the schema.
		/// </summary>
		private string _name;
		
		/// <summary>
		/// Current selected notation type.
		/// </summary>
		private Type _notationType;
        
		/// <summary>
		/// Indicates if the element is in InitMode.
		/// </summary>
		/// <remarks>InitMode will suppress all changing events.</remarks>
		[NonSerialized]
		private bool _initMode = false;
		
		/// <summary>
		/// Indicates if the element has been cloned.
		/// </summary>
        /// <remarks>Beware: events on clones will be suppressed.</remarks>
		private bool _isClone = false;
        
		/// <summary>
		/// Holds all extended properties for the schema.
		/// </summary>
		private Dictionary<string, object> _extendedProperties = new Dictionary<string, object>();
        
		/// <summary>
		/// List of all tables contained in the schema.
		/// </summary>
		private List<PhysicalRelationalTable> _tables = new List<PhysicalRelationalTable>();
        
		/// <summary>
		/// List of all relationships contained in the schema.
		/// </summary>
		private List<PhysicalRelationalRelationship> _relationships = new List<PhysicalRelationalRelationship>();
        
		/// <summary>
		/// Instance of the schema converter.
		/// </summary>
		[NonSerialized]
        private PhysicalRelationalSchemaConverter _converter = new PhysicalRelationalSchemaConverter();
        
		/// <summary>
		/// Index counter to create uniqueness of table names when having equal names.
		/// </summary>
        private int _uniqueTableNameIndex = 0;
		#endregion
		
		#region Constructors / Destructors
		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="id">Id of the schema.</param>
        public PhysicalRelationalSchema(string id)
            : this(id, PhysicalRelationalSchema.DefaultName)
        { 
        }

		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="id">Id of the schema.</param>
		/// <param name="name">Name of the schema.</param>
        public PhysicalRelationalSchema(string id, string name)
		{
            this._id = id;
            this._name = this.EnsureName(name);
		}

        /// <summary>
        /// Default constructor.
        /// </summary>
        /// <remarks>Only used for serialization.</remarks>
        private PhysicalRelationalSchema()
        {
        }
		#endregion
		
		#region Properties
		/// <summary>
		/// Id of the schema.
		/// </summary>
		[Browsable(false)]
		public string Id 
		{ 
			get { return this._id; }
		}
		
		/// <summary>
		/// Name of the schema.
		/// </summary>
		public string Name
		{
			get { return this._name; }
            set
            {
                if (this._name != value)
                {
                    value = this.EnsureName(value);
                    if (!this._isClone)
                    {
	                    DataContainer.Instance.PhysicalData.UpdateRestoreDataItem(this, this.Id, PhysicalRelationalSchema.NamePropertyName, value);
                    }
                    this._name = value;
                    this.FireSchemaChangedEvent(this, SchemaChangedAction.ChangedProperty);
                }
            }
		}

		/// <summary>
		/// Indicates if the element has been cloned.
		/// </summary>
        /// <remarks>Beware: events on clones will be suppressed.</remarks>
		[Browsable(false)]
		public bool IsClone 
		{ 
			get { return this._isClone; }
		}
		
		/// <summary>
		/// Indicates if the element is in InitMode.
		/// </summary>
		/// <remarks>InitMode will suppress all changing events.</remarks>
		[Browsable(false)]
		public bool InitMode
		{
			get { return this._initMode; }
		}

		/// <summary>
		/// Current selected notation type.
		/// </summary>
		[Browsable(false)]
		public Type NotationType
		{
			get { return this._notationType; }
			set 
			{ 
				if (this._notationType != value)
				{
					this._notationType = value;
					this.FireSchemaChangedEvent(this, SchemaChangedAction.ChangedProperty);
				}
			}
		}

		/// <summary>
		/// Count all contained tables.
		/// </summary>
		[Browsable(false)]
        public int CountTables
        {
            get { return this._tables.Count; }
        }

        /// <summary>
		/// Count all contained relationships.
		/// </summary>
		[Browsable(false)]
        public int CountRelationships
        {
            get { return this._relationships.Count; }
        }
		#endregion
		
		#region Events
		/// <summary>
		/// Event when the schema has been changed.
		/// </summary>
        // Very important: Mark this event as NonSerialized!! The compiler, will behind the scences,
        // create a hidden field for us to maintain to a collection of all your listeners. 
        // But our listeners are type of UserControl which can not be serialized. Therefore,
        // we have to explicitely mark as NonSerialized. Esoteric indeed!
        [field: NonSerialized]	// VIC = Very Important Code!
		public event SchemaChangedHandler SchemaChanged;
		#endregion
		
		#region Methodes
		/// <summary>
		/// String representation of the object.
		/// </summary>
		/// <returns>Name of the schema.</returns>
		public override string ToString()
		{
			return this.Name;
		}

		/// <summary>
		/// Ensures that the object has a name.
		/// </summary>
		/// <remarks>Spaces will be replaced with nothing.</remarks>
		/// <param name="name">Name to check.</param>
		/// <returns>New name.</returns>
		private string EnsureName(string name)
        {
            if (null == name)
            {
                name = String.Empty;
            }
            return name.Replace(" ", "");
        }

		/// <summary>
		/// Begin initialization mode.
		/// </summary>
		/// <remarks>Prevents event handling.</remarks>
		public void BeginInit()
        {
            this._initMode = true;

            foreach (PhysicalRelationalTable table in this._tables)
            {
                table.BeginInit();
            }

            foreach (PhysicalRelationalRelationship relationship in this._relationships)
            {
                relationship.BeginInit();
            }
        }

		/// <summary>
		/// End initialization mode.
		/// </summary>
		/// <remarks>Event handling will continue.</remarks>
		/// <param name="forceEvent">Indicates if an Undefined event has to be fired to inform other listeners.</param>
        public void EndInit(bool forceEvent)
        {
            this._initMode = false;

            foreach (PhysicalRelationalTable table in this._tables)
            {
                table.EndInit(forceEvent);
            }

            foreach (PhysicalRelationalRelationship relationship in this._relationships)
            {
                relationship.EndInit(forceEvent);
            }

            if (forceEvent)
            {
                this.FireSchemaChangedEvent(this, SchemaChangedAction.Undefined);
            }
        }
        
		/// <summary>
		/// Clear the schema.
		/// </summary>
		public void Clear()
		{
			this._tables.Clear();
			this._relationships.Clear();
			this._extendedProperties.Clear();
            this._uniqueTableNameIndex = 0;
            this.FireSchemaChangedEvent(this, SchemaChangedAction.Undefined);
		}

		/// <summary>
		/// Exchange a datatype inside the schema.
		/// </summary>
		/// <param name="newDataType">New data tyoe.</param>
		/// <param name="oldDataType">Old data type.</param>
        public void ExchangeDataType(DataType newDataType, DataType oldDataType)
        {
            foreach (PhysicalRelationalTable table in this._tables)
            {
                IEnumerator colEnumerator = table.GetColumns();
                while (colEnumerator.MoveNext())
                {
                    PhysicalRelationalColumn column = (PhysicalRelationalColumn)colEnumerator.Current;
                    if (null != column.DataType.Name && null != oldDataType.Name &&
                        column.DataType.Name.ToLower().Equals(oldDataType.Name.ToLower()))
                    {
                        column.DataType = newDataType;
                    }
                }
            }
        }

        /// <summary>
        /// Add a new table.
        /// </summary>
        /// <param name="table">Table to add.</param>
        public void AddTable(PhysicalRelationalTable table)
        {
            if (!this._tables.Contains(table))
            {
                table.Name = this.EnsureUniqueTableName(table);
                this._tables.Add(table);
            	this.FireSchemaChangedEvent(this, new SchemaEventArgs(this, SchemaChangedAction.AddedElement, table));
            }
        }

        /// <summary>
        /// Remove a table.
        /// </summary>
        /// <param name="table">Table to remove.</param>
        public void RemoveTable(PhysicalRelationalTable table)
        {
            this._tables.Remove(table);
            this.FireSchemaChangedEvent(this, new SchemaEventArgs(this, SchemaChangedAction.RemovedElement, table));
        }

        /// <summary>
        /// Checks if an table has already been added.
        /// </summary>
        /// <param name="table">Table to check.</param>
        /// <returns>true if the table is contained, else false.</returns>
        public bool ContainsTable(PhysicalRelationalTable table)
        {
            return this._tables.Contains(table);
        }

        /// <summary>
		/// Find a table on the schema by id.
		/// </summary>
		/// <param name="id">Id to search for.</param>
		/// <returns>The found table or null if nothing has been found.</returns>
        public PhysicalRelationalTable FindTable(string id)
        {
            foreach (PhysicalRelationalTable table in this._tables)
            {
                if (table.Id.Equals(id))
                {
                    return table;
                }
            }
            return null;
        }

        /// <summary>
        /// Ensure that the table name is unique on the schema.
        /// </summary>
        /// <param name="table">Table to check.</param>
        /// <returns>New name.</returns>
        private string EnsureUniqueTableName(PhysicalRelationalTable table)
        {
            string name = table.Name;
            foreach (PhysicalRelationalTable item in this._tables)
            {
                if (item.Name.ToLower().Equals(table.Name.ToLower()))
                {
                    name += (++this._uniqueTableNameIndex).ToString();
                    return name;
                }
            }
            return name;
        }

        /// <summary>
        /// Get all tables.
        /// </summary>
        /// <returns>Enumerator to parse.</returns>
        public IEnumerator GetTables()
        {
            return this._tables.GetEnumerator();
        }

        /// <summary>
        /// Add a new relationship.
        /// </summary>
        /// <param name="relationship">Relationship to add.</param>
        public void AddRelationship(PhysicalRelationalRelationship relationship)
        {
            if (!this._relationships.Contains(relationship))
            {
                this._relationships.Add(relationship);
                this.FireSchemaChangedEvent(this, new SchemaEventArgs(this, SchemaChangedAction.AddedElement, relationship));
            }
        }

        /// <summary>
        /// Remove a relationship.
        /// </summary>
        /// <param name="relationship">Relationship to remove.</param>
        public void RemoveRelationship(PhysicalRelationalRelationship relationship)
        {
            this._relationships.Remove(relationship);
            this.FireSchemaChangedEvent(this, new SchemaEventArgs(this, SchemaChangedAction.RemovedElement, relationship));
        }

        /// <summary>
        /// Checks of an relationship has already been added.
        /// </summary>
        /// <param name="relationship">Relationship to check.</param>
        /// <returns>true if the relationship is contained, else false.</returns>
        public bool ContainsRelationship(PhysicalRelationalRelationship relationship)
        {
            return this._relationships.Contains(relationship);
        }

        /// <summary>
		/// Find a relationship on the schema by id.
		/// </summary>
		/// <param name="id">Id to search for.</param>
		/// <returns>The found relationship or null if nothing has been found.</returns>
        public PhysicalRelationalRelationship FindRelationship(string id)
        {
            foreach (PhysicalRelationalRelationship relationship in this._relationships)
            {
                if (relationship.Id.Equals(id))
                {
                    return relationship;
                }
            }
            return null;
        }

        /// <summary>
        /// Get all relationships.
        /// </summary>
        /// <returns>Enumerator to parse</returns>
        public IEnumerator GetRelationships()
        {
            return this._relationships.GetEnumerator();
        }

        /// <summary>
		/// Find a column on the table by id.
		/// </summary>
		/// <param name="id">Id to search for.</param>
		/// <returns>The found column or null if nothing has been found.</returns>
        public PhysicalRelationalColumn FindColumn(string id)
        {
            foreach (PhysicalRelationalTable table in this._tables)
            {
                PhysicalRelationalColumn column = table.FindColumn(id);
                if (null != column)
                {
                    return column;
                }
            }
            return null;
        }

        /// <summary>
        /// Restore all physical stored data.
        /// </summary>
		private void RestoreData()
		{
            // Restore physical changes if available
            object nameToRestore = DataContainer.Instance.PhysicalData.GetRestoreDataItem(this, this.Id, PhysicalRelationalSchema.NamePropertyName);
            if (null != nameToRestore)
            {
                this._name = (string)nameToRestore;
            }

            object extendedPropertiesToRestore = DataContainer.Instance.PhysicalData.GetRestoreDataItem(this, this.Id, PhysicalRelationalSchema.ExtendedPropertiesPropertyName);
            if (null != extendedPropertiesToRestore)
            {
                this._extendedProperties = (Dictionary<string, object>)extendedPropertiesToRestore;
            }
		}
		
		/// <summary>
        /// Fires an schema changed event.
        /// </summary>
        /// <param name="sender">Sender object.</param>
        /// <param name="action">Action on the schema.</param>
        public void FireSchemaChangedEvent(object sender, SchemaChangedAction action)
        {
        	if (null != this.SchemaChanged && !this._initMode && !this._isClone)
        	{
        		this.SchemaChanged(sender, new SchemaEventArgs(this, action));
        	}
        }

        /// <summary>
        /// Fires an schema changed event.
        /// </summary>
        /// <param name="sender">Sender object.</param>
        /// <param name="args">SchemaEventArgs.</param>
		public void FireSchemaChangedEvent(object sender, SchemaEventArgs args)
        {
        	if (null != this.SchemaChanged && !this._initMode && !this._isClone)
        	{
        		this.SchemaChanged(sender, args);
        	}
        }
        
		/// <summary>
		/// Generate the physical schema out of the logical one.
		/// </summary>
		/// <param name="userInteraction">Instance for potential user interactions.</param>
		public void Generate(IUserInteraction userInteraction)
		{
			this.Clear();
			this.RestoreData();

			this._converter.UserInteraction = userInteraction;
			this._converter.Convert(this);
			
			this.FireSchemaChangedEvent(this, SchemaChangedAction.RebuildedSchema);
		}
		
		/// <summary>
		/// Ensure that the restore data item only gets updated when the schema not has been cloned.
		/// </summary>
		/// <param name="physicalSchema">Physical schema.</param>
		/// <param name="propertyName">Name of the property to update.</param>
		/// <param name="value">Value to update.</param>
	    private void EnsureUpdateOriginalRestoreDataItem(IPhysicalSchema physicalSchema, string propertyName, object value)
	    {
	    	if (!physicalSchema.IsClone)
	    	{
				DataContainer.Instance.PhysicalData.UpdateRestoreDataItem(physicalSchema, physicalSchema.Id, propertyName, value);
	    	}
	    }
        #endregion

        #region IXmlGeneratorWriter Members
        /// <summary>
        /// Writes a the instace of this class to a writer.
        /// </summary>
        /// <param name="writer">Writer object to write into.</param>
        public void WriteXml(System.Xml.XmlWriter writer)
        {
            writer.WriteStartElement("PhysicalRelationalSchema");

            writer.WriteAttributeString("id", this.Id);
            writer.WriteAttributeString("name", this.Name);
            writer.WriteStartAttribute("notationType");
            if (null != this.NotationType)
            {
                writer.WriteString(this.NotationType.FullName);
            }
            writer.WriteEndAttribute();

            writer.WriteStartElement("Tables");
            foreach (PhysicalRelationalTable table in this._tables)
            {
                table.WriteXml(writer);
            }
            writer.WriteEndElement();

            writer.WriteStartElement("Relationships");
            foreach (PhysicalRelationalRelationship relationship in this._relationships)
            {
                relationship.WriteXml(writer);
            }
            writer.WriteEndElement();

            writer.WriteEndElement();
        }
        #endregion

        #region ICloneable Members
        /// <summary>
        /// Clones the element.
        /// </summary>
        /// <remarks>Beware: events on clones will be suppressed.</remarks>
        /// <returns>Cloned object.</returns>
        public object Clone()
        {
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            MemoryStream stream = new MemoryStream();
            binaryFormatter.Serialize(stream, this);
            stream.Seek(0, SeekOrigin.Begin);
            PhysicalRelationalSchema cloneSchema = (PhysicalRelationalSchema)binaryFormatter.Deserialize(stream);
            cloneSchema._isClone = true;
            return cloneSchema;
        }
        #endregion
    }
}
