using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;
using System.Data;
using System.IO;
using System.Linq;
using System.Collections;
using EasyDevCore.Common;

namespace EasyDev.Database.EntityTable
{
    /// <summary>
    /// 
    /// </summary>
    public enum DataTableSerializerSchema
    {
        /// <summary>
        /// The none
        /// </summary>
        None,
        /// <summary>
        /// The data table schema
        /// </summary>
        DataTableSchema,
        /// <summary>
        /// The column schema
        /// </summary>
        ColumnSchema
    }

    /// <summary>
    /// Convert DataTable to XML
    /// </summary>
    [Serializable]
    public class DataTableSerializer : IXmlSerializable
    {
        private DataTable _Table;
        private DataTableSerializerSchema _ProxySchema = DataTableSerializerSchema.None;
        private bool _ForSynchValues = false;
        private bool _OptimumSize = true;
        private string _KeyFields = string.Empty;
        private string _ColumnNames = string.Empty;
        private string _ExceptColumnNames = string.Empty;

        /// <summary>
        /// use for XmlSerialization
        /// </summary>
        private DataTableSerializer()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DataTableSerializer" /> class.
        /// </summary>
        /// <param name="entities">The entities.</param>
        /// <param name="forSynchValues">if set to <c>true</c> [only changed values has been serialize for synchronized].</param>
        /// <param name="columnNames">The colum names filter (can be use = for mapping; ex: ColumnName = PropertyName).</param>
        /// <param name="exceptColumnNames">The except column names.</param>
        /// <param name="keyFields">The key fields.</param>
        /// <param name="proxySchema">The proxy schema.</param>
        /// <param name="optimumSize">if set to <c>true</c> [optimum size but slower].</param>
        public DataTableSerializer(DataTable entities, bool forSynchValues = false, string columnNames = "", string exceptColumnNames = "", string keyFields = "", DataTableSerializerSchema proxySchema = DataTableSerializerSchema.None, bool optimumSize = true)
        {
            _Table = entities;
            _ForSynchValues = forSynchValues;
            _ColumnNames = columnNames;
            _ExceptColumnNames = exceptColumnNames;
            _KeyFields = keyFields;
            _OptimumSize = optimumSize;
            _ProxySchema = proxySchema;
            if (proxySchema == DataTableSerializerSchema.None && entities != null)
            {
                _ProxySchema = DataTableSerializerSchema.DataTableSchema;
            }
        }

        /// <summary>
        /// Gets the table.
        /// </summary>
        /// <value>The entities.</value>
        public virtual DataTable Table
        {
            get
            {
                return _Table;
            }
        }

        #region IXmlSerializable Members

        /// <summary>
        /// This property is reserved, apply the <see cref="T:System.Xml.Serialization.XmlSchemaProviderAttribute"></see> to the class instead.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Xml.Schema.XmlSchema"></see> that describes the XML representation of the object that is produced by the <see cref="M:System.Xml.Serialization.IXmlSerializable.WriteXml(System.Xml.XmlWriter)"></see> method and consumed by the <see cref="M:System.Xml.Serialization.IXmlSerializable.ReadXml(System.Xml.XmlReader)"></see> method.
        /// </returns>
        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null;
        }

        /// <summary>
        /// Gets the type of the entity collection.
        /// </summary>
        /// <returns></returns>
        public virtual Type GetDataTableType()
        {
            return null;
        }

