using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;

using BLToolkit.Aspects;
using BLToolkit.Data;
using BLToolkit.Data.DataProvider;
using BLToolkit.Mapping;
using BLToolkit.Reflection;
using BLToolkit.Reflection.Extension;

namespace BLToolkit.DataAccess
{
	public abstract class SqlQueryBase : DataAccessorBase
	{
		#region Constructors

		protected SqlQueryBase()
		{
		}

		protected SqlQueryBase(DbManager dbManager)
			: base(dbManager)
		{
		}

		protected SqlQueryBase(DbManager dbManager, bool dispose)
			: base(dbManager, dispose)
		{
		}

		#endregion

		#region Protected Members

		[NoInterception]
		protected virtual MemberMapper[] GetFieldList(ObjectMapper om)
		{
			var list = new List<MemberMapper>(om.Count);

			foreach (MemberMapper mm in om)
				if (mm.MapMemberInfo.SqlIgnore == false)
					list.Add(mm);

			return list.ToArray();
		}

		[NoInterception]
		protected virtual MemberMapper[] GetNonKeyFieldList(ObjectMapper om)
		{
			var typeExt = TypeExtension.GetTypeExtension(om.TypeAccessor.OriginalType, Extensions);
			var list    = new List<MemberMapper>();

			foreach (MemberMapper mm in om)
			{
				if (mm.MapMemberInfo.SqlIgnore)
					continue;

				var ma = mm.MapMemberInfo.MemberAccessor;

				bool isSet;
				MappingSchema.MetadataProvider.GetPrimaryKeyOrder(om.TypeAccessor.OriginalType, typeExt, ma, out isSet);

				if (!isSet)
					list.Add(mm);
			}

			return list.ToArray();
		}

		struct MemberOrder
		{
			public MemberOrder(MemberMapper memberMapper, int order)
			{
				MemberMapper = memberMapper;
				Order        = order;
			}

			public readonly MemberMapper MemberMapper;
			public readonly int          Order;
		}

		private static readonly Hashtable _keyList = new Hashtable();

		[NoInterception]
		protected internal virtual MemberMapper[] GetKeyFieldList(DbManager db, Type type)
		{
			var key    = type.FullName + "$" + db.DataProvider.UniqueName;
			var mmList = (MemberMapper[])_keyList[key];

			if (mmList == null)
			{
				var typeExt = TypeExtension.GetTypeExtension(type, Extensions);
				var list    = new List<MemberOrder>();

				foreach (MemberMapper mm in db.MappingSchema.GetObjectMapper(type))
				{
					if (mm.MapMemberInfo.SqlIgnore)
						continue;

					var ma = mm.MapMemberInfo.MemberAccessor;

					if (TypeHelper.IsScalar(ma.Type))
					{
						bool isSet;
						var order = MappingSchema.MetadataProvider.GetPrimaryKeyOrder(type, typeExt, ma, out isSet);

						if (isSet)
							list.Add(new MemberOrder(mm, order));
					}
				}

				list.Sort((x, y) => x.Order - y.Order);

				_keyList[key] = mmList = new MemberMapper[list.Count];

				for (var i = 0; i < list.Count; i++)
					mmList[i] = list[i].MemberMapper;
			}

			return mmList;
		}

		[NoInterception]
		protected virtual void AddWherePK(DbManager db, SqlQueryInfo query, StringBuilder sb, int nParameter)
		{
			sb.Append("WHERE\n");

			var memberMappers = GetKeyFieldList(db, query.ObjectType);

			if (memberMappers.Length == 0)
				throw new DataAccessException(
						string.Format("No primary key field(s) in the type '{0}'.", query.ObjectType.FullName));

			foreach (var mm in memberMappers)
			{
				var p = query.AddParameter(
					db.DataProvider.Convert(mm.Name + "_W", ConvertType.NameToQueryParameter).ToString(),
					mm.Name);

				sb.AppendFormat("\t{0} = ", db.DataProvider.Convert(p.FieldName, ConvertType.NameToQueryField));

				if (nParameter < 0)
					sb.AppendFormat("{0} AND\n", p.ParameterName);
				else
					sb.AppendFormat("{{{0}}} AND\n", nParameter++);
			}

			sb.Remove(sb.Length - 5, 5);
		}

