using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Core.Mapping;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Infrastructure;
using System.Linq;
using DatabaseEditor.BL.Enums;
using DatabaseEditor.BL.Filters;
using DatabaseEditor.BL.Filters.Conditions;
using DatabaseEditor.BL.Interfaces;
using DatabaseEditor.BL.Models;
using DatabaseEditor.DAL;
using MySql.Data.MySqlClient;

namespace DatabaseEditor.BL.ItemsProviders
{
    public abstract class MySQLItemsProvider<TListModel, TDetail> : IItemsProvider<TListModel>,
        IDetailProvider<TDetail>
        where TDetail : TListModel
        where TListModel : ItemModelBase
    {
        private string _dbName;
        private string _tableName;

        private DateTime? _lastDbChangeTime;

        protected Filter<TListModel> Filter = new Filter<TListModel>();
        protected string ConnectionString { get; }


        protected MySQLItemsProvider(string connectionString)
        {
            ConnectionString = connectionString;
        }

        public bool DatabaseExists
        {
            get
            {
                var connection = new MySqlConnection(ConnectionString);
                using (var dbContext = new LibraryDbContext(connection, false))
                {
                    return dbContext.Database.Exists();
                }
            }
        }

        public abstract int FetchCount();

        public abstract IList<TListModel> FetchRange(int startIndex, int count);

        public abstract bool Contains(TListModel item);

        public SortDirection SortDirection { get; set; } = SortDirection.NotSorted;

        public void SortBy(string columnName, SortDirection direction)
        {
            SortedColumnName = columnName;
            SortDirection = direction;
        }

        public string SortedColumnName { get; set; }

        public TDetail GetItemDetail(TListModel item)
        {
            return GetItemById(item.Id);
        }

        public void UseConditions(List<PropertyCondition<TListModel>> conditions)
        {
            Filter.Conditions = conditions;
        }

        public abstract TDetail GetItemById(Guid id);

        public abstract void UpdateOrInsert(TDetail item);

        public bool DeleteItem(TListModel item)
        {
            return DeleteItem(item.Id);
        }

        public abstract bool DeleteItem(Guid id);

        public abstract bool SourceChanged { get; }

        protected bool TableChanged(Type type)
        {
            var dbChangeTime = GetLastUpdateTime(type);
            if (!dbChangeTime.HasValue) return false;

            if (_lastDbChangeTime == null)
            {
                _lastDbChangeTime = dbChangeTime;
            }
            else if (dbChangeTime != _lastDbChangeTime)
            {
                _lastDbChangeTime = dbChangeTime.Value;
                return true;
            }
            return false;
        }

        private DateTime? GetLastUpdateTime(Type type)
        {
            using (var dbContext = new LibraryDbContext(ConnectionString))
            {
                var dbName = GetDatabaseName(dbContext);
                var tableName = GetTableName(type, dbContext);
                return ((IObjectContextAdapter) dbContext)
                    .ObjectContext.ExecuteStoreQuery<DateTime?>(
                        "SELECT UPDATE_TIME " +
                        "FROM   information_schema.tables " +
                        "WHERE  TABLE_SCHEMA = @p0 " +
                        "AND TABLE_NAME = @p1", dbName, tableName).FirstOrDefault();
            }
        }


        protected string GetDatabaseName(DbContext context)
        {
            if (_dbName == null)
                _dbName = context.Database.Connection.Database;
            return _dbName;
        }
        
        private string GetTableName(Type type, DbContext context)
        {
            if (_tableName == null)
            {
                var metadata = ((IObjectContextAdapter) context).ObjectContext.MetadataWorkspace;

                // Get the part of the model that contains info about the actual CLR types
                var objectItemCollection = ((ObjectItemCollection) metadata.GetItemCollection(DataSpace.OSpace));

                // Get the entity type from the model that maps to the CLR type
                var entityType = metadata
                    .GetItems<EntityType>(DataSpace.OSpace)
                    .Single(e => objectItemCollection.GetClrType(e) == type);

                // Get the entity set that uses this entity type
                var entitySet = metadata
                    .GetItems<EntityContainer>(DataSpace.CSpace)
                    .Single()
                    .EntitySets
                    .Single(s => s.ElementType.Name == entityType.Name);

                // Find the mapping between conceptual and storage model for this entity set
                var mapping = metadata.GetItems<EntityContainerMapping>(DataSpace.CSSpace)
                    .Single()
                    .EntitySetMappings
                    .Single(s => s.EntitySet == entitySet);

                // Find the storage entity set (table) that the entity is mapped
                var table = mapping
                    .EntityTypeMappings.Single()
                    .Fragments.Single()
                    .StoreEntitySet;

                // Return the table name from the storage entity set
                _tableName = ((string) table.MetadataProperties["Table"].Value ?? table.Name)?.ToLower();
            }
            return _tableName;
        }
    }
}