using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace BLToolkit.Data.Linq.Builder
{
	using BLToolkit.Linq;
	using Data.Sql;
	using Mapping;
	using Reflection;
	using Reflection.Extension;

	class TableBuilder : ISequenceBuilder
	{
		#region TableBuilder

		int ISequenceBuilder.BuildCounter { get; set; }

		static T Find<T>(ExpressionBuilder builder, BuildInfo buildInfo, Func<int,IBuildContext,T> action)
		{
			var expression = buildInfo.Expression;

			switch (expression.NodeType)
			{
				case ExpressionType.Constant:
					{
						var c = (ConstantExpression)expression;
						if (c.Value is IQueryable)
							return action(1, null);

						break;
					}

				case ExpressionType.Call:
					{
						var mc = (MethodCallExpression)expression;

						if (mc.Method.Name == "GetTable")
							if (expression.Type.IsGenericType && expression.Type.GetGenericTypeDefinition() == typeof(Table<>))
								return action(2, null);

						var attr = builder.GetTableFunctionAttribute(mc.Method);

						if (attr != null)
							return action(5, null);

						break;
					}

				case ExpressionType.MemberAccess:

					if (expression.Type.IsGenericType && expression.Type.GetGenericTypeDefinition() == typeof(Table<>))
						return action(3, null);

					// Looking for association.
					//
					if (buildInfo.IsSubQuery && buildInfo.SqlQuery.From.Tables.Count == 0)
					{
						var ctx = builder.GetContext(buildInfo.Parent, expression);
						if (ctx != null)
							return action(4, ctx);
					}

					break;

				case ExpressionType.Parameter:
					{
						if (buildInfo.IsSubQuery && buildInfo.SqlQuery.From.Tables.Count == 0)
						{
							var ctx = builder.GetContext(buildInfo.Parent, expression);
							if (ctx != null)
								return action(4, ctx);
						}

						break;
					}
			}

			return action(0, null);
		}

		public bool CanBuild(ExpressionBuilder builder, BuildInfo buildInfo)
		{
			return Find(builder, buildInfo, (n,_) => n > 0);
		}

		public IBuildContext BuildSequence(ExpressionBuilder builder, BuildInfo buildInfo)
		{
			return Find(builder, buildInfo, (n,ctx) =>
			{
				switch (n)
				{
					case 0 : return null;
					case 1 : return new TableContext(builder, buildInfo, ((IQueryable)((ConstantExpression)buildInfo.Expression).Value).ElementType);
					case 2 :
					case 3 : return new TableContext(builder, buildInfo, buildInfo.Expression.Type.GetGenericArguments()[0]);
					case 4 : return ctx.GetContext(buildInfo.Expression, 0, buildInfo);
					case 5 : return new TableContext(builder, buildInfo);
				}

				throw new InvalidOperationException();
			});
		}

		public SequenceConvertInfo Convert(ExpressionBuilder builder, BuildInfo buildInfo, ParameterExpression param)
		{
			return null;
		}

		public bool IsSequence(ExpressionBuilder builder, BuildInfo buildInfo)
		{
			return true;
		}

		#endregion

		#region TableContext

		public class TableContext : IBuildContext
		{
			#region Properties

#if DEBUG
			public string _sqlQueryText { get { return SqlQuery == null ? "" : SqlQuery.SqlText; } }
#endif

			public ExpressionBuilder Builder    { get; private set; }
			public Expression        Expression { get; private set; }
			public SqlQuery          SqlQuery   { get; set; }

			public virtual IBuildContext Parent { get; set; }

			public Type         OriginalType;
			public Type         ObjectType;
			public ObjectMapper ObjectMapper;
			public SqlTable     SqlTable;

			#endregion

			#region Init

			public TableContext(ExpressionBuilder builder, BuildInfo buildInfo, Type originalType)
			{
				Builder      = builder;
				Parent       = buildInfo.Parent;
				Expression   = buildInfo.Expression;
				SqlQuery     = buildInfo.SqlQuery;

				OriginalType = originalType;
				ObjectType   = GetObjectType();
				SqlTable     = new SqlTable(builder.MappingSchema, ObjectType);
				ObjectMapper = Builder.MappingSchema.GetObjectMapper(ObjectType);

				SqlQuery.From.Table(SqlTable);

				Init();
			}

			protected TableContext(ExpressionBuilder builder, SqlQuery sqlQuery)
			{
				Builder  = builder;
				SqlQuery = sqlQuery;
			}

			public TableContext(ExpressionBuilder builder, BuildInfo buildInfo)
			{
				Builder    = builder;
				Parent     = buildInfo.Parent;
				Expression = buildInfo.Expression;
				SqlQuery   = buildInfo.SqlQuery;

				var mc   = (MethodCallExpression)Expression;
				var attr = builder.GetTableFunctionAttribute(mc.Method);

				if (!mc.Method.ReturnType.IsGenericType || mc.Method.ReturnType.GetGenericTypeDefinition() != typeof(Table<>))
					throw new LinqException("Table function has to return Table<T>.");

				OriginalType = mc.Method.ReturnType.GetGenericArguments()[0];
				ObjectType   = GetObjectType();
				SqlTable     = new SqlTable(builder.MappingSchema, ObjectType);
				ObjectMapper = Builder.MappingSchema.GetObjectMapper(ObjectType);

				SqlQuery.From.Table(SqlTable);

				var args = mc.Arguments.Select(a => builder.ConvertToSql(this, a, false));

				attr.SetTable(SqlTable, mc.Method, mc.Arguments, args);

				Init();
			}

			protected Type GetObjectType()
			{
				for (var type = OriginalType.BaseType; type != null && type != typeof(object); type = type.BaseType)
				{
					var extension = TypeExtension.GetTypeExtension(type, Builder.MappingSchema.Extensions);
					var mapping   = Builder.MappingSchema.MetadataProvider.GetInheritanceMapping(type, extension);

					if (mapping.Length > 0)
						return type;
				}

				return OriginalType;
			}

			public List<InheritanceMappingAttribute> InheritanceMapping;
			public List<string>                      InheritanceDiscriminators;

			protected void Init()
			{
				Builder.Contexts.Add(this);

				InheritanceMapping = ObjectMapper.InheritanceMapping;

				if (InheritanceMapping.Count > 0)
					InheritanceDiscriminators = GetInheritanceDiscriminators(Builder, SqlTable, ObjectType, InheritanceMapping);

				// Original table is a parent.
				//
				if (ObjectType != OriginalType)
				{
					var predicate = Builder.MakeIsPredicate(this, OriginalType);

					if (predicate.GetType() != typeof(SqlQuery.Predicate.Expr))
						SqlQuery.Where.SearchCondition.Conditions.Add(new SqlQuery.Condition(false, predicate));
				}
			}

			internal static List<string> GetInheritanceDiscriminators(
				ExpressionBuilder                 builder,
				SqlTable                          sqlTable,
				Type                              objectType,
				List<InheritanceMappingAttribute> inheritanceMapping)
			{
				var inheritanceDiscriminators = new List<string>(inheritanceMapping.Count);

				foreach (var mapping in inheritanceMapping)
				{
					string discriminator = null;

					foreach (MemberMapper mm in builder.MappingSchema.GetObjectMapper(mapping.Type))
					{
						if (mm.MapMemberInfo.SqlIgnore == false && !sqlTable.Fields.Any(f => f.Value.Name == mm.MemberName))
						{
							var field = new SqlField(mm.Type, mm.MemberName, mm.Name, mm.MapMemberInfo.Nullable, int.MinValue, null, mm);
							sqlTable.Fields.Add(field);
						}

						if (mm.MapMemberInfo.IsInheritanceDiscriminator)
							discriminator = mm.MapMemberInfo.MemberName;
					}

					inheritanceDiscriminators.Add(discriminator);
				}

				var dname = inheritanceDiscriminators.FirstOrDefault(s => s != null);

				if (dname == null)
					throw new LinqException("Inheritance Discriminator is not defined for the '{0}' hierarchy.", objectType);

				for (var i = 0; i < inheritanceDiscriminators.Count; i++)
					if (inheritanceDiscriminators[i] == null)
						inheritanceDiscriminators[i] = dname;

				return inheritanceDiscriminators;
			}

			#endregion

			#region BuildQuery

			class MappingData
			{
				public MappingSchema  MappingSchema;
				public ObjectMapper   ObjectMapper;
				public int[]          Index;
				public IValueMapper[] ValueMappers;
			}

			static object MapDataReaderToObject1(IDataReader dataReader, MappingData data)
			{
				var source     = data.MappingSchema.CreateDataReaderMapper(dataReader);
				var destObject = data.ObjectMapper.CreateInstance();

				if (data.ValueMappers == null)
				{
					var mappers = new IValueMapper[data.Index.Length];

					for (var i = 0; i < data.Index.Length; i++)
					{
						var n = data.Index[i];

						if (n < 0)
							continue;

						if (!data.ObjectMapper.SupportsTypedValues(i))
						{
							mappers[i] = data.MappingSchema.DefaultValueMapper;
							continue;
						}

						var sourceType = source.           GetFieldType(n) ?? typeof(object);
						var destType   = data.ObjectMapper.GetFieldType(i) ?? typeof(object);

						IValueMapper t;

						if (sourceType == destType)
						{
							lock (data.MappingSchema.SameTypeMappers)
								if (!data.MappingSchema.SameTypeMappers.TryGetValue(sourceType, out t))
									data.MappingSchema.SameTypeMappers.Add(sourceType, t = data.MappingSchema.GetValueMapper(sourceType, destType));
						}
						else
						{
							var key = new KeyValuePair<Type,Type>(sourceType, destType);

							lock (data.MappingSchema.DifferentTypeMappers)
								if (!data.MappingSchema.DifferentTypeMappers.TryGetValue(key, out t))
									data.MappingSchema.DifferentTypeMappers.Add(key, t = data.MappingSchema.GetValueMapper(sourceType, destType));
						}

						mappers[i] = t;
					}

					data.ValueMappers = mappers;
				}

				var dest = data.ObjectMapper;
				var idx  = data.Index;
				var ms   = data.ValueMappers;

				for (var i = 0; i < idx.Length; i++)
				{
					var n = idx[i];

					if (n >= 0)
						ms[i].Map(source, dataReader, n, dest, destObject, i);
				}

				return destObject;
			}

			static object MapDataReaderToObject2(IDataReader dataReader, MappingData data)
			{
				var source = data.MappingSchema.CreateDataReaderMapper(dataReader);

				var initContext = new InitContext
				{
					MappingSchema = data.MappingSchema,
					DataSource    = source,
					SourceObject  = dataReader,
					ObjectMapper  = data.ObjectMapper,
                    MappingIndex  = data.Index
				};

				var destObject = data.ObjectMapper.CreateInstance(initContext);

				if (initContext.StopMapping)
					return destObject;

				var smDest = destObject as ISupportMapping;

				if (smDest != null)
				{
					smDest.BeginMapping(initContext);

					if (initContext.StopMapping)
						return destObject;
				}

				if (data.ValueMappers == null)
				{
					var mappers = new IValueMapper[data.Index.Length];

					for (var i = 0; i < data.Index.Length; i++)
					{
						var n = data.Index[i];

						if (n < 0)
							continue;

						if (!data.ObjectMapper.SupportsTypedValues(i))
						{
							mappers[i] = data.MappingSchema.DefaultValueMapper;
							continue;
						}

						var sourceType = source.           GetFieldType(n) ?? typeof(object);
						var destType   = data.ObjectMapper.GetFieldType(i) ?? typeof(object);

						IValueMapper t;

						if (sourceType == destType)
						{
							lock (data.MappingSchema.SameTypeMappers)
								if (!data.MappingSchema.SameTypeMappers.TryGetValue(sourceType, out t))
									data.MappingSchema.SameTypeMappers.Add(sourceType, t = data.MappingSchema.GetValueMapper(sourceType, destType));
						}
						else
						{
							var key = new KeyValuePair<Type,Type>(sourceType, destType);

							lock (data.MappingSchema.DifferentTypeMappers)
								if (!data.MappingSchema.DifferentTypeMappers.TryGetValue(key, out t))
									data.MappingSchema.DifferentTypeMappers.Add(key, t = data.MappingSchema.GetValueMapper(sourceType, destType));
						}

						mappers[i] = t;
					}

					data.ValueMappers = mappers;
				}

				var dest = data.ObjectMapper;
				var idx  = data.Index;
				var ms   = data.ValueMappers;

				for (var i = 0; i < idx.Length; i++)
				{
					var n = idx[i];

					if (n >= 0)
						ms[i].Map(source, dataReader, n, dest, destObject, i);
				}

				if (smDest != null)
					smDest.EndMapping(initContext);

				return destObject;
			}

			static object DefaultInheritanceMappingException(object value, Type type)
			{
				throw new LinqException("Inheritance mapping is not defined for discriminator value '{0}' in the '{1}' hierarchy.", value, type);
			}

			static readonly MethodInfo _mapperMethod1 = ReflectionHelper.Expressor<object>.MethodExpressor(_ => MapDataReaderToObject1(null, null));
			static readonly MethodInfo _mapperMethod2 = ReflectionHelper.Expressor<object>.MethodExpressor(_ => MapDataReaderToObject2(null, null));

#if FW4 || SILVERLIGHT
			ParameterExpression _variable;
			static int _varIndex;
#endif

			Expression BuildTableExpression(bool buildBlock, Type objectType, int[] index)
			{
#if FW4 || SILVERLIGHT
				if (buildBlock && _variable != null)
					return _variable;
#endif

				var data = new MappingData
				{
					MappingSchema = Builder.MappingSchema,
					ObjectMapper  = Builder.MappingSchema.GetObjectMapper(objectType),
					Index         = index
				};

				Expression expr;

				if (Builder.DataContextInfo.DataContext == null ||
					TypeHelper.IsSameOrParent(typeof(ISupportMapping), objectType) ||
					TypeHelper.GetFirstAttribute(objectType, typeof(ObjectFactoryAttribute)) != null)
				{
					expr = Expression.Convert(
						Expression.Call(null, _mapperMethod2,
							ExpressionBuilder.DataReaderParam,
							Expression.Constant(data)),
						objectType);
				}
				else
				{
					expr = Expression.Convert(
						Expression.Call(null, _mapperMethod1,
							ExpressionBuilder.DataReaderParam,
							Expression.Constant(data)),
						objectType);
				}

				expr = ProcessExpression(expr);

#if FW4 || SILVERLIGHT

				if (!buildBlock)
					return expr;

				Builder.BlockVariables.  Add(_variable = Expression.Variable(expr.Type, expr.Type.Name + _varIndex++));
				Builder.BlockExpressions.Add(Expression.Assign(_variable, expr));

				return _variable;

#else
				return expr;
#endif
			}

			protected virtual Expression ProcessExpression(Expression expression)
			{
				return expression;
			}

			int[] BuildIndex(int[] index, Type objectType)
			{
				var names = new Dictionary<string,int>();
				var n     = 0;

				foreach (MemberMapper mm in Builder.MappingSchema.GetObjectMapper(objectType))
					if (mm.MapMemberInfo.SqlIgnore == false)
						names.Add(mm.MemberName, n++);

				var q =
					from r in SqlTable.Fields.Values.Select((f,i) => new { f, i })
					where names.ContainsKey(r.f.Name)
					orderby names[r.f.Name]
					select index[r.i];

				return q.ToArray();
			}

			Expression BuildQuery(Type tableType)
			{
				SqlInfo[] info;

				if (ObjectType == tableType)
				{
					info = ConvertToIndex(null, 0, ConvertFlags.All);
				}
				else
				{
					info = ConvertToSql(null, 0, ConvertFlags.All);

					var table = new SqlTable(Builder.MappingSchema, tableType);
					var q     =
						from fld1 in table.Fields.Values.Select((f,i) => new { f, i })
						join fld2 in info on fld1.f.Name equals ((SqlField)fld2.Sql).Name
						orderby fld1.i
						select GetIndex(fld2);

					info = q.ToArray();
				}

				var index = info.Select(idx => ConvertToParentIndex(idx.Index, null)).ToArray();

				if (ObjectType != tableType || InheritanceMapping.Count == 0)
					return BuildTableExpression(!Builder.IsBlockDisable, tableType, index);

				Expression expr;

				var defaultMapping = InheritanceMapping.SingleOrDefault(m => m.IsDefault);

				if (defaultMapping != null)
				{
					expr = Expression.Convert(
						BuildTableExpression(false, defaultMapping.Type, BuildIndex(index, defaultMapping.Type)),
						ObjectType);
				}
				else
				{
					var exceptionMethod = ReflectionHelper.Expressor<object>.MethodExpressor(_ => DefaultInheritanceMappingException(null, null));
					var dindex          =
						(
							from f in SqlTable.Fields.Values
							where f.Name == InheritanceDiscriminators[0]
							select ConvertToParentIndex(_indexes[f].Index, null)
						).First();

					expr = Expression.Convert(
						Expression.Call(null, exceptionMethod,
							Expression.Call(
								ExpressionBuilder.DataReaderParam,
								ReflectionHelper.DataReader.GetValue,
								Expression.Constant(dindex)),
							Expression.Constant(ObjectType)),
						ObjectType);
				}

				foreach (var mapping in InheritanceMapping.Select((m,i) => new { m, i }).Where(m => m.m != defaultMapping))
				{
					var dindex =
						(
							from f in SqlTable.Fields.Values
							where f.Name == InheritanceDiscriminators[mapping.i]
							select ConvertToParentIndex(_indexes[f].Index, null)
						).First();

					Expression testExpr;

					if (mapping.m.Code == null)
					{
						testExpr = Expression.Call(
							ExpressionBuilder.DataReaderParam,
							ReflectionHelper.DataReader.IsDBNull,
							Expression.Constant(dindex));
					}
					else
					{
						var codeType = mapping.m.Code.GetType();

						testExpr = Expression.Equal(
							Expression.Constant(mapping.m.Code),
							Builder.BuildSql(codeType, dindex));
					}

					expr = Expression.Condition(
						testExpr,
						Expression.Convert(BuildTableExpression(false, mapping.m.Type, BuildIndex(index, mapping.m.Type)), ObjectType),
						expr);
				}

				return expr;
			}

			public void BuildQuery<T>(Query<T> query, ParameterExpression queryParameter)
			{
				var expr   = BuildQuery(typeof(T));
				var mapper = Builder.BuildMapper<T>(expr);

				query.SetQuery(mapper.Compile());
			}

			#endregion

			#region BuildExpression

			public Expression BuildExpression(Expression expression, int level)
			{
				// Build table.
				//
				var table = FindTable(expression, level, false);

				if (table == null)
				{
					if (expression is MemberExpression)
					{
						var memberExpression = (MemberExpression)expression;

						if (ObjectMapper != null &&
							ObjectMapper.TypeAccessor.OriginalType == memberExpression.Member.DeclaringType)
						{
							throw new LinqException("Member '{0}.{1}' is not a table column.",
								memberExpression.Member.Name, memberExpression.Member.Name);
						}
					}

					throw new InvalidOperationException();
				}

				if (table.Field == null)
					return table.Table.BuildQuery(table.Table.OriginalType);

				// Build field.
				//
				var info = ConvertToIndex(expression, level, ConvertFlags.Field).Single();
				var idx  = ConvertToParentIndex(info.Index, null);

				if (expression is MemberExpression)
				{
					var me = (MemberExpression)expression;
					var memberAccessor = TypeAccessor.GetAccessor(me.Member.DeclaringType)[me.Member.Name];
					return Builder.BuildSql(memberAccessor, idx);
				}
				else
				{
					return Builder.BuildSql(expression.Type, idx);
				}
			}

			#endregion

			#region ConvertToSql

			public SqlInfo[] ConvertToSql(Expression expression, int level, ConvertFlags flags)
			{
				switch (flags)
				{
					case ConvertFlags.All   :
						{
							var table = FindTable(expression, level, false);

							if (table.Field == null)
								return table.Table.SqlTable.Fields.Values
									.Select(_ => new SqlInfo(_.MemberMapper.MemberAccessor.MemberInfo) { Sql = _ })
									.ToArray();

							break;
						}

					case ConvertFlags.Key   :
						{
							var table = FindTable(expression, level, false);

							if (table.Field == null)
							{
								var q =
									from f in table.Table.SqlTable.Fields.Values
									where f.IsPrimaryKey
									orderby f.PrimaryKeyOrder
									select new SqlInfo(f.MemberMapper.MemberAccessor.MemberInfo) { Sql = f };

								var key = q.ToArray();

								return key.Length != 0 ? key : ConvertToSql(expression, level, ConvertFlags.All);
							}

							break;
						}

					case ConvertFlags.Field :
						{
							var table = FindTable(expression, level, true);

							if (table.Field != null)
								return new[]
								{
									new SqlInfo(table.Field.MemberMapper.MemberAccessor.MemberInfo) { Sql = table.Field }
								};

							break;
						}
				}

				throw new InvalidOperationException();
			}

			#endregion

			#region ConvertToIndex

			readonly Dictionary<ISqlExpression,SqlInfo> _indexes = new Dictionary<ISqlExpression,SqlInfo>();

			protected SqlInfo GetIndex(SqlInfo expr)
			{
				SqlInfo n;

				if (_indexes.TryGetValue(expr.Sql, out n))
					return n;

				if (expr.Sql is SqlField)
				{
					var field = (SqlField)expr.Sql;
					expr.Index = SqlQuery.Select.Add(field, field.Alias);
				}
				else
				{
					expr.Index = SqlQuery.Select.Add(expr.Sql);
				}

				expr.Query = SqlQuery;

				_indexes.Add(expr.Sql, expr);

				return expr;
			}

			public SqlInfo[] ConvertToIndex(Expression expression, int level, ConvertFlags flags)
			{
				switch (flags)
				{
					case ConvertFlags.Field :
					case ConvertFlags.Key   :
					case ConvertFlags.All   :

						var info = ConvertToSql(expression, level, flags);

						for (var i = 0; i < info.Length; i++)
							info[i] = GetIndex(info[i]);

						return info;
				}

				throw new InvalidOperationException();
			}

			#endregion

			#region IsExpression

			public IsExpressionResult IsExpression(Expression expression, int level, RequestFor requestFor)
			{
				switch (requestFor)
				{
					case RequestFor.Field      :
						{
							var table = FindTable(expression, level, false);
							return new IsExpressionResult(table != null && table.Field != null);
						}

					case RequestFor.Table       :
					case RequestFor.Object      :
						{
							var table   = FindTable(expression, level, false);
							var isTable =
								table       != null &&
								table.Field == null &&
								(expression == null || expression.GetLevelExpression(table.Level) == expression);

							return new IsExpressionResult(isTable, isTable ? table.Table : null);
						}

					case RequestFor.Expression :
						{
							if (expression == null)
								return IsExpressionResult.False;

							var levelExpression = expression.GetLevelExpression(level);

							switch (levelExpression.NodeType)
							{
								case ExpressionType.MemberAccess :
								case ExpressionType.Parameter    :
								case ExpressionType.Call         :

									var table = FindTable(expression, level, false);
									return new IsExpressionResult(table == null);
							}

							return IsExpressionResult.True;
						}

					case RequestFor.Association      :
						{
							if (ObjectMapper.Associations.Count > 0)
							{
								var table = FindTable(expression, level, false);
								var isat  =
									table       != null &&
									table.Table is AssociatedTableContext &&
									table.Field == null &&
									(expression == null || expression.GetLevelExpression(table.Level) == expression);

								return new IsExpressionResult(isat, isat ? table.Table : null);
							}

							return IsExpressionResult.False;
						}
				}

				return IsExpressionResult.False;
			}

			#endregion

			#region GetContext

			interface IAssociationHelper
			{
				Expression GetExpression(Expression parent, AssociatedTableContext association);
			}

			class AssociationHelper<T> : IAssociationHelper
				where T : class
			{
				public Expression GetExpression(Expression parent, AssociatedTableContext association)
				{
					Expression expr  = null;
					var        param = Expression.Parameter(typeof(T), "c");

					foreach (var cond in (association).ParentAssociationJoin.Condition.Conditions)
					{
						var p  = (SqlQuery.Predicate.ExprExpr)cond.Predicate;
						var e1 = Expression.MakeMemberAccess(parent, ((SqlField)p.Expr1).MemberMapper.MemberAccessor.MemberInfo);
						var e2 = Expression.MakeMemberAccess(param,  ((SqlField)p.Expr2).MemberMapper.MemberAccessor.MemberInfo) as Expression;

						while (e1.Type != e2.Type)
						{
							if (TypeHelper.IsNullableType(e1.Type))
							{
								e1 = Expression.PropertyOrField(e1, "Value");
								continue;
							}

							if (TypeHelper.IsNullableType(e2.Type))
							{
								e2 = Expression.PropertyOrField(e2, "Value");
								continue;
							}

							e2 = Expression.Convert(e2, e1.Type);
						}

						var ex = Expression.Equal(e1, e2);
							
						expr = expr == null ? ex : Expression.AndAlso(expr, ex);
					}

					var predicate = Expression.Lambda<Func<T,bool>>(expr, param);

					return association.Builder.DataContextInfo.DataContext.GetTable<T>().Where(predicate).Expression;
				}
			}

			public IBuildContext GetContext(Expression expression, int level, BuildInfo buildInfo)
			{
				if (expression == null)
				{
					if (buildInfo != null && buildInfo.IsSubQuery)
					{
						var table = new TableContext(
							Builder,
							new BuildInfo(Parent is SelectManyBuilder.SelectManyContext ? this : Parent, Expression, buildInfo.SqlQuery),
							SqlTable.ObjectType);

						return table;
					}

					return this;
				}

				if (ObjectMapper.Associations.Count > 0)
				{
					var levelExpression = expression.GetLevelExpression(level);

					if (buildInfo != null && buildInfo.IsSubQuery)
					{
						if (levelExpression == expression && expression.NodeType == ExpressionType.MemberAccess)
						{
							var tableLevel  = GetAssociation(expression, level);
							var association = (AssociatedTableContext)tableLevel.Table;

							if (association.IsList)
							{
								var ma     = (MemberExpression)buildInfo.Expression;
								var atype  = typeof(AssociationHelper<>).MakeGenericType(association.ObjectType);
								var helper = (IAssociationHelper)Activator.CreateInstance(atype);
								var expr   = helper.GetExpression(ma.Expression, association);

								buildInfo.IsAssociationBuilt = true;

								if (tableLevel.IsNew || buildInfo.CopyTable)
									association.ParentAssociationJoin.IsWeak = true;

								return Builder.BuildSequence(new BuildInfo(buildInfo, expr));
							}
						}
						else
						{
							var association = GetAssociation(levelExpression, level);
							var paj         = ((AssociatedTableContext)association.Table).ParentAssociationJoin;

							paj.IsWeak = paj.IsWeak && buildInfo.CopyTable;

							return association.Table.GetContext(expression, level + 1, buildInfo);
						}
					}
				}

				throw new InvalidOperationException();
			}

			#endregion

			#region ConvertToParentIndex

			public int ConvertToParentIndex(int index, IBuildContext context)
			{
				return Parent == null ? index : Parent.ConvertToParentIndex(index, this);
			}

			#endregion

			#region SetAlias

			public void SetAlias(string alias)
			{
				if (alias.Contains('<'))
					return;

				if (SqlTable.Alias == null)
					SqlTable.Alias = alias;
			}

			#endregion

			#region GetSubQuery

			public ISqlExpression GetSubQuery(IBuildContext context)
			{
				return null;
			}

			#endregion

			#region Helpers

			SqlField GetField(Expression expression, int level, bool throwException)
			{
				if (expression.NodeType == ExpressionType.MemberAccess)
				{
					var memberExpression = (MemberExpression)expression;
					var levelExpression  = expression.GetLevelExpression(level);

					if (levelExpression.NodeType == ExpressionType.MemberAccess)
					{
						if (levelExpression != expression)
						{
							var levelMember = (MemberExpression)levelExpression;

							if (TypeHelper.IsNullableValueMember(memberExpression.Member) && memberExpression.Expression == levelExpression)
								memberExpression = levelMember;
							else
							{
								var sameType =
									levelMember.Member.ReflectedType == SqlTable.ObjectType ||
									levelMember.Member.DeclaringType == SqlTable.ObjectType;

								if (!sameType)
								{
									var mi = SqlTable.ObjectType.GetMember(levelMember.Member.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
									sameType = mi.Any(_ => _.DeclaringType == levelMember.Member.DeclaringType);
								}

								if (sameType || InheritanceMapping.Count > 0)
								{
									foreach (var field in SqlTable.Fields.Values)
									{
										if (field.MemberMapper is MemberMapper.ComplexMapper)
										{
											var name = levelMember.Member.Name;

											for (var ex = (MemberExpression)expression; ex != levelMember; ex = (MemberExpression)ex.Expression)
												name += "." + ex.Member.Name;

											if (field.MemberMapper.MemberName == name)
												return field;
										}
									}
								}
							}
						}

						if (levelExpression == memberExpression)
						{
							foreach (var field in SqlTable.Fields.Values)
							{
								if (TypeHelper.Equals(field.MemberMapper.MapMemberInfo.MemberAccessor.MemberInfo, memberExpression.Member, SqlTable.ObjectType))
								{
									if (field.MemberMapper is MemberMapper.ComplexMapper &&
										field.MemberMapper.MemberName.IndexOf('.') > 0)
									{
										var name = memberExpression.Member.Name;
										var me   = memberExpression;

										if (!(me.Expression is MemberExpression))
											return null;

										while (me.Expression is MemberExpression)
										{
											me   = (MemberExpression)me.Expression;
											name = me.Member.Name + '.' + name;
										}

										return SqlTable.Fields.Values.FirstOrDefault(f => f.MemberMapper.MemberName == name);
									}

									return field;
								}

								if (InheritanceMapping.Count > 0 && field.Name == memberExpression.Member.Name)
									foreach (var mapping in InheritanceMapping)
										foreach (MemberMapper mm in Builder.MappingSchema.GetObjectMapper(mapping.Type))
											if (TypeHelper.Equals(mm.MapMemberInfo.MemberAccessor.MemberInfo, memberExpression.Member))
												return field;
							}

							if (throwException &&
								ObjectMapper != null &&
								ObjectMapper.TypeAccessor.OriginalType == memberExpression.Member.DeclaringType)
							{
								throw new LinqException("Member '{0}.{1}' is not a table column.",
									memberExpression.Member.DeclaringType.Name, memberExpression.Member.Name);
							}
						}
					}
				}

				return null;
			}

			[JetBrains.Annotations.NotNull]
			readonly Dictionary<MemberInfo,AssociatedTableContext> _associations =
				new Dictionary<MemberInfo,AssociatedTableContext>(new MemberInfoComparer());

			class TableLevel
			{
				public TableContext Table;
				public SqlField     Field;
				public int          Level;
				public bool         IsNew;
			}

			TableLevel FindTable(Expression expression, int level, bool throwException)
			{
				if (expression == null)
					return new TableLevel { Table = this };

				var levelExpression = expression.GetLevelExpression(level);

				switch (levelExpression.NodeType)
				{
					case ExpressionType.MemberAccess :
					case ExpressionType.Parameter    :
						{
							var field = GetField(expression, level, throwException);

							if (field != null || (level == 0 && levelExpression == expression))
								return new TableLevel { Table = this, Field = field, Level = level };

							return GetAssociation(expression, level);
						}
				}

				return null;
			}

			TableLevel GetAssociation(Expression expression, int level)
			{
				var objectMapper    = ObjectMapper;
				var levelExpression = expression.GetLevelExpression(level);
				var inheritance     =
					(
						from m in InheritanceMapping
						let om = Builder.MappingSchema.GetObjectMapper(m.Type)
						where om.Associations.Count > 0
						select om
					).ToList();

				if (objectMapper.Associations.Count > 0 || inheritance.Count > 0)
				{
					if (levelExpression.NodeType == ExpressionType.MemberAccess)
					{
						var memberExpression = (MemberExpression)levelExpression;
						var isNew = false;

						AssociatedTableContext tableAssociation;

						if (!_associations.TryGetValue(memberExpression.Member, out tableAssociation))
						{
							var q =
								from a in objectMapper.Associations.Concat(inheritance.SelectMany(om => om.Associations))
								where TypeHelper.Equals(a.MemberAccessor.MemberInfo, memberExpression.Member)
								select new AssociatedTableContext(Builder, this, a) { Parent = Parent };

							tableAssociation = q.FirstOrDefault();

							isNew = true;

							_associations.Add(memberExpression.Member, tableAssociation);
						}

						if (tableAssociation != null)
						{
							if (levelExpression == expression)
								return new TableLevel { Table = tableAssociation, Level = level, IsNew = isNew };

							var al = tableAssociation.GetAssociation(expression, level + 1);

							if (al != null)
								return al;

							var field = tableAssociation.GetField(expression, level + 1, false);

							return new TableLevel { Table = tableAssociation, Field = field, Level = field == null ? level : level + 1, IsNew = isNew };
						}
					}
				}

				return null;
			}

			#endregion
		}

		#endregion

		#region AssociatedTableContext

		public class AssociatedTableContext : TableContext
		{
			public readonly TableContext          ParentAssociation;
			public readonly SqlQuery.JoinedTable  ParentAssociationJoin;
			public readonly Association           Association;
			public readonly bool                  IsList;

			public override IBuildContext Parent
			{
				get { return ParentAssociation.Parent; }
				set { }
			}

			public AssociatedTableContext(ExpressionBuilder builder, TableContext parent, Association association)
				: base(builder, parent.SqlQuery)
			{
				var type = TypeHelper.GetMemberType(association.MemberAccessor.MemberInfo);
				var left = association.CanBeNull;

				if (TypeHelper.IsSameOrParent(typeof(IEnumerable), type))
				{
					var etypes = TypeHelper.GetGenericArguments(type, typeof(IEnumerable));
					type       = etypes != null && etypes.Length > 0 ? etypes[0] : TypeHelper.GetListItemType(type);
					IsList     = true;
				}

				OriginalType = type;
				ObjectType   = GetObjectType();
				ObjectMapper = Builder.MappingSchema.GetObjectMapper(ObjectType);
				SqlTable     = new SqlTable(builder.MappingSchema, ObjectType);

				var psrc = parent.SqlQuery.From[parent.SqlTable];
				var join = left ? SqlTable.WeakLeftJoin() : IsList ? SqlTable.InnerJoin() : SqlTable.WeakInnerJoin();

				Association           = association;
				ParentAssociation     = parent;
				ParentAssociationJoin = join.JoinedTable;

				psrc.Joins.Add(join.JoinedTable);

				for (var i = 0; i < association.ThisKey.Length; i++)
				{
					SqlField field1;
					SqlField field2;

					if (!parent.SqlTable.Fields.TryGetValue(association.ThisKey[i], out field1))
						throw new LinqException("Association key '{0}' not found for type '{1}.", association.ThisKey[i], parent.ObjectType);

					if (!SqlTable.Fields.TryGetValue(association.OtherKey[i], out field2))
						throw new LinqException("Association key '{0}' not found for type '{1}.", association.OtherKey[i], ObjectType);

					join.Field(field1).Equal.Field(field2);
				}

				Init();
			}

			protected override Expression ProcessExpression(Expression expression)
			{
				var isLeft = false;

				for (
					var association = this;
					isLeft == false && association != null;
					association = association.ParentAssociation as AssociatedTableContext)
				{
					isLeft =
						association.ParentAssociationJoin.JoinType == SqlQuery.JoinType.Left ||
						association.ParentAssociationJoin.JoinType == SqlQuery.JoinType.OuterApply;
				}

				if (isLeft)
				{
					Expression cond = null;

					var keys = ConvertToIndex(null, 0, ConvertFlags.Key);

					foreach (var key in keys)
					{
						var index2  = ConvertToParentIndex(key.Index, null);

						Expression e = Expression.Call(
							ExpressionBuilder.DataReaderParam,
							ReflectionHelper.DataReader.IsDBNull,
							Expression.Constant(index2));

						cond = cond == null ? e : Expression.AndAlso(cond, e);
					}

					expression = Expression.Condition(cond, Expression.Constant(null, expression.Type), expression);
				}

				return expression;
			}
		}

		#endregion
	}
}
