// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using Microsoft.Build.Collections;
using Microsoft.Build.Construction;
using Microsoft.Build.ObjectModelRemoting;
using Microsoft.Build.Shared;

#nullable disable

namespace Microsoft.Build.Evaluation
{
    /// <summary>
    /// An evaluated design-time metadatum.
    /// Parented either by a ProjectItemDefinition or a ProjectItem.
    /// </summary>
    /// <remarks>
    /// Never used to represent built-in metadata, like %(Filename). There is always a backing XML object.
    /// </remarks>
    [DebuggerDisplay("{Name}={EvaluatedValue} [{_xml.Value}]")]
    public class ProjectMetadata : IEquatable<ProjectMetadata>, IMetadatum
    {
        /// <summary>
        /// Parent item or item definition that this metadatum lives in.
        /// ProjectMetadata's always live in a project and always have a parent.
        /// The project can be gotten from this parent.
        /// Used to evaluate any updates.
        /// </summary>
        private readonly IProjectMetadataParent _parent;

        /// <summary>
        /// Backing XML metadata.
        /// Can never be null.
        /// </summary>
        private readonly ProjectMetadataElement _xml;

        /// <summary>
        /// Evaluated value
        /// </summary>
        private string _evaluatedValueEscaped;

        /// <summary>
        /// Any immediately previous metadatum (from item definition or item) that was overridden by this one during evaluation.
        /// This would include all metadata with the same name that lie above in the logical
        /// project file, who are on item definitions of the same type, and whose conditions evaluated to true.
        /// If this metadatum is on an item, it would include any previous metadatum with the same name on the same item whose condition
        /// evaluated to true, and following that any item definition metadata.
        /// If there are none above this is null.
        /// If the project has not been reevaluated since the last modification this value may be incorrect.
        /// </summary>
        private ProjectMetadata _predecessor;

        /// <summary>
        /// External projects support
        /// </summary>
        internal ProjectMetadata(object parent, ProjectMetadataElement xml)
        {
            ErrorUtilities.VerifyThrowArgumentNull(parent);
            ErrorUtilities.VerifyThrowArgumentNull(xml);

            _parent = (IProjectMetadataParent)parent;
            _xml = xml;
        }

        /// <summary>
        /// Creates a metadata backed by XML.
        /// Constructed during evaluation of a project.
        /// </summary>
        internal ProjectMetadata(IProjectMetadataParent parent, ProjectMetadataElement xml, string evaluatedValueEscaped, ProjectMetadata predecessor)
        {
            ErrorUtilities.VerifyThrowArgumentNull(parent);
            ErrorUtilities.VerifyThrowArgumentNull(xml);
            ErrorUtilities.VerifyThrowArgumentNull(evaluatedValueEscaped);

            _parent = parent;
            _xml = xml;
            _evaluatedValueEscaped = evaluatedValueEscaped;
            _predecessor = predecessor;
        }

        internal virtual ProjectMetadataLink Link => null;

        /// <summary>
        /// Name of the metadata
        /// </summary>
        public string Name
        {
            [DebuggerStepThrough]
            get
            { return _xml.Name; }
        }

        /// <summary>
        /// Gets the evaluated metadata value.
        /// Cannot be set directly: only the unevaluated value can be set.
        /// Is never null.
        /// </summary>
        public string EvaluatedValue
        {
            [DebuggerStepThrough]
            get
            { return EscapingUtilities.UnescapeAll(EvaluatedValueEscaped); }
        }

        /// <summary>
        /// Gets or sets the unevaluated metadata value.
        ///
        /// As well as updating the unevaluated value, the setter updates the evaluated value, but does not affect anything else in the project until reevaluation. For example,
        ///     --if a piece of metadata named "m" is modified on item of type "i", it does not affect "j" which is evaluated from "@(j->'%(m)')" until reevaluation.
        ///     --if the unevaluated value of "m" is set to something that is modified by evaluation, such as "$(p)", the evaluated value will be set to "$(p)" until reevaluation.
        /// This is a convenience that it is understood does not necessarily leave the project in a perfectly self consistent state.
        ///
        /// Setting metadata through a ProjectItem may cause the underlying ProjectItemElement to be split, if it originated with an itemlist, wildcard, or semicolon expression,
        /// because it was clear that the caller intended to only affect that particular item.
        /// Setting metadata through a ProjectMetadata does not cause any splitting, because we assume the caller presumably intends to affect all items using the underlying
        /// ProjectMetadataElement. At least, this seems a reasonable assumption, and it avoids the need for metadata to hold a pointer to their containing items.
        /// </summary>
        /// <remarks>
        /// The containing project will be dirtied by the XML modification.  Unevaluated values are assumed to be passed in escaped as necessary.
        /// </remarks>
        public string UnevaluatedValue
        {
            [DebuggerStepThrough]
            get
            {
                return _xml.Value;
            }

            [DebuggerStepThrough]
            set
            {
                ErrorUtilities.VerifyThrowArgumentNull(value, "value");
                Project.VerifyThrowInvalidOperationNotImported(_xml.ContainingProject);
                ErrorUtilities.VerifyThrowInvalidOperation(_xml.Parent?.Parent?.Parent != null, "OM_ObjectIsNoLongerActive");

                if (String.Equals(_xml.Value, value, StringComparison.Ordinal))
                {
                    return;
                }

                _xml.Value = value;

                if (_evaluatedValueEscaped != null)
                {
                    // Clear out the current value of this metadata, so the new value can't refer to the old one.
                    // The expansion call below otherwise passes in the parent item's metadata - including this one's
                    // current value.
                    _evaluatedValueEscaped = String.Empty;

                    _evaluatedValueEscaped = _parent.Project.ExpandMetadataValueBestEffortLeaveEscaped(_parent, value, Location);
                }
            }
        }

