// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.

namespace System.Data.Entity.Core.Objects.Internal
{
    using System.Collections.Generic;
    using System.Data.Common;
    using System.Data.Entity.Core.Metadata.Edm;
    using System.Data.Entity.Resources;
    using System.Data.Entity.Utilities;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Text;

    // <summary>
    // Provides Entity-SQL query building services for <see cref="EntitySqlQueryState" />.
    // Knowledge of how to compose Entity-SQL fragments using query builder operators resides entirely in this class.
    // </summary>
    internal static class EntitySqlQueryBuilder
    {
        // <summary>
        // Helper method to extract the Entity-SQL command text from an <see cref="ObjectQueryState" /> instance if that
        // instance models an Entity-SQL-backed ObjectQuery, or to throw an exception indicating that query builder methods
        // are not supported on this query.
        // </summary>
        // <param name="query"> The instance from which the Entity-SQL command text should be retrieved </param>
        // <returns> The Entity-SQL command text, if the specified query state instance is based on Entity-SQL </returns>
        // <exception cref="NotSupportedException">If the specified instance is not based on Entity-SQL command text, and so does not support Entity-SQL query builder methods</exception>
        private static string GetCommandText(ObjectQueryState query)
        {
            string commandText = null;
            if (!query.TryGetCommandText(out commandText))
            {
                throw new NotSupportedException(Strings.ObjectQuery_QueryBuilder_NotSupportedLinqSource);
            }

            return commandText;
        }

        // <summary>
        // Merges <see cref="ObjectParameter" />s from a source ObjectQuery with ObjectParameters specified as an argument to a builder method.
        // A new <see cref="ObjectParameterCollection" /> is returned that contains copies of parameters from both
        // <paramref
        //     name="sourceQueryParams" />
        // and <paramref name="builderMethodParams" />.
        // </summary>
        // <param name="context">
        // The <see cref="ObjectContext" /> to use when constructing the new parameter collection
        // </param>
        // <param name="sourceQueryParams"> ObjectParameters from the ObjectQuery on which the query builder method was called </param>
        // <param name="builderMethodParams"> ObjectParameters that were specified as an argument to the builder method </param>
        // <returns> A new ObjectParameterCollection containing copies of all parameters </returns>
        private static ObjectParameterCollection MergeParameters(
            ObjectContext context, ObjectParameterCollection sourceQueryParams, ObjectParameter[] builderMethodParams)
        {
            DebugCheck.NotNull(builderMethodParams);
            if (sourceQueryParams == null
                && builderMethodParams.Length == 0)
            {
                return null;
            }

            var mergedParams = ObjectParameterCollection.DeepCopy(sourceQueryParams);
            if (mergedParams == null)
            {
                mergedParams = new ObjectParameterCollection(context.Perspective);
            }

            foreach (var builderParam in builderMethodParams)
            {
                mergedParams.Add(builderParam);
            }

            return mergedParams;
        }

        // <summary>
        // Merges <see cref="ObjectParameter" />s from two ObjectQuery arguments to SetOp builder methods (Except, Intersect, Union, UnionAll).
        // A new <see cref="ObjectParameterCollection" /> is returned that contains copies of parameters from both
        // <paramref
        //     name="query1Params" />
        // and <paramref name="query2Params" />.
        // </summary>
        // <param name="query1Params"> ObjectParameters from the first ObjectQuery argument (on which the query builder method was called) </param>
        // <param name="query2Params"> ObjectParameters from the second ObjectQuery argument (specified as an argument to the builder method) </param>
        // <returns> A new ObjectParameterCollection containing copies of all parameters </returns>
        private static ObjectParameterCollection MergeParameters(
            ObjectParameterCollection query1Params, ObjectParameterCollection query2Params)
        {
            if (query1Params == null
                && query2Params == null)
            {
                return null;
            }

            ObjectParameterCollection mergedParams;
            ObjectParameterCollection sourceParams;
            if (query1Params != null)
            {
                mergedParams = ObjectParameterCollection.DeepCopy(query1Params);
                sourceParams = query2Params;
            }
            else
            {
                mergedParams = ObjectParameterCollection.DeepCopy(query2Params);
                sourceParams = query1Params;
            }

            if (sourceParams != null)
            {
                foreach (var sourceParam in sourceParams)
                {
                    mergedParams.Add(sourceParam.ShallowCopy());
                }
            }

            return mergedParams;
        }

