#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.ComponentModel;
using System.Collections.Generic;
using System.Text;
using dataMagus.Common.Logical;
using System.Collections;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
#endregion

namespace dataMagus.Common.Physical.Objectoriented
{
	/// <summary>
	/// This class represents a physical objectoriented schema.
	/// </summary>
    [Serializable]
	public sealed class PhysicalObjectorientedSchema : IPhysicalSchema
	{
		#region Constants
		/// <summary>
		/// Default name.
		/// </summary>
        private static readonly string DefaultName = global::dataMagus.Common.OtherResources.DefaultPhysicalObjectorientedSchemaName;

        /// <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 classes contained in the schema.
		/// </summary>
		private List<PhysicalObjectorientedClass> _classes = new List<PhysicalObjectorientedClass>();
		
		/// <summary>
		/// List of all associations contained in the schema.
		/// </summary>
		private List<PhysicalObjectorientedAssociation> _associations = new List<PhysicalObjectorientedAssociation>();
        
		/// <summary>
		/// Instance of the schema converter.
		/// </summary>
		[NonSerialized]
		private PhysicalObjectorientedSchemaConverter _converter = new PhysicalObjectorientedSchemaConverter();
        
		/// <summary>
		/// Index counter to create uniqueness of class names when having equal names.
		/// </summary>
		private int _uniqueClassNameIndex = 0;
		#endregion

		#region Constructors / Destructors
		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="id">Id of the schema.</param>
		public PhysicalObjectorientedSchema(string id)
			: this(id, PhysicalObjectorientedSchema.DefaultName)
		{
		}

		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="id">Id of the schema.</param>
		/// <param name="name">Name of the schema.</param>
		public PhysicalObjectorientedSchema(string id, string name)
		{
			this._id = id;
            this._name = this.EnsureName(name);
		}
		
        /// <summary>
        /// Default constructor.
        /// </summary>
        /// <remarks>Only used for serialization.</remarks>
        private PhysicalObjectorientedSchema()
        {
        }
		#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);
                    this.EnsureUpdateOriginalRestoreDataItem(this, PhysicalObjectorientedSchema.NamePropertyName, value);
                    this._name = 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 classes.
		/// </summary>
        [Browsable(false)]
        public int CountClasses
        {
            get { return this._classes.Count; }
        }
        