		protected SqlQueryInfo CreateSelectByKeySqlText(DbManager db, Type type)
		{
			var om    = db.MappingSchema.GetObjectMapper(type);
			var sb    = new StringBuilder();
			var query = new SqlQueryInfo(om);

			sb.Append("SELECT\n");

			foreach (var mm in GetFieldList(om))
				sb.AppendFormat("\t{0},\n",
					db.DataProvider.Convert(mm.Name, ConvertType.NameToQueryField));

			sb.Remove(sb.Length - 2, 1);

			sb.Append("FROM\n\t");

			AppendTableName(sb, db, type);

			AddWherePK(db, query, sb, -1);

			query.QueryText = sb.ToString();

			return query;
		}

		// NOTE changed to virtual
		protected virtual void AppendTableName(StringBuilder sb, DbManager db, Type type)
		{
			var database = GetDatabaseName(type);
			var owner    = GetOwnerName   (type);
			var name     = GetTableName   (type);

			db.DataProvider.CreateSqlProvider().BuildTableName(sb,
				database == null ? null : db.DataProvider.Convert(database, ConvertType.NameToDatabase).  ToString(),
				owner    == null ? null : db.DataProvider.Convert(owner,    ConvertType.NameToOwner).     ToString(),
				name     == null ? null : db.DataProvider.Convert(name,     ConvertType.NameToQueryTable).ToString());

			sb.AppendLine();
		}

		protected SqlQueryInfo CreateSelectAllSqlText(DbManager db, Type type)
		{
			var om    = db.MappingSchema.GetObjectMapper(type);
			var sb    = new StringBuilder();
			var query = new SqlQueryInfo(om);

			sb.Append("SELECT\n");

			foreach (var mm in GetFieldList(om))
				sb.AppendFormat("\t{0},\n",
					db.DataProvider.Convert(mm.Name, ConvertType.NameToQueryField));

			sb.Remove(sb.Length - 2, 1);

			sb.Append("FROM\n\t");
			AppendTableName(sb, db, type);

			query.QueryText = sb.ToString();

			return query;
		}

		protected SqlQueryInfo CreateInsertSqlText(DbManager db, Type type, int nParameter)
		{
			var typeExt = TypeExtension.GetTypeExtension(type, Extensions);
			var om      = db.MappingSchema.GetObjectMapper(type);
			var list    = new List<MemberMapper>();
			var sb      = new StringBuilder();
			var query   = new SqlQueryInfo(om);
			var mp      = MappingSchema.MetadataProvider;

			sb.Append("INSERT INTO ");
			AppendTableName(sb, db, type);
			sb.Append(" (\n");

			foreach (var mm in GetFieldList(om))
			{
				// IT: This works incorrectly for complex mappers.
				//
				// [2009-03-24] ili: use mm.MemberAccessor instead of mm.ComplexMemberAccessor
				// as in CreateUpdateSqlText
				//

				bool isSet;
				var nonUpdatableAttribute = mp.GetNonUpdatableAttribute(type, typeExt, mm.MapMemberInfo.MemberAccessor, out isSet);

				if (nonUpdatableAttribute == null || !isSet || nonUpdatableAttribute.OnInsert == false)
				{
					sb.AppendFormat("\t{0},\n",
						db.DataProvider.Convert(mm.Name, ConvertType.NameToQueryField));
					list.Add(mm);
				}
			}

			sb.Remove(sb.Length - 2, 1);

			sb.Append(") VALUES (\n");

			foreach (var mm in list)
			{
				var p = query.AddParameter(
					db.DataProvider.Convert(mm.Name + "_P", ConvertType.NameToQueryParameter).ToString(),
					mm.Name);

				if (nParameter < 0)
					sb.AppendFormat("\t{0},\n", p.ParameterName);
					//sb.AppendFormat("\t{0},\n", db.DataProvider.Convert(p.ParameterName, ConvertType.NameToQueryParameter));
				else
					sb.AppendFormat("\t{{{0}}},\n", nParameter++);
			}

			sb.Remove(sb.Length - 2, 1);

			sb.Append(")");

			query.QueryText = sb.ToString();

			return query;
		}