        private static ObjectQueryState NewBuilderQuery(
            ObjectQueryState sourceQuery, Type elementType, StringBuilder queryText, Span newSpan,
            IEnumerable<ObjectParameter> enumerableParams)
        {
            return NewBuilderQuery(sourceQuery, elementType, queryText, false, newSpan, enumerableParams);
        }

        private static ObjectQueryState NewBuilderQuery(
            ObjectQueryState sourceQuery, Type elementType, StringBuilder queryText, bool allowsLimit, Span newSpan,
            IEnumerable<ObjectParameter> enumerableParams)
        {
            var queryParams = enumerableParams as ObjectParameterCollection;
            if (queryParams == null
                && enumerableParams != null)
            {
                queryParams = new ObjectParameterCollection(sourceQuery.ObjectContext.Perspective);
                foreach (var objectParam in enumerableParams)
                {
                    queryParams.Add(objectParam);
                }
            }

            var newState = new EntitySqlQueryState(
                elementType, queryText.ToString(), allowsLimit, sourceQuery.ObjectContext, queryParams, newSpan);

            sourceQuery.ApplySettingsTo(newState);

            return newState;
        }

        // Note that all query builder string constants contain embedded newlines to prevent manipulation of the
        // query text by single line comments (--) that might appear in user-supplied portions of the string such
        // as a filter predicate, projection list, etc.

        #region SetOp Helpers

        private const string _setOpEpilog =
            @"
)";

        private const string _setOpProlog =
            @"(
";

