//------------------------------------------------------------------------------
// <copyright file="DbProviderServices.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//
// @owner  [....]
// @backupOwner [....]
//------------------------------------------------------------------------------

using System.Data.SqlClient;

namespace System.Data.Common
{
    using System.Data.Common.CommandTrees;
    using System.Data.Entity;
    using System.Data.Metadata.Edm;
    using System.Data.Spatial;
    using System.Diagnostics;
    using System.IO;
    using System.Reflection;
    using System.Xml;

    /// <summary>
    /// The factory for building command definitions; use the type of this object
    /// as the argument to the IServiceProvider.GetService method on the provider
    /// factory;
    /// </summary>
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Db")]
    [CLSCompliant(false)]
    abstract public class DbProviderServices
    {
        /// <summary>
        /// Create a Command Definition object given a command tree.
        /// </summary>
        /// <param name="commandTree">command tree for the statement</param>
        /// <returns>an exectable command definition object</returns>
        /// <remarks>
        /// This method simply delegates to the provider's implementation of CreateDbCommandDefinition.
        /// </remarks>
        public DbCommandDefinition CreateCommandDefinition(DbCommandTree commandTree)
        {
            EntityUtil.CheckArgumentNull(commandTree, "commandTree");
            ValidateDataSpace(commandTree);
            StoreItemCollection storeMetadata = (StoreItemCollection)commandTree.MetadataWorkspace.GetItemCollection(DataSpace.SSpace);
            Debug.Assert(storeMetadata.StoreProviderManifest != null, "StoreItemCollection has null StoreProviderManifest?");
            
            return CreateDbCommandDefinition(storeMetadata.StoreProviderManifest, commandTree);
        }

        /// <summary>
        /// Create a Command Definition object given a command tree.
        /// </summary>
        /// <param name="commandTree">command tree for the statement</param>
        /// <returns>an exectable command definition object</returns>
        /// <remarks>
        /// This method simply delegates to the provider's implementation of CreateDbCommandDefinition.
        /// </remarks>
        public DbCommandDefinition CreateCommandDefinition(DbProviderManifest providerManifest, DbCommandTree commandTree)
        {
            try
            {
                return CreateDbCommandDefinition(providerManifest, commandTree);
            }
            catch (ProviderIncompatibleException)
            {
                throw;
            }
            catch (Exception e)
            {
                if (EntityUtil.IsCatchableExceptionType(e))
                {
                    throw EntityUtil.ProviderIncompatible(Strings.ProviderDidNotCreateACommandDefinition, e);
                }
                throw;
            }
        }

        /// <summary>
        /// Create a Command Definition object, given the provider manifest and command tree
        /// </summary>
        /// <param name="connection">provider manifest previously retrieved from the store provider</param>
        /// <param name="commandTree">command tree for the statement</param>
        /// <returns>an exectable command definition object</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Db")]
        protected abstract DbCommandDefinition CreateDbCommandDefinition(DbProviderManifest providerManifest, DbCommandTree commandTree);

        /// <summary>
        /// Ensures that the data space of the specified command tree is the target (S-) space
        /// </summary>
        /// <param name="commandTree">The command tree for which the data space should be validated</param>
        internal virtual void ValidateDataSpace(DbCommandTree commandTree)
        {
            Debug.Assert(commandTree != null, "Ensure command tree is non-null before calling ValidateDataSpace");

            if (commandTree.DataSpace != DataSpace.SSpace)
            {
                throw EntityUtil.ProviderIncompatible(Strings.ProviderRequiresStoreCommandTree);
            }
        }

        /// <summary>
        /// Create a DbCommand object given a command tree.
        /// </summary>
        /// <param name="commandTree">command tree for the statement</param>
        /// <returns>a command object</returns>
        internal virtual DbCommand CreateCommand(DbCommandTree commandTree) {
            DbCommandDefinition commandDefinition = CreateCommandDefinition(commandTree);
            DbCommand command = commandDefinition.CreateCommand();
            return command;
        }

