#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.Xml;
using System.ComponentModel;
using System.Collections;
using System.Collections.Generic;
using System.Xml.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using dataMagus.Common.PlugIn.Notation;
#endregion

namespace dataMagus.Common.Logical
{
	/// <summary>
	/// This class represents a logical schema.
	/// </summary>
	[Serializable]
	public sealed class LogicalSchema : ILogicalSchema
	{
		#region Constants
		/// <summary>
		/// Default name.
		/// </summary>
        private static readonly string DefaultName = global::dataMagus.Common.OtherResources.DefaultLogicalSchemaName;
		#endregion
		
		#region Members
		/// <summary>
		/// Indicates if the element is in InitMode.
		/// </summary>
		/// <remarks>InitMode will suppress all changing events.</remarks>
        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>
		/// 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>
		/// List of all entities contained in the schema.
		/// </summary>
		private List<LogicalEntity> _entities = new List<LogicalEntity>();
        
		/// <summary>
		/// List of all relationships contained in the schema.
		/// </summary>
		private List<LogicalRelationship> _relationships = new List<LogicalRelationship>();
        
		/// <summary>
		/// Index counter to create uniqueness of entity names when having equal names.
		/// </summary>
		private int _uniqueEntityNameIndex = 0;
        
		/// <summary>
		/// Index counter to create uniqueness of relationship names when having equal names.
		/// </summary>
		private int _uniqueRelationshipNameIndex = 0;
        
		/// <summary>
		/// Index counter to create uniqueness of first relationship rolename when having equal names.
		/// </summary>
		private int _uniqueRelationshipRolename1Index = 0;
        
		/// <summary>
		/// Index counter to create uniqueness of second relationship rolename when having equal names.
		/// </summary>
		private int _uniqueRelationshipRolename2Index = 0;
		#endregion
		
		#region Constructors / Destructors
		/// <summary>
		/// Default constructor.
		/// </summary>
		public LogicalSchema()
		{
			this._id = Guid.NewGuid().ToString();
			this._name = LogicalSchema.DefaultName;
		}
		#endregion
		
		#region Properties
		/// <summary>
		/// Id of the element.
		/// </summary>
		[Browsable(false)]
		public string Id 
		{ 
			get { return this._id; }
		}
		
		/// <summary>
		/// Name of the element.
		/// </summary>
		public string Name
		{
			get { return this._name; }
			set 
            {
                if (this._name != value)
                {
                	this._name = this.EnsureName(value);
                    this.FireSchemaChangedEvent(this, SchemaChangedAction.ChangedProperty);
                }
            }
		}
		
