// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis
{
    /// <summary>
    /// This type is responsible for parsing a ruleset xml file and producing a <see cref="RuleSet"/> object.
    /// </summary>
    internal class RuleSetProcessor
    {
        // Strings for the RuleSet node
        private const string RuleSetNodeName = "RuleSet";
        private const string RuleSetNameAttributeName = "Name";
        private const string RuleSetDescriptionAttributeName = "Description";
        private const string RuleSetToolsVersionAttributeName = "ToolsVersion";

        // Strings for the Rules node
        private const string RulesNodeName = "Rules";
        private const string RulesAnalyzerIdAttributeName = "AnalyzerId";
        private const string RulesNamespaceAttributeName = "RuleNamespace";

        // Strings for the Rule node
        private const string RuleNodeName = "Rule";
        private const string RuleIdAttributeName = "Id";

        // Strings for the Include node
        private const string IncludeNodeName = "Include";
        private const string IncludePathAttributeName = "Path";

        // Strings for the IncludeAll node
        private const string IncludeAllNodeName = "IncludeAll";

        // Strings for the Action attribute and its values
        private const string RuleActionAttributeName = "Action";
        private const string RuleActionNoneValue = "None";
        private const string RuleActionHiddenValue = "Hidden";
        private const string RuleActionInfoValue = "Info";
        private const string RuleActionWarningValue = "Warning";
        private const string RuleActionErrorValue = "Error";
        private const string RuleActionDefaultValue = "Default";

        /// <summary>
        /// Creates and loads the rule set from a file
        /// </summary>
        /// <param name="filePath">The file path to load the rule set</param>
#nullable enable
        public static RuleSet LoadFromFile(string filePath)
        {
            // First read the file without doing any validation
            filePath = FileUtilities.NormalizeAbsolutePath(filePath);
            XmlReaderSettings settings = GetDefaultXmlReaderSettings();

            XDocument? ruleSetDocument = null;
            XElement? ruleSetNode = null;

            using (Stream stream = FileUtilities.OpenRead(filePath))
            using (XmlReader xmlReader = XmlReader.Create(stream, settings))
            {
                try
                {
                    ruleSetDocument = XDocument.Load(xmlReader);
                }
                catch (Exception e)
                {
                    throw new InvalidRuleSetException(e.Message);
                }

                // Find the top level rule set node
                List<XElement> nodeList = ruleSetDocument.Elements(RuleSetNodeName).ToList();
                Debug.Assert(nodeList.Count == 1, "Multiple top-level nodes!");
                Debug.Assert(nodeList[0].Name == RuleSetNodeName);
                ruleSetNode = nodeList[0];
            }

            return ReadRuleSet(ruleSetNode, filePath);
        }

        /// <summary>
        /// Load the rule set from the XML node
        /// </summary>
        /// <param name="ruleSetNode">The rule set node from which to create a rule set object</param>
        /// <param name="filePath">The file path to the rule set file</param>
        /// <returns>A rule set object with data from the given XML node</returns>
        private static RuleSet ReadRuleSet(XElement ruleSetNode, string filePath)
        {
            var specificOptions = ImmutableDictionary.CreateBuilder<string, ReportDiagnostic>();
            var generalOption = ReportDiagnostic.Default;
            var includes = ImmutableArray.CreateBuilder<RuleSetInclude>();

            ValidateAttribute(ruleSetNode, RuleSetToolsVersionAttributeName);
            ValidateAttribute(ruleSetNode, RuleSetNameAttributeName);

            // Loop through each rules or include node in this rule set
            foreach (XElement childNode in ruleSetNode.Elements())
            {
                if (childNode.Name == RulesNodeName)
                {
                    var rules = ReadRules(childNode);
                    foreach (var rule in rules)
                    {
                        var ruleId = rule.Key;
                        var action = rule.Value;

                        ReportDiagnostic existingAction;
                        if (specificOptions.TryGetValue(ruleId, out existingAction))
                        {
                            if (existingAction != action)
                            {
                                throw new InvalidRuleSetException(string.Format(CodeAnalysisResources.RuleSetHasDuplicateRules, ruleId, existingAction, action));
                            }
                        }
                        else
                        {
                            specificOptions.Add(ruleId, action);
                        }
                    }
                }
                else if (childNode.Name == IncludeNodeName)
                {
                    includes.Add(ReadRuleSetInclude(childNode));
                }
                else if (childNode.Name == IncludeAllNodeName)
                {
                    generalOption = ReadIncludeAll(childNode);
                }
            }

            return new RuleSet(filePath, generalOption, specificOptions.ToImmutable(), includes.ToImmutable());
        }
#nullable disable

        /// <summary>
        /// Load the rules from the XML node
        /// </summary>
        /// <param name="rulesNode">The rules node from which to loop through each child rule node</param>
        /// <returns>A list of rule objects with data from the given XML node</returns>
        private static List<KeyValuePair<string, ReportDiagnostic>> ReadRules(XElement rulesNode)
        {
            string analyzerId = ReadNonEmptyAttribute(rulesNode, RulesAnalyzerIdAttributeName);
            string ruleNamespace = ReadNonEmptyAttribute(rulesNode, RulesNamespaceAttributeName);

            var rules = new List<KeyValuePair<string, ReportDiagnostic>>();

            // Loop through each rule node
            foreach (XElement ruleNode in rulesNode.Elements())
            {
                if (ruleNode.Name == RuleNodeName)
                {
                    rules.Add(ReadRule(ruleNode, analyzerId, ruleNamespace));
                }
                else
                {
                    // Schema validation should prevent us from getting here
                    Debug.Assert(false, "Unknown child node in Rules node");
                }
            }

            return rules;
        }

        /// <summary>
        /// Load the rule from the XML node
        /// </summary>
        /// <param name="ruleNode">The rule node from which to create a rule object</param>
        /// <param name="analyzer">The analyzer this rule belongs to</param>
        /// <param name="space">The namespace this rule belongs to</param>
        /// <returns>A rule object with data from the given XML node</returns>
        private static KeyValuePair<string, ReportDiagnostic> ReadRule(XElement ruleNode, string analyzer, string space)
        {
            string ruleId = ReadNonEmptyAttribute(ruleNode, RuleIdAttributeName);
            ReportDiagnostic action = ReadAction(ruleNode, allowDefault: false);

            return new KeyValuePair<string, ReportDiagnostic>(ruleId, action);
        }

        /// <summary>
        /// Load the included rule set from the XML node
        /// </summary>
        /// <param name="includeNode">The include node from which to create a RuleSetInclude object</param>
        /// <returns>A RuleSetInclude object with data from the given XML node</returns>
        private static RuleSetInclude ReadRuleSetInclude(XElement includeNode)
        {
            string includePath = ReadNonEmptyAttribute(includeNode, IncludePathAttributeName);
            ReportDiagnostic action = ReadAction(includeNode, allowDefault: true);

            return new RuleSetInclude(includePath, action);
        }

        /// <summary>
        /// Reads the action from the given node
        /// </summary>
        /// <param name="node">The node to read the action, it can be a rule node or an include node.</param>
        /// <param name="allowDefault">Whether or not the default value is allowed.</param>
        /// <returns>The rule action</returns>
        private static ReportDiagnostic ReadAction(XElement node, bool allowDefault)
        {
            string action = ReadNonEmptyAttribute(node, RuleActionAttributeName);

            if (string.Equals(action, RuleActionWarningValue))
            {
                return ReportDiagnostic.Warn;
            }
            else if (string.Equals(action, RuleActionErrorValue))
            {
                return ReportDiagnostic.Error;
            }
            else if (string.Equals(action, RuleActionInfoValue))
            {
                return ReportDiagnostic.Info;
            }
            else if (string.Equals(action, RuleActionHiddenValue))
            {
                return ReportDiagnostic.Hidden;
            }
            else if (string.Equals(action, RuleActionNoneValue))
            {
                return ReportDiagnostic.Suppress;
            }
            else if (string.Equals(action, RuleActionDefaultValue) && allowDefault)
            {
                return ReportDiagnostic.Default;
            }

            throw new InvalidRuleSetException(string.Format(CodeAnalysisResources.RuleSetBadAttributeValue, RuleActionAttributeName, action));
        }

        /// <summary>
        /// Load the IncludedAll from the XML node
        /// </summary>
        /// <param name="includeAllNode">The IncludeAll node from which to create a IncludeAll object</param>
        /// <returns>A IncludeAll object with data from the given XML node</returns>
        private static ReportDiagnostic ReadIncludeAll(XElement includeAllNode)
        {
            return ReadAction(includeAllNode, allowDefault: false);
        }

        /// <summary>
        /// Reads an attribute from a node and validates that it is not empty.
        /// </summary>
        /// <param name="node">The XML node that contains the attribute</param>
        /// <param name="attributeName">The name of the attribute to read</param>
        /// <returns>The attribute value</returns>
        private static string ReadNonEmptyAttribute(XElement node, string attributeName)
        {
            XAttribute attribute = node.Attribute(attributeName);
            if (attribute == null)
            {
                throw new InvalidRuleSetException(string.Format(CodeAnalysisResources.RuleSetMissingAttribute, node.Name, attributeName));
            }

            if (string.IsNullOrEmpty(attribute.Value))
            {
                throw new InvalidRuleSetException(string.Format(CodeAnalysisResources.RuleSetBadAttributeValue, attributeName, attribute.Value));
            }

            return attribute.Value;
        }

        /// <summary>
        /// Gets the default settings to read the ruleset xml file.
        /// </summary>
        private static XmlReaderSettings GetDefaultXmlReaderSettings()
        {
            XmlReaderSettings xmlReaderSettings = new XmlReaderSettings();

            xmlReaderSettings.CheckCharacters = true;
            xmlReaderSettings.CloseInput = true;
            xmlReaderSettings.ConformanceLevel = ConformanceLevel.Document;
            xmlReaderSettings.IgnoreComments = true;
            xmlReaderSettings.IgnoreProcessingInstructions = true;
            xmlReaderSettings.IgnoreWhitespace = true;
            xmlReaderSettings.DtdProcessing = DtdProcessing.Prohibit;

            return xmlReaderSettings;
        }

        private static void ValidateAttribute(XElement node, string attributeName)
        {
            ReadNonEmptyAttribute(node, attributeName);
        }
    }
}
