using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Text;

namespace ZeroLevel.MsSql
{
    public abstract class BaseSqlDbMapper
    {
        protected abstract IDbMapper Mapper { get; }
        protected readonly string _tableName;

        public string TableName { get { return _tableName; } }

        protected BaseSqlDbMapper(string tableName)
        {
            _tableName = tableName;
        }

        public object GetIdentity(object entity)
        {
            if (Mapper.IdentityField != null)
                return Mapper.IdentityField.Getter(entity);
            return null;
        }

        public string IdentityName
        {
            get
            {
                return Mapper.IdentityField?.Name;
            }
        }

        #region QUERIES
        #region INDEXES
        public string GetIndexExistsQuery(IDbField field)
        {
            if (field.IsIndexed)
            {
                return string.Format(
                    "SELECT COUNT(*) FROM sys.indexes WHERE name = 'idx_{0}_{1}' AND object_id = OBJECT_ID('{2}')",
                    _tableName, field.Name, _tableName);
            }
            return null;
        }

        public string GetCreateIndexQuery(IDbField field)
        {
            if (field.IsIndexed)
            {
                return string.Format("CREATE INDEX idx_{0}_{1} ON [{2}]({3});", _tableName, field.Name, _tableName, field.Name);
            }
            return null;
        }
        #endregion

        #region CREATE
        public static HashSet<DbType> FieldsHasSize = new HashSet<DbType>
        {
            DbType.String, DbType.Decimal, DbType.AnsiString, DbType.AnsiStringFixedLength,
            DbType.StringFixedLength, DbType.VarNumeric
        };

        private string _createString = null;
        private readonly object _createStringBuildLocker = new object();
        public string GetCreateQuery(bool rebuild = false)
        {
            lock (_createStringBuildLocker)
            {
                if (_createString == null || rebuild)
                {
                    StringBuilder create = new StringBuilder("CREATE TABLE [" + _tableName + "]");
                    create.Append("(");
                    Mapper.TraversalFields(f =>
                    {
                        var sqlType = DbTypeMapper.ToSqlDbType(f.ClrType);
                        create.Append("[" + f.Name + "] " + sqlType);
                        if (FieldsHasSize.Contains(f.DbType) && f.Size != 0)
                        {
                            if (f.DbType == DbType.Decimal)
                            {
                                int p = 19, s = 4;
                                if (f.Size > 0)
                                {
                                    p = (int)f.Size;
                                    if (s >= p)
                                    {
                                        if (p <= 2) s = 0;
                                        else s = p - 1;
                                    }
                                }
                                create.AppendFormat("({0},{1})", p, s);
                            }
                            else
                            {
                                create.AppendFormat("({0})", ((f.Size == -1) ? "max" : f.Size.ToString()));
                            }
                        }
                        if (f.IsIdentity)
                        {
                            create.Append(" PRIMARY KEY");
                        }
                        if (f.AllowNull)
                        {
                            create.Append(" NULL");
                        }
                        else
                        {
                            create.Append(" NOT NULL");
                        }
                        if (f.AutoIncrement)
                        {
                            create.Append(" IDENTITY (0, 1)");
                        }
                        create.Append(",");
                    });
                    _createString = create.ToString().TrimEnd(',') + ")";
                }
            }
            return _createString;
        }
        #endregion
        #endregion

        public SqlParameter[] CreateSqlDbParameters(object entity)
        {
            if (entity.GetType() != Mapper.EntityType)
                throw new InvalidCastException("Entity type is different from serializer entity type");
            var list = new List<SqlParameter>();
            Mapper.TraversalFields(field => 
            {
                var par = new SqlParameter();
                par.Value = ValueToSqlServerObject(field.Getter(entity), field.ClrType);
                // ADO.NET bug
                // https://connect.microsoft.com/VisualStudio/feedback/details/381934/sqlparameter-dbtype-dbtype-time-sets-the-parameter-to-sqldbtype-datetime-instead-of-sqldbtype-time
                if (field.DbType == DbType.Time)
                {
                    par.SqlDbType = SqlDbType.Time;
                }
                else
                {
                    par.DbType = field.DbType; // Если задать в конструкторе, то тип может переопределиться при задании значения
                }
                par.ParameterName = field.Name;
                list.Add(par);
            });
            return list.ToArray();
        }

        #region Datetime helper
        private static DateTime MinSqlDbDateTimeValue = new DateTime(1753, 01, 01);

        protected object ValueToSqlServerObject(object obj, Type type)
        {
            if (type == typeof(DateTime))
            {
                return DateTimeToSqlDbValue((DateTime)obj);
            }
            return obj ?? DBNull.Value;
        }
        /// <summary>
        /// Подготовка даты к записи в SQLServer
        /// (минимальные значения даты в .NET и SQL Server отличаются)
        /// </summary>
        protected object DateTimeToSqlDbValue(DateTime dt)
        {
            if (DateTime.Compare(dt, MinSqlDbDateTimeValue) <= 0)
                return DBNull.Value;
            return dt;
        }
        /// <summary>
        /// Конвертер из элементов строки DataTable в DonNet тип
        /// </summary>
        /// <typeparam name="Tout">Тип на выходе</typeparam>
        /// <param name="value">Значение из БД</param>
        /// <returns>Результат</returns>
        protected Tout Convert<Tout>(object value)
        {
            if (null == value || DBNull.Value == value)
                return default(Tout);
            return (Tout)System.Convert.ChangeType(value, typeof(Tout));
        }
        #endregion
    }
}
