/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace Apache.Ignite.Linq.Impl
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Text;
    using Apache.Ignite.Linq.Impl.Dml;
    using Remotion.Linq;
    using Remotion.Linq.Clauses;
    using Remotion.Linq.Clauses.Expressions;
    using Remotion.Linq.Clauses.ResultOperators;

    /// <summary>
    /// Query visitor, transforms LINQ expression to SQL.
    /// </summary>
    internal sealed class CacheQueryModelVisitor : QueryModelVisitorBase
    {
        /** */
        private readonly StringBuilder _builder = new StringBuilder();

        /** */
        private readonly List<object> _parameters = new List<object>();

        /** */
        private readonly AliasDictionary _aliases = new AliasDictionary();

        /** */
        private static readonly Type DefaultIfEmptyEnumeratorType = new object[0]
            .DefaultIfEmpty()
            .GetType()
            .GetGenericTypeDefinition();

        /// <summary>
        /// Generates the query.
        /// </summary>
        public QueryData GenerateQuery(QueryModel queryModel)
        {
            Debug.Assert(_builder.Length == 0);
            Debug.Assert(_parameters.Count == 0);

            VisitQueryModel(queryModel);

            if (char.IsWhiteSpace(_builder[_builder.Length - 1]))
                _builder.Remove(_builder.Length - 1, 1);  // TrimEnd

            var qryText = _builder.ToString();

            return new QueryData(qryText, _parameters);
        }

        /// <summary>
        /// Gets the builder.
        /// </summary>
        public StringBuilder Builder
        {
            get { return _builder; }
        }

        /// <summary>
        /// Gets the parameters.
        /// </summary>
        public IList<object> Parameters
        {
            get { return _parameters; }
        }

        /// <summary>
        /// Gets the aliases.
        /// </summary>
        public AliasDictionary Aliases
        {
            get { return _aliases; }
        }

        /** <inheritdoc /> */
        public override void VisitQueryModel(QueryModel queryModel)
        {
            VisitQueryModel(queryModel, false);
        }

        /// <summary>
        /// Visits the query model.
        /// </summary>
        internal void VisitQueryModel(QueryModel queryModel, bool includeAllFields, bool copyAliases = false)
        {
            _aliases.Push(copyAliases);

            var lastResultOp = queryModel.ResultOperators.LastOrDefault();
            if (lastResultOp is RemoveAllResultOperator)
            {
                VisitRemoveOperator(queryModel);
            }
            else if(lastResultOp is UpdateAllResultOperator)
            {
                VisitUpdateAllOperator(queryModel);
            }
            else
            {
                // SELECT
                _builder.Append("select ");

                // TOP 1 FLD1, FLD2
                VisitSelectors(queryModel, includeAllFields);

                // FROM ... WHERE ... JOIN ...
                base.VisitQueryModel(queryModel);

                // UNION ...
                ProcessResultOperatorsEnd(queryModel);
            }

            _aliases.Pop();
        }

        /// <summary>
        /// Visits the remove operator.
        /// </summary>
        private void VisitRemoveOperator(QueryModel queryModel)
        {
            var resultOps = queryModel.ResultOperators;

            _builder.Append("delete ");

            if (resultOps.Count == 2)
            {
                var resOp = resultOps[0] as TakeResultOperator;

                if (resOp == null)
                    throw new NotSupportedException(
                        "RemoveAll can not be combined with result operators (other than Take): " +
                        resultOps[0].GetType().Name);

                _builder.Append("top ");
                BuildSqlExpression(resOp.Count);
                _builder.Append(' ');
            }
            else if (resultOps.Count > 2)
            {
                throw new NotSupportedException(
                    "RemoveAll can not be combined with result operators (other than Take): " +
                    string.Join(", ", resultOps.Select(x => x.GetType().Name)));
            }

            // FROM ... WHERE ... JOIN ...
            base.VisitQueryModel(queryModel);
        }

        /// <summary>
        /// Visits the UpdateAll operator.
        /// </summary>
        private void VisitUpdateAllOperator(QueryModel queryModel)
        {
            var resultOps = queryModel.ResultOperators;

            _builder.Append("update ");

            // FROM ... WHERE ...
            base.VisitQueryModel(queryModel);

            if (resultOps.Count == 2)
            {
                var resOp = resultOps[0] as TakeResultOperator;

                if (resOp == null)
                    throw new NotSupportedException(
                        "UpdateAll can not be combined with result operators (other than Take): " +
                        resultOps[0].GetType().Name);

                _builder.Append("limit ");
                BuildSqlExpression(resOp.Count);
            }
            else if (resultOps.Count > 2)
            {
                throw new NotSupportedException(
                    "UpdateAll can not be combined with result operators (other than Take): " +
                    string.Join(", ", resultOps.Select(x => x.GetType().Name)));
            }
        }

        /// <summary>
        /// Visits the selectors.
        /// </summary>
        public void VisitSelectors(QueryModel queryModel, bool includeAllFields)
        {
            var parenCount = ProcessResultOperatorsBegin(queryModel);

            if (parenCount >= 0)
            {
                // FIELD1, FIELD2
                BuildSqlExpression(queryModel.SelectClause.Selector, parenCount > 0, includeAllFields);
                _builder.Append(')', parenCount).Append(' ');
            }
        }

        /// <summary>
        /// Processes the result operators that come right after SELECT: min/max/count/sum/distinct
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
        private int ProcessResultOperatorsBegin(QueryModel queryModel)
        {
            int parenCount = 0;

            foreach (var op in queryModel.ResultOperators.Reverse())
            {
                if (op is CountResultOperator || op is AnyResultOperator || op is LongCountResultOperator)
                {
                    _builder.Append("count (");
                    parenCount++;
                }
                else if (op is SumResultOperator)
                {
                    _builder.Append("sum (");
                    parenCount++;
                }
                else if (op is MinResultOperator)
                {
                    _builder.Append("min (");
                    parenCount++;
                }
                else if (op is MaxResultOperator)
                {
                    _builder.Append("max (");
                    parenCount++;
                }
                else if (op is AverageResultOperator)
                {
                    _builder.Append("avg (");
                    parenCount++;
                }
                else if (op is DistinctResultOperator)
                    _builder.Append("distinct ");
                else if (op is FirstResultOperator || op is SingleResultOperator)
                    _builder.Append("top 1 ");
                else if (op is UnionResultOperator || op is IntersectResultOperator || op is ExceptResultOperator
                         || op is DefaultIfEmptyResultOperator || op is SkipResultOperator || op is TakeResultOperator)
                    // Will be processed later
                    break;
                else if (op is ContainsResultOperator)
                    // Should be processed already
                    break;
                else
                    throw new NotSupportedException("Operator is not supported: " + op);
            }
            return parenCount;
        }

        /// <summary>
        /// Processes the result operators that go in the end of the query: limit/offset/union/intersect/except
        /// </summary>
        private void ProcessResultOperatorsEnd(QueryModel queryModel)
        {
            ProcessSkipTake(queryModel);

            foreach (var op in queryModel.ResultOperators.Reverse())
            {
                string keyword = null;
                Expression source = null;

                var union = op as UnionResultOperator;
                if (union != null)
                {
                    keyword = "union";
                    source = union.Source2;
                }

                var intersect = op as IntersectResultOperator;
                if (intersect != null)
                {
                    keyword = "intersect";
                    source = intersect.Source2;
                }

                var except = op as ExceptResultOperator;
                if (except != null)
                {
                    keyword = "except";
                    source = except.Source2;
                }

                if (keyword != null)
                {
                    _builder.Append(keyword).Append(" (");

                    var subQuery = source as SubQueryExpression;

                    if (subQuery != null)  // Subquery union
                        VisitQueryModel(subQuery.QueryModel);
                    else
                    {
                        // Direct cache union, source is ICacheQueryable
                        var innerExpr = source as ConstantExpression;

                        if (innerExpr == null)
                            throw new NotSupportedException("Unexpected UNION inner sequence: " + source);

                        var queryable = innerExpr.Value as ICacheQueryableInternal;

                        if (queryable == null)
                            throw new NotSupportedException("Unexpected UNION inner sequence " +
                                                            "(only results of cache.ToQueryable() are supported): " +
                                                            innerExpr.Value);

                        VisitQueryModel(queryable.GetQueryModel());
                    }

                    _builder.Append(')');
                }
            }
        }

        /// <summary>
        /// Processes the pagination (skip/take).
        /// </summary>
        private void ProcessSkipTake(QueryModel queryModel)
        {
            var limit = queryModel.ResultOperators.OfType<TakeResultOperator>().FirstOrDefault();
            var offset = queryModel.ResultOperators.OfType<SkipResultOperator>().FirstOrDefault();

            if (limit == null && offset == null)
                return;

            // "limit" is mandatory if there is "offset", but not vice versa
            _builder.Append("limit ");

            if (limit == null)
            {
                // Workaround for unlimited offset (IGNITE-2602)
                // H2 allows NULL & -1 for unlimited, but Ignite indexing does not
                // Maximum limit that works is (int.MaxValue - offset)

                if (offset.Count is ParameterExpression)
                    throw new NotSupportedException("Skip() without Take() is not supported in compiled queries.");

                var offsetInt = (int) ((ConstantExpression) offset.Count).Value;
                _builder.Append((int.MaxValue - offsetInt).ToString(CultureInfo.InvariantCulture));
            }
            else
                BuildSqlExpression(limit.Count);

            if (offset != null)
            {
                _builder.Append(" offset ");
                BuildSqlExpression(offset.Count);
            }
        }

        /** <inheritdoc /> */
        protected override void VisitBodyClauses(ObservableCollection<IBodyClause> bodyClauses, QueryModel queryModel)
        {
            var i = 0;
            foreach (var join in bodyClauses.OfType<JoinClause>())
                VisitJoinClause(join, queryModel, i++);

            var hasGroups = ProcessGroupings(queryModel);

            i = 0;
            foreach (var where in bodyClauses.OfType<WhereClause>())
                VisitWhereClause(where, i++, hasGroups);

            i = 0;
            foreach (var orderBy in bodyClauses.OfType<OrderByClause>())
                VisitOrderByClause(orderBy, queryModel, i++);
        }

        /// <summary>
        /// Processes the groupings.
        /// </summary>
        private bool ProcessGroupings(QueryModel queryModel)
        {
            var subQuery = queryModel.MainFromClause.FromExpression as SubQueryExpression;

            if (subQuery == null)
                return false;

            var groupBy = subQuery.QueryModel.ResultOperators.OfType<GroupResultOperator>().FirstOrDefault();

            if (groupBy == null)
                return false;

            // Visit inner joins before grouping
            var i = 0;
            foreach (var join in subQuery.QueryModel.BodyClauses.OfType<JoinClause>())
                VisitJoinClause(join, queryModel, i++);

            i = 0;
            foreach (var where in subQuery.QueryModel.BodyClauses.OfType<WhereClause>())
                VisitWhereClause(where, i++, false);

            // Append grouping
            _builder.Append("group by (");

            BuildSqlExpression(groupBy.KeySelector);

            _builder.Append(") ");

            return true;
        }

        /** <inheritdoc /> */
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public override void VisitMainFromClause(MainFromClause fromClause, QueryModel queryModel)
        {
            base.VisitMainFromClause(fromClause, queryModel);

            var isUpdateQuery = queryModel.ResultOperators.LastOrDefault() is UpdateAllResultOperator;
            if (!isUpdateQuery)
            {
                _builder.Append("from ");
            }

            ValidateFromClause(fromClause);
            _aliases.AppendAsClause(_builder, fromClause).Append(' ');

            var i = 0;
            foreach (var additionalFrom in queryModel.BodyClauses.OfType<AdditionalFromClause>())
            {
                _builder.Append(", ");
                ValidateFromClause(additionalFrom);

                VisitAdditionalFromClause(additionalFrom, queryModel, i++);
            }

            if (isUpdateQuery)
            {
                BuildSetClauseForUpdateAll(queryModel);
            }
        }

        /// <summary>
        /// Validates from clause.
        /// </summary>
        // ReSharper disable once UnusedParameter.Local
        private static void ValidateFromClause(IFromClause clause)
        {
            // Only IQueryable can be used in FROM clause. IEnumerable is not supported.
            if (!typeof(IQueryable).IsAssignableFrom(clause.FromExpression.Type))
            {
                throw new NotSupportedException("FROM clause must be IQueryable: " + clause);
            }
        }

        /** <inheritdoc /> */
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public override void VisitWhereClause(WhereClause whereClause, QueryModel queryModel, int index)
        {
            base.VisitWhereClause(whereClause, queryModel, index);

            VisitWhereClause(whereClause, index, false);
        }

        /// <summary>
        /// Visits the where clause.
        /// </summary>
        private void VisitWhereClause(WhereClause whereClause, int index, bool hasGroups)
        {
            _builder.Append(index > 0
                ? "and "
                : hasGroups
                    ? "having"
                    : "where ");

            BuildSqlExpression(whereClause.Predicate);

            _builder.Append(' ');
        }

        /** <inheritdoc /> */
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public override void VisitOrderByClause(OrderByClause orderByClause, QueryModel queryModel, int index)
        {
            base.VisitOrderByClause(orderByClause, queryModel, index);

            _builder.Append("order by ");

            for (int i = 0; i < orderByClause.Orderings.Count; i++)
            {
                var ordering = orderByClause.Orderings[i];

                if (i > 0)
                    _builder.Append(", ");

                _builder.Append('(');

                BuildSqlExpression(ordering.Expression);

                _builder.Append(')');

                _builder.Append(ordering.OrderingDirection == OrderingDirection.Asc ? " asc" : " desc");
            }

            _builder.Append(' ');
        }

        /** <inheritdoc /> */
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public override void VisitJoinClause(JoinClause joinClause, QueryModel queryModel, int index)
        {
            base.VisitJoinClause(joinClause, queryModel, index);
            var queryable = ExpressionWalker.GetCacheQueryable(joinClause, false);

            if (queryable != null)
            {
                var subQuery = joinClause.InnerSequence as SubQueryExpression;

                if (subQuery != null)
                {
                    var isOuter = subQuery.QueryModel.ResultOperators.OfType<DefaultIfEmptyResultOperator>().Any();

                    _builder.AppendFormat("{0} join (", isOuter ? "left outer" : "inner");

                    VisitQueryModel(subQuery.QueryModel, true);

                    var alias = _aliases.GetTableAlias(subQuery.QueryModel.MainFromClause);
                    _builder.AppendFormat(") as {0} on (", alias);
                }
                else
                {
                    var tableName = ExpressionWalker.GetTableNameWithSchema(queryable);
                    var alias = _aliases.GetTableAlias(joinClause);
                    _builder.AppendFormat("inner join {0} as {1} on (", tableName, alias);
                }
            }
            else
            {
                VisitJoinWithLocalCollectionClause(joinClause);
            }

            BuildJoinCondition(joinClause.InnerKeySelector, joinClause.OuterKeySelector);

            _builder.Append(") ");
        }

        /** <inheritdoc /> */
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods")]
        public override void VisitAdditionalFromClause(AdditionalFromClause fromClause, QueryModel queryModel,
            int index)
        {
            base.VisitAdditionalFromClause(fromClause, queryModel, index);

            var subQuery = fromClause.FromExpression as SubQueryExpression;
            if (subQuery != null)
            {
                _builder.Append('(');

                VisitQueryModel(subQuery.QueryModel, true);

                var alias = _aliases.GetTableAlias(subQuery.QueryModel.MainFromClause);
                _builder.AppendFormat(") as {0} ", alias);
            }
            else
            {
                _aliases.AppendAsClause(_builder, fromClause).Append(' ');
            }
        }

        /// <summary>
        /// Visists Join clause in case of join with local collection
        /// </summary>
        private void VisitJoinWithLocalCollectionClause(JoinClause joinClause)
        {
            var type = joinClause.InnerSequence.Type;

            var itemType = EnumerableHelper.GetIEnumerableItemType(type);

            var sqlTypeName = SqlTypes.GetSqlTypeName(itemType);

            if (string.IsNullOrWhiteSpace(sqlTypeName))
            {
                throw new NotSupportedException("Not supported item type for Join with local collection: " + type.Name);
            }

            var isOuter = false;
            var sequenceExpression = joinClause.InnerSequence;
            object values;

            var subQuery = sequenceExpression as SubQueryExpression;
            if (subQuery != null)
            {
                isOuter = subQuery.QueryModel.ResultOperators.OfType<DefaultIfEmptyResultOperator>().Any();
                sequenceExpression = subQuery.QueryModel.MainFromClause.FromExpression;
            }

            switch (sequenceExpression.NodeType)
            {
                case ExpressionType.Constant:
                    var constantValueType = ((ConstantExpression)sequenceExpression).Value.GetType();
                    if (constantValueType.IsGenericType)
                    {
                        isOuter = DefaultIfEmptyEnumeratorType == constantValueType.GetGenericTypeDefinition();
                    }
                    values = ExpressionWalker.EvaluateEnumerableValues(sequenceExpression);
                    break;

                case ExpressionType.Parameter:
                    values = ExpressionWalker.EvaluateExpression<object>(sequenceExpression);
                    break;

                default:
                    throw new NotSupportedException("Expression not supported for Join with local collection: "
                                                    + sequenceExpression);
            }

            var tableAlias = _aliases.GetTableAlias(joinClause);
            var fieldAlias = _aliases.GetFieldAlias(joinClause.InnerKeySelector);

            _builder.AppendFormat("{0} join table ({1} {2} = ?) {3} on (",
                isOuter ? "left outer" : "inner",
                fieldAlias,
                sqlTypeName,
                tableAlias);

            Parameters.Add(values);
        }

        /// <summary>
        /// Builds the join condition ('x=y AND foo=bar').
        /// </summary>
        /// <param name="innerKey">The inner key selector.</param>
        /// <param name="outerKey">The outer key selector.</param>
        /// <exception cref="System.NotSupportedException">
        /// </exception>
        private void BuildJoinCondition(Expression innerKey, Expression outerKey)
        {
            var innerNew = innerKey as NewExpression;
            var outerNew = outerKey as NewExpression;

            if (innerNew == null && outerNew == null)
            {
                BuildJoinSubCondition(innerKey, outerKey);
                return;
            }

            if (innerNew != null && outerNew != null)
            {
                if (innerNew.Constructor != outerNew.Constructor)
                    throw new NotSupportedException(
                        string.Format("Unexpected JOIN condition. Multi-key joins should have " +
                                      "the same initializers on both sides: '{0} = {1}'", innerKey, outerKey));

                for (var i = 0; i < innerNew.Arguments.Count; i++)
                {
                    if (i > 0)
                        _builder.Append(" and ");

                    BuildJoinSubCondition(innerNew.Arguments[i], outerNew.Arguments[i]);
                }

                return;
            }

            throw new NotSupportedException(
                string.Format("Unexpected JOIN condition. Multi-key joins should have " +
                              "anonymous type instances on both sides: '{0} = {1}'", innerKey, outerKey));
        }

        /// <summary>
        /// Builds the join sub condition.
        /// </summary>
        /// <param name="innerKey">The inner key.</param>
        /// <param name="outerKey">The outer key.</param>
        private void BuildJoinSubCondition(Expression innerKey, Expression outerKey)
        {
            BuildSqlExpression(innerKey);
            _builder.Append(" = ");
            BuildSqlExpression(outerKey);
        }

        /// <summary>
        /// Builds the SQL expression.
        /// </summary>
        private void BuildSqlExpression(Expression expression, bool useStar = false, bool includeAllFields = false,
            bool visitSubqueryModel = false)
        {
            new CacheQueryExpressionVisitor(this, useStar, includeAllFields, visitSubqueryModel).Visit(expression);
        }

        /// <summary>
        /// Builds SET clause of UPDATE statement
        /// </summary>
        private void BuildSetClauseForUpdateAll(QueryModel queryModel)
        {
            var updateAllResultOperator = queryModel.ResultOperators.LastOrDefault() as UpdateAllResultOperator;
            if (updateAllResultOperator != null)
            {
                _builder.Append("set ");
                var first = true;
                foreach (var update in updateAllResultOperator.Updates)
                {
                    if (!first) _builder.Append(", ");
                    first = false;
                    BuildSqlExpression(update.Selector);
                    _builder.Append(" = ");
                    BuildSqlExpression(update.Value, visitSubqueryModel: true);
                }

                _builder.Append(' ');
            }
        }
    }
}
