using System;
using System.Collections.Generic;
using System.Linq;
using NHibernate.Dialect.Function;
using NHibernate.Engine;
using NHibernate.Hql.Ast.ANTLR.Tree;
using NHibernate.Hql.Util;
using NHibernate.Persister.Collection;
using NHibernate.Persister.Entity;
using NHibernate.SqlCommand;
using NHibernate.Type;
using NHibernate.Util;
using IASTNode=NHibernate.Hql.Ast.ANTLR.Tree.IASTNode;
using IQueryable = NHibernate.Persister.Entity.IQueryable;

namespace NHibernate.Hql.Ast.ANTLR
{
	[CLSCompliant(false)]
	public class SessionFactoryHelperExtensions
	{
		private readonly ISessionFactoryImplementor _sfi;
		private readonly NullableDictionary<string, IPropertyMapping> _collectionPropertyMappingByRole;
		private readonly SessionFactoryHelper helper;

		/// <summary>
		/// Construct a new SessionFactoryHelperExtensions instance.
		/// </summary>
		/// <param name="sfi">The SessionFactory impl to be encapsulated.</param>
		public SessionFactoryHelperExtensions(ISessionFactoryImplementor sfi)
		{
			_sfi = sfi;
			helper = new SessionFactoryHelper(_sfi);
			_collectionPropertyMappingByRole = new NullableDictionary<string, IPropertyMapping>();
		}

		public ISessionFactoryImplementor Factory
		{
			get { return _sfi; }
		}

		/// <summary>
		/// Locate a registered sql function by name.
		/// </summary>
		/// <param name="functionName">The name of the function to locate</param>
		/// <returns>The sql function, or null if not found.</returns>
		public ISQLFunction FindSQLFunction(string functionName)
		{
			return _sfi.SQLFunctionRegistry.FindSQLFunction(functionName);
		}

		/// <summary>
		/// Locate a registered sql function by name.
		/// </summary>
		/// <param name="functionName">The name of the function to locate</param>
		/// <returns>The sql function, or throws QueryException if no matching sql functions could be found.</returns>
		private ISQLFunction RequireSQLFunction(string functionName)
		{
			ISQLFunction f = FindSQLFunction(functionName);

			if (f == null)
			{
				throw new QueryException("Unable to find SQL function: " + functionName);
			}
			return f;
		}

		/// <summary>
		/// Find the function return type given the function name and the first argument expression node.
		/// </summary>
		/// <param name="functionName">The function name.</param>
		/// <param name="first">The first argument expression.</param>
		/// <returns>the function return type given the function name and the first argument expression node.</returns>
		// Since v5.3
		[Obsolete("Please use overload with a IEnumerable<IASTNode> parameter instead.")]
		public IType FindFunctionReturnType(String functionName, IASTNode first)
		{
			// locate the registered function by the given name
			ISQLFunction sqlFunction = RequireSQLFunction(functionName);

			// determine the type of the first argument...
			IType argumentType = null;

			if (first != null)
			{
				if (sqlFunction is CastFunction)
				{
					argumentType = TypeFactory.HeuristicType(first.NextSibling.Text);
				}
				else if (first is SqlNode)
				{
					argumentType = ((SqlNode) first).DataType;
				}
			}

			return sqlFunction.ReturnType(argumentType, _sfi);
		}

		/// <summary>
		/// Find the function return type given the function name and the arguments expression nodes.
		/// </summary>
		/// <param name="functionName">The function name.</param>
		/// <param name="arguments">The function arguments expression nodes.</param>
		/// <returns>The function return type given the function name and the arguments expression nodes.</returns>
		public IType FindFunctionReturnType(string functionName, IEnumerable<IASTNode> arguments)
		{
			var sqlFunction = RequireSQLFunction(functionName);
			if (!(sqlFunction is ISQLFunctionExtended extendedSqlFunction))
			{
#pragma warning disable 618
				return FindFunctionReturnType(functionName, arguments.FirstOrDefault());
#pragma warning restore 618
			}

			var argumentTypes = new List<IType>();
			if (sqlFunction is CastFunction)
			{
				argumentTypes.Add(TypeFactory.HeuristicType(arguments.First().NextSibling.Text));
			}
			else
			{
				foreach (var argument in arguments)
				{
					IType type = null;
					if (argument is SqlNode sqlNode)
					{
						type = sqlNode.DataType;
					}

					argumentTypes.Add(type);
				}
			}

			return extendedSqlFunction.GetReturnType(argumentTypes, _sfi, true);
		}

