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

namespace System.Data.Entity.Core.Mapping.ViewGeneration.CqlGeneration
{
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Data.Entity.Core.Common.CommandTrees;
    using System.Data.Entity.Core.Common.CommandTrees.ExpressionBuilder;
    using System.Data.Entity.Core.Common.Utils;
    using System.Data.Entity.Core.Mapping.ViewGeneration.Structures;
    using System.Data.Entity.Utilities;
    using System.Diagnostics;
    using System.Linq;
    using System.Text;

    // <summary>
    // A class that holds an expression of the form "(SELECT .. FROM .. WHERE) AS alias".
    // Essentially, it allows generating Cql query in a localized manner, i.e., all global decisions about nulls, constants,
    // case statements, etc have already been made.
    // </summary>
    internal abstract class CqlBlock : InternalBase
    {
        // <summary>
        // Initializes a <see cref="CqlBlock" /> with the SELECT (<paramref name="slotInfos" />), FROM (
        // <paramref
        //     name="children" />
        // ),
        // WHERE (<paramref name="whereClause" />), AS (<paramref name="blockAliasNum" />).
        // </summary>
        protected CqlBlock(
            SlotInfo[] slotInfos, List<CqlBlock> children, BoolExpression whereClause, CqlIdentifiers identifiers, int blockAliasNum)
        {
            m_slots = new ReadOnlyCollection<SlotInfo>(slotInfos);
            m_children = new ReadOnlyCollection<CqlBlock>(children);
            m_whereClause = whereClause;
            m_blockAlias = identifiers.GetBlockAlias(blockAliasNum);
        }

        // <summary>
        // Essentially, SELECT. May be replaced with another collection after block construction.
        // </summary>
        private ReadOnlyCollection<SlotInfo> m_slots;

        // <summary>
        // FROM inputs.
        // </summary>
        private readonly ReadOnlyCollection<CqlBlock> m_children;

        // <summary>
        // WHERER.
        // </summary>
        private readonly BoolExpression m_whereClause;

        // <summary>
        // Alias of the whole block for cql generation.
        // </summary>
        private readonly string m_blockAlias;

        // <summary>
        // See <see cref="JoinTreeContext" /> for more info.
        // </summary>
        private JoinTreeContext m_joinTreeContext;

        // <summary>
        // Returns all the slots for this block (SELECT).
        // </summary>
        internal ReadOnlyCollection<SlotInfo> Slots
        {
            get { return m_slots; }
            set { m_slots = value; }
        }

        // <summary>
        // Returns all the child (input) blocks of this block (FROM).
        // </summary>
        protected ReadOnlyCollection<CqlBlock> Children
        {
            get { return m_children; }
        }

        // <summary>
        // Returns the where clause of this block (WHERE).
        // </summary>
        protected BoolExpression WhereClause
        {
            get { return m_whereClause; }
        }

        // <summary>
        // Returns an alias for this block that can be used for "AS".
        // </summary>
        internal string CqlAlias
        {
            get { return m_blockAlias; }
        }

        // <summary>
        // Returns a string corresponding to the eSQL representation of this block (and its children below).
        // </summary>
        internal abstract StringBuilder AsEsql(StringBuilder builder, bool isTopLevel, int indentLevel);

        // <summary>
        // Returns a string corresponding to the CQT representation of this block (and its children below).
        // </summary>
        internal abstract DbExpression AsCqt(bool isTopLevel);

        // <summary>
        // For the given <paramref name="slotNum" /> creates a <see cref="QualifiedSlot" /> qualified with
        // <see
        //     cref="CqlAlias" />
        // of the current block:
        // "<see cref="CqlAlias" />.slot_alias"
        // </summary>
        internal QualifiedSlot QualifySlotWithBlockAlias(int slotNum)
        {
            Debug.Assert(
                IsProjected(slotNum),
                StringUtil.FormatInvariant("Slot {0} that is to be qualified with the block alias is not projected in this block", slotNum));
            var slotInfo = m_slots[slotNum];
            return new QualifiedSlot(this, slotInfo.SlotValue);
        }

        internal ProjectedSlot SlotValue(int slotNum)
        {
            Debug.Assert(slotNum < m_slots.Count, "Slotnum too high");
            return m_slots[slotNum].SlotValue;
        }

        internal MemberPath MemberPath(int slotNum)
        {
            Debug.Assert(slotNum < m_slots.Count, "Slotnum too high");
            return m_slots[slotNum].OutputMember;
        }

        // <summary>
        // Returns true iff <paramref name="slotNum" /> is being projected by this block.
        // </summary>
        internal bool IsProjected(int slotNum)
        {
            Debug.Assert(slotNum < m_slots.Count, "Slotnum too high");
            return m_slots[slotNum].IsProjected;
        }

