// 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.Update.Internal
{
    using System.Collections.Generic;
    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.Metadata.Edm;
    using System.Data.Entity.Core.Metadata.Edm.Provider;
    using System.Data.Entity.Resources;
    using System.Data.Entity.Utilities;
    using System.Diagnostics;
    using System.Globalization;
    using System.Linq;

    /// <summary>
    ///     This class implements compilation of DML operation requests to some
    ///     format (e.g. canonical query tree or T-SQL)
    /// </summary>
    internal sealed class UpdateCompiler
    {
        /// <summary>
        ///     Initialize an update compiler.
        /// </summary>
        /// <param name="translator"> Update context. </param>
        internal UpdateCompiler(UpdateTranslator translator)
        {
            m_translator = translator;
        }

        internal readonly UpdateTranslator m_translator;
        private const string s_targetVarName = "target";

        /// <summary>
        ///     Builds a delete command.
        /// </summary>
        /// <param name="oldRow"> Value of the row being deleted. </param>
        /// <param name="processor"> Context for the table containing row. </param>
        /// <returns> Delete command. </returns>
        internal UpdateCommand BuildDeleteCommand(PropagatorResult oldRow, TableChangeProcessor processor)
        {
            // If we're deleting a row, the row must always be touched
            var rowMustBeTouched = true;

            // Initialize DML command tree
            var target = GetTarget(processor);

            // Create delete predicate
            var predicate = BuildPredicate(target, oldRow, null, processor, ref rowMustBeTouched);
            var commandTree = new DbDeleteCommandTree(m_translator.MetadataWorkspace, DataSpace.SSpace, target, predicate);

            // Set command
            // Initialize delete command
            UpdateCommand command = new DynamicUpdateCommand(
                processor, m_translator, ModificationOperator.Delete, oldRow, null, commandTree, null);

            return command;
        }

        /// <summary>
        ///     Builds an update command.
        /// </summary>
        /// <param name="oldRow"> Old value of the row being updated. </param>
        /// <param name="newRow"> New value for the row being updated. </param>
        /// <param name="processor"> Context for the table containing row. </param>
        /// <returns> Update command. </returns>
        internal UpdateCommand BuildUpdateCommand(
            PropagatorResult oldRow,
            PropagatorResult newRow, TableChangeProcessor processor)
        {
            // If we're updating a row, the row may not need to be touched (e.g., no concurrency validation required)
            var rowMustBeTouched = false;

            var target = GetTarget(processor);

            // Create set clauses and returning parameter
            Dictionary<int, string> outputIdentifiers;
            DbExpression returning;
            var setClauses = new List<DbModificationClause>();
            foreach (var clause in BuildSetClauses(
                target, newRow, oldRow, processor, /* insertMode */ false, out outputIdentifiers, out returning,
                ref rowMustBeTouched))
            {
                setClauses.Add(clause);
            }

            // Construct predicate identifying the row to modify
            var predicate = BuildPredicate(target, oldRow, newRow, processor, ref rowMustBeTouched);

            if (0 == setClauses.Count)
            {
                if (rowMustBeTouched)
                {
                    var stateEntries = new List<IEntityStateEntry>();
                    stateEntries.AddRange(
                        SourceInterpreter.GetAllStateEntries(
                            oldRow, m_translator, processor.Table));
                    stateEntries.AddRange(
                        SourceInterpreter.GetAllStateEntries(
                            newRow, m_translator, processor.Table));
                    if (stateEntries.All(it => (it.State == EntityState.Unchanged)))
                    {
                        rowMustBeTouched = false;
                    }
                }

                // Determine if there is nothing to do (i.e., no values to set, 
                // no computed columns, and no concurrency validation required)
                if (!rowMustBeTouched)
                {
                    return null;
                }
            }

            // Initialize DML command tree
            var commandTree =
                new DbUpdateCommandTree(
                    m_translator.MetadataWorkspace, DataSpace.SSpace, target, predicate, setClauses.AsReadOnly(), returning);

            // Create command
            UpdateCommand command = new DynamicUpdateCommand(
                processor, m_translator, ModificationOperator.Update, oldRow, newRow, commandTree, outputIdentifiers);

            return command;
        }

        /// <summary>
        ///     Builds insert command.
        /// </summary>
        /// <param name="newRow"> Row to insert. </param>
        /// <param name="processor"> Context for the table we're inserting into. </param>
        /// <returns> Insert command. </returns>
        internal UpdateCommand BuildInsertCommand(PropagatorResult newRow, TableChangeProcessor processor)
        {
            // Bind the insert target
            var target = GetTarget(processor);

            // Create set clauses and returning parameter
            Dictionary<int, string> outputIdentifiers;
            DbExpression returning;
            var rowMustBeTouched = true; // for inserts, the row must always be touched
            var setClauses = new List<DbModificationClause>();
            foreach (var clause in BuildSetClauses(
                target, newRow, null, processor, /* insertMode */ true, out outputIdentifiers,
                out returning, ref rowMustBeTouched))
            {
                setClauses.Add(clause);
            }

            // Initialize DML command tree
            var commandTree =
                new DbInsertCommandTree(m_translator.MetadataWorkspace, DataSpace.SSpace, target, setClauses.AsReadOnly(), returning);

            // Create command
            UpdateCommand command = new DynamicUpdateCommand(
                processor, m_translator, ModificationOperator.Insert, null, newRow, commandTree, outputIdentifiers);

            return command;
        }

        /// <summary>
        ///     Determines column/value used to set values for a row.
        /// </summary>
        /// <remarks>
        ///     The following columns are not included in the result:
        ///     <list>
        ///         <item>Keys in non-insert operations (keys are only set for inserts).</item>
        ///         <item>Values flagged 'preserve' (these are values the propagator claims are untouched).</item>
        ///         <item>Server generated values.</item>
        ///     </list>
        /// </remarks>
        /// <param name="target"> Expression binding representing the table. </param>
        /// <param name="row"> Row containing values to set. </param>
        /// <param name="processor"> Context for table. </param>
        /// <param name="insertMode"> Determines whether key columns and 'preserve' columns are omitted from the list. </param>
        /// <param name="outputIdentifiers"> Dictionary listing server generated identifiers. </param>
        /// <param name="returning"> DbExpression describing result projection for server generated values. </param>
        /// <param name="rowMustBeTouched"> Indicates whether the row must be touched because it produces a value (e.g. computed) </param>
        /// <returns> Column value pairs. </returns>
        private IEnumerable<DbModificationClause> BuildSetClauses(
            DbExpressionBinding target, PropagatorResult row,
            PropagatorResult originalRow, TableChangeProcessor processor, bool insertMode, out Dictionary<int, string> outputIdentifiers,
            out DbExpression returning,
            ref bool rowMustBeTouched)
        {
            var setClauses = new Dictionary<EdmProperty, PropagatorResult>();
            var returningArguments = new List<KeyValuePair<string, DbExpression>>();
            outputIdentifiers = new Dictionary<int, string>();

            // Determine which flags indicate a property should be omitted from the set list.
            var omitMask = insertMode
                               ? PropagatorFlags.NoFlags
                               : PropagatorFlags.Preserve | PropagatorFlags.Unknown;

            for (var propertyOrdinal = 0; propertyOrdinal < processor.Table.ElementType.Properties.Count; propertyOrdinal++)
            {
                var property = processor.Table.ElementType.Properties[propertyOrdinal];

                // Type members and result values are ordinally aligned
                var propertyResult = row.GetMemberValue(propertyOrdinal);

                if (PropagatorResult.NullIdentifier
                    != propertyResult.Identifier)
                {
                    // retrieve principal value
                    propertyResult = propertyResult.ReplicateResultWithNewValue(
                        m_translator.KeyManager.GetPrincipalValue(propertyResult));
                }

                var omitFromSetList = false;

                Debug.Assert(propertyResult.IsSimple);

                // Determine if this is a key value
                var isKey = false;
                for (var i = 0; i < processor.KeyOrdinals.Length; i++)
                {
                    if (processor.KeyOrdinals[i] == propertyOrdinal)
                    {
                        isKey = true;
                        break;
                    }
                }

                // check if this value should be omitted
                var flags = PropagatorFlags.NoFlags;
                if (!insertMode && isKey)
                {
                    // Keys are only set for inserts
                    omitFromSetList = true;
                }
                else
                {
                    // See if this value has been marked up with some context. If so, add the flag information
                    // from the markup. Markup includes information about whether the property is a concurrency value,
                    // whether it is known (it may be a property that is preserved across an update for instance)
                    flags |= propertyResult.PropagatorFlags;
                }

                // Determine if this value is server-generated
                var genPattern = MetadataHelper.GetStoreGeneratedPattern(property);
                var isServerGen = genPattern == StoreGeneratedPattern.Computed ||
                                  (insertMode && genPattern == StoreGeneratedPattern.Identity);
                if (isServerGen)
                {
                    var propertyExpression = target.Variable.Property(property);
                    returningArguments.Add(new KeyValuePair<string, DbExpression>(property.Name, propertyExpression));

                    // check if this is a server generated identifier
                    var identifier = propertyResult.Identifier;
                    if (PropagatorResult.NullIdentifier != identifier)
                    {
                        if (m_translator.KeyManager.HasPrincipals(identifier))
                        {
                            throw new InvalidOperationException(Strings.Update_GeneratedDependent(property.Name));
                        }
                        outputIdentifiers.Add(identifier, property.Name);

                        // If this property maps an identifier (in the update pipeline) it may
                        // also be a store key. If so, the pattern had better be "Identity"
                        // since otherwise we're dealing with a mutable key.
                        if (genPattern != StoreGeneratedPattern.Identity
                            &&
                            processor.IsKeyProperty(propertyOrdinal))
                        {
                            throw new NotSupportedException(
                                Strings.Update_NotSupportedComputedKeyColumn(
                                    EdmProviderManifest.StoreGeneratedPatternFacetName,
                                    XmlConstants.Computed,
                                    XmlConstants.Identity,
                                    property.Name,
                                    property.DeclaringType.FullName));
                        }
                    }
                }

                if (PropagatorFlags.NoFlags
                    != (flags & (omitMask)))
                {
                    // column value matches "omit" pattern, therefore should not be set
                    omitFromSetList = true;
                }
                else if (isServerGen)
                {
                    // column value does not match "omit" pattern, but it is server generated
                    // so it cannot be set
                    omitFromSetList = true;

                    // if the row has a modified value overridden by server gen,
                    // it must still be touched in order to retrieve the value
                    rowMustBeTouched = true;
                }

                // make the user is not updating an identity value
                if (!omitFromSetList
                    && !insertMode
                    && genPattern == StoreGeneratedPattern.Identity)
                {
                    //throw the error only if the value actually changed
                    Debug.Assert(originalRow != null, "Updated records should have a original row");
                    var originalPropertyResult = originalRow.GetMemberValue(propertyOrdinal);
                    Debug.Assert(originalPropertyResult.IsSimple, "Server Gen property that is not primitive?");
                    Debug.Assert(propertyResult.IsSimple, "Server Gen property that is not primitive?");

                    if (!ByValueEqualityComparer.Default.Equals(originalPropertyResult.GetSimpleValue(), propertyResult.GetSimpleValue()))
                    {
                        throw new InvalidOperationException(
                            Strings.Update_ModifyingIdentityColumn(
                                XmlConstants.Identity,
                                property.Name,
                                property.DeclaringType.FullName));
                    }
                    else
                    {
                        omitFromSetList = true;
                    }
                }

                if (!omitFromSetList)
                {
                    setClauses.Add(property, propertyResult);
                }
            }

            // Construct returning projection
            if (0 < returningArguments.Count)
            {
                returning = DbExpressionBuilder.NewRow(returningArguments);
            }
            else
            {
                returning = null;
            }

            // Construct clauses corresponding to the set clauses
            var result = new List<DbModificationClause>(setClauses.Count);
            foreach (var setClause in setClauses)
            {
                var property = setClause.Key;

                result.Add(
                    new DbSetClause(
                        GeneratePropertyExpression(target, setClause.Key),
                        GenerateValueExpression(setClause.Key, setClause.Value)));
            }

            return result;
        }

        /// <summary>
        ///     Determines predicate used to identify a row in a table.
        /// </summary>
        /// <remarks>
        ///     Columns are included in the list when:
        ///     <list>
        ///         <item>They are keys for the table</item>
        ///         <item>They are concurrency values</item>
        ///     </list>
        /// </remarks>
        /// <param name="target"> Expression binding representing the table containing the row </param>
        /// <param name="referenceRow"> Values for the row being located. </param>
        /// <param name="current"> Values being updated (may be null). </param>
        /// <param name="processor"> Context for the table containing the row. </param>
        /// <param name="rowMustBeTouched"> Output parameter indicating whether a row must be touched (whether it's being modified or not) because it contains a concurrency value </param>
        /// <returns> Column/value pairs. </returns>
        private DbExpression BuildPredicate(
            DbExpressionBinding target, PropagatorResult referenceRow, PropagatorResult current,
            TableChangeProcessor processor, ref bool rowMustBeTouched)
        {
            var whereClauses = new Dictionary<EdmProperty, PropagatorResult>();

            // add all concurrency tokens (note that keys are always concurrency tokens as well)
            var propertyOrdinal = 0;
            foreach (var member in processor.Table.ElementType.Properties)
            {
                // members and result values are ordinally aligned
                var expectedValue = referenceRow.GetMemberValue(propertyOrdinal);
                var newValue = null == current ? null : current.GetMemberValue(propertyOrdinal);

                // check if the rowMustBeTouched value should be set to true (if it isn't already
                // true and we've come across a concurrency value)
                if (!rowMustBeTouched
                    &&
                    (HasFlag(expectedValue, PropagatorFlags.ConcurrencyValue) ||
                     HasFlag(newValue, PropagatorFlags.ConcurrencyValue)))
                {
                    rowMustBeTouched = true;
                }

                // determine if this is a concurrency value
                if (!whereClauses.ContainsKey(member)
                    && // don't add to the set clause twice
                    (HasFlag(expectedValue, PropagatorFlags.ConcurrencyValue | PropagatorFlags.Key) ||
                     HasFlag(newValue, PropagatorFlags.ConcurrencyValue | PropagatorFlags.Key))) // tagged as concurrency value
                {
                    whereClauses.Add(member, expectedValue);
                }
                propertyOrdinal++;
            }

            // Build a binary AND expression tree from the clauses
            DbExpression predicate = null;
            foreach (var clause in whereClauses)
            {
                var clauseExpression = GenerateEqualityExpression(target, clause.Key, clause.Value);
                if (null == predicate)
                {
                    predicate = clauseExpression;
                }
                else
                {
                    predicate = predicate.And(clauseExpression);
                }
            }

            Debug.Assert(null != predicate, "some predicate term must exist");

            return predicate;
        }

        // Effects: given a "clause" in the form of a property/value pair, produces an equality expression. If the
        // value is null, creates an IsNull expression
        // Requires: all arguments are set
        private DbExpression GenerateEqualityExpression(DbExpressionBinding target, EdmProperty property, PropagatorResult value)
        {
            DebugCheck.NotNull(target);
            DebugCheck.NotNull(property);
            DebugCheck.NotNull(value);

            var propertyExpression = GeneratePropertyExpression(target, property);
            var valueExpression = GenerateValueExpression(property, value);
            if (valueExpression.ExpressionKind
                == DbExpressionKind.Null)
            {
                return propertyExpression.IsNull();
            }
            return propertyExpression.Equal(valueExpression);
        }

        // Effects: given a property, produces a property expression
        // Requires: all arguments are set
        private static DbExpression GeneratePropertyExpression(DbExpressionBinding target, EdmProperty property)
        {
            DebugCheck.NotNull(target);
            DebugCheck.NotNull(property);

            return target.Variable.Property(property);
        }

        // Effects: given a propagator result, produces a constant expression describing that value.
        // Requires: all arguments are set, and the value must be simple (scalar)
        private DbExpression GenerateValueExpression(EdmProperty property, PropagatorResult value)
        {
            DebugCheck.NotNull(property);
            DebugCheck.NotNull(value);

            Debug.Assert(value.IsSimple);
            Debug.Assert(Helper.IsPrimitiveType(property.TypeUsage.EdmType), "Properties in SSpace should be primitive.");

            if (value.IsNull)
            {
                return Helper.GetModelTypeUsage(property).Null();
            }
            var principalValue = m_translator.KeyManager.GetPrincipalValue(value);

            if (Convert.IsDBNull(principalValue))
            {
                // although the result may be marked non-null (because it is an identifier) it is possible
                // there is no corresponding real value for the property yet
                return Helper.GetModelTypeUsage(property).Null();
            }
            else
            {
                // At this point we have already done any needed type checking and we potentially translated the type 
                // of the property to the SSpace (the property parameter is a property in the SSpace). However the value 
                // is here is a CSpace value. As a result it does not have to match the type of the property in SSpace.
                // Two cases here are:
                // - the type in CSpace does not exactly match the type in the SSpace (but is promotable)
                // - the type in CSpace is enum type and in this case it never matches the type in SSpace where enum type  
                //   does not exist
                // Since the types have already been checked it is safe just to convert the value from CSpace to the type
                // from SSpace.

                Debug.Assert(Nullable.GetUnderlyingType(principalValue.GetType()) == null, "Unexpected nullable type.");

                var propertyType = Helper.GetModelTypeUsage(property);
                var principalType = principalValue.GetType();

                if (principalType.IsEnum)
                {
                    principalValue = Convert.ChangeType(principalValue, principalType.GetEnumUnderlyingType(), CultureInfo.InvariantCulture);
                }

                var columnClrEquivalentType = ((PrimitiveType)propertyType.EdmType).ClrEquivalentType;

                if (principalType != columnClrEquivalentType)
                {
                    principalValue = Convert.ChangeType(principalValue, columnClrEquivalentType, CultureInfo.InvariantCulture);
                }

                return propertyType.Constant(principalValue);
            }
        }

        // Effects: returns true iff. the input propagator result has some flag defined in "flags"
        // Requires: input is set
        private static bool HasFlag(PropagatorResult input, PropagatorFlags flags)
        {
            if (null == input)
            {
                return false;
            }
            return (PropagatorFlags.NoFlags != (flags & input.PropagatorFlags));
        }

        // Effects: initializes the target (table being modified) for the given DML command tree according
        // to the table managed by the processor.
        // Requires: all arguments set
        private static DbExpressionBinding GetTarget(TableChangeProcessor processor)
        {
            DebugCheck.NotNull(processor);

            // use a fixed var name since the command trees all have exactly one binding
            return processor.Table.Scan().BindAs(s_targetVarName);
        }
    }
}