		/// <summary>
		/// Given a (potentially unqualified) class name, locate its imported qualified name.
		/// </summary>
		/// <param name="className">The potentially unqualified class name</param>
		/// <returns>The qualified class name.</returns>
		public string GetImportedClassName(string className)
		{
			return _sfi.GetImportedClassName(className);
		}

		/// <summary>
		/// Does the given persister define a physical discriminator column
		/// for the purpose of inheritance discrimination?
		/// </summary>
		/// <param name="persister">The persister to be checked.</param>
		/// <returns>True if the persister does define an actual discriminator column.</returns>
		public bool HasPhysicalDiscriminatorColumn(IQueryable persister)
		{
			if (persister.DiscriminatorType != null)
			{
				string discrimColumnName = persister.DiscriminatorColumnName;
				// Needed the "clazz_" check to work around union-subclasses
				// TODO : is there a way to tell whether a persister is truly discrim-column based inheritence?
				if (discrimColumnName != null && "clazz_" != discrimColumnName)
				{
					return true;
				}
			}
			return false;
		}

		/// <summary>
		/// Locate the collection persister by the collection role.
		/// </summary>
		/// <param name="collectionFilterRole">The collection role name.</param>
		/// <returns>The defined CollectionPersister for this collection role, or null.</returns>
		public IQueryableCollection GetCollectionPersister(string collectionFilterRole)
		{
			try
			{
				return (IQueryableCollection)_sfi.GetCollectionPersister(collectionFilterRole);
			}
			catch (InvalidCastException cce)
			{
				throw new QueryException("collection is not queryable: " + collectionFilterRole, cce);
			}
			catch (Exception e)
			{
				throw new QueryException("collection not found: " + collectionFilterRole, e);
			}
		}

		/// <summary>
		/// Determine the name of the property for the entity encapsulated by the
		/// given type which represents the id or unique-key.
		/// </summary>
		/// <param name="entityType">The type representing the entity.</param>
		/// <returns>The corresponding property name</returns>
		public string GetIdentifierOrUniqueKeyPropertyName(EntityType entityType)
		{
			try
			{
				return entityType.GetIdentifierOrUniqueKeyPropertyName(_sfi);
			}
			catch (MappingException me)
			{
				throw new QueryException(me);
			}
		}

		/// <summary>
		/// Retrieves the column names corresponding to the collection elements for the given
		/// collection role.
		/// </summary>
		/// <param name="role">The collection role</param>
		/// <param name="roleAlias">The sql column-qualification alias (i.e., the table alias)</param>
		/// <returns>the collection element columns</returns>
		public string[] GetCollectionElementColumns(string role, string roleAlias)
		{
			// TODO - add CollectionPropertyNames.COLLECTION_ELEMENTS (involves core NH change)
			return GetCollectionPropertyMapping(role).ToColumns(roleAlias, "elements");
		}

		/// <summary>
		/// Essentially the same as GetElementType, but requiring that the
		/// element type be an association type.
		/// </summary>
		/// <param name="collectionType">The collection type to be checked.</param>
		/// <returns>The AssociationType of the elements of the collection.</returns>
		public IAssociationType GetElementAssociationType(CollectionType collectionType)
		{
			return (IAssociationType)GetElementType(collectionType);
		}

		/// <summary>
		/// Locate the collection persister by the collection role, requiring that
		/// such a persister exist.
		/// </summary>
		/// <param name="role">The collection role name.</param>
		/// <returns>The defined CollectionPersister for this collection role.</returns>
		public IQueryableCollection RequireQueryableCollection(string role)
		{
			try
			{
				IQueryableCollection queryableCollection = (IQueryableCollection)_sfi.GetCollectionPersister(role);
				if (queryableCollection != null)
				{
					_collectionPropertyMappingByRole.Add(role, new CollectionPropertyMapping(queryableCollection));
				}
				return queryableCollection;
			}
			catch (InvalidCastException cce)
			{
				throw new QueryException("collection role is not queryable: " + role, cce);
			}
			catch (Exception e)
			{
				throw new QueryException("collection role not found: " + role, e);
			}
		}

		/// <summary>
		/// Locate the persister by class or entity name, requiring that such a persister
		/// exist.
		/// </summary>
		/// <param name="name">The class or entity name</param>
		/// <returns>The defined persister for this entity</returns>
		public IEntityPersister RequireClassPersister(string name)
		{
			IEntityPersister cp;
			try 
			{
				cp = FindEntityPersisterByName( name );
				if ( cp == null ) 
				{
					throw new QuerySyntaxException( name + " is not mapped" );
				}
			}
			catch ( MappingException e ) 
			{
				throw new QueryException( e.Message, e );
			}
			return cp;
		}

