﻿namespace Dapper.FastCrud
{
    using System;
    using System.Collections.Generic;
    using System.Runtime.CompilerServices;
    using System.Threading;
    using Dapper.FastCrud.Mappings;
    using Dapper.FastCrud.SqlStatements;

    /// <summary>
    /// Sets up various FastCrud settings.
    /// </summary>
    public static class OrmConfiguration
    {
        private static SqlDialect _currentDefaultDialect = SqlDialect.MsSql;
        private static Dictionary<Type, EntityDescriptor> _entityDescriptorCache = new Dictionary<Type, EntityDescriptor>();

        /// <summary>
        /// Returns the default entity mapping for an entity.
        /// This was either previously set by you in a call to <see cref="SetDefaultEntityMapping{TEntity}"/> or it was auto-generated by the library.
        /// 
        /// You can use the returned mappings to create new temporary mappings for the query calls or to override the defaults.
        /// Once the mappings have been used in query calls, the instance will be frozen and it won't support further modifications, but you can always call <see cref="EntityMapping{TEntity}.Clone"/> to create a new instance.
        /// </summary>
        /// <typeparam name="TEntity">Entity type</typeparam>
        public static EntityMapping<TEntity> GetDefaultEntityMapping<TEntity>()
        {
            return (GetEntityDescriptor<TEntity>().DefaultEntityMapping??new AutoGeneratedEntityMapping<TEntity>()) as EntityMapping<TEntity>;
        }

        /// <summary>
        /// Registers a new entity. Please continue setting up property mappings and other entity options with the returned default entity mapping instance.
        /// </summary>
        /// <typeparam name="TEntity">Entity type</typeparam>
        public static EntityMapping<TEntity> RegisterEntity<TEntity>()
        {
            return SetDefaultEntityMapping(new EntityMapping<TEntity>());
        }

        /// <summary>
        /// Sets the default entity type mapping for the entity type.
        /// This must be called before any query operations were made on the entity.
        /// </summary>
        /// <typeparam name="TEntity">Entity type</typeparam>
        public static EntityMapping<TEntity> SetDefaultEntityMapping<TEntity>(EntityMapping<TEntity> mappings)
        {
            if (mappings == null)
            {
                throw new ArgumentNullException(nameof(mappings));
            }

            if (mappings.IsFrozen)
            {
                throw new ArgumentException("The entity mappings were frozen and can't be used as defaults. They must be cloned first.", nameof(mappings));
            }

            GetEntityDescriptor<TEntity>().DefaultEntityMapping = mappings;
            return mappings;
        }

        /// <summary>
        /// Gets or sets the default dialect. 
        /// Setting this value will clear out all the existing entity mappings.
        /// </summary>
        public static SqlDialect DefaultDialect
        {
            get
            {
                return _currentDefaultDialect;
            }
            set
            {
                _currentDefaultDialect = value;
                Interlocked.Exchange(ref _entityDescriptorCache, new Dictionary<Type, EntityDescriptor>());
            }
        }

        /// <summary>
        /// Returns an SQL builder helpful for constructing verbatim SQL queries.
        /// </summary>
        /// <typeparam name="TEntity">Entity type</typeparam>
        /// <param name="entityMapping">If NULL, de default entity mapping will be used.</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static ISqlBuilder GetSqlBuilder<TEntity>(EntityMapping<TEntity> entityMapping = null)
        {
            return GetEntityDescriptor<TEntity>().GetSqlStatements<TEntity>(entityMapping).SqlBuilder;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static EntityDescriptor GetEntityDescriptor<TEntity>()
        {
            var entityType = typeof(TEntity);
            EntityDescriptor entityDescriptor = null;

            var originalEntityDescriptorCache = _entityDescriptorCache;
            if (!originalEntityDescriptorCache.TryGetValue(entityType, out entityDescriptor))
            {
                entityDescriptor = new EntityDescriptor();
                Interlocked.Exchange(
                    ref _entityDescriptorCache,
                    new Dictionary<Type, EntityDescriptor>(originalEntityDescriptorCache) {[entityType] = entityDescriptor });
            }

            return entityDescriptor;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static ISqlStatements<TEntity> GetSqlStatements<TEntity>(EntityMapping entityMapping = null)
        {
            return (ISqlStatements<TEntity>)GetEntityDescriptor<TEntity>().GetSqlStatements<TEntity>(entityMapping);
        }
    }
}