		protected SqlQueryInfo CreateUpdateSqlText(DbManager db, Type type, int nParameter)
		{
			var typeExt = TypeExtension.GetTypeExtension(type, Extensions);
			var om      = db.MappingSchema.GetObjectMapper(type);
			var sb      = new StringBuilder();
			var query   = new SqlQueryInfo(om);
			var mp      = MappingSchema.MetadataProvider;

			sb.Append("UPDATE\n\t");
			AppendTableName(sb, db, type);
			sb.Append("\nSET\n");

			var fields    = GetFieldList(om);
			var hasFields = false;

			foreach (var mm in fields)
			{
				bool isSet;

				var nonUpdatableAttribute = mp.GetNonUpdatableAttribute(type, typeExt, mm.MapMemberInfo.MemberAccessor, out isSet);

				if (nonUpdatableAttribute != null && isSet && nonUpdatableAttribute.OnUpdate == true)
					continue;

				mp.GetPrimaryKeyOrder(type, typeExt, mm.MapMemberInfo.MemberAccessor, out isSet);

				if (isSet)
					continue;

				hasFields = true;

				var p = query.AddParameter(
					db.DataProvider.Convert(mm.Name + "_P", ConvertType.NameToQueryParameter).ToString(),
					mm.Name);

				sb.AppendFormat("\t{0} = ", db.DataProvider.Convert(p.FieldName, ConvertType.NameToQueryField));

				if (nParameter < 0)
					sb.AppendFormat("{0},\n", p.ParameterName);
				else
					sb.AppendFormat("\t{{{0}}},\n", nParameter++);
			}

			if (!hasFields)
				throw new DataAccessException(
						string.Format("There are no fields to update in the type '{0}'.", query.ObjectType.FullName));

			sb.Remove(sb.Length - 2, 1);

			AddWherePK(db, query, sb, nParameter);

			query.QueryText = sb.ToString();

			return query;
		}

		protected SqlQueryInfo CreateDeleteSqlText(DbManager db, Type type, int nParameter)
		{
			var om    = db.MappingSchema.GetObjectMapper(type);
			var sb    = new StringBuilder();
			var query = new SqlQueryInfo(om);

			sb.Append("DELETE FROM\n\t");
			AppendTableName(sb, db, type);
			sb.AppendLine();

			AddWherePK(db, query, sb, nParameter);

			query.QueryText = sb.ToString();

			return query;
		}

		[NoInterception]
		protected virtual SqlQueryInfo CreateSqlText(DbManager db, Type type, string actionName)
		{
			switch (actionName)
			{
				case "SelectByKey": return CreateSelectByKeySqlText(db, type);
				case "SelectAll":   return CreateSelectAllSqlText  (db, type);
				case "Insert":      return CreateInsertSqlText     (db, type, -1);
				case "InsertBatch": return CreateInsertSqlText     (db, type,  0);
				case "Update":      return CreateUpdateSqlText     (db, type, -1);
				case "UpdateBatch": return CreateUpdateSqlText     (db, type,  0);
				case "Delete":      return CreateDeleteSqlText     (db, type, -1);
				case "DeleteBatch": return CreateDeleteSqlText     (db, type,  0);
				default:
					throw new DataAccessException(
						string.Format("Unknown action '{0}'.", actionName));
			}
		}

		private static readonly Hashtable _actionSqlQueryInfo = new Hashtable();

		[NoInterception]
		public virtual SqlQueryInfo GetSqlQueryInfo(DbManager db, Type type, string actionName)
		{
			var key   = type.FullName + "$" + actionName + "$" + db.DataProvider.UniqueName + "$" + GetTableName(type);
			var query = (SqlQueryInfo)_actionSqlQueryInfo[key];

			if (query == null)
			{
				query = CreateSqlText(db, type, actionName);
				_actionSqlQueryInfo[key] = query;
			}

			return query;
		}

		#endregion
	}
}