        /// <summary>
        /// Loads the XML only for changes.
        /// </summary>
        /// <param name="reader">The reader.</param>
        /// <param name="optimumSize">if set to <c>true</c> [optimum size].</param>
        /// <param name="mapColumns">The map columns.</param>
        private void LoadXMLOnlyForSync(System.Xml.XmlReader reader, bool optimumSize, Dictionary<string, int> mapColumns)
        {
            DataRow row;
            DataColumn column;
            string rowState;
            while (reader.MoveToContent() == System.Xml.XmlNodeType.Element && reader.LocalName == "R")
            {
                row = Table.NewRow();
                rowState = reader.GetAttribute("rowStated");
                reader.ReadStartElement();
                while (reader.MoveToContent() != System.Xml.XmlNodeType.EndElement)
                {
                    if (reader.LocalName == "CS") //Special columns
                    {
                        string[] colNuls = reader.GetAttribute("N").SplitCommaString();
                        foreach (string col in colNuls)
                        {
                            column = Table.Columns[mapColumns["C" + col]];
                            row[column.Ordinal] = DBNull.Value;
                        }
                        if (optimumSize)
                        {
                            string[] colZoEs = reader.GetAttribute("E").SplitCommaString();
                            foreach (string col in colZoEs)
                            {
                                column = Table.Columns[mapColumns["C" + col]];
                                row[column.Ordinal] = GetZeroOrEmptyValue(column.DataType);

                            }
                        }
                        reader.Read();
                    }
                    else
                    {
                        column = Table.Columns[mapColumns[reader.LocalName]];
                        row[column.Ordinal] = reader.ReadElementString().StringTo(column.DataType);
                    }
                }
                Table.Rows.Add(row);
                switch (rowState)
                {
                    case "Modified":
                        row.AcceptChanges();
                        row.SetModified();
                        break;
                    case "Deleted":
                        row.Delete();
                        break;
                }
                reader.ReadEndElement();
            }
        }

        /// <summary>
        /// Loads the XML all rows.
        /// </summary>
        /// <param name="reader">The reader.</param>
        /// <param name="optimumSize">if set to <c>true</c> [optimum size].</param>
        /// <param name="mapColumns">The map columns.</param>
        private void LoadXMLAllRows(System.Xml.XmlReader reader, bool optimumSize, Dictionary<string, int> mapColumns)
        {
            DataRow row;
            DataColumn column;
            while (reader.MoveToContent() == System.Xml.XmlNodeType.Element && reader.LocalName == "R")
            {
                row = Table.NewRow();
                reader.ReadStartElement();
                while (reader.MoveToContent() != System.Xml.XmlNodeType.EndElement)
                {
                    if (reader.LocalName == "CS") //Special columns
                    {
                        string colNuls = reader.GetAttribute("N");
                        if (!string.IsNullOrWhiteSpace(colNuls))
                        {
                            foreach (string col in colNuls.SplitCommaString())
                            {
                                column = Table.Columns[mapColumns["C" + col]];
                                row[column.Ordinal] = DBNull.Value;
                            }
                        }
                        if (optimumSize)
                        {
                            string colZoEs = reader.GetAttribute("E");
                            if (!string.IsNullOrWhiteSpace(colZoEs))
                            {
                                foreach (string col in colZoEs.SplitCommaString())
                                {
                                    column = Table.Columns[mapColumns["C" + col]];
                                    row[column.Ordinal] = GetZeroOrEmptyValue(column.DataType);
                                }
                            }
                        }
                        reader.Read();
                    }
                    else
                    {
                        column = Table.Columns[mapColumns[reader.LocalName]];
                        row[column.Ordinal] = reader.ReadElementString().StringTo(column.DataType);
                    }
                }
                Table.Rows.Add(row);
                reader.ReadEndElement();
            }
            Table.AcceptChanges();
        }

