/***

 * FdpDataProvider
needed FirebirdClient http://sourceforge.net/project/showfiles.php?group_id=9028&package_id=62107
tested with FirebirdClient 2.1.0 Beta 3

Known troubles:
1) Some tests fails due to Fb SQL-syntax specific
2) ResultSet mapping doesn't work - not supported by client
3) UnitTests.CS.DataAccess.OutRefTest tests: Test2 && TestNullable2 doesnt work:
	parameters directions should be provided correctly to functions run, that's why
	output parameterd would be mapped to Entity e, so asserts should be same a in Test1.

"Features"
1) Type conversation due to http://www.firebirdsql.org/manual/migration-mssql-data-types.html
	BUT! for Binary types BLOB is used! not CHAR!
2) InOut parameters faking: InOut parameters are not suppotred by Fb, but they could be
	emulated: each InOut parameter should be defined in RETURNS() section, and allso has a mirror 
	in parameter section with name [prefix][inOutParameterName], see OutRefTest SP. Faking settings:
	FdpDataProvider.InOutInputParameterPrefix = "in_";
	FdpDataProvider.IsInOutParameterEmulation = true;
3) Returned values faking. Each parameter with "magic name" woul be treated as ReturnValue.
	see Scalar_ReturnParameter SP. Faking settings:
	FdpDataProvider.ReturnParameterName = "RETURN_VALUE";
	FdpDataProvider.IsReturnValueEmulation = true;

 */

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Data;
using System.Data.Common;
using System.Linq;

using BLToolkit.Data.Sql.SqlProvider;
using BLToolkit.Mapping;
using BLToolkit.Reflection;

using FirebirdSql.Data.FirebirdClient;

namespace BLToolkit.Data.DataProvider
{
	public class FdpDataProvider : DataProviderBase
	{
		public FdpDataProvider()
		{
			MappingSchema = new FbMappingSchema();
		}

		#region InOut & ReturnValue emulation
		public static string InOutInputParameterPrefix = "in_";
		public static string ReturnParameterName = "RETURN_VALUE";

		public static bool IsReturnValueEmulation = true;
		public static bool IsInOutParameterEmulation = true;

		public static bool QuoteIdentifiers
		{
			get { return FirebirdSqlProvider.QuoteIdentifiers; }
			set { FirebirdSqlProvider.QuoteIdentifiers = value; }
		}
		#endregion

		#region Overloads
		public override Type ConnectionType
		{
			get { return typeof (FbConnection); }
		}

		public override string Name
		{
			get { return DataProvider.ProviderName.Firebird; }
		}

		public override int MaxBatchSize
		{
			get { return 0; }
		}

		public override IDbConnection CreateConnectionObject()
		{
			return new FbConnection();
		}

		public override DbDataAdapter CreateDataAdapterObject()
		{
			return new FbDataAdapter();
		}

		public override bool DeriveParameters(IDbCommand command)
		{
			if (command is FbCommand)
			{
				FbCommandBuilder.DeriveParameters((FbCommand) command);

				if (IsReturnValueEmulation)
					foreach (IDbDataParameter par in command.Parameters)
						if (IsReturnValue(par))
							par.Direction = ParameterDirection.ReturnValue;

				return true;
			}

			return false;
		}

		public override object Convert(object value, ConvertType convertType)
		{
			switch (convertType)
			{
				case ConvertType.ExceptionToErrorNumber:
					if (value is FbException)
					{
						var ex = (FbException) value;
						if (ex.Errors.Count > 0)
							foreach (FbError error in ex.Errors)
								return error.Number;
					}

					break;
			}

			return SqlProvider.Convert(value, convertType);
		}

		public override ISqlProvider CreateSqlProvider()
		{
			return new FirebirdSqlProvider();
		}

		public override bool IsValueParameter(IDbDataParameter parameter)
		{
			return parameter.Direction != ParameterDirection.ReturnValue
			       && parameter.Direction != ParameterDirection.Output;
		}

		private string GetInputParameterName(string ioParameterName)
		{
			return (string) Convert(
				InOutInputParameterPrefix + (string) Convert(ioParameterName, ConvertType.SprocParameterToName),
				ConvertType.NameToSprocParameter);
		}

		private static IDbDataParameter GetParameter(string parameterName, IEnumerable<IDbDataParameter> commandParameters)
		{
			return commandParameters.FirstOrDefault(par => string.Compare(parameterName, par.ParameterName, true) == 0);
		}

		private bool IsReturnValue(IDbDataParameter parameter)
		{
			if (string.Compare(parameter.ParameterName,
			                   (string) Convert(ReturnParameterName, ConvertType.NameToSprocParameter), true) == 0
				)
				return true;

			return false;
		}