        // SetOp helper - note that this doesn't merge Spans, since Except uses the original query's Span
        // while Intersect/Union/UnionAll use the merged Span.
        [SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly")]
        private static ObjectQueryState BuildSetOp(ObjectQueryState leftQuery, ObjectQueryState rightQuery, Span newSpan, string setOp)
        {
            // Assert that the arguments aren't null (should have been verified by ObjectQuery)
            DebugCheck.NotNull(leftQuery);
            DebugCheck.NotNull(rightQuery);
            Debug.Assert(
                leftQuery.ElementType.Equals(rightQuery.ElementType),
                "Incompatible element types in arguments to Except<T>/Intersect<T>/Union<T>/UnionAll<T>?");

            // Retrieve the left and right arguments to the set operation - 
            // this will throw if either input query is not an Entity-SQL query.
            var left = GetCommandText(leftQuery);
            var right = GetCommandText(rightQuery);

            // ObjectQuery arguments must be associated with the same ObjectContext instance as the implemented query
            if (!ReferenceEquals(leftQuery.ObjectContext, rightQuery.ObjectContext))
            {
                throw new ArgumentException(Strings.ObjectQuery_QueryBuilder_InvalidQueryArgument, "query");
            }

            // Create a string builder only large enough to contain the new query text
            var queryLength = _setOpProlog.Length + left.Length + setOp.Length + right.Length + _setOpEpilog.Length;
            var builder = new StringBuilder(queryLength);

            // Build the new query
            builder.Append(_setOpProlog);
            builder.Append(left);
            builder.Append(setOp);
            builder.Append(right);
            builder.Append(_setOpEpilog);

            // Create a new query implementation and apply the state of this implementation to it.
            // The Span of the query argument will be merged into the new query's Span by the caller, iff the Set Op is NOT Except.
            // See the Except, Intersect, Union and UnionAll methods in this class for examples.
            return NewBuilderQuery(
                leftQuery, leftQuery.ElementType, builder, newSpan, MergeParameters(leftQuery.Parameters, rightQuery.Parameters));
        }

        #endregion

        #region Select/SelectValue Helpers

        private const string _fromOp =
            @"
FROM (
";

        private const string _asOp =
            @"
) AS ";

        private static ObjectQueryState BuildSelectOrSelectValue(
            ObjectQueryState query, string alias, string projection, ObjectParameter[] parameters, string projectOp, Type elementType)
        {
            DebugCheck.NotEmpty(alias);
            DebugCheck.NotEmpty(projection);

            var queryText = GetCommandText(query);

            // Build the new query string - "<project op> <projection> FROM (<this query>) AS <alias>"
            var queryLength = projectOp.Length +
                              projection.Length +
                              _fromOp.Length +
                              queryText.Length +
                              _asOp.Length +
                              alias.Length;

            var builder = new StringBuilder(queryLength);
            builder.Append(projectOp);
            builder.Append(projection);
            builder.Append(_fromOp);
            builder.Append(queryText);
            builder.Append(_asOp);
            builder.Append(alias);

            // Create a new EntitySqlQueryImplementation that uses the new query as its command text.
            // Span should not be carried over from a Select or SelectValue operation.
            return NewBuilderQuery(query, elementType, builder, null, MergeParameters(query.ObjectContext, query.Parameters, parameters));
        }

        #endregion

        #region OrderBy/Where Helper

        private static ObjectQueryState BuildOrderByOrWhere(
            ObjectQueryState query, string alias, string predicateOrKeys, ObjectParameter[] parameters, string op, string skipCount,
            bool allowsLimit)
        {
            DebugCheck.NotEmpty(alias);
            DebugCheck.NotEmpty(predicateOrKeys);
            Debug.Assert(null == skipCount || op == _orderByOp, "Skip clause used with WHERE operator?");

            var queryText = GetCommandText(query);

            // Build the new query string:
            // Either: "SELECT VALUE <alias> FROM (<this query>) AS <alias> WHERE <predicate>"
            //  (for Where)
            // Or:  "SELECT VALUE <alias> FROM (<this query>) AS <alias> ORDER BY <keys> <optional: SKIP <skip>>"
            // Depending on the value of 'op'
            var queryLength = _selectValueOp.Length +
                              alias.Length +
                              _fromOp.Length +
                              queryText.Length +
                              _asOp.Length +
                              alias.Length +
                              op.Length +
                              predicateOrKeys.Length;

            if (skipCount != null)
            {
                queryLength += (_skipOp.Length + skipCount.Length);
            }

            var builder = new StringBuilder(queryLength);
            builder.Append(_selectValueOp);
            builder.Append(alias);
            builder.Append(_fromOp);
            builder.Append(queryText);
            builder.Append(_asOp);
            builder.Append(alias);
            builder.Append(op);
            builder.Append(predicateOrKeys);
            if (skipCount != null)
            {
                builder.Append(_skipOp);
                builder.Append(skipCount);
            }

            // Create a new EntitySqlQueryImplementation that uses the new query as its command text.
            // Span is carried over, no adjustment is needed.
            return NewBuilderQuery(
                query, query.ElementType, builder, allowsLimit, query.Span,
                MergeParameters(query.ObjectContext, query.Parameters, parameters));
        }

        #endregion

        #region Distinct

        private const string _distinctProlog =
            @"SET(
";

        private const string _distinctEpilog =
            @"
)";

        internal static ObjectQueryState Distinct(ObjectQueryState query)
        {
            // Build the new query string - "SET(<this query>)"
            var queryText = GetCommandText(query);
            var builder = new StringBuilder(_distinctProlog.Length + queryText.Length + _distinctEpilog.Length);
            builder.Append(_distinctProlog);
            builder.Append(queryText);
            builder.Append(_distinctEpilog);

            // Span is carried over, no adjustment is needed

            return NewBuilderQuery(query, query.ElementType, builder, query.Span, ObjectParameterCollection.DeepCopy(query.Parameters));
        }

        #endregion

        #region Except

