using System;
using System.Data.Common;
using System.Reflection;

namespace EntityFrameworkCore.Jet.Data
{
    /// <summary>
    /// Jet provider factory
    /// </summary>
    public class JetFactory : DbProviderFactory
    {
        public static readonly Version MinimumRequiredOdbcVersion = new Version(6, 0, 0);
        public static readonly Version MinimumRequiredOleDbVersion = new Version(6, 0, 0);

        public static readonly JetFactory Instance = new JetFactory(null, null);

        public JetConnection? Connection { get; }

        internal DbProviderFactory? InnerFactory { get; }

        internal JetFactory(JetConnection? connection, DbProviderFactory? innerFactory)
        {
            if (innerFactory is JetFactory)
                throw new ArgumentException("JetProviderFactory cannot use a JetProviderFactory as its underlying provider factory. Supported provider factories are OdbcFactory and OleDbFactory.");

            Connection = connection;
            InnerFactory = innerFactory;
        }

        /// <summary>
        /// Specifies whether the specific <see cref="T:System.Data.Common.DbProviderFactory" /> supports the <see cref="T:System.Data.Common.DbDataSourceEnumerator" /> class.
        /// </summary>
        public override bool CanCreateDataSourceEnumerator
            => false;

        /// <summary>
        /// Returns a new instance of the provider's class that implements the <see cref="T:System.Data.Common.DbCommand" /> class.
        /// </summary>
        /// <returns>
        /// A new instance of <see cref="T:System.Data.Common.DbCommand" />.
        /// </returns>
        public override DbCommand CreateCommand()
            => InnerFactory == null
                ? throw new InvalidOperationException(Messages.CannotCallJetProviderFactoryMethodOnSingletonInstance(nameof(CreateCommand)))
                : new JetCommand(Connection);

        /// <summary>
        /// Returns a new instance of the provider's class that implements the <see cref="T:System.Data.Common.DbCommandBuilder" /> class.
        /// </summary>
        /// <returns>
        /// A new instance of <see cref="T:System.Data.Common.DbCommandBuilder" />.
        /// </returns>
        public override DbCommandBuilder CreateCommandBuilder()
        {
            if (InnerFactory == null)
                throw new InvalidOperationException(Messages.CannotCallJetProviderFactoryMethodOnSingletonInstance(nameof(CreateCommandBuilder)));

            var commandBuilder = InnerFactory.CreateCommandBuilder();
            commandBuilder.QuotePrefix = "`";
            commandBuilder.QuoteSuffix = "`";

            return commandBuilder;
        }

        /// <summary>
        /// Returns a new instance of the provider's class that implements the <see cref="T:System.Data.Common.DbConnection" /> class.
        /// </summary>
        /// <returns>
        /// A new instance of <see cref="T:System.Data.Common.DbConnection" />.
        /// </returns>
        public override DbConnection CreateConnection()
            => InnerFactory == null
                ? new JetConnection()
                : new JetConnection(InnerFactory);

        /// <summary>
        /// Returns a new instance of the provider's class that implements the <see cref="T:System.Data.Common.DbConnectionStringBuilder" /> class.
        /// </summary>
        /// <returns>
        /// A new instance of <see cref="T:System.Data.Common.DbConnectionStringBuilder" />.
        /// </returns>
        public override DbConnectionStringBuilder CreateConnectionStringBuilder()
            => InnerFactory == null
                ? throw new InvalidOperationException(Messages.CannotCallJetProviderFactoryMethodOnSingletonInstance(nameof(CreateConnectionStringBuilder)))
                : new JetConnectionStringBuilder(InnerFactory);

        /// <summary>
        /// Returns a new instance of the provider's class that implements the <see cref="T:System.Data.Common.DbDataAdapter" /> class.
        /// </summary>
        /// <returns>
        /// A new instance of <see cref="T:System.Data.Common.DbDataAdapter" />.
        /// </returns>
        public override DbDataAdapter CreateDataAdapter()
            => InnerFactory == null
                ? throw new InvalidOperationException(Messages.CannotCallJetProviderFactoryMethodOnSingletonInstance(nameof(CreateDataAdapter)))
                : InnerFactory.CreateDataAdapter();

        /// <summary>
        /// Returns a new instance of the provider's class that implements the <see cref="T:System.Data.Common.DbDataSourceEnumerator" /> class.
        /// </summary>
        /// <returns>
        /// A new instance of <see cref="T:System.Data.Common.DbDataSourceEnumerator" />.
        /// </returns>
        public override DbDataSourceEnumerator? CreateDataSourceEnumerator()
            => null;

        /// <summary>
        /// Returns a new instance of the provider's class that implements the <see cref="T:System.Data.Common.DbParameter" /> class.
        /// </summary>
        /// <returns>
        /// A new instance of <see cref="T:System.Data.Common.DbParameter" />.
        /// </returns>
        public override DbParameter CreateParameter()
            => InnerFactory == null
                ? throw new InvalidOperationException(Messages.CannotCallJetProviderFactoryMethodOnSingletonInstance(nameof(CreateDataAdapter)))
                : InnerFactory.CreateParameter();

        public virtual DbProviderFactory GetDataAccessProviderFactory(DataAccessProviderType dataAccessProviderType)
        {
            if (dataAccessProviderType == DataAccessProviderType.OleDb)
            {
                try
                {
                    var type = Type.GetType("System.Data.OleDb.OleDbFactory, System.Data.OleDb");
                    var assemblyName = type.Assembly.GetName();
                    var version = assemblyName.Version;

                    if (version < MinimumRequiredOleDbVersion &&
                        assemblyName.Name != "System.Data") // For .NET Framework, System.Data.OleDb is just a stub that references the .NET Framework implementation.
                    {
                        throw new TypeLoadException($"The referenced version '{version}' of 'System.Data.OleDb' is lower than the minimum required version {MinimumRequiredOleDbVersion}.");
                    }
                    
                    return (DbProviderFactory) type
                        .GetField("Instance", BindingFlags.Static | BindingFlags.Public)
                        .GetValue(null);
                }
                catch (Exception e)
                {
                    throw new TypeLoadException($"To use OLE DB in conjunction with Jet, please reference the 'System.Data.OleDb' (version >= {MinimumRequiredOleDbVersion}) NuGet package.", e);
                }
            }
            else
            {
                try
                {
                    var type = Type.GetType("System.Data.Odbc.OdbcFactory, System.Data.Odbc");
                    var assemblyName = type.Assembly.GetName();
                    var version = assemblyName.Version;

                    if (version < MinimumRequiredOdbcVersion &&
                        assemblyName.Name != "System.Data") // For .NET Framework, System.Data.Odbc is just a stub that references the .NET Framework implementation.
                    {
                        throw new TypeLoadException($"The referenced version '{version}' of 'System.Data.Odbc' is lower than the minimum required version {MinimumRequiredOdbcVersion}.");
                    }
                    
                    return (DbProviderFactory) type
                        .GetField("Instance", BindingFlags.Static | BindingFlags.Public)
                        .GetValue(null);
                }
                catch (Exception e)
                {
                    throw new TypeLoadException($"To use ODBC in conjunction with Jet, please reference the 'System.Data.Odbc' (version >= {MinimumRequiredOdbcVersion}) NuGet package.", e);
                }
            }
        }
    }
}