        internal IProjectMetadataParent Parent => _parent;

        /// <summary>
        /// Backing XML metadata.
        /// Can never be null.
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public ProjectMetadataElement Xml
        {
            [DebuggerStepThrough]
            get
            { return _xml; }
        }

        /// <summary>
        /// Project that this metadatum lives in.
        /// ProjectMetadata's always live in a project.
        /// </summary>
        public Project Project
        {
            [DebuggerStepThrough]
            get
            { return _parent.Project; }
        }

        /// <summary>
        /// The item type of the parent item definition or item.
        /// </summary>
        public string ItemType
        {
            get { return _parent.ItemType; }
        }

        /// <summary>
        /// Any immediately previous metadatum (from item definition or item) that was overridden by this one during evaluation.
        /// This would include all metadata with the same name that lie above in the logical
        /// project file, who are on item definitions of the same type, and whose conditions evaluated to true.
        /// If this metadatum is on an item, it would include any previous metadatum with the same name on the same item whose condition
        /// evaluated to true, and following that any item definition metadata.
        /// If there are none above this is null.
        /// If the project has not been reevaluated since the last modification this value may be incorrect.
        /// </summary>
        public ProjectMetadata Predecessor
        {
            [DebuggerStepThrough]
            get
            { return Link != null ? Link.Predecessor : _predecessor; }
        }

        /// <summary>
        /// If the metadatum originated in an imported file, returns true.
        /// Otherwise returns false.
        /// </summary>
        public bool IsImported
        {
            get
            {
                bool isImported = !Object.ReferenceEquals(_xml.ContainingProject, _parent.Project.Xml);

                return isImported;
            }
        }

        /// <summary>
        /// Location of the element
        /// </summary>
        public ElementLocation Location
        {
            get { return _xml.Location; }
        }

        /// <summary>
        /// Location of the condition attribute
        /// </summary>
        public ElementLocation ConditionLocation
        {
            get { return _xml.ConditionLocation; }
        }

        /// <summary>
        /// Implementation of IKeyed exposing the metadata name, so metadata
        /// can be put in a dictionary conveniently.
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        string IKeyed.Key
        {
            [DebuggerStepThrough]
            get
            { return Name; }
        }

        /// <summary>
        /// Implementation of IValued
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        string IValued.EscapedValue
        {
            [DebuggerStepThrough]
            get
            { return EvaluatedValueEscaped; }
        }

        /// <summary>
        /// Gets the evaluated metadata value.
        /// Cannot be set directly: only the unevaluated value can be set.
        /// Is never null.
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        internal string EvaluatedValueEscaped
        {
            [DebuggerStepThrough]
            get => Link != null ? Link.EvaluatedValueEscaped : _evaluatedValueEscaped;
        }

        #region IEquatable<ProjectMetadata> Members

        /// <summary>
        /// Compares this metadata to another for equivalence.
        /// </summary>
        /// <param name="other">The other metadata</param>
        /// <returns>True if they are equivalent, false otherwise.</returns>
        bool IEquatable<ProjectMetadata>.Equals(ProjectMetadata other)
        {
            if (Object.ReferenceEquals(this, other))
            {
                return true;
            }

            if (other == null)
            {
                return false;
            }

            return _xml == other._xml &&
                    EvaluatedValueEscaped == other.EvaluatedValueEscaped;
        }

        #endregion

        /// <summary>
        /// Deep clone a metadatum, retaining the same parent.
        /// </summary>
        internal ProjectMetadata DeepClone()
        {
            // The new metadatum's predecessor is the same as its original's predecessor, just as the XML is the same
            // as its original's XML. Predecessors map to XML elements.
            return new ProjectMetadata(_parent, this.Xml, this.EvaluatedValueEscaped, this.Predecessor);
        }
    }
}
