using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using GreenTree.Nachtragsmanagement.Core;
using GreenTree.Nachtragsmanagement.Core.Domain.Appendix;
using GreenTree.Nachtragsmanagement.Core.Domain.Deviation;
using GreenTree.Nachtragsmanagement.Core.Domain.Invoice;
using GreenTree.Nachtragsmanagement.Core.Domain.Misc;
using GreenTree.Nachtragsmanagement.Core.Domain.Site;
using GreenTree.Nachtragsmanagement.Core.Domain.User;
using GreenTree.Nachtragsmanagement.Data.Mapping.Appendix;
using GreenTree.Nachtragsmanagement.Data.Mapping.Deviation;
using GreenTree.Nachtragsmanagement.Data.Mapping.Misc;
using GreenTree.Nachtragsmanagement.Data.Mapping.Site;
using GreenTree.Nachtragsmanagement.Data.Mapping.User;
using System.Data.Entity.Migrations;
using GreenTree.Nachtragsmanagement.Core.Domain.Config;
using GreenTree.Nachtragsmanagement.Data.Mapping.Config;
using GreenTree.Nachtragsmanagement.Core.Domain.Logging;
using GreenTree.Nachtragsmanagement.Data.Mapping.Logging;

namespace GreenTree.Nachtragsmanagement.Data
{
    public class AppendixObjectContext : DbContext, IDbContext
    {
        #region DbSets

        private List<object> _dbSets = new List<object>();

        #endregion

        #region Ctor

        /// <summary>
        /// Initializes a new instance of the AppendixObjectContext-class
        /// </summary>
        public AppendixObjectContext() : base("DefaultConnection")
        {
            SetDbContextSettings("DefaultConnection");
        }

        /// <summary>
        /// Initializes a new instance of the AppendixObjectContext class
        /// </summary>
        /// <param name="nameOrConnectionString"></param>
        public AppendixObjectContext(string nameOrConnectionString)
            : base(nameOrConnectionString)
        {
            SetDbContextSettings(nameOrConnectionString);
        }

        /// <summary>
        /// Sets initial DbContext settings and sets
        /// </summary>
        private void SetDbContextSettings(string nameOrConnectionString)
        {
            var userSet = Set<User>();
            var roleSet = Set<Role>();
            var functionSet = Set<Function>();
            var siteSet = Set<Site>();
            var invoiceSet = Set<Invoice>();
            var statusSet = Set<Status>();
            var kindSet = Set<Kind>();
            var disturbanceSet = Set<Disturbance>();
            var deviationSet = Set<Deviation>();
            var categorySet = Set<Category>();
            var stateSet = Set<State>();
            var appendixSet = Set<Appendix>();
            var mailNotificationSet = Set<MailNotification>();
            var dbContextSpecSet = Set<DbContextSpec>();
            var configItemSet = Set<ConfigItem>();
            var loggingSet = Set<Log>();
            var colorMarkerSet = Set<ColorMarker>();
            var helpPageSet = Set<HelpPage>();
            var userConfigItemSet = Set<UserConfigItem>();

            _dbSets.AddRange(
                new object[]
                {
                    userSet,
                    roleSet,
                    functionSet,
                    siteSet,
                    invoiceSet,
                    statusSet,
                    kindSet,
                    disturbanceSet,
                    deviationSet,
                    categorySet,
                    stateSet,
                    appendixSet,
                    mailNotificationSet,
                    dbContextSpecSet,
                    configItemSet,
                    loggingSet,
                    colorMarkerSet,
                    helpPageSet,
                    userConfigItemSet
                }
            );

            //Database.SetInitializer(new CreateDatabaseIfNotExists<AppendixObjectContext>());
            //Database.SetInitializer(new DropCreateDatabaseIfModelChanges<AppendixObjectContext>());
            Database.SetInitializer(
                new MigrateDatabaseToLatestVersion<AppendixObjectContext, Migrations.Configuration>("DefaultConnection"));

            Database.CreateIfNotExists();

            ((IObjectContextAdapter)this).ObjectContext.ContextOptions.LazyLoadingEnabled = true;

            SaveChanges();
        }

        #endregion

