using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using NHibernate.Engine;
using NHibernate.SqlCommand;
using NHibernate.SqlTypes;
using NHibernate.Util;
using Environment = NHibernate.Cfg.Environment;

namespace NHibernate.Driver
{
	/// <summary>
	/// Base class for the implementation of IDriver
	/// </summary>
	public abstract class DriverBase : IDriver, ISqlParameterFormatter
	{
		private static readonly IInternalLogger log = LoggerProvider.LoggerFor(typeof(DriverBase));

		private int commandTimeout;
		private bool prepareSql;

		public virtual void Configure(IDictionary<string, string> settings)
		{
			// Command timeout
			commandTimeout = PropertiesHelper.GetInt32(Environment.CommandTimeout, settings, -1);
			if (commandTimeout > -1 && log.IsInfoEnabled)
			{
				log.Info(string.Format("setting ADO.NET command timeout to {0} seconds", commandTimeout));
			}

			// Prepare SQL
			prepareSql = PropertiesHelper.GetBoolean(Environment.PrepareSql, settings, false);
			if (prepareSql && SupportsPreparingCommands)
			{
				log.Info("preparing SQL enabled");
			}
		}

		protected bool IsPrepareSqlEnabled
		{
			get { return prepareSql; }
		}

		public abstract DbConnection CreateConnection();
		public abstract DbCommand CreateCommand();

		/// <summary>
		/// Does this Driver require the use of a Named Prefix in the SQL statement.  
		/// </summary>
		/// <remarks>
		/// For example, SqlClient requires <c>select * from simple where simple_id = @simple_id</c>
		/// If this is false, like with the OleDb provider, then it is assumed that  
		/// the <c>?</c> can be a placeholder for the parameter in the SQL statement.
		/// </remarks>
		public abstract bool UseNamedPrefixInSql { get; }

		/// <summary>
		/// Does this Driver require the use of the Named Prefix when trying
		/// to reference the Parameter in the Command's Parameter collection.  
		/// </summary>
		/// <remarks>
		/// This is really only useful when the UseNamedPrefixInSql == true.  When this is true the
		/// code will look like:
		/// <code>DbParameter param = cmd.Parameters["@paramName"]</code>
		/// if this is false the code will be 
		/// <code>DbParameter param = cmd.Parameters["paramName"]</code>.
		/// </remarks>
		public abstract bool UseNamedPrefixInParameter { get; }

		/// <summary>
		/// The Named Prefix for parameters.  
		/// </summary>
		/// <remarks>
		/// Sql Server uses <c>"@"</c> and Oracle uses ":".
		/// </remarks>
		public abstract string NamedPrefix { get; }

		/// <summary>
		/// Change the parameterName into the correct format DbCommand.CommandText
		/// for the ConnectionProvider
		/// </summary>
		/// <param name="parameterName">The unformatted name of the parameter</param>
		/// <returns>A parameter formatted for an DbCommand.CommandText</returns>
		public string FormatNameForSql(string parameterName)
		{
			return UseNamedPrefixInSql ? (NamedPrefix + parameterName) : StringHelper.SqlParameter;
		}

		/// <summary>
		/// Changes the parameterName into the correct format for an DbParameter
		/// for the Driver.
		/// </summary>
		/// <remarks>
		/// For SqlServerConnectionProvider it will change <c>id</c> to @id
		/// </remarks>
		/// <param name="parameterName">The unformatted name of the parameter</param>
		/// <returns>A parameter formatted for an DbParameter.</returns>
		public string FormatNameForParameter(string parameterName)
		{
			return UseNamedPrefixInParameter ? (NamedPrefix + parameterName) : parameterName;
		}

		public virtual bool SupportsMultipleOpenReaders
		{
			get { return true; }
		}

		/// <summary>
		/// Does this Driver support DbCommand.Prepare().
		/// </summary>
		/// <remarks>
		/// <para>
		/// A value of <see langword="false" /> indicates that an exception would be thrown or the 
		/// company that produces the Driver we are wrapping does not recommend using
		/// DbCommand.Prepare().
		/// </para>
		/// <para>
		/// A value of <see langword="true" /> indicates that calling DbCommand.Prepare() will function
		/// fine on this Driver.
		/// </para>
		/// </remarks>
		protected virtual bool SupportsPreparingCommands
		{
			get { return true; }
		}

		public virtual DbCommand GenerateCommand(CommandType type, SqlString sqlString, SqlType[] parameterTypes)
		{
			var cmd = CreateCommand();
			cmd.CommandType = type;

			SetCommandTimeout(cmd);
			SetCommandText(cmd, sqlString);
			SetCommandParameters(cmd, parameterTypes);

			return cmd;
		}

		protected virtual void SetCommandTimeout(DbCommand cmd)
		{
			if (commandTimeout >= 0)
			{
				try
				{
					cmd.CommandTimeout = commandTimeout;
				}
				catch (Exception e)
				{
					if (log.IsWarnEnabled)
					{
						log.Warn(e.ToString());
					}
				}
			}
		}

		private static string ToParameterName(int index)
		{
			return "p" + index;
		}

		string ISqlParameterFormatter.GetParameterName(int index)
		{
			return FormatNameForSql(ToParameterName(index));
		}

		private void SetCommandText(DbCommand cmd, SqlString sqlString)
		{
			SqlStringFormatter formatter = GetSqlStringFormatter();
			formatter.Format(sqlString);
			cmd.CommandText = formatter.GetFormattedText();
		}