        private const string _exceptOp =
            @"
) EXCEPT (
";

        internal static ObjectQueryState Except(ObjectQueryState leftQuery, ObjectQueryState rightQuery)
        {
            // Call the SetOp helper.
            // Span is taken from the leftmost query.
            return BuildSetOp(leftQuery, rightQuery, leftQuery.Span, _exceptOp);
        }

        #endregion

        #region GroupBy

        private const string _groupByOp =
            @"
GROUP BY
";

        internal static ObjectQueryState GroupBy(
            ObjectQueryState query, string alias, string keys, string projection, ObjectParameter[] parameters)
        {
            DebugCheck.NotEmpty(alias);
            DebugCheck.NotEmpty(keys);
            DebugCheck.NotEmpty(projection);

            var queryText = GetCommandText(query);

            // Build the new query string:
            // "SELECT <projection> FROM (<this query>) AS <alias> GROUP BY <keys>"
            var queryLength = _selectOp.Length +
                              projection.Length +
                              _fromOp.Length +
                              queryText.Length +
                              _asOp.Length +
                              alias.Length +
                              _groupByOp.Length +
                              keys.Length;

            var builder = new StringBuilder(queryLength);
            builder.Append(_selectOp);
            builder.Append(projection);
            builder.Append(_fromOp);
            builder.Append(queryText);
            builder.Append(_asOp);
            builder.Append(alias);
            builder.Append(_groupByOp);
            builder.Append(keys);

            // Create a new EntitySqlQueryImplementation that uses the new query as its command text.
            // Span should not be carried over from a GroupBy operation.
            return NewBuilderQuery(
                query, typeof(DbDataRecord), builder, null, MergeParameters(query.ObjectContext, query.Parameters, parameters));
        }

        #endregion

        #region Intersect

        private const string _intersectOp =
            @"
) INTERSECT (
";

        internal static ObjectQueryState Intersect(ObjectQueryState leftQuery, ObjectQueryState rightQuery)
        {
            // Ensure the Spans of the query arguments are merged into the new query's Span.
            var newSpan = Span.CopyUnion(leftQuery.Span, rightQuery.Span);
            // Call the SetOp helper.
            return BuildSetOp(leftQuery, rightQuery, newSpan, _intersectOp);
        }

        #endregion

        #region OfType

        private const string _ofTypeProlog =
            @"OFTYPE(
(
";

        private const string _ofTypeInfix =
            @"
),
[";

        private const string _ofTypeInfix2 = "].[";

        private const string _ofTypeEpilog =
            @"]
)";

        internal static ObjectQueryState OfType(ObjectQueryState query, EdmType newType, Type clrOfType)
        {
            Debug.Assert(newType != null, "OfType cannot be null");
            Debug.Assert(Helper.IsEntityType(newType) || Helper.IsComplexType(newType), "OfType must be Entity or Complex type");

            var queryText = GetCommandText(query);

            // Build the new query string - "OFTYPE((<query>), [<type namespace>].[<type name>])"
            var queryLength = _ofTypeProlog.Length +
                              queryText.Length +
                              _ofTypeInfix.Length +
                              newType.NamespaceName.Length +
                              (!string.IsNullOrEmpty(newType.NamespaceName) ? _ofTypeInfix2.Length : 0) +
                              newType.Name.Length +
                              _ofTypeEpilog.Length;

            var builder = new StringBuilder(queryLength);
            builder.Append(_ofTypeProlog);
            builder.Append(queryText);
            builder.Append(_ofTypeInfix);
            if (!string.IsNullOrEmpty(newType.NamespaceName))
            {
                builder.Append(newType.NamespaceName);
                builder.Append(_ofTypeInfix2);
            }
            builder.Append(newType.Name);
            builder.Append(_ofTypeEpilog);

            // Create a new EntitySqlQueryImplementation that uses the new query as its command text.
            // Span is carried over, no adjustment is needed
            return NewBuilderQuery(query, clrOfType, builder, query.Span, ObjectParameterCollection.DeepCopy(query.Parameters));
        }

        #endregion

        #region OrderBy

        private const string _orderByOp =
            @"
ORDER BY
";

        internal static ObjectQueryState OrderBy(ObjectQueryState query, string alias, string keys, ObjectParameter[] parameters)
        {
            return BuildOrderByOrWhere(query, alias, keys, parameters, _orderByOp, null, true);
        }

        #endregion

        #region Select

        private const string _selectOp = "SELECT ";

        internal static ObjectQueryState Select(ObjectQueryState query, string alias, string projection, ObjectParameter[] parameters)
        {
            return BuildSelectOrSelectValue(query, alias, projection, parameters, _selectOp, typeof(DbDataRecord));
        }

        #endregion

        #region SelectValue

        private const string _selectValueOp = "SELECT VALUE ";

        internal static ObjectQueryState SelectValue(
            ObjectQueryState query, string alias, string projection, ObjectParameter[] parameters, Type projectedType)
        {
            return BuildSelectOrSelectValue(query, alias, projection, parameters, _selectValueOp, projectedType);
        }

        #endregion

        #region Skip

        private const string _skipOp =
            @"
SKIP
";

        internal static ObjectQueryState Skip(ObjectQueryState query, string alias, string keys, string count, ObjectParameter[] parameters)
        {
            DebugCheck.NotEmpty(count);

            return BuildOrderByOrWhere(query, alias, keys, parameters, _orderByOp, count, true);
        }

        #endregion

        #region Top

        private const string _limitOp =
            @"
LIMIT
";

        private const string _topOp =
            @"SELECT VALUE TOP(
";

        private const string _topInfix =
            @"
) ";

        internal static ObjectQueryState Top(ObjectQueryState query, string alias, string count, ObjectParameter[] parameters)
        {
            var queryLength = count.Length;
            var queryText = GetCommandText(query);
            var limitAllowed = ((EntitySqlQueryState)query).AllowsLimitSubclause;

            if (limitAllowed)
            {
                // Build the new query string:
                // <this query> LIMIT <count>
                queryLength += (queryText.Length +
                                _limitOp.Length
                               // + count.Length is added above
                               );
            }
            else
            {
                // Build the new query string:
                // "SELECT VALUE TOP(<count>) <alias> FROM (<this query>) AS <alias>"
                queryLength += (_topOp.Length +
                                // count.Length + is added above
                                _topInfix.Length +
                                alias.Length +
                                _fromOp.Length +
                                queryText.Length +
                                _asOp.Length +
                                alias.Length);
            }

            var builder = new StringBuilder(queryLength);
            if (limitAllowed)
            {
                builder.Append(queryText);
                builder.Append(_limitOp);
                builder.Append(count);
            }
            else
            {
                builder.Append(_topOp);
                builder.Append(count);
                builder.Append(_topInfix);
                builder.Append(alias);
                builder.Append(_fromOp);
                builder.Append(queryText);
                builder.Append(_asOp);
                builder.Append(alias);
            }

            // Create a new EntitySqlQueryImplementation that uses the new query as its command text.
            // Span is carried over, no adjustment is needed.
            return NewBuilderQuery(
                query, query.ElementType, builder, query.Span, MergeParameters(query.ObjectContext, query.Parameters, parameters));
        }

        #endregion

        #region Union

        private const string _unionOp =
            @"
) UNION (
";

        internal static ObjectQueryState Union(ObjectQueryState leftQuery, ObjectQueryState rightQuery)
        {
            // Ensure the Spans of the query arguments are merged into the new query's Span.
            var newSpan = Span.CopyUnion(leftQuery.Span, rightQuery.Span);
            // Call the SetOp helper.
            return BuildSetOp(leftQuery, rightQuery, newSpan, _unionOp);
        }

        #endregion

        #region Union

        private const string _unionAllOp =
            @"
) UNION ALL (
";

        internal static ObjectQueryState UnionAll(ObjectQueryState leftQuery, ObjectQueryState rightQuery)
        {
            // Ensure the Spans of the query arguments are merged into the new query's Span.
            var newSpan = Span.CopyUnion(leftQuery.Span, rightQuery.Span);
            // Call the SetOp helper.
            return BuildSetOp(leftQuery, rightQuery, newSpan, _unionAllOp);
        }

        #endregion

        #region Where

        private const string _whereOp =
            @"
WHERE
";

        internal static ObjectQueryState Where(ObjectQueryState query, string alias, string predicate, ObjectParameter[] parameters)
        {
            return BuildOrderByOrWhere(query, alias, predicate, parameters, _whereOp, null, false);
        }

        #endregion
    }
}
