// 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.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
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 item definition for a particular item-type.
    /// </summary>
    /// <remarks>
    /// Note that these are somewhat different to items. Like items, they can have metadata; like properties, the metadata
    /// can override each other. So during evaluation all the item definitions for a type are rolled together (assuming
    /// their conditions are true) to create one ProjectItemDefinition for each type. For this reason, the ProjectItemDefinition
    /// often will not point to a single ProjectItemDefinitionElement. The metadata within, however, will each point to a single
    /// ProjectMetadataElement, and these can be added, removed, and modified.
    /// </remarks>
    [DebuggerDisplay("{_itemType} #Metadata={MetadataCount}")]
    public class ProjectItemDefinition : IKeyed, IMetadataTable, IItemDefinition<ProjectMetadata>, IProjectMetadataParent, IItemTypeDefinition
    {
        /// <summary>
        /// Project that this item definition lives in.
        /// ProjectItemDefinitions always live in a project.
        /// Used to evaluate any updates to child metadata.
        /// </summary>
        private readonly Project _project;

        /// <summary>
        /// Item type, for example "Compile", that this item definition applies to
        /// </summary>
        private readonly string _itemType;

        /// <summary>
        /// Collection of metadata that link the XML metadata and instance metadata
        /// Since evaluation has occurred, this is an unordered collection.
        /// </summary>
        private PropertyDictionary<ProjectMetadata> _metadata;

        /// <summary>
        /// Called by the Evaluator during project evaluation.
        /// </summary>
        /// <remarks>
        /// Assumes that the itemType string originated in a ProjectItemDefinitionElement and therefore
        /// was already validated.
        /// </remarks>
        internal ProjectItemDefinition(Project project, string itemType)
        {
            ErrorUtilities.VerifyThrowInternalNull(project);
            ErrorUtilities.VerifyThrowArgumentLength(itemType);

            _project = project;
            _itemType = itemType;
            _metadata = null;
        }

        internal virtual ProjectItemDefinitionLink Link => null;

        /// <summary>
        /// Project that this item lives in.
        /// ProjectDefinitions always live in a project.
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public Project Project
        {
            [DebuggerStepThrough]
            get
            { return _project; }
        }

        /// <summary>
        /// Type of this item definition.
        /// </summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public string ItemType
        {
            [DebuggerStepThrough]
            get
            { return _itemType; }
        }

        /// <summary>
        /// Metadata on the item definition.
        /// If there is no metadata, returns empty collection.
        /// This is a read-only collection.
        /// </summary>
        [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is a reasonable choice. API review approved")]
        public IEnumerable<ProjectMetadata> Metadata => Link != null ? Link.Metadata : _metadata ?? [];

        /// <summary>
        /// Count of metadata on the item definition.
        /// </summary>
        public int MetadataCount
        {
            get { return Link != null ? Link.Metadata.Count : (_metadata == null) ? 0 : _metadata.Count; }
        }

        /// <summary>
        /// Implementation of IKeyed exposing the item type, so these
        /// can be put in a dictionary conveniently.
        /// </summary>
        string IKeyed.Key
        {
            get { return ItemType; }
        }

        /// <summary>
        /// Get any metadata in the item that has the specified name,
        /// otherwise returns null
        /// </summary>
        [DebuggerStepThrough]
        public ProjectMetadata GetMetadata(string name)
        {
            return Link != null ? Link.GetMetadata(name) : _metadata?[name];
        }

        /// <summary>
        /// Get the value of any metadata in the item that has the specified
        /// name, otherwise returns null
        /// </summary>
        public string GetMetadataValue(string name)
        {
            if (Link != null)
            {
                return Link.GetMetadataValue(name);
            }

            string escapedValue = (this as IMetadataTable).GetEscapedValue(name);

            return (escapedValue == null) ? null : EscapingUtilities.UnescapeAll(escapedValue);
        }

        /// <summary>
        /// Sets a new metadata value on the ItemDefinition.
        /// </summary>
        /// <remarks>Unevaluated value is assumed to be escaped as necessary</remarks>
        public ProjectMetadata SetMetadataValue(string name, string unevaluatedValue)
        {
            if (Link != null)
            {
                return Link.SetMetadataValue(name, unevaluatedValue);
            }

            XmlUtilities.VerifyThrowArgumentValidElementName(name);
            ErrorUtilities.VerifyThrowArgument(!FileUtilities.ItemSpecModifiers.IsItemSpecModifier(name), "ItemSpecModifierCannotBeCustomMetadata", name);
            ErrorUtilities.VerifyThrowInvalidOperation(!XMakeElements.ReservedItemNames.Contains(name), "CannotModifyReservedItemMetadata", name);

            ProjectMetadata metadatum;

            if (_metadata != null)
            {
                metadatum = _metadata[name];

                if (metadatum != null)
                {
                    Project.VerifyThrowInvalidOperationNotImported(metadatum.Xml.ContainingProject);
                    metadatum.UnevaluatedValue = unevaluatedValue;
                    return metadatum;
                }
            }

            // We can't use the item definition that this object came from as a root, as it doesn't map directly
            // to a single XML element. Instead, add a new one to the project. Best we can do.
            ProjectItemDefinitionElement itemDefinition = _project.Xml.AddItemDefinition(_itemType);

            ProjectMetadataElement metadatumXml = itemDefinition.AddMetadata(name, unevaluatedValue);

            _metadata ??= new PropertyDictionary<ProjectMetadata>();

            string evaluatedValueEscaped = _project.ExpandMetadataValueBestEffortLeaveEscaped(this, unevaluatedValue, metadatumXml.Location);

            metadatum = new ProjectMetadata(this, metadatumXml, evaluatedValueEscaped, null /* predecessor unknown */);

            _metadata.Set(metadatum);

            return metadatum;
        }

        #region IItemDefinition Members

        /// <summary>
        /// Sets a new metadata value on the ItemDefinition.
        /// This is ONLY called during evaluation and does not affect the XML.
        /// </summary>
        ProjectMetadata IItemDefinition<ProjectMetadata>.SetMetadata(ProjectMetadataElement metadataElement, string evaluatedValue, ProjectMetadata predecessor)
        {
            _metadata ??= new PropertyDictionary<ProjectMetadata>();

            ProjectMetadata metadatum = new ProjectMetadata(this, metadataElement, evaluatedValue, predecessor);
            _metadata.Set(metadatum);

            return metadatum;
        }

        #endregion

        #region IMetadataTable Members

        /// <summary>
        /// Retrieves the value of the named metadatum.
        /// </summary>
        /// <param name="name">The metadatum to retrieve.</param>
        /// <returns>The value, or an empty string if there is none by that name.</returns>
        string IMetadataTable.GetEscapedValue(string name)
        {
            return ((IMetadataTable)this).GetEscapedValue(null, name);
        }

        /// <summary>
        /// Retrieves the value of the named metadatum.
        /// </summary>
        /// <param name="specifiedItemType">The type of item.</param>
        /// <param name="name">The metadatum to retrieve.</param>
        /// <returns>The value, or an empty string if there is none by that name.</returns>
        string IMetadataTable.GetEscapedValue(string specifiedItemType, string name)
        {
            return ((IMetadataTable)this).GetEscapedValueIfPresent(specifiedItemType, name) ?? String.Empty;
        }

        /// <summary>
        /// Retrieves the value of the named metadatum, or null if it doesn't exist
        /// </summary>
        /// <param name="specifiedItemType">The type of item.</param>
        /// <param name="name">The metadatum to retrieve.</param>
        /// <returns>The value, or null if there is none by that name.</returns>
        string IMetadataTable.GetEscapedValueIfPresent(string specifiedItemType, string name)
        {
            if (_metadata == null)
            {
                return null;
            }

            if (specifiedItemType == null || String.Equals(_itemType, specifiedItemType, StringComparison.OrdinalIgnoreCase))
            {
                ProjectMetadata metadatum = GetMetadata(name);
                if (metadatum != null)
                {
                    return metadatum.EvaluatedValueEscaped;
                }
            }

            return null;
        }

        #endregion
    }
}