		protected virtual SqlStringFormatter GetSqlStringFormatter()
		{
			return new SqlStringFormatter(this, ";");
		}

		private void SetCommandParameters(DbCommand cmd, SqlType[] sqlTypes)
		{
			for (int i = 0; i < sqlTypes.Length; i++)
			{
				string paramName = ToParameterName(i);
				var dbParam = GenerateParameter(cmd, paramName, sqlTypes[i]);
				cmd.Parameters.Add(dbParam);
			}
		}

		protected virtual void InitializeParameter(DbParameter dbParam, string name, SqlType sqlType)
		{
			if (sqlType == null)
			{
				throw new QueryException(String.Format("No type assigned to parameter '{0}'", name));
			}

			dbParam.ParameterName = FormatNameForParameter(name);
			dbParam.DbType = sqlType.DbType;
		}

		/// <summary>
		/// Generates an DbParameter for the DbCommand.  It does not add the DbParameter to the DbCommand's
		/// Parameter collection.
		/// </summary>
		/// <param name="command">The DbCommand to use to create the DbParameter.</param>
		/// <param name="name">The name to set for DbParameter.Name</param>
		/// <param name="sqlType">The SqlType to set for DbParameter.</param>
		/// <returns>An DbParameter ready to be added to an DbCommand.</returns>
		public DbParameter GenerateParameter(DbCommand command, string name, SqlType sqlType)
		{
			var dbParam = command.CreateParameter();
			InitializeParameter(dbParam, name, sqlType);
			return dbParam;
		}

		public void RemoveUnusedCommandParameters(DbCommand cmd, SqlString sqlString)
		{
			if (!UseNamedPrefixInSql)
				return; // Applicable only to named parameters

			var formatter = GetSqlStringFormatter();
			formatter.Format(sqlString);
			var assignedParameterNames = new HashSet<string>(formatter.AssignedParameterNames);

			cmd.Parameters
				.Cast<DbParameter>()
				.Select(p => p.ParameterName)
				.Where(p => !assignedParameterNames.Contains(UseNamedPrefixInParameter ? p : FormatNameForSql(p)))
				.ToList()
				.ForEach(unusedParameterName => cmd.Parameters.RemoveAt(unusedParameterName));
		}

		public virtual void ExpandQueryParameters(DbCommand cmd, SqlString sqlString, SqlType[] parameterTypes)
		{
			if (UseNamedPrefixInSql)
				return;  // named parameters are ok

			var expandedParameters = new List<DbParameter>();
			foreach (object part in sqlString)
			{
				var parameter = part as Parameter;
				if (parameter != null)
				{
					var index = parameter.ParameterPosition.Value;
					var originalParameter = cmd.Parameters[index];
					var originalType = parameterTypes[index];
					expandedParameters.Add(CloneParameter(cmd, originalParameter, originalType));
				}
			}

			cmd.Parameters.Clear();
			foreach (var parameter in expandedParameters)
				cmd.Parameters.Add(parameter);
		}

		public virtual IResultSetsCommand GetResultSetsCommand(ISessionImplementor session)
		{
			throw new NotSupportedException(string.Format("The driver {0} does not support multiple queries.", GetType().FullName));
		}

		public virtual bool SupportsMultipleQueries
		{
			get { return false; }
		}

		protected virtual DbParameter CloneParameter(DbCommand cmd, DbParameter originalParameter, SqlType originalType)
		{
			var clone = GenerateParameter(cmd, originalParameter.ParameterName, originalType);
			clone.Value = originalParameter.Value;
			return clone;
		}

		public void PrepareCommand(DbCommand command)
		{
			AdjustCommand(command);
			OnBeforePrepare(command);

			if (SupportsPreparingCommands && prepareSql)
			{
				command.Prepare();
			}
		}

		/// <summary>
		/// Override to make any adjustments to the DbCommand object.  (e.g., Oracle custom OUT parameter)
		/// Parameters have been bound by this point, so their order can be adjusted too.
		/// This is analogous to the RegisterResultSetOutParameter() function in Hibernate.
		/// </summary>
		protected virtual void OnBeforePrepare(DbCommand command)
		{
		}

		/// <summary>
		/// Override to make any adjustments to each DbCommand object before it added to the batcher.
		/// </summary>
		/// <param name="command">The command.</param>
		/// <remarks>
		/// This method is similar to the <see cref="OnBeforePrepare"/> but, instead be called just before execute the command (that can be a batch)
		/// is executed before add each single command to the batcher and before <see cref="OnBeforePrepare"/> .
		/// If you have to adjust parameters values/type (when the command is full filled) this is a good place where do it.
		/// </remarks>
		public virtual void AdjustCommand(DbCommand command)
		{
		}

		public DbParameter GenerateOutputParameter(DbCommand command)
		{
			var param = GenerateParameter(command, "ReturnValue", SqlTypeFactory.Int32);
			param.Direction = ParameterDirection.Output;
			return param;
		}

		public virtual bool RequiresTimeSpanForTime => false;

		public virtual bool SupportsSystemTransactions => true;

		public virtual bool SupportsNullEnlistment => true;

		/// <inheritdoc />
		public virtual bool SupportsEnlistmentWhenAutoEnlistmentIsDisabled => true;

		public virtual bool HasDelayedDistributedTransactionCompletion => false;

		/// <inheritdoc />
		public virtual DateTime MinDate => DateTime.MinValue;
	}
}