        /// <summary>
        /// Generates an object from its XML representation.
        /// </summary>
        /// <param name="reader">The <see cref="T:System.Xml.XmlReader"></see> stream from which the object is deserialized.</param>
        public void ReadXml(System.Xml.XmlReader reader)
        {
            bool forSynchValues = false;
            bool includeSchema = false;
            string type = string.Empty;
            string name = string.Empty;
            DataColumnCollection schemaColumns = null;
            reader.Read();
            if (reader.MoveToContent() == System.Xml.XmlNodeType.Element && reader.LocalName == "Option")
            {
                type = reader.GetAttribute("type");
                name = reader.GetAttribute("name");
                forSynchValues = reader.GetAttribute("forSynchValues").StringTo(false);
                includeSchema = reader.GetAttribute("includeSchema").StringTo(false);
                if (Table == null)
                {
                    if (string.IsNullOrEmpty(type) && GetDataTableType() != null)
                    {
                        type = GetDataTableType().AssemblyQualifiedName;
                    }
                    if (string.IsNullOrEmpty(type))
                    {
                        type = typeof(DataTable).AssemblyQualifiedName;
                    }
                    _Table = (DataTable)ReflectionHelper.CreateInstance(type);
                    if (!string.IsNullOrWhiteSpace(name)) _Table.TableName = name;
                }
                else
                {
                    Table.Clear();
                }
                string[] extendedProperties = reader.GetAttribute("extendedProperties").Split(new char[] { ',', '|' }, StringSplitOptions.RemoveEmptyEntries);
                for (int i = 0; i < extendedProperties.Length; i = i + 2)
                {
                    string attributeName = "extP_" + (i / 2).ToString();
                    string value = reader.GetAttribute(attributeName);
                    string typeName = extendedProperties[i + 1];
                    string propertyName = extendedProperties[i];
                    Table.ExtendedProperties[propertyName] = value.StringTo(extendedProperties[i + 1]);
                }
                reader.Read();
            }
            schemaColumns = Table.Columns;
            if (includeSchema)
            {
                if (reader.MoveToContent() == System.Xml.XmlNodeType.Element && reader.LocalName == "Schema")
                {
                    string schemaType = reader.GetAttribute("type");
                    if (schemaType == DataTableSerializerSchema.ColumnSchema.ToString())
                    {
                        reader.ReadStartElement();
                        while (reader.MoveToContent() != System.Xml.XmlNodeType.EndElement)
                        {
                            DataColumn col = Table.Columns.Add(reader.LocalName, Type.GetType(reader.GetAttribute("type")));
                            string defaultValue = reader.GetAttribute("defaultValue");
                            col.DefaultValue = defaultValue == "<DBNull>" ? DBNull.Value : defaultValue.StringTo(col.DataType);
                            reader.Read();
                        }
                        reader.ReadEndElement();
                    }
                    else //DataTable schema
                    {
                        string schema = reader.ReadInnerXml();
                        if (!string.IsNullOrEmpty(schema))
                        {
                            using (StringReader sr = new StringReader(schema))
                            {
                                DataTable tempTable = new DataTable(name ?? "DataTable"); //Use only for get schema
                                tempTable.ReadXmlSchema(sr);
                                schemaColumns = tempTable.Columns;
                            }
                            using (StringReader sr = new StringReader(schema))
                            {
                                Table.ReadXmlSchema(sr);
                            }
                        }
                    }
                }
            }
            Dictionary<string, int> mapColumns = new Dictionary<string, int>();
            foreach (DataColumn column in schemaColumns)
            {
                mapColumns.Add(string.Format("C{0}", column.Ordinal), Table.Columns[column.ColumnName].Ordinal);
            }

            if (forSynchValues)
            {
                LoadXMLOnlyForSync(reader, _OptimumSize, mapColumns);
            }
            else
            {
                LoadXMLAllRows(reader, _OptimumSize, mapColumns);
            }
            reader.ReadEndElement();
        }

        /// <summary>
        /// Determines whether [is zero or empty] [the specified value].
        /// </summary>
        /// <param name="value">The value.</param>
        /// <returns>
        ///   <c>true</c> if [is zero or empty] [the specified value]; otherwise, <c>false</c>.
        /// </returns>
        protected bool IsZeroOrEmpty(object value)
        {
            Type type = value.GetType();
            if (type == typeof(string))
            {
                return (string)value == string.Empty;
            }
            else
            {
                if (type.IsNumericType())
                {
                    return value.Equals(0);
                }
            }
            return false;
        }

        /// <summary>
        /// Gets the zero or empty value.
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        protected object GetZeroOrEmptyValue(Type type)
        {
            if (type == typeof(string))
            {
                return string.Empty;
            }
            else
            {
                if (type.IsNumericType())
                {
                    return 0;
                }
            }
            return string.Empty;
        }