        // <summary>
        // Generates "A, B, C, ..." for all the slots in the block.
        // </summary>
        protected void GenerateProjectionEsql(
            StringBuilder builder, string blockAlias, bool addNewLineAfterEachSlot, int indentLevel, bool isTopLevel)
        {
            var isFirst = true;
            foreach (var slotInfo in Slots)
            {
                if (false == slotInfo.IsRequiredByParent)
                {
                    // Ignore slots that are not needed
                    continue;
                }
                if (isFirst == false)
                {
                    builder.Append(", ");
                }

                if (addNewLineAfterEachSlot)
                {
                    StringUtil.IndentNewLine(builder, indentLevel + 1);
                }

                slotInfo.AsEsql(builder, blockAlias, indentLevel);

                // Print the field alias for complex expressions that don't produce default alias.
                // Don't print alias for qualified fields as they reproduce their alias.
                // Don't print alias if it's a top level query using SELECT VALUE.
                if (!isTopLevel
                    && (!(slotInfo.SlotValue is QualifiedSlot) || slotInfo.IsEnforcedNotNull))
                {
                    builder.Append(" AS ")
                           .Append(slotInfo.CqlFieldAlias);
                }
                isFirst = false;
            }
            if (addNewLineAfterEachSlot)
            {
                StringUtil.IndentNewLine(builder, indentLevel);
            }
        }

        // <summary>
        // Generates "NewRow(A, B, C, ...)" for all the slots in the block.
        // If <paramref name="isTopLevel" />=true then generates "A" for the only slot that is marked as
        // <see
        //     cref="SlotInfo.IsRequiredByParent" />
        // .
        // </summary>
        protected DbExpression GenerateProjectionCqt(DbExpression row, bool isTopLevel)
        {
            if (isTopLevel)
            {
                Debug.Assert(Slots.Where(slot => slot.IsRequiredByParent).Count() == 1, "Top level projection must project only one slot.");
                return Slots.Where(slot => slot.IsRequiredByParent).Single().AsCqt(row);
            }
            else
            {
                return DbExpressionBuilder.NewRow(
                    Slots.Where(slot => slot.IsRequiredByParent).Select(
                        slot => new KeyValuePair<string, DbExpression>(slot.CqlFieldAlias, slot.AsCqt(row))));
            }
        }

        // <summary>
        // Initializes context positioning in the join tree that owns the <see cref="CqlBlock" />.
        // For more info see <see cref="JoinTreeContext" />.
        // </summary>
        internal void SetJoinTreeContext(IList<string> parentQualifiers, string leafQualifier)
        {
            Debug.Assert(m_joinTreeContext == null, "Join tree context is already set.");
            m_joinTreeContext = new JoinTreeContext(parentQualifiers, leafQualifier);
        }

        // <summary>
        // Searches the input <paramref name="row" /> for the property that represents the current <see cref="CqlBlock" />.
        // In all cases except JOIN, the <paramref name="row" /> is returned as is.
        // In case of JOIN, <paramref name="row" />.JoinVarX.JoinVarY...blockVar is returned.
        // See <see cref="SetJoinTreeContext" /> for more info.
        // </summary>
        internal DbExpression GetInput(DbExpression row)
        {
            return m_joinTreeContext != null ? m_joinTreeContext.FindInput(row) : row;
        }

        internal override void ToCompactString(StringBuilder builder)
        {
            for (var i = 0; i < m_slots.Count; i++)
            {
                StringUtil.FormatStringBuilder(builder, "{0}: ", i);
                m_slots[i].ToCompactString(builder);
                builder.Append(' ');
            }
            m_whereClause.ToCompactString(builder);
        }

        // <summary>
        // The class represents a position of a <see cref="CqlBlock" /> in a join tree.
        // It is expected that the join tree is left-recursive (not balanced) and looks like this:
        // ___J___
        // /       \
        // L3/         \R3
        // /           \
        // __J__           \
        // /     \           \
        // L2/       \R2         \
        // /         \           \
        // _J_          \           \
        // /   \          \           \
        // L1/     \R1        \           \
        // /       \          \           \
        // CqlBlock1   CqlBlock2   CqlBlock3   CqlBlock4
        // Example of <see cref="JoinTreeContext" />s for the <see cref="CqlBlock" />s:
        // block#   m_parentQualifiers   m_indexInParentQualifiers   m_leafQualifier    FindInput(row) = ...
        // 1          (L2, L3)                    0                      L1             row.(L3.L2).L1
        // 2          (L2, L3)                    0                      R1             row.(L3.L2).R1
        // 3          (L2, L3)                    1                      R2             row.(L3).R2
        // 4          (L2, L3)                    2                      R3             row.().R3
        // </summary>
        private sealed class JoinTreeContext
        {
            internal JoinTreeContext(IList<string> parentQualifiers, string leafQualifier)
            {
                DebugCheck.NotNull(parentQualifiers);
                DebugCheck.NotNull(leafQualifier);

                m_parentQualifiers = parentQualifiers;
                m_indexInParentQualifiers = parentQualifiers.Count;
                m_leafQualifier = leafQualifier;
            }

            private readonly IList<string> m_parentQualifiers;
            private readonly int m_indexInParentQualifiers;
            private readonly string m_leafQualifier;

            internal DbExpression FindInput(DbExpression row)
            {
                var cqt = row;
                for (var i = m_parentQualifiers.Count - 1; i >= m_indexInParentQualifiers; --i)
                {
                    cqt = cqt.Property(m_parentQualifiers[i]);
                }
                return cqt.Property(m_leafQualifier);
            }
        }
    }
}