        /// <summary>
        /// Create the default DbCommandDefinition object based on the prototype command
        /// This method is intended for provider writers to build a default command definition
        /// from a command. 
        /// Note: This will clone the prototype
        /// </summary>
        /// <param name="prototype">the prototype command</param>
        /// <returns>an executable command definition object</returns>
        public virtual DbCommandDefinition CreateCommandDefinition(DbCommand prototype) {
            return DbCommandDefinition.CreateCommandDefinition(prototype);
        }
                
        /// <summary>
        /// Retrieve the provider manifest token based on the specified connection.
        /// </summary>
        /// <param name="connection">The connection for which the provider manifest token should be retrieved.</param>
        /// <returns>
        /// The provider manifest token that describes the specified connection, as determined by the provider.
        /// </returns>
        /// <remarks>
        /// This method simply delegates to the provider's implementation of GetDbProviderManifestToken.
        /// </remarks>
        public string GetProviderManifestToken(DbConnection connection) {
            try
            {
                string providerManifestToken = GetDbProviderManifestToken(connection);
                if (providerManifestToken == null)
                {
                    throw EntityUtil.ProviderIncompatible(Strings.ProviderDidNotReturnAProviderManifestToken);
                }
                return providerManifestToken;
            }
            catch (ProviderIncompatibleException)
            {
                throw;
            }
            catch (Exception e)
            {
                if (EntityUtil.IsCatchableExceptionType(e))
                {
                    throw EntityUtil.ProviderIncompatible(Strings.ProviderDidNotReturnAProviderManifestToken, e);
                }
                throw;
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Db")]
        protected abstract string GetDbProviderManifestToken(DbConnection connection);

        public DbProviderManifest GetProviderManifest(string manifestToken) {
            try
            {
                DbProviderManifest providerManifest = GetDbProviderManifest(manifestToken);
                if (providerManifest == null)
                {
                    throw EntityUtil.ProviderIncompatible(Strings.ProviderDidNotReturnAProviderManifest);
                }

                return providerManifest;
            }
            catch (ProviderIncompatibleException)
            {
                throw;
            }
            catch (Exception e)
            {
                if (EntityUtil.IsCatchableExceptionType(e)) {
                    throw EntityUtil.ProviderIncompatible(System.Data.Entity.Strings.ProviderDidNotReturnAProviderManifest, e);
                }
                throw;
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Db")]
        protected abstract DbProviderManifest GetDbProviderManifest(string manifestToken);

        public DbSpatialDataReader GetSpatialDataReader(DbDataReader fromReader, string manifestToken)
        {
            try
            {
                DbSpatialDataReader spatialReader = GetDbSpatialDataReader(fromReader, manifestToken);
                if (spatialReader == null)
                {
                    throw EntityUtil.ProviderIncompatible(Strings.ProviderDidNotReturnSpatialServices);
                }

                return spatialReader;
            }
            catch (ProviderIncompatibleException)
            {
                throw;
            }
            catch (Exception e)
            {
                if (EntityUtil.IsCatchableExceptionType(e))
                {
                    throw EntityUtil.ProviderIncompatible(System.Data.Entity.Strings.ProviderDidNotReturnSpatialServices, e);
                }
                throw;
            }
        }

        public DbSpatialServices GetSpatialServices(string manifestToken)
        {
            try
            {
                DbSpatialServices spatialServices = DbGetSpatialServices(manifestToken);
                if (spatialServices == null)
                {
                    throw EntityUtil.ProviderIncompatible(Strings.ProviderDidNotReturnSpatialServices);
                }

                return spatialServices;
            }
            catch (ProviderIncompatibleException)
            {
                throw;
            }
            catch (Exception e)
            {
                if (EntityUtil.IsCatchableExceptionType(e))
                {
                    throw EntityUtil.ProviderIncompatible(System.Data.Entity.Strings.ProviderDidNotReturnSpatialServices, e);
                }
                throw;
            }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Db")]
        protected virtual DbSpatialDataReader GetDbSpatialDataReader(DbDataReader fromReader, string manifestToken)
        {
            // Must be a virtual method; abstract would break previous implementors of DbProviderServices
            throw EntityUtil.ProviderIncompatible(Strings.ProviderDidNotReturnSpatialServices);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Db")]
        protected virtual DbSpatialServices DbGetSpatialServices(string manifestToken)
        {
            // Must be a virtual method; abstract would break previous implementors of DbProviderServices
            throw EntityUtil.ProviderIncompatible(Strings.ProviderDidNotReturnSpatialServices);
        }

        internal void SetParameterValue(DbParameter parameter, TypeUsage parameterType, object value)
        {
            Debug.Assert(parameter != null, "Validate parameter before calling SetParameterValue");
            Debug.Assert(parameterType != null, "Validate parameterType before calling SetParameterValue");

            this.SetDbParameterValue(parameter, parameterType, value);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Db")]
        protected virtual void SetDbParameterValue(DbParameter parameter, TypeUsage parameterType, object value)
        {
            EntityUtil.CheckArgumentNull(parameter, "parameter");
            EntityUtil.CheckArgumentNull(parameterType, "parameterType");

            parameter.Value = value;
        }

        /// <summary>
        /// Create an instance of DbProviderServices based on the supplied DbConnection
        /// </summary>
        /// <param name="connection">The DbConnection to use</param>
        /// <returns>An instance of DbProviderServices</returns>
        public static DbProviderServices GetProviderServices(DbConnection connection) {
            return GetProviderServices(GetProviderFactory(connection));
        }

        internal static DbProviderFactory GetProviderFactory(string providerInvariantName)
        {
            EntityUtil.CheckArgumentNull(providerInvariantName, "providerInvariantName");
            DbProviderFactory factory;
            try
            {
                factory = DbProviderFactories.GetFactory(providerInvariantName);
            }
            catch (ArgumentException e)
            {
                throw EntityUtil.Argument(Strings.EntityClient_InvalidStoreProvider, e);
            }
            return factory;
        }

        /// <summary>
        /// Retrieve the DbProviderFactory based on the specified DbConnection
        /// </summary>
        /// <param name="connection">The DbConnection to use</param>
        /// <returns>An instance of DbProviderFactory</returns>
        public static DbProviderFactory GetProviderFactory(DbConnection connection)
        {
            EntityUtil.CheckArgumentNull(connection, "connection");
            DbProviderFactory factory = DbProviderFactories.GetFactory(connection);
            if (factory == null)
            {
                throw EntityUtil.ProviderIncompatible(System.Data.Entity.Strings.EntityClient_ReturnedNullOnProviderMethod(
                        "get_ProviderFactory",
                        connection.GetType().ToString()));
            }
            Debug.Assert(factory != null, "Should have thrown on null"); 
            return factory;
        }

        internal static DbProviderServices GetProviderServices(DbProviderFactory factory) {
            EntityUtil.CheckArgumentNull(factory, "factory");

            // Special case SQL client so that it will work with System.Data from .NET 4.0 even without
            // a binding redirect.
            if (factory is SqlClientFactory)
            {
                return SqlProviderServices.Instance;
            }

            IServiceProvider serviceProvider = factory as IServiceProvider;
            if (serviceProvider == null) {
                throw EntityUtil.ProviderIncompatible(System.Data.Entity.Strings.EntityClient_DoesNotImplementIServiceProvider(
                    factory.GetType().ToString()));
            }

            DbProviderServices providerServices = serviceProvider.GetService(typeof(DbProviderServices)) as DbProviderServices;
            if (providerServices == null) {
                throw EntityUtil.ProviderIncompatible(
                    System.Data.Entity.Strings.EntityClient_ReturnedNullOnProviderMethod(
                        "GetService",
                        factory.GetType().ToString()));
            }

            return providerServices;
        }

        /// <summary>
        /// Return an XML reader which represents the CSDL description
        /// </summary>
        /// <returns>An XmlReader that represents the CSDL description</returns>
        internal static XmlReader GetConceptualSchemaDefinition(string csdlName) {
            return DbProviderServices.GetXmlResource("System.Data.Resources.DbProviderServices." + csdlName + ".csdl");
        }

        internal static XmlReader GetXmlResource(string resourceName) {
            Assembly executingAssembly = Assembly.GetExecutingAssembly();
            Stream stream = executingAssembly.GetManifestResourceStream(resourceName);
            return XmlReader.Create(stream, null, resourceName);
        }

        /// <summary>
        /// Generates a DDL script which creates schema objects (tables, primary keys, foreign keys) 
        /// based on the contents of the storeItemCollection and targeted for the version of the backend corresponding to 
        /// the providerManifestToken.
        /// Individual statements should be separated using database-specific DDL command separator. 
        /// It is expected that the generated script would be executed in the context of existing database with 
        /// sufficient permissions, and it should not include commands to create the database, but it may include 
        /// commands to create schemas and other auxiliary objects such as sequences, etc.
        /// </summary>
        /// <param name="providerManifestToken">The provider manifest token identifying the target version</param>
        /// <param name="storeItemCollection">The collection of all store items based on which the script should be created</param>
        /// <returns>
        /// A DDL script which creates schema objects based on contents of storeItemCollection 
        /// and targeted for the version of the backend corresponding to the providerManifestToken.
        /// </returns>
        public string CreateDatabaseScript(string providerManifestToken, StoreItemCollection storeItemCollection)
        {
           return DbCreateDatabaseScript(providerManifestToken, storeItemCollection);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Db")]
        protected virtual string DbCreateDatabaseScript(string providerManifestToken, StoreItemCollection storeItemCollection)
        {
            throw EntityUtil.ProviderIncompatible(Strings.ProviderDoesNotSupportCreateDatabaseScript);
        }

        /// <summary>
        /// Creates a database indicated by connection and creates schema objects 
        /// (tables, primary keys, foreign keys) based on the contents of storeItemCollection. 
        /// </summary>
        /// <param name="connection">Connection to a non-existent database that needs to be created 
        /// and be populated with the store objects indicated by the storeItemCollection</param>
        /// <param name="commandTimeout">Execution timeout for any commands needed to create the database.</param>
        /// <param name="storeItemCollection">The collection of all store items based on which the script should be created<</param>
        public void CreateDatabase(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection)
        {
            DbCreateDatabase(connection, commandTimeout, storeItemCollection);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Db")]
        protected virtual void DbCreateDatabase(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection)
        {
            throw EntityUtil.ProviderIncompatible(Strings.ProviderDoesNotSupportCreateDatabase);
        }

        /// <summary>
        /// Returns a value indicating whether given database exists on the server 
        /// and/or whether schema objects contained in teh storeItemCollection have been created.
        /// If the provider can deduct the database only based on the connection, they do not need
        /// to additionally verify all elements of the storeItemCollection.
        /// </summary>
        /// <param name="connection">Connection to a database whose existence is checked by this method</param>
        /// <param name="commandTimeout">Execution timeout for any commands needed to determine the existence of the database</param>
        /// <param name="storeItemCollection">The collection of all store items contained in the database 
        /// whose existence is determined by this method<</param>
        /// <returns>Whether the database indicated by the connection and the storeItemCollection exist</returns>
        public bool DatabaseExists(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection)
        {
            return DbDatabaseExists(connection, commandTimeout, storeItemCollection);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Db")]
        protected virtual bool DbDatabaseExists(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection)
        {
            throw EntityUtil.ProviderIncompatible(Strings.ProviderDoesNotSupportDatabaseExists);
        }


        /// <summary>
        /// Deletes all store objects specified in the store item collection from the database and the database itself.
        /// </summary>
        /// <param name="connection">Connection to an existing database that needs to be deleted</param>
        /// <param name="commandTimeout">Execution timeout for any commands needed to delete the database</param>
        /// <param name="storeItemCollection">The collection of all store items contained in the database that should be deleted<</param>
        public void DeleteDatabase(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection)
        {
            DbDeleteDatabase(connection, commandTimeout, storeItemCollection);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Db")]
        protected virtual void DbDeleteDatabase(DbConnection connection, int? commandTimeout, StoreItemCollection storeItemCollection)
        {
            throw EntityUtil.ProviderIncompatible(Strings.ProviderDoesNotSupportDeleteDatabase);
        }
    }

}