        /// <summary>
        /// Creates the XML only for sync values.
        /// </summary>
        /// <param name="writer">The writer.</param>
        private void CreateXMLOnlyForSync(System.Xml.XmlWriter writer)
        {
            string[] exceptColNames = _ExceptColumnNames.SplitCommaString(trimSpace: true);
            string[] colNames = _ColumnNames.SplitCommaString(trimSpace: true);
            string[] keyFields = _KeyFields.SplitCommaString(trimSpace: true);

            Dictionary<int, string> columns = new Dictionary<int, string>();
            Dictionary<int, string> modifiedColumns = new Dictionary<int, string>();
            Dictionary<int, string> primaryKeysColumns = new Dictionary<int, string>();
            foreach (DataColumn column in Table.Columns)
            {
                if (string.IsNullOrEmpty(column.Expression) && !column.ReadOnly && (colNames.Length == 0 || Array.IndexOf(colNames, column.ColumnName) > -1)
                            && (exceptColNames.Length == 0 || Array.IndexOf(exceptColNames, column.ColumnName) == -1))
                {
                    string colName = string.Format("C{0}", column.Ordinal);
                    columns.Add(column.Ordinal, colName);
                }
            }


            foreach (string columnName in keyFields)
            {
                int colNo = Table.Columns[columnName].Ordinal;
                string colName = string.Format("C{0}", Table.Columns[columnName].Ordinal);
                primaryKeysColumns.Add(colNo, colName);
                modifiedColumns.Add(colNo, colName);
            }

            DataView dataView = new DataView(Table, "", "", DataViewRowState.Added | DataViewRowState.Deleted | DataViewRowState.ModifiedCurrent);
            DataRow entity;
            foreach (DataRowView rowView in dataView)
            {
                entity = rowView.Row;
                writer.WriteStartElement("R");
                writer.WriteAttributeString("rowStated", entity.RowState.ToString());
                string colNuls = string.Empty;
                string colZeroOrEmpty = string.Empty;
                switch (entity.RowState)
                {
                    case DataRowState.Added:
                        foreach (KeyValuePair<int, string> column in columns)
                        {
                            if (!entity.IsNull(column.Key))
                            {
                                if (_OptimumSize && IsZeroOrEmpty(entity[column.Key]))
                                {
                                    colZeroOrEmpty += !string.IsNullOrWhiteSpace(colZeroOrEmpty) ? "," + column.Key.ToString() : column.Key.ToString();
                                }
                                else
                                {
                                    writer.WriteElementString(column.Value, entity[column.Key].StringFrom());
                                }
                            }
                            else
                            {
                                colNuls += !string.IsNullOrWhiteSpace(colNuls) ? "," + column.Key.ToString() : column.Key.ToString();
                            }
                        }
                        break;
                    case DataRowState.Deleted:
                        foreach (KeyValuePair<int, string> column in primaryKeysColumns)
                        {
                            writer.WriteElementString(column.Value, entity[column.Key, DataRowVersion.Original].StringFrom());
                        }
                        break;
                    case DataRowState.Modified:
                        //Saves only columns has value change
                        if (!entity.IsChanged())
                        {
                            continue;
                        }
                        foreach (KeyValuePair<int, string> column in columns)
                        {
                            if (entity.IsChanged(column.Key))
                            {
                                if (!entity.IsNull(column.Key))
                                {
                                    if (_OptimumSize && IsZeroOrEmpty(entity[column.Key]))
                                    {
                                        colZeroOrEmpty += !string.IsNullOrWhiteSpace(colZeroOrEmpty) ? "," + column.Key.ToString() : column.Key.ToString();
                                    }
                                    else
                                    {
                                        writer.WriteElementString(column.Value, entity[column.Key].StringFrom());
                                    }
                                }
                                else
                                {
                                    colNuls += !string.IsNullOrWhiteSpace(colNuls) ? "," + column.Key.ToString() : column.Key.ToString();
                                }
                            }
                        }
                        break;
                }
                bool isNull = !string.IsNullOrWhiteSpace(colNuls);
                bool isZeroOrEmpty = !string.IsNullOrWhiteSpace(colZeroOrEmpty);
                if (isNull || isZeroOrEmpty)
                {
                    writer.WriteStartElement("CS");
                    if (isNull) writer.WriteAttributeString("N", colNuls);
                    if (isZeroOrEmpty) writer.WriteAttributeString("E", colZeroOrEmpty);
                    writer.WriteEndElement();
                }
                writer.WriteEndElement();
            }
        }