		public override void PrepareCommand(ref CommandType commandType, ref string commandText,
		                                    ref IDbDataParameter[] commandParameters)
		{
			if (commandParameters != null)
				foreach (var par in commandParameters)
				{
					if (par.Value is bool)
					{
						var value = (bool) par.Value ? "1" : "0";

						par.DbType = DbType.AnsiString;
						par.Value = value;
						par.Size = value.Length;
					}
					else if (par.Value is Guid)
					{
						var value = par.Value.ToString();

						par.DbType = DbType.AnsiStringFixedLength;
						par.Value = value;
						par.Size = value.Length;
					}

					#region "smart" input-output parameter detection
					if (commandType == CommandType.StoredProcedure && IsInOutParameterEmulation)
					{
						var iParameterName = GetInputParameterName(par.ParameterName);
						var fakeIOParameter = GetParameter(iParameterName, commandParameters);

						if (fakeIOParameter != null)
						{
							fakeIOParameter.Value = par.Value;

							// direction should be output, or parameter mistmath for procedure exception
							// would be thrown
							par.Direction = ParameterDirection.Output;

							// direction should be Input
							fakeIOParameter.Direction = ParameterDirection.Input;
						}
					}
					#endregion
				}

			base.PrepareCommand(ref commandType, ref commandText, ref commandParameters);
		}

		public override bool InitParameter(IDbDataParameter parameter)
		{
			if (parameter.Value is bool)
			{
				var value = (bool) parameter.Value ? "1" : "0";

				parameter.DbType = DbType.AnsiString;
				parameter.Value = value;
				parameter.Size = value.Length;
			}
			else if (parameter.Value is Guid)
			{
				var value = parameter.Value.ToString();

				parameter.DbType = DbType.AnsiStringFixedLength;
				parameter.Value = value;
				parameter.Size = value.Length;
			}

			return base.InitParameter(parameter);
		}

		public override void Configure(NameValueCollection attributes)
		{
			var inOutInputParameterPrefix = attributes["InOutInputParameterPrefix"];
			if (inOutInputParameterPrefix != null)
				InOutInputParameterPrefix = inOutInputParameterPrefix;

			var returnParameterName = attributes["ReturnParameterName"];
			if (returnParameterName != null)
				ReturnParameterName = returnParameterName;

			var isReturnValueEmulation = attributes["IsReturnValueEmulation"];
			if (isReturnValueEmulation != null)
				IsReturnValueEmulation = Common.Convert.ToBoolean(isReturnValueEmulation);

			var isInOutParameterEmulation = attributes["IsInOutParameterEmulation"];
			if (isInOutParameterEmulation != null)
				IsInOutParameterEmulation = Common.Convert.ToBoolean(isInOutParameterEmulation);

			var quoteIdentifiers = attributes["QuoteIdentifiers"];
			if (quoteIdentifiers != null)
				QuoteIdentifiers = Common.Convert.ToBoolean(quoteIdentifiers);

			base.Configure(attributes);
		}
		#endregion

		#region FbDataReaderEx
		public override IDataReader GetDataReader(MappingSchema schema, IDataReader dataReader)
		{
			return
				dataReader is FbDataReader
					? new FbDataReaderEx((FbDataReader) dataReader)
					: base.GetDataReader(schema, dataReader);
		}

		private class FbDataReaderEx : DataReaderBase<FbDataReader>, IDataReader
		{
			public FbDataReaderEx(FbDataReader rd) : base(rd)
			{
			}

			#region IDataReader Members
			public new object GetValue(int i)
			{
				var value = DataReader.GetValue(i);

				if (value is DateTime)
				{
					var dt = (DateTime) value;

					if (dt.Year == 1970 && dt.Month == 1 && dt.Day == 1)
						return new DateTime(1, 1, 1, dt.Hour, dt.Minute, dt.Second, dt.Millisecond);
				}

				return value;
			}

			public new DateTime GetDateTime(int i)
			{
				var dt = DataReader.GetDateTime(i);

				if (dt.Year == 1970 && dt.Month == 1 && dt.Day == 1)
					return new DateTime(1, 1, 1, dt.Hour, dt.Minute, dt.Second, dt.Millisecond);

				return dt;
			}
			#endregion
		}
		#endregion

		#region FbMappingSchema
		public class FbMappingSchema : FirebirdMappingSchema
		{
			protected override object MapInternal(InitContext initContext)
			{
				var dr = initContext.SourceObject as FbDataReader;

				// Fb's SP returns single row with nulls if selected object doesn't exists
				// so for all DBNull's (null) should be returned, instead of object instance
				//
				if (dr != null)
				{
					var i = dr.FieldCount;
					while (--i >= 0)
						if (!dr.IsDBNull(i))
							break;

					// All field are DBNull.
					//
					if (i < 0)
						return null;
				}
				return base.MapInternal(initContext);
			}
		}
		#endregion
	}
}