        #region Utilities

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new UserMap());
            modelBuilder.Configurations.Add(new RoleMap());
            modelBuilder.Configurations.Add(new FunctionMap());
            modelBuilder.Configurations.Add(new SiteMap());
            modelBuilder.Configurations.Add(new InvoiceMap());
            modelBuilder.Configurations.Add(new StatusMap());
            modelBuilder.Configurations.Add(new KindMap());
            modelBuilder.Configurations.Add(new DisturbanceMap());
            modelBuilder.Configurations.Add(new DisturbanceValueMap());
            modelBuilder.Configurations.Add(new DeviationMap());
            modelBuilder.Configurations.Add(new CategoryMap());
            modelBuilder.Configurations.Add(new CategoryValueMap());
            modelBuilder.Configurations.Add(new AppendixMap());
            modelBuilder.Configurations.Add(new MailNotificationMap());
            modelBuilder.Configurations.Add(new DbContextSpecMap());
            modelBuilder.Configurations.Add(new ConfigItemMap());
            modelBuilder.Configurations.Add(new LogMap());
            modelBuilder.Configurations.Add(new ColorMarkerMap());
            modelBuilder.Configurations.Add(new HelpPageMap());
            modelBuilder.Configurations.Add(new UserConfigItemMap());

            base.OnModelCreating(modelBuilder);
        }

        /// <summary>
        /// Attach an entity to the context or return an already attached entity (if it was already attached)
        /// </summary>
        /// <typeparam name="TEntity">TEntity</typeparam>
        /// <param name="entity">Entity</param>
        /// <returns>Attached entity</returns>
        protected virtual TEntity AttachEntityToContext<TEntity>(TEntity entity) where TEntity : BaseEntity, new()
        {
            //little hack here until Entity Framework really supports stored procedures
            //otherwise, navigation properties of loaded entities are not loaded until an entity is attached to the context
            var alreadyAttached = Set<TEntity>().Local.FirstOrDefault(x => x.Id == entity.Id);
            if (alreadyAttached == null)
            {
                //attach new entity
                Set<TEntity>().Attach(entity);
                return entity;
            }

            //entity is already loaded
            return alreadyAttached;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Create database script
        /// </summary>
        /// <returns>SQL to generate database</returns>
        public string CreateDatabaseScript()
        {
            return ((IObjectContextAdapter)this).ObjectContext.CreateDatabaseScript();
        }

        /// <summary>
        /// Get the DbSet of a specific entity which inherits the BaseEntity class
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <returns></returns>
        public IDbSet<TEntity> Get<TEntity>() where TEntity : BaseEntity
        {
            var entityType = typeof(TEntity);
            var dbSet = _dbSets
                .FirstOrDefault(d => d.GetType().GetGenericArguments()[0].FullName == entityType.FullName);

            return (IDbSet<TEntity>)dbSet;
        }

        /// <summary>
        /// Set DbSet
        /// </summary>
        /// <typeparam name="TEntity">Entity type</typeparam>
        /// <returns>DbSet</returns>
        public new IDbSet<TEntity> Set<TEntity>() where TEntity : BaseEntity
        {
            return base.Set<TEntity>();
        }

        /// <summary>
        /// Creates a raw SQL query that will return elements of the given generic type.  The type can be any type that has properties that match the names of the columns returned from the query, or can be a simple primitive type. The type does not have to be an entity type. The results of this query are never tracked by the context even if the type of object returned is an entity type.
        /// </summary>
        /// <typeparam name="TElement">The type of object returned by the query.</typeparam>
        /// <param name="sql">The SQL query string.</param>
        /// <param name="parameters">The parameters to apply to the SQL query string.</param>
        /// <returns>Result</returns>
        public IEnumerable<TElement> SqlQuery<TElement>(string sql, params object[] parameters)
        {
            return Database.SqlQuery<TElement>(sql, parameters);
        }

        /// <summary>
        /// Executes the given DDL/DML command against the database.
        /// </summary>
        /// <param name="sql">The command string</param>
        /// <param name="doNotEnsureTransaction">false - the transaction creation is not ensured; true - the transaction creation is ensured.</param>
        /// <param name="timeout">Timeout value, in seconds. A null value indicates that the default value of the underlying provider will be used</param>
        /// <param name="parameters">The parameters to apply to the command string.</param>
        /// <returns>The result returned by the database after executing the command.</returns>
        public int ExecuteSqlCommand(string sql, bool doNotEnsureTransaction = false, int? timeout = null, params object[] parameters)
        {
            int? previousTimeout = null;
            if (timeout.HasValue)
            {
                //store previous timeout
                previousTimeout = ((IObjectContextAdapter)this).ObjectContext.CommandTimeout;
                ((IObjectContextAdapter)this).ObjectContext.CommandTimeout = timeout;
            }

            var transactionalBehavior = doNotEnsureTransaction
                ? TransactionalBehavior.DoNotEnsureTransaction
                : TransactionalBehavior.EnsureTransaction;
            var result = this.Database.ExecuteSqlCommand(transactionalBehavior, sql, parameters);

            if (timeout.HasValue)
            {
                //Set previous timeout back
                ((IObjectContextAdapter)this).ObjectContext.CommandTimeout = previousTimeout;
            }

            //return result
            return result;
        }

        #endregion
    }
}