// 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.Structures
{
    using System.Collections.Generic;
    using System.Data.Entity.Core.Common;
    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.CqlGeneration;
    using System.Data.Entity.Core.Metadata.Edm;
    using System.Diagnostics;
    using System.Linq;
    using System.Text;

    // <summary>
    // A class that corresponds to a path in some extent, e.g., Person, Person.addr, Person.addr.state
    // Empty path represents path to the extent.
    // </summary>
    internal sealed class MemberPath : InternalBase, IEquatable<MemberPath>
    {
        // <summary>
        // The base entity set.
        // </summary>
        private readonly EntitySetBase m_extent;

        // <summary>
        // List of members in the path.
        // </summary>
        private readonly List<EdmMember> m_path;

        internal static readonly IEqualityComparer<MemberPath> EqualityComparer = new Comparer();

        // <summary>
        // Creates a member path that corresponds to <paramref name="path" /> in the <paramref name="extent" /> (or the extent itself).
        // </summary>
        internal MemberPath(EntitySetBase extent, IEnumerable<EdmMember> path)
        {
            m_extent = extent;
            m_path = path.ToList();
        }

        // <summary>
        // Creates a member path that corresponds to the <paramref name="extent" />.
        // </summary>
        internal MemberPath(EntitySetBase extent)
            : this(extent, Enumerable.Empty<EdmMember>())
        {
        }

        // <summary>
        // Creates a path corresponding to <paramref name="extent" />.<paramref name="member" />
        // </summary>
        internal MemberPath(EntitySetBase extent, EdmMember member)
            : this(extent, Enumerable.Repeat(member, 1))
        {
        }

        // <summary>
        // Creates a member path corresponding to the path <paramref name="prefix" />.<paramref name="last" />
        // </summary>
        internal MemberPath(MemberPath prefix, EdmMember last)
        {
            m_extent = prefix.m_extent;
            m_path = new List<EdmMember>(prefix.m_path);
            m_path.Add(last);
        }

        // <summary>
        // Returns the first path item in a non-empty path, otherwise null.
        // </summary>
        internal EdmMember RootEdmMember
        {
            get { return m_path.Count > 0 ? m_path[0] : null; }
        }

        // <summary>
        // Returns the last path item in a non-empty path, otherwise null.
        // </summary>
        internal EdmMember LeafEdmMember
        {
            get { return m_path.Count > 0 ? m_path[m_path.Count - 1] : null; }
        }

        // <summary>
        // For non-empty paths returns name of the last path item, otherwise returns name of <see cref="Extent" />.
        // </summary>
        internal string LeafName
        {
            get
            {
                if (m_path.Count == 0)
                {
                    return m_extent.Name;
                }
                else
                {
                    return LeafEdmMember.Name;
                }
            }
        }

        // <summary>
        // Tells path represents a computed slot.
        // </summary>
        internal bool IsComputed
        {
            get
            {
                if (m_path.Count == 0)
                {
                    return false;
                }
                else
                {
                    return RootEdmMember.IsStoreGeneratedComputed;
                }
            }
        }

        // <summary>
        // Returns the default value the slot represented by the path. If no default value is present, returns null.
        // </summary>
        internal object DefaultValue
        {
            get
            {
                if (m_path.Count == 0)
                {
                    return null;
                }
                Facet facet;
                if (LeafEdmMember.TypeUsage.Facets.TryGetValue(DbProviderManifest.DefaultValueFacetName, false, out facet))
                {
                    return facet.Value;
                }
                return null;
            }
        }

        // <summary>
        // Returns true if slot represented by the path is part of a key.
        // </summary>
        internal bool IsPartOfKey
        {
            get
            {
                if (m_path.Count == 0)
                {
                    return false;
                }
                return MetadataHelper.IsPartOfEntityTypeKey(LeafEdmMember);
            }
        }

        // <summary>
        // Returns true if slot represented by the path is nullable.
        // </summary>
        internal bool IsNullable
        {
            get
            {
                if (m_path.Count == 0)
                {
                    return false;
                }
                return MetadataHelper.IsMemberNullable(LeafEdmMember);
            }
        }

        // <summary>
        // If path corresponds to an entity set (empty path) or an association end (<see cref="Extent" /> is as association set, and path length is 1),
        // returns <see cref="EntitySet" /> associated with the value of the slot represented by this path, otherwise returns null.
        // </summary>
        internal EntitySet EntitySet
        {
            get
            {
                if (m_path.Count == 0)
                {
                    return m_extent as EntitySet;
                }
                else if (m_path.Count == 1)
                {
                    var endMember = RootEdmMember as AssociationEndMember;
                    if (endMember != null)
                    {
                        var result = MetadataHelper.GetEntitySetAtEnd((AssociationSet)m_extent, endMember);
                        return result;
                    }
                }
                return null;
            }
        }

        // <summary>
        // Extent of the path.
        // </summary>
        internal EntitySetBase Extent
        {
            get { return m_extent; }
        }

        // <summary>
        // Returns the type of attribute denoted by the path.
        // For example, member type of Person.addr.zip would be integer. For extent, it is the element type.
        // </summary>
        internal EdmType EdmType
        {
            get
            {
                if (m_path.Count > 0)
                {
                    return LeafEdmMember.TypeUsage.EdmType;
                }
                else
                {
                    return m_extent.ElementType;
                }
            }
        }

        // <summary>
        // Returns Cql field alias generated from the path items.
        // </summary>
        internal string CqlFieldAlias
        {
            get
            {
                var alias = PathToString(true);
                if (false == alias.Contains("_"))
                {
                    // if alias of the member does not contain any "_", we can replace "." with "_" so that we can get a simple identifier.
                    alias = alias.Replace('.', '_');
                }
                var builder = new StringBuilder();
                CqlWriter.AppendEscapedName(builder, alias);
                return builder.ToString();
            }
        }

        // <summary>
        // Returns false iff the path is
        // * A descendant of some nullable property
        // * A descendant of an optional composition/collection
        // * A descendant of a property that does not belong to the basetype/rootype of its parent.
        // </summary>
        internal bool IsAlwaysDefined(Dictionary<EntityType, Set<EntityType>> inheritanceGraph)
        {
            if (m_path.Count == 0)
            {
                // Extents are always defined
                return true;
            }

            var member = m_path.Last();

            //Dont check last member, thats the property we are testing
            for (var i = 0; i < m_path.Count - 1; i++)
            {
                var current = m_path[i];
                // If member is nullable then "this" will not always be defined
                if (MetadataHelper.IsMemberNullable(current))
                {
                    return false;
                }
            }

            //Now check if there are any concrete types other than all subtypes of Type defining this member

            //by definition association types member are always present since they are IDs
            if (m_path[0].DeclaringType is AssociationType)
            {
                return true;
            }

            var entitySetType = m_extent.ElementType as EntityType;
            if (entitySetType == null) //association type
            {
                return true;
            }

            //well, we handle the first case because we don't knwo how to get to subtype (i.e. the edge to avoid)
            var memberDeclaringType = m_path[0].DeclaringType as EntityType;
            var parentType = memberDeclaringType.BaseType as EntityType;

            if (entitySetType.EdmEquals(memberDeclaringType)
                || MetadataHelper.IsParentOf(memberDeclaringType, entitySetType)
                || parentType == null)
            {
                return true;
            }
            else if (!parentType.Abstract
                     && !MetadataHelper.DoesMemberExist(parentType, member))
            {
                return false;
            }

            var result = !RecurseToFindMemberAbsentInConcreteType(parentType, memberDeclaringType, member, entitySetType, inheritanceGraph);
            return result;
        }

        private static bool RecurseToFindMemberAbsentInConcreteType(
            EntityType current, EntityType avoidEdge, EdmMember member, EntityType entitySetType,
            Dictionary<EntityType, Set<EntityType>> inheritanceGraph)
        {
            var edges = inheritanceGraph[current];

            //for each outgoing edge (from current) where the edge is not the one to avoid,
            // navigate depth-first
            foreach (var edge in edges.Where(type => !type.EdmEquals(avoidEdge)))
            {
                //Dont traverse above the EntitySet's Element type
                if (entitySetType.BaseType != null
                    && entitySetType.BaseType.EdmEquals(edge))
                {
                    continue;
                }

                if (!edge.Abstract
                    && !MetadataHelper.DoesMemberExist(edge, member))
                {
                    //found it.. I'm the concrete type that has member absent.
                    return true;
                }

                if (RecurseToFindMemberAbsentInConcreteType(
                    edge, current /*avoid traversing down back here*/, member, entitySetType, inheritanceGraph))
                {
                    //one of the edges reachable from me found it
                    return true;
                }
            }
            //no body found this counter example
            return false;
        }

        // <summary>
        // Determines all the identifiers used in the path and adds them to <paramref name="identifiers" />.
        // </summary>
        internal void GetIdentifiers(CqlIdentifiers identifiers)
        {
            // Get the extent name and extent type name
            identifiers.AddIdentifier(m_extent.Name);
            identifiers.AddIdentifier(m_extent.ElementType.Name);
            foreach (var member in m_path)
            {
                identifiers.AddIdentifier(member.Name);
            }
        }

        // <summary>
        // Returns true iff all members are nullable properties, i.e., if even one of them is non-nullable, returns false.
        // </summary>
        internal static bool AreAllMembersNullable(IEnumerable<MemberPath> members)
        {
            foreach (var path in members)
            {
                if (path.m_path.Count == 0)
                {
                    return false; // Extents are not nullable
                }
                if (path.IsNullable == false)
                {
                    return false;
                }
            }
            return true;
        }

        // <summary>
        // Returns a string that has the list of properties in <paramref name="members" /> (i.e., just the last name) if
        // <paramref
        //     name="fullPath" />
        // is false.
        // Else the <paramref name="fullPath" /> is added.
        // </summary>
        internal static string PropertiesToUserString(IEnumerable<MemberPath> members, bool fullPath)
        {
            var isFirst = true;
            var builder = new StringBuilder();
            foreach (var path in members)
            {
                if (isFirst == false)
                {
                    builder.Append(", ");
                }
                isFirst = false;
                if (fullPath)
                {
                    builder.Append(path.PathToString(false));
                }
                else
                {
                    builder.Append(path.LeafName);
                }
            }
            return builder.ToString();
        }

        // <summary>
        // Given a member path and an alias, returns an eSQL string correspondng to the fully-qualified name
        // <paramref
        //     name="blockAlias" />
        // .path, e.g., T1.Address.Phone.Zip.
        // If a subcomponent belongs to subclass, generates a treat for it, e.g. "TREAT(T1 as Customer).Address".
        // Or even "TREAT(TREAT(T1 AS Customer).Address as USAddress).Zip".
        // </summary>
        internal StringBuilder AsEsql(StringBuilder inputBuilder, string blockAlias)
        {
            // Due to the TREAT stuff, we cannot build incrementally.
            // So we use a local StringBuilder - it should not be that inefficient (one extra copy).
            var builder = new StringBuilder();

            // Add blockAlias as a starting point for blockAlias.member1.member2...
            CqlWriter.AppendEscapedName(builder, blockAlias);

            // Process all items in the path.
            AsCql(
                // accessMember action
                (memberName) =>
                    {
                        builder.Append('.');
                        CqlWriter.AppendEscapedName(builder, memberName);
                    },
                // getKey action
                () =>
                    {
                        builder.Insert(0, "Key(");
                        builder.Append(")");
                    },
                // treatAs action
                (treatAsType) =>
                    {
                        builder.Insert(0, "TREAT(");
                        builder.Append(" AS ");
                        CqlWriter.AppendEscapedTypeName(builder, treatAsType);
                        builder.Append(')');
                    });

            inputBuilder.Append(builder);
            return inputBuilder;
        }

        internal DbExpression AsCqt(DbExpression row)
        {
            var cqt = row;

            // Process all items in the path.
            AsCql(
                // accessMember action
                (memberName) => { cqt = cqt.Property(memberName); },
                // getKey action
                () => { cqt = cqt.GetRefKey(); },
                // treatAs action
                (treatAsType) =>
                    {
                        var typeUsage = TypeUsage.Create(treatAsType);
                        cqt = cqt.TreatAs(typeUsage);
                    });

            return cqt;
        }

        internal void AsCql(Action<string> accessMember, Action getKey, Action<StructuralType> treatAs)
        {
            // Keep track of the previous type so that we can determine if we need to cast or not.
            EdmType prevType = m_extent.ElementType;

            foreach (var member in m_path)
            {
                // If prevType is a ref (e.g., ref to CPerson), we need to get the type that it is pointing to and then look for this member in that type.
                StructuralType prevStructuralType;
                RefType prevRefType;
                if (Helper.IsRefType(prevType))
                {
                    prevRefType = (RefType)prevType;
                    prevStructuralType = prevRefType.ElementType;
                }
                else
                {
                    prevRefType = null;
                    prevStructuralType = (StructuralType)prevType;
                }

                // Check whether the prevType has the present member in it.
                // If not, we will need to cast the prev type to the appropriate subtype.
                var found = MetadataHelper.DoesMemberExist(prevStructuralType, member);

                if (prevRefType != null)
                {
                    // For reference types, the key must be present in the element type itself.
                    // E.g., if we have Ref(CPerson), the key must be present as CPerson.pid or CPerson.Address.Phone.Number (i.e., in a complex type).
                    // Note that it cannot be present in the subtype of address or phone either, i.e., this path better not have any TREATs.
                    // We are at CPerson right now. So if we say Key(CPerson), we will get a row with all the key elements.
                    // Then we can continue going down the path in CPerson

                    Debug.Assert(found, "We did not find the key property in a ref's element type - it cannot be in a subtype");
                    Debug.Assert(MetadataHelper.IsPartOfEntityTypeKey(member), "Member is expected to be a key property");

                    // Emit KEY(current path segment)
                    getKey();
                }
                else if (false == found)
                {
                    // Need to add Treat(... as ...) expression in the beginning.
                    // Note that it does handle cases like TREAT(TREAT(T1 AS Customer).Address as USAddress).Zip

                    Debug.Assert(prevRefType == null, "We do not allow subtyping in key extraction from Refs");

                    // Emit TREAT(current path segment as member.DeclaringType)
                    treatAs(member.DeclaringType);
                }

                // Add the member's access. We had a path "T1.A.B" till now.
                accessMember(member.Name);

                prevType = member.TypeUsage.EdmType;
            }
        }

        public bool Equals(MemberPath right)
        {
            return EqualityComparer.Equals(this, right);
        }

        public override bool Equals(object obj)
        {
            var right = obj as MemberPath;
            if (obj == null)
            {
                return false;
            }
            return Equals(right);
        }

        public override int GetHashCode()
        {
            return EqualityComparer.GetHashCode(this);
        }

        // <summary>
        // Returns true if the member denoted by the path corresponds to a scalar (primitive or enum).
        // </summary>
        internal bool IsScalarType()
        {
            return EdmType.BuiltInTypeKind == BuiltInTypeKind.PrimitiveType ||
                   EdmType.BuiltInTypeKind == BuiltInTypeKind.EnumType;
        }

        internal static IEnumerable<MemberPath> GetKeyMembers(EntitySetBase extent, MemberDomainMap domainMap)
        {
            var extentPath = new MemberPath(extent);
            var keyAttributes = new List<MemberPath>(
                extentPath.GetMembers(
                    extentPath.Extent.ElementType, null /* isScalar */, null /* isConditional */, true /* isPartOfKey */, domainMap));
            Debug.Assert(keyAttributes.Any(), "No key attributes?");
            return keyAttributes;
        }

        internal IEnumerable<MemberPath> GetMembers(
            EdmType edmType, bool? isScalar, bool? isConditional, bool? isPartOfKey, MemberDomainMap domainMap)
        {
            var currentPath = this;
            var structuralType = (StructuralType)edmType;
            foreach (var edmMember in structuralType.Members)
            {
                if (edmMember is AssociationEndMember)
                {
                    // get end's keys
                    foreach (var endKey in new MemberPath(currentPath, edmMember).GetMembers(
                        ((RefType)edmMember.TypeUsage.EdmType).ElementType,
                        isScalar, isConditional, true /*isPartOfKey*/, domainMap))
                    {
                        yield return endKey;
                    }
                }
                var isActuallyScalar = MetadataHelper.IsNonRefSimpleMember(edmMember);
                if (isScalar == null
                    || isScalar == isActuallyScalar)
                {
                    var childProperty = edmMember as EdmProperty;
                    if (childProperty != null)
                    {
                        var isActuallyKey = MetadataHelper.IsPartOfEntityTypeKey(childProperty);
                        if (isPartOfKey == null
                            || isPartOfKey == isActuallyKey)
                        {
                            var childPath = new MemberPath(currentPath, childProperty);
                            var isActuallyConditional = domainMap.IsConditionMember(childPath);
                            if (isConditional == null
                                || isConditional == isActuallyConditional)
                            {
                                yield return childPath;
                            }
                        }
                    }
                }
            }
        }

        // <summary>
        // Returns true if this path and <paramref name="path1" /> are equivalent on the C-side via a referential constraint.
        // </summary>
        internal bool IsEquivalentViaRefConstraint(MemberPath path1)
        {
            var path0 = this;

            // Now check if they are equivalent via referential constraint

            // For example,
            // * Person.pid and PersonAddress.Person.pid are equivalent
            // * Person.pid and PersonAddress.Address.pid are equivalent
            // * Person.pid and Address.pid are equivalent if there is a referential constraint
            // * PersonAddress.Person.pid and PersonAddress.Address.pid are
            //   equivalent if there is a referential constraint

            // In short, Person.pid, Address.pid, PersonAddress.Address.pid,
            // PersonAddress.Person.pid are the same

            if (path0.EdmType is EntityTypeBase
                || path1.EdmType is EntityTypeBase
                ||
                MetadataHelper.IsNonRefSimpleMember(path0.LeafEdmMember) == false
                ||
                MetadataHelper.IsNonRefSimpleMember(path1.LeafEdmMember) == false)
            {
                // If the path corresponds to a top level extent only, ignore
                // it. Or if it is not a scalar
                return false;
            }

            var assocSet0 = path0.Extent as AssociationSet;
            var assocSet1 = path1.Extent as AssociationSet;
            var entitySet0 = path0.Extent as EntitySet;
            var entitySet1 = path1.Extent as EntitySet;
            var result = false;

            if (assocSet0 != null
                && assocSet1 != null)
            {
                // PersonAddress.Person.pid and PersonAddress.Address.pid case
                // Check if they are the same association or not
                if (assocSet0.Equals(assocSet1) == false)
                {
                    return false;
                }
                result = AreAssocationEndPathsEquivalentViaRefConstraint(path0, path1, assocSet0);
            }
            else if (entitySet0 != null
                     && entitySet1 != null)
            {
                // Person.pid, Address.pid case
                // Find all the associations between the two sets. If the
                // fields are equivalent via any association + referential
                // constraint, return true
                var assocSets = MetadataHelper.GetAssociationsForEntitySets(entitySet0, entitySet1);
                foreach (var assocSet in assocSets)
                {
                    // For Person.pid, get PersonAddress.Person.pid or
                    var assocEndPath0 = path0.GetCorrespondingAssociationPath(assocSet);
                    var assocEndPath1 = path1.GetCorrespondingAssociationPath(assocSet);
                    if (AreAssocationEndPathsEquivalentViaRefConstraint(assocEndPath0, assocEndPath1, assocSet))
                    {
                        result = true;
                        break;
                    }
                }
            }
            else
            {
                // One of them is an assocSet and the other is an entity set
                var assocSet = assocSet0 != null ? assocSet0 : assocSet1;
                var entitySet = entitySet0 != null ? entitySet0 : entitySet1;
                Debug.Assert(
                    assocSet != null && entitySet != null,
                    "One set must be association and the other must be entity set");

                var assocEndPathA = path0.Extent is AssociationSet ? path0 : path1;
                var entityPath = path0.Extent is EntitySet ? path0 : path1;
                var assocEndPathB = entityPath.GetCorrespondingAssociationPath(assocSet);
                if (assocEndPathB == null)
                {
                    //An EntitySet might participate in multiple AssociationSets
                    //and this might not be the association set that defines the expected referential
                    //constraint
                    //Return false since this does not have any referential constraint specified
                    result = false;
                }
                else
                {
                    result = AreAssocationEndPathsEquivalentViaRefConstraint(assocEndPathA, assocEndPathB, assocSet);
                }
            }

            return result;
        }

        // <summary>
        // Returns true if <paramref name="assocPath0" /> and <paramref name="assocPath1" /> are equivalent via a referential constraint in
        // <paramref
        //     name="assocSet" />
        // .
        // Requires: <paramref name="assocPath0" /> and <paramref name="assocPath1" /> correspond to paths in
        // <paramref
        //     name="assocSet" />
        // .
        // </summary>
        private static bool AreAssocationEndPathsEquivalentViaRefConstraint(
            MemberPath assocPath0,
            MemberPath assocPath1,
            AssociationSet assocSet)
        {
            Debug.Assert(
                assocPath0.Extent.Equals(assocSet) && assocPath1.Extent.Equals(assocSet),
                "Extent for paths must be assocSet");

            var end0 = assocPath0.RootEdmMember as AssociationEndMember;
            var end1 = assocPath1.RootEdmMember as AssociationEndMember;
            var property0 = assocPath0.LeafEdmMember as EdmProperty;
            var property1 = assocPath1.LeafEdmMember as EdmProperty;

            if (end0 == null
                || end1 == null
                || property0 == null
                || property1 == null)
            {
                return false;
            }

            // Now check if these fields are connected via a referential constraint
            var assocType = assocSet.ElementType;
            var foundConstraint = false;

            foreach (var constraint in assocType.ReferentialConstraints)
            {
                var isFrom0 = end0.Name == constraint.FromRole.Name &&
                              end1.Name == constraint.ToRole.Name;
                var isFrom1 = end1.Name == constraint.FromRole.Name &&
                              end0.Name == constraint.ToRole.Name;

                if (isFrom0 || isFrom1)
                {
                    // Found an RI for the two sets. Make sure that the properties are at the same ordinal

                    // isFrom0 is true when end0 corresponds to FromRole and end1 to ToRole
                    var properties0 = isFrom0 ? constraint.FromProperties : constraint.ToProperties;
                    var properties1 = isFrom0 ? constraint.ToProperties : constraint.FromProperties;
                    var indexForPath0 = properties0.IndexOf(property0);
                    var indexForPath1 = properties1.IndexOf(property1);
                    if (indexForPath0 == indexForPath1
                        && indexForPath0 != -1)
                    {
                        foundConstraint = true;
                        break;
                    }
                }
            }
            return foundConstraint;
        }

        // <summary>
        // Returns the member path corresponding to that field in the <paramref name="assocSet" />. E.g., given Address.pid, returns PersonAddress.Address.pid.
        // For self-associations, such as ManagerEmployee with referential constraints (and we have
        // [ManagerEmployee.Employee.mid, ManagerEmployee.Employee.eid, ManagerEmployee.Manager.mid]), given Employee.mid, returns
        // ManagerEmployee.Employee.mid or ManagerEmployee.Manager.mid
        // Note: the path need not correspond to a key field of an entity set <see cref="Extent" />.
        // </summary>
        private MemberPath GetCorrespondingAssociationPath(AssociationSet assocSet)
        {
            Debug.Assert(Extent is EntitySet, "path must be in the context of an entity set");

            // Find the end corresponding to the entity set
            var end = MetadataHelper.GetSomeEndForEntitySet(assocSet, m_extent);
            // An EntitySet might participate in multiple AssociationSets and
            // this might not be the association set that defines the expected referential constraint.
            if (end == null)
            {
                return null;
            }
            // Create the new members using the end
            var newMembers = new List<EdmMember>();
            newMembers.Add(end);
            newMembers.AddRange(m_path);
            // The extent is the assocSet
            var result = new MemberPath(assocSet, newMembers);
            return result;
        }

        // <summary>
        // If member path identifies a relationship end, return its scope. Otherwise, returns null.
        // </summary>
        internal EntitySet GetScopeOfRelationEnd()
        {
            if (m_path.Count == 0)
            {
                return null;
            }

            var relationEndMember = LeafEdmMember as AssociationEndMember;
            if (relationEndMember == null)
            {
                return null;
            }

            // Yes, it's a reference, determine its entity set refScope
            var associationSet = (AssociationSet)m_extent;
            var result = MetadataHelper.GetEntitySetAtEnd(associationSet, relationEndMember);
            return result;
        }

        // <summary>
        // Returns a string of the form "a.b.c" that corresponds to the items in the path. This string can be used for tests or localization.
        // If <paramref name="forAlias" />=true, we return a string that is relevant for Cql aliases, else we return the exact path.
        // </summary>
        internal string PathToString(bool? forAlias)
        {
            var builder = new StringBuilder();

            if (forAlias != null)
            {
                if (forAlias == true)
                {
                    // For the 0th entry, we just choose the type of the element in
                    // which the first entry belongs, e.g., if Addr belongs to CCustomer,
                    // we choose CCustomer and not CPerson. 
                    if (m_path.Count == 0)
                    {
                        var type = m_extent.ElementType;
                        return type.Name;
                    }
                    builder.Append(m_path[0].DeclaringType.Name); // Get CCustomer here
                }
                else
                {
                    // Append the extent name
                    builder.Append(m_extent.Name);
                }
            }

            // Just join the path using "."
            for (var i = 0; i < m_path.Count; i++)
            {
                builder.Append('.');
                builder.Append(m_path[i].Name);
            }
            return builder.ToString();
        }

        // <summary>
        // Returns a human-readable string corresponding to the path.
        // </summary>
        internal override void ToCompactString(StringBuilder builder)
        {
            builder.Append(PathToString(false));
        }

        internal void ToCompactString(StringBuilder builder, string instanceToken)
        {
            builder.Append(instanceToken + PathToString(null));
        }

        private sealed class Comparer : IEqualityComparer<MemberPath>
        {
            public bool Equals(MemberPath left, MemberPath right)
            {
                if (ReferenceEquals(left, right))
                {
                    return true;
                }
                // One of them is non-null at least. So if the other one is
                // null, we cannot be equal
                if (left == null
                    || right == null)
                {
                    return false;
                }
                // Both are non-null at this point
                // Checks that the paths are equal component-wise
                if (left.m_extent.Equals(right.m_extent) == false
                    || left.m_path.Count != right.m_path.Count)
                {
                    return false;
                }

                for (var i = 0; i < left.m_path.Count; i++)
                {
                    // Comparing MemberMetadata -- can use Equals
                    if (false == left.m_path[i].Equals(right.m_path[i]))
                    {
                        return false;
                    }
                }
                return true;
            }

            public int GetHashCode(MemberPath key)
            {
                var result = key.m_extent.GetHashCode();
                foreach (var member in key.m_path)
                {
                    result ^= member.GetHashCode();
                }
                return result;
            }
        }
    }
}