		/// <summary>
		/// Given a (potentially unqualified) class name, locate its persister.
		/// </summary>
		/// <param name="className">The (potentially unqualified) class name.</param>
		/// <returns>The defined persister for this class, or null if none found.</returns>
		public IQueryable FindQueryableUsingImports(string className)
		{
			return FindQueryableUsingImports(_sfi, className);
		}

		/// <summary>
		/// Given a (potentially unqualified) class name, locate its persister.
		/// </summary>
		/// <param name="sfi">The session factory implementor.</param>
		/// <param name="className">The (potentially unqualified) class name.</param>
		/// <returns>The defined persister for this class, or null if none found.</returns>
		private static IQueryable FindQueryableUsingImports(ISessionFactoryImplementor sfi, string className) 
		{
			return new SessionFactoryHelper(sfi).FindQueryableUsingImports(className);
		}

		/// <summary>
		/// Locate the persister by class or entity name.
		/// </summary>
		/// <param name="name">The class or entity name</param>
		/// <returns>The defined persister for this entity, or null if none found.</returns>
		private IEntityPersister FindEntityPersisterByName(string name)
		{
			return helper.FindEntityPersisterUsingImports(name);
		}

		/// <summary>
		/// Create a join sequence rooted at the given collection.
		/// </summary>
		/// <param name="collPersister">The persister for the collection at which the join should be rooted.</param>
		/// <param name="collectionName">The alias to use for qualifying column references.</param>
		/// <returns>The generated join sequence.</returns>
		public JoinSequence CreateCollectionJoinSequence(IQueryableCollection collPersister, String collectionName)
		{
			JoinSequence joinSequence = CreateJoinSequence();
			joinSequence.SetRoot(collPersister, collectionName);
			joinSequence.SetUseThetaStyle(true);		// TODO: figure out how this should be set.

			///////////////////////////////////////////////////////////////////////////////
			// This was the reason for failures regarding INDEX_OP and subclass joins on
			// theta-join dialects; not sure what behaviour we were trying to emulate ;)
			//		joinSequence = joinSequence.getFromPart();	// Emulate the old addFromOnly behavior.
			return joinSequence;
		}

		/// <summary>
		/// Generate an empty join sequence instance.
		/// </summary>
		/// <returns>The generated join sequence.</returns>
		public JoinSequence CreateJoinSequence()
		{
			return new JoinSequence(_sfi);
		}

		/// <summary>
		/// Generate a join sequence representing the given association type.
		/// </summary>
		/// <param name="implicitJoin">Should implicit joins (theta-style) or explicit joins (ANSI-style) be rendered</param>
		/// <param name="associationType">The type representing the thing to be joined into.</param>
		/// <param name="tableAlias">The table alias to use in qualifying the join conditions</param>
		/// <param name="joinType">The type of join to render (inner, outer, etc)</param>
		/// <param name="columns">The columns making up the condition of the join.</param>
		/// <returns>The generated join sequence.</returns>
		public JoinSequence CreateJoinSequence(bool implicitJoin, IAssociationType associationType, string tableAlias, JoinType joinType, string[] columns) 
		{
			JoinSequence joinSequence = CreateJoinSequence();
			joinSequence.SetUseThetaStyle(implicitJoin);	// Implicit joins use theta style (WHERE pk = fk), explicit joins use JOIN (after from)
			joinSequence.AddJoin( associationType, tableAlias, joinType, columns );
			return joinSequence;
		}

		public String[][] GenerateColumnNames(IType[] sqlResultTypes)
		{
			return NameGenerator.GenerateColumnNames(sqlResultTypes, _sfi);
		}

		public bool IsStrictJPAQLComplianceEnabled
		{
			get
			{
				// TODO - is this required at all?
				//return _sfi.Settings.isStrictJPAQLCompliance();
				return false;
			}
		}

		/// <summary>
		/// Retrieve a PropertyMapping describing the given collection role.
		/// </summary>
		/// <param name="role">The collection role for which to retrieve the property mapping.</param>
		/// <returns>The property mapping.</returns>
		private IPropertyMapping GetCollectionPropertyMapping(string role)
		{
			return _collectionPropertyMappingByRole[role];
		}

		/// <summary>
		/// Given a collection type, determine the Type representing elements
		/// within instances of that collection.
		/// </summary>
		/// <param name="collectionType">The collection type to be checked.</param>
		/// <returns>The Type of the elements of the collection.</returns>
		private IType GetElementType(CollectionType collectionType)
		{
			return collectionType.GetElementType(_sfi);
		}
	}
}