		/// <summary>
		/// Count all contained associations.
		/// </summary>
        [Browsable(false)]
        public int CountAssociations
        {
            get { return this._associations.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 (PhysicalObjectorientedClass classItem in this._classes)
            {
                classItem.BeginInit();
            }

            foreach (PhysicalObjectorientedAssociation association in this._associations)
            {
                association.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 (PhysicalObjectorientedClass classItem in this._classes)
            {
                classItem.EndInit(forceEvent);
            }

            foreach (PhysicalObjectorientedAssociation association in this._associations)
            {
                association.EndInit(forceEvent);
            }

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

		/// <summary>
		/// Clear the schema.
		/// </summary>
        public void Clear()
		{
			this._classes.Clear();
			this._associations.Clear();
			this._extendedProperties.Clear();
            this._uniqueClassNameIndex = 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 (PhysicalObjectorientedClass classItem in this._classes)
            {
                IEnumerator propEnumerator = classItem.GetProperties();
                while (propEnumerator.MoveNext())
                {
                    PhysicalObjectorientedProperty property = (PhysicalObjectorientedProperty)propEnumerator.Current;
                    if (null != property.DataType.Name && null != oldDataType.Name &&
                        property.DataType.Name.ToLower().Equals(oldDataType.Name.ToLower()))
                    {
                        property.DataType = newDataType;
                    }
                }
            }
        }

        /// <summary>
        /// Add an new class.
        /// </summary>
        /// <param name="classItem">Class to add.</param>
        public void AddClass(PhysicalObjectorientedClass classItem)
        {
            if (!this._classes.Contains(classItem))
            {
                this._classes.Add(classItem);
                this.FireSchemaChangedEvent(this, new SchemaEventArgs(this, SchemaChangedAction.AddedElement, classItem));
            }
        }

        /// <summary>
        /// Remove a class.
        /// </summary>
        /// <param name="classItem">Class to remove.</param>
        public void RemoveClass(PhysicalObjectorientedClass classItem)
        {
            this._classes.Remove(classItem);
            this.FireSchemaChangedEvent(this, new SchemaEventArgs(this, SchemaChangedAction.RemovedElement, classItem));
        }

        /// <summary>
        /// Ensure that the class name is unique on the schema.
        /// </summary>
        /// <param name="classItem">Class to check.</param>
        /// <returns>New name.</returns>
        private string EnsureUniqueClassName(PhysicalObjectorientedClass classItem)
        {
            string name = classItem.Name;
            foreach (PhysicalObjectorientedClass item in this._classes)
            {
                if (item.Name.ToLower().Equals(classItem.Name.ToLower()))
                {
                    name += (++this._uniqueClassNameIndex).ToString();
                    return name;
                }
            }
            return name;
        }

        /// <summary>
        /// Checks if an class has already been added.
        /// </summary>
        /// <param name="classItem">Class to check.</param>
        /// <returns>true if the class is contained, else false.</returns>
        public bool ContainsClass(PhysicalObjectorientedClass classItem)
        {
            return this._classes.Contains(classItem);
        }

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

        /// <summary>
        /// Get all classes.
        /// </summary>
        /// <returns>Enumerator to parse.</returns>
        public IEnumerator GetClasses()
        {
            return this._classes.GetEnumerator();
        }

        /// <summary>
        /// Add a new association.
        /// </summary>
        /// <param name="association">Association to add.</param>
        public void AddAssociation(PhysicalObjectorientedAssociation association)
        {
            if (!this._associations.Contains(association))
            {
                this._associations.Add(association);
                this.FireSchemaChangedEvent(this, new SchemaEventArgs(this, SchemaChangedAction.AddedElement, association));
            }
        }

        /// <summary>
        /// Remove an association.
        /// </summary>
        /// <param name="association">Association to remove.</param>
        public void RemoveAssociation(PhysicalObjectorientedAssociation association)
        {
            this._associations.Remove(association);
            this.FireSchemaChangedEvent(this, new SchemaEventArgs(this, SchemaChangedAction.RemovedElement, association));
        }

        /// <summary>
        /// Checks of an association has already been added.
        /// </summary>
        /// <param name="association">Association to check.</param>
        /// <returns>true if the association is contained, else false.</returns>
        public bool ContainsClass(PhysicalObjectorientedAssociation association)
        {
            return this._associations.Contains(association);
        }

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

        /// <summary>
        /// Get all associations.
        /// </summary>
        /// <returns>Enumerator to parse.</returns>
        public IEnumerator GetAssociations()
        {
            return this._associations.GetEnumerator();
        }

		/// <summary>
		/// Find a property on the class by id.
		/// </summary>
		/// <param name="id">Id to search for.</param>
		/// <returns>The found property or null if nothing has been found.</returns>
        public PhysicalObjectorientedProperty FindProperty(string id)
        {
            foreach (PhysicalObjectorientedClass classItem in this._classes)
            {
                PhysicalObjectorientedProperty property = classItem.FindProperty(id);
                if (null != property)
                {
                    return property;
                }
            }
            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, PhysicalObjectorientedSchema.NamePropertyName);
			if (null != nameToRestore)
			{
				this._name = (string)nameToRestore;
			}

			object extendedPropertiesToRestore = DataContainer.Instance.PhysicalData.GetRestoreDataItem(this, this.Id, PhysicalObjectorientedSchema.ExtendedPropertiesPropertyName);
			if (null != extendedPropertiesToRestore)
			{
				this._extendedProperties = (Dictionary<string, object>)extendedPropertiesToRestore;
			}
		}
		
		/// <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>
        /// 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>
		/// 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 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);
            PhysicalObjectorientedSchema cloneSchema = (PhysicalObjectorientedSchema)binaryFormatter.Deserialize(stream);
            cloneSchema._isClone = true;
            return cloneSchema;
        }
        #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("PhysicalObjectorientedSchema");

            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("Classes");
            foreach (PhysicalObjectorientedClass classItem in this._classes)
            {
                classItem.WriteXml(writer);
            }
            writer.WriteEndElement();

            writer.WriteStartElement("Associations");
            foreach (PhysicalObjectorientedAssociation association in this._associations)
            {
                association.WriteXml(writer);
            }
            writer.WriteEndElement();

            writer.WriteEndElement();
        }
        #endregion
    }
}