		/// <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>
		/// 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>
		/// 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 entities.
		/// </summary>
        [Browsable(false)]
        public int CountEntities
        {
            get { return this._entities.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 (LogicalEntity entity in this._entities)
            {
                entity.BeginInit();
            }

            foreach (LogicalRelationship 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 (LogicalEntity entity in this._entities)
            {
                entity.EndInit(forceEvent);
            }

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

            if (forceEvent)
            {
                this.FireSchemaChangedEvent(this, SchemaChangedAction.Undefined);
            }
        }

        /// <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>
		/// Find an entity on the schema by id.
		/// </summary>
		/// <param name="id">Id to search for.</param>
		/// <returns>The found entity or null if nothing has been found.</returns>
        public LogicalEntity FindEntity(string id)
        {
            foreach (LogicalEntity entity in this._entities)
            {
                if (entity.Id.Equals(id))
                {
                    return entity;
                }
            }
            return null;
        }

        /// <summary>
        /// Checks if the given attributes collection contains an attribute with the given id.
        /// </summary>
        /// <param name="id">Id to check.</param>
        /// <param name="attributes">Attributes collection to parse.</param>
        /// <returns>The found attribute or null if nothing has been found.</returns>
        public static LogicalAttribute ContainsKeyAttribute(string id, List<LogicalAttribute> attributes)
        {
            foreach (LogicalAttribute attribute in attributes)
            {
                if (attribute.Id.Equals(id))
                {
                    return attribute;
                }
            }
            return null;
        }

        /// <summary>
        /// Add an entity.
        /// </summary>
        /// <param name="entity">Entity to add.</param>
        public void AddEntity(LogicalEntity entity)
        {
            if (!this._entities.Contains(entity))
            {
                entity.Name = this.EnsureUniqueEntityName(entity);
                this._entities.Add(entity);
                this.FireSchemaChangedEvent(this, new SchemaEventArgs(this, SchemaChangedAction.AddedElement, entity));
            }
        }

        /// <summary>
        /// Ensure that the entity name is unique on the schema.
        /// </summary>
        /// <param name="entity">Entity to check.</param>
        /// <returns>New name.</returns>
        private string EnsureUniqueEntityName(LogicalEntity entity)
        {
            string name = entity.Name;
            foreach (LogicalEntity item in this._entities)
            {
                if (item.Name.ToLower().Equals(entity.Name.ToLower()))
                { 
                    name += (++this._uniqueEntityNameIndex).ToString();
                    return name;
                }
            }
            return name;
        }

        /// <summary>
        /// Ensure that the relationship name is unique on the schema.
        /// </summary>
        /// <param name="relationship">Relationship to check.</param>
        /// <returns>New name.</returns>
        private string EnsureUniqueRelationshipName(LogicalRelationship relationship)
        {
            string name = relationship.Name;
            foreach (LogicalRelationship item in this._relationships)
            {
                if (item.Entity1 == relationship.Entity1 && item.Entity2 == relationship.Entity2 ||
                    item.Entity2 == relationship.Entity2 && item.Entity1 == relationship.Entity1)
                {
                    if (item.Name.ToLower().Equals(relationship.Name.ToLower()))
                    { 
                        name += (++this._uniqueRelationshipNameIndex).ToString();
                        return name;
                    }
                }
            }
            return name;
        }

        /// <summary>
        /// Ensure that the first rolename on the relationship is unique on the schema.
        /// </summary>
        /// <param name="relationship">Relationship to check.</param>
        /// <returns>New name.</returns>
        private string EnsureUniqueRelationshipRolename1(LogicalRelationship relationship)
        {
            string name = relationship.Rolename1;
            foreach (LogicalRelationship item in this._relationships)
            {
                if (item.Entity1 == relationship.Entity1 && item.Entity2 == relationship.Entity2 ||
                    item.Entity2 == relationship.Entity2 && item.Entity1 == relationship.Entity1)
                {
                    if (item.Rolename1.ToLower().Equals(relationship.Rolename1.ToLower()))
                    {
                        name += (++this._uniqueRelationshipRolename1Index).ToString();
                        return name;
                    }
                }
            }
            return name;
        }

        /// <summary>
        /// Ensure that the second rolename on the relationship is unique on the schema.
        /// </summary>
        /// <param name="relationship">Relationship to check.</param>
        /// <returns>New name.</returns>
        private string EnsureUniqueRelationshipRolename2(LogicalRelationship relationship)
        {
            string name = relationship.Rolename2;
            foreach (LogicalRelationship item in this._relationships)
            {
                if (item.Entity1 == relationship.Entity1 && item.Entity2 == relationship.Entity2 ||
                    item.Entity2 == relationship.Entity2 && item.Entity1 == relationship.Entity1)
                {
                    if (item.Rolename2.ToLower().Equals(relationship.Rolename2.ToLower()))
                    {
                        name += (++this._uniqueRelationshipRolename2Index).ToString();
                        return name;
                    }
                }
            }
            return name;
        }

        /// <summary>
        /// Remove an entity.
        /// </summary>
        /// <param name="entity">Entity to remove.</param>
        public void RemoveEntity(LogicalEntity entity)
        {
            // Remove all related relationships first
            List<LogicalRelationship> relationshipsToDelete = new List<LogicalRelationship>();
            foreach (LogicalRelationship relationship in this._relationships)
            {
                if (relationship.Entity1.Equals(entity) || relationship.Entity2.Equals(entity))
                {
                    relationshipsToDelete.Add(relationship);
                }
            }
            foreach (LogicalRelationship relationship in relationshipsToDelete)
            {
                this.RemoveRelationship(relationship);
            }

            // This includes also physical data to the attributes. Key attributes are automatically deleted because they are only
            // referenced.
            List<LogicalAttribute> attributesToDelete = new List<LogicalAttribute>();
            IEnumerator attributesEnumerator = entity.GetAttributes();
            while (attributesEnumerator.MoveNext())
            {
                LogicalAttribute attribute = (LogicalAttribute)attributesEnumerator.Current;
                attributesToDelete.Add(attribute);
            }
            foreach (LogicalAttribute attribute in attributesToDelete)
            {
                entity.RemoveAttribute(attribute);
            }

            // Remove all related physical data for the entity. 
            DataContainer.Instance.PhysicalData.CleanPhysicalData(entity.Id);

            this._entities.Remove(entity);
            this.FireSchemaChangedEvent(this, new SchemaEventArgs(this, SchemaChangedAction.RemovedElement, entity));
        }

        /// <summary>
        /// Checks if an entity has already been added.
        /// </summary>
        /// <param name="entity">Entity to check.</param>
        /// <returns>true if the entity is contained, else false.</returns>
        public bool ContainsEntity(LogicalEntity entity)
        {
            return this._entities.Contains(entity);
        }

        /// <summary>
        /// Get all entities.
        /// </summary>
        /// <returns>Enumerator to parse.</returns>
        public IEnumerator GetEntities()
        {
            return this._entities.GetEnumerator();
        }

        /// <summary>
        /// Add a relationship.
        /// </summary>
        /// <param name="relationship">Relationship to add.</param>
        public void AddRelationship(LogicalRelationship relationship)
        {
            if (!this._relationships.Contains(relationship))
            {
                relationship.Name = this.EnsureUniqueRelationshipName(relationship);
                relationship.Rolename1 = this.EnsureUniqueRelationshipRolename1(relationship);
                relationship.Rolename2 = this.EnsureUniqueRelationshipRolename2(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(LogicalRelationship relationship)
        {
            // Remove all related physical data for the relationship. 
            // This includes also physical data to the attributes. Key attributes are automatically deleted because they are only
            // referenced.
            DataContainer.Instance.PhysicalData.CleanPhysicalData(relationship.Id);
            
            List<LogicalAttribute> attribToDelete = new List<LogicalAttribute>();
            if (relationship is IAttributable)
            {
                IEnumerator attribEnumerator = ((IAttributable)relationship).GetAttributes();
                while (attribEnumerator.MoveNext())
                {
                    LogicalAttribute attribute = (LogicalAttribute)attribEnumerator.Current;
                    attribToDelete.Add(attribute);
                }
                foreach (LogicalAttribute attribute in attribToDelete)
                {
                    ((IAttributable)relationship).RemoveAttribute(attribute);
                }
            }
        	this._relationships.Remove(relationship);
        	this.FireSchemaChangedEvent(this, new SchemaEventArgs(this, SchemaChangedAction.RemovedElement, relationship));
        }

        /// <summary>
        /// Checks of a 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(LogicalRelationship relationship)
        {
            return this._relationships.Contains(relationship);
        }

        /// <summary>
        /// Get all relationships.
        /// </summary>
        /// <returns>Enumerator to parse.</returns>
        public IEnumerator GetRelationships()
        {
            return this._relationships.GetEnumerator();
        }
        #endregion
        
        #region IXmlSerializable Members
        /// <summary>
        /// Gets the xml schema object.
        /// </summary>
        /// <returns>Always null.</returns>
        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null;
        }

        /// <summary>
        /// Reads back an instance of this class from a reader.
        /// </summary>
        /// <remarks>Reader must stand on a LogicalSchema-Element.</remarks>
        /// <param name="reader">Reader object to read from.</param>
        public void ReadXml(System.Xml.XmlReader reader)
        {
            if (reader.IsStartElement("LogicalSchema"))
            {
            this._id = reader.GetAttribute("id");
            this._name = reader.GetAttribute("name");

            if (!reader.IsEmptyElement)
            {
                reader.ReadStartElement();
                if (reader.IsStartElement("Entities"))
                {
                    if (!reader.IsEmptyElement)
                    {
                        while(!reader.EOF)
                        {
                            reader.Read();
                            if (reader.NodeType == System.Xml.XmlNodeType.EndElement && reader.Name == "Entities")
                            {
                                break;
                            }
                            if (reader.IsStartElement("Entity"))
                            {
                                if (!reader.IsEmptyElement)
                                {
                                    LogicalEntity entity = new LogicalEntity(reader.GetAttribute("name"));
                                    entity.ReadXml(reader);
                                    this._entities.Add(entity);
                                }
                            }
                        }
                    }
                }
                if (!reader.IsEmptyElement)
                {
                    reader.ReadEndElement();
                }
                else
                {
                    reader.ReadStartElement();
                }

                if (reader.IsStartElement("Relationships"))
                {
                    if (!reader.IsEmptyElement)
                    {
                        while (!reader.EOF)
                        {
                            reader.Read();
                            if (reader.NodeType == System.Xml.XmlNodeType.EndElement && reader.Name == "Relationships")
                            {
                                break;
                            }
                            LogicalRelationship rel = null;
                            LogicalEntity entity1 = this.FindEntity(reader.GetAttribute("entity1"));
                            LogicalEntity entity2 = this.FindEntity(reader.GetAttribute("entity2"));
                            if (reader.IsStartElement("IsARelationship"))
                            {
                                rel = IsALogicalRelationship.CreateInstance(reader);
                            }
                            if (reader.IsStartElement("OneToOneRelationship"))
                            {
                                rel = OneToOneLogicalRelationship.CreateInstance(reader);
                            }
                            if (reader.IsStartElement("OneToManyRelationship"))
                            {
                                rel = OneToManyLogicalRelationship.CreateInstance(reader);
                            }
                            if (reader.IsStartElement("ManyToManyRelationship"))
                            {
                                rel = ManyToManyLogicalRelationship.CreateInstance(reader);
                            }
                            if (null != rel)
                            {
                                rel.Entity1 = entity1;
                                rel.Entity2 = entity2;
                                this.AddRelationship(rel);
                            }
                        }
                    }
                }
            }
            }
        }

        /// <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("LogicalSchema");

            writer.WriteAttributeString("id", this.Id);
            writer.WriteAttributeString("name", this.Name);

            writer.WriteStartElement("Entities");
            foreach (LogicalEntity entity in this._entities)
            {
                entity.WriteXml(writer);
            }
            writer.WriteEndElement();

            writer.WriteStartElement("Relationships");
            foreach (LogicalRelationship 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);
            LogicalSchema cloneSchema = (LogicalSchema)binaryFormatter.Deserialize(stream);
            cloneSchema._isClone = true;
            return cloneSchema;
        }
        #endregion
	}
}