        /// <summary>
        /// Creates the XML all rows.
        /// </summary>
        /// <param name="writer">The writer.</param>
        private void CreateXMLAllRows(System.Xml.XmlWriter writer)
        {
            string[] exceptColNames = _ExceptColumnNames.SplitCommaString(trimSpace: true);
            string[] colNames = _ColumnNames.SplitCommaString(trimSpace: true);
            DataView dataView = new DataView(Table, "", "", DataViewRowState.CurrentRows);
            DataRow entity;
            Dictionary<int, string> columns = new Dictionary<int, string>();
            foreach (DataColumn column in Table.Columns)
            {
                if (string.IsNullOrEmpty(column.Expression) && !column.ReadOnly && (colNames.Length == 0 || Array.IndexOf(colNames, column.ColumnName) > -1)
                        && (exceptColNames.Length == 0 || Array.IndexOf(exceptColNames, column.ColumnName) == -1))
                {
                    columns.Add(column.Ordinal, string.Format("C{0}", column.Ordinal));
                }
            }
            foreach (DataRowView rowView in dataView)
            {
                entity = rowView.Row;
                writer.WriteStartElement("R");

                string colNuls = string.Empty;
                string colZeroOrEmpty = string.Empty;
                foreach (KeyValuePair<int, string> column in columns)
                {
                    if (!entity.IsNull(column.Key))
                    {
                        if (_OptimumSize && IsZeroOrEmpty(entity[column.Key]))
                        {
                            colZeroOrEmpty += !string.IsNullOrWhiteSpace(colZeroOrEmpty) ? "," + column.Key.ToString() : column.Key.ToString();
                        }
                        else
                        {
                            writer.WriteElementString(column.Value, entity[column.Key].StringFrom());
                        }
                    }
                    else
                    {
                        colNuls += !string.IsNullOrWhiteSpace(colNuls) ? "," + column.Key.ToString() : column.Key.ToString();
                    }
                }
                bool isNull = !string.IsNullOrWhiteSpace(colNuls);
                bool isZeroOrEmpty = !string.IsNullOrWhiteSpace(colZeroOrEmpty);
                if (isNull || isZeroOrEmpty)
                {
                    writer.WriteStartElement("CS");
                    if (isNull) writer.WriteAttributeString("N", colNuls);
                    if (isZeroOrEmpty) writer.WriteAttributeString("E", colZeroOrEmpty);
                    writer.WriteEndElement();
                }
                writer.WriteEndElement();
            }
        }

        /// <summary>
        /// Converts an object into its XML representation.
        /// </summary>
        /// <param name="writer">The <see cref="T:System.Xml.XmlWriter"></see> stream to which the object is serialized.</param>
        public void WriteXml(System.Xml.XmlWriter writer)
        {
            writer.WriteStartElement("Option");
            writer.WriteAttributeString("name", Table.TableName);
            writer.WriteAttributeString("includeSchema", (_ProxySchema != DataTableSerializerSchema.None).ToString());
            writer.WriteAttributeString("type", _ProxySchema != DataTableSerializerSchema.None ? string.Empty : Table.GetType().AssemblyQualifiedName);
            writer.WriteAttributeString("forSynchValues", _ForSynchValues.ToString());
            writer.WriteAttributeString("optimumSize", _OptimumSize.ToString());
            string extendedProperites = string.Empty;
            int p = 0;
            foreach (DictionaryEntry prop in Table.ExtendedProperties)
            {
                extendedProperites += (!string.IsNullOrWhiteSpace(extendedProperites) ? "," : string.Empty) + prop.Key + "|" + prop.Value.GetType().FullName;
                writer.WriteAttributeString("extP_" + p++.ToString(), prop.Value.StringFrom());
            }
            writer.WriteAttributeString("extendedProperties", extendedProperites);
            writer.WriteEndElement();
            if (_ProxySchema != DataTableSerializerSchema.None)
            {
                writer.WriteStartElement("Schema");
                if (_ProxySchema == DataTableSerializerSchema.DataTableSchema)
                {
                    Table.WriteXmlSchema(writer);
                }
                if (_ProxySchema == DataTableSerializerSchema.ColumnSchema)
                {
                    writer.WriteAttributeString("type", _ProxySchema.ToString());
                    foreach (DataColumn col in Table.Columns)
                    {
                        writer.WriteStartElement(col.ColumnName);
                        writer.WriteAttributeString("type", col.DataType.FullName);
                        writer.WriteAttributeString("defaultValue", col.DefaultValue == DBNull.Value ? "<DBNull>" : col.DefaultValue.ToString());
                        writer.WriteEndElement();
                    }
                }
                writer.WriteEndElement();
            }
            if (_ForSynchValues)
            {
                CreateXMLOnlyForSync(writer);
            }
            else
            {
                CreateXMLAllRows(writer);
            }
        }
        #endregion
    }
}
