// 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.Concurrent;
using System.Xml.Linq;
using Microsoft.Build.Framework;

#nullable disable

namespace Microsoft.Build.Logging.StructuredLogger
{
    /// <summary>
    /// Class representation of an MSBuild overall build execution.
    /// </summary>
    internal sealed class Build : LogProcessNode
    {
        /// <summary>
        /// A lookup table mapping of project identifiers to project nodes (which can be nested multiple layers). 
        /// </summary>
        private readonly ConcurrentDictionary<int, Project> _projectIdToProjectMap = new ConcurrentDictionary<int, Project>();

        /// <summary>
        /// A mapping of task names to assembly locations.
        /// </summary>
        private readonly ConcurrentDictionary<string, string> _taskToAssemblyMap = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        /// <summary>
        /// Initializes a new instance of the <see cref="Build"/> class.
        /// </summary>
        /// <param name="buildStartedEventArgs">The <see cref="BuildStartedEventArgs"/> instance containing the event data.</param>
        public Build(BuildStartedEventArgs buildStartedEventArgs)
        {
            StartTime = buildStartedEventArgs.Timestamp;
            Properties = new PropertyBag(buildStartedEventArgs.BuildEnvironment);
        }

        /// <summary>
        /// Completes the build and writes to the XML log file.
        /// </summary>
        /// <param name="buildFinishedEventArgs">The <see cref="BuildFinishedEventArgs"/> instance containing the event data.</param>
        /// <param name="logFile">The XML log file.</param>
        public void CompleteBuild(BuildFinishedEventArgs buildFinishedEventArgs, string logFile, int errorCount, int warningCount)
        {
            EndTime = buildFinishedEventArgs.Timestamp;
            var document = new XDocument();
            var root = new XElement("Build",
                new XAttribute("BuildSucceeded", buildFinishedEventArgs.Succeeded),
                new XAttribute("StartTime", StartTime),
                new XAttribute("EndTime", EndTime),
                new XAttribute("Errors", errorCount),
                new XAttribute("Warnings", warningCount));

            document.Add(root);
            SaveToElement(root);

            document.Save(logFile);
        }

        /// <summary>
        /// Writes the build and its children to XML XElement representation.
        /// </summary>
        /// <param name="parentElement">The parent element.</param>
        public override void SaveToElement(XElement parentElement)
        {
            WriteChildren<Message>(parentElement, () => new XElement("BuildMessageEvents"));
            WriteProperties(parentElement);
            WriteChildren<Project>(parentElement);
        }

        /// <summary>
        /// Handler for a TargetStarted log event. Adds the target to the object structure.
        /// </summary>
        /// <param name="targetStartedEventArgs">The <see cref="TargetStartedEventArgs"/> instance containing the event data.</param>
        public void AddTarget(TargetStartedEventArgs targetStartedEventArgs)
        {
            var project = GetOrAddProject(targetStartedEventArgs.BuildEventContext.ProjectContextId);
            project.AddTarget(targetStartedEventArgs);
        }

        /// <summary>
        /// Handler for a BuildMessage log event. Adds the node to the appropriate target.
        /// </summary>
        /// <param name="buildMessageEventArgs">The <see cref="BuildMessageEventArgs"/> instance containing the event data.</param>
        /// <param name="messagePrefix">The message prefix.</param>
        public void AddTaskParameter(BuildMessageEventArgs buildMessageEventArgs, string messagePrefix)
        {
            var project = GetOrAddProject(buildMessageEventArgs.BuildEventContext.ProjectContextId);
            var target = project.GetTargetById(buildMessageEventArgs.BuildEventContext.TargetId);
            var task = target.GetTaskById(buildMessageEventArgs.BuildEventContext.TaskId);

            task.AddParameter(TaskParameter.Create(buildMessageEventArgs.Message, messagePrefix));
        }

        /// <summary>
        /// Handler for a TaskCommandLine log event. Sets the command line arguments on the appropriate task. 
        /// </summary>
        /// <param name="taskCommandLineEventArgs">The <see cref="TaskCommandLineEventArgs"/> instance containing the event data.</param>
        public void AddCommandLine(TaskCommandLineEventArgs taskCommandLineEventArgs)
        {
            var project = GetOrAddProject(taskCommandLineEventArgs.BuildEventContext.ProjectContextId);
            var target = project.GetTargetById(taskCommandLineEventArgs.BuildEventContext.TargetId);
            var task = target.GetTaskById(taskCommandLineEventArgs.BuildEventContext.TaskId);

            task.CommandLineArguments = taskCommandLineEventArgs.CommandLine;
        }

        /// <summary>
        /// Handles BuildMessage event when a property discovery/evaluation is logged.
        /// </summary>
        /// <param name="buildMessageEventArgs">The <see cref="BuildMessageEventArgs"/> instance containing the event data.</param>
        /// <param name="prefix">The prefix string.</param>
        public void AddPropertyGroup(BuildMessageEventArgs buildMessageEventArgs, string prefix)
        {
            string message = buildMessageEventArgs.Message.Substring(prefix.Length);

            var project = GetOrAddProject(buildMessageEventArgs.BuildEventContext.ProjectContextId);
            var target = project.GetTargetById(buildMessageEventArgs.BuildEventContext.TargetId);

            var equals = message.IndexOf('=');
            var name = message.Substring(0, equals);
            var value = message.Substring(equals + 1);

            target.AddProperty(name, value);
        }

        /// <summary>
        /// Handles BuildMessage event when an ItemGroup discovery/evaluation is logged.
        /// </summary>
        /// <param name="buildMessageEventArgs">The <see cref="BuildMessageEventArgs"/> instance containing the event data.</param>
        /// <param name="prefix">The prefix string.</param>
        public void AddItemGroup(BuildMessageEventArgs buildMessageEventArgs, string prefix)
        {
            var project = GetOrAddProject(buildMessageEventArgs.BuildEventContext.ProjectContextId);
            var target = project.GetTargetById(buildMessageEventArgs.BuildEventContext.TargetId);

            target.AddItemGroup((ItemGroup)TaskParameter.Create(buildMessageEventArgs.Message, prefix));
        }

        /// <summary>
        /// Handles a generic BuildMessage event and assigns it to the appropriate logging node.
        /// </summary>
        /// <param name="buildMessageEventArgs">The <see cref="BuildMessageEventArgs"/> instance containing the event data.</param>
        public void AddMessage(LazyFormattedBuildEventArgs buildMessageEventArgs, string message)
        {
            LogProcessNode node = this;

            if (buildMessageEventArgs.BuildEventContext.TaskId > 0)
            {
                node = GetOrAddProject(buildMessageEventArgs.BuildEventContext.ProjectContextId)
                    .GetTargetById(buildMessageEventArgs.BuildEventContext.TargetId)
                    .GetTaskById(buildMessageEventArgs.BuildEventContext.TaskId);
            }
            else if (buildMessageEventArgs.BuildEventContext.TargetId > 0)
            {
                node = GetOrAddProject(buildMessageEventArgs.BuildEventContext.ProjectContextId)
                    .GetTargetById(buildMessageEventArgs.BuildEventContext.TargetId);
            }
            else if (buildMessageEventArgs.BuildEventContext.ProjectContextId > 0)
            {
                node = GetOrAddProject(buildMessageEventArgs.BuildEventContext.ProjectContextId);
            }

            node.AddMessage(new Message(message, buildMessageEventArgs.Timestamp));
        }

        /// <summary>
        /// Handles a TaskStarted event from the log. Creates the task and assigns to the appropriate target.
        /// </summary>
        /// <param name="taskStartedEventArgs">The <see cref="TaskStartedEventArgs"/> instance containing the event data.</param>
        public void AddTask(TaskStartedEventArgs taskStartedEventArgs)
        {
            var project = GetOrAddProject(taskStartedEventArgs.BuildEventContext.ProjectContextId);
            var target = project.GetTargetById(taskStartedEventArgs.BuildEventContext.TargetId);

            target.AddChildTask(new Task(taskStartedEventArgs.TaskName, taskStartedEventArgs, GetTaskAssembly(taskStartedEventArgs.TaskName)));
        }

        /// <summary>
        /// Handles a ProjectStarted event from the log. Creates the project and assigns it to the correct parent project (if any).
        /// </summary>
        /// <param name="projectStartedEventArgs">The <see cref="ProjectStartedEventArgs"/> instance containing the event data.</param>
        public void AddProject(ProjectStartedEventArgs projectStartedEventArgs)
        {
            Project parent = null;

            if (projectStartedEventArgs.ParentProjectBuildEventContext?.ProjectContextId >= 0)
            {
                parent = GetOrAddProject(projectStartedEventArgs.ParentProjectBuildEventContext.ProjectContextId);
            }

            var project = GetOrAddProject(projectStartedEventArgs.BuildEventContext.ProjectContextId, projectStartedEventArgs, parent);

            if (parent != null)
            {
                parent.AddChildProject(project);
            }
            else
            {
                // This is a "Root" project (no parent project).
                AddChildNode(project);
            }
        }

        /// <summary>
        /// Gets a project instance for the given identifier. Will create if it doesn't exist.
        /// </summary>
        /// <remarks>If the ProjectStartedEventArgs is not known at this time (null), a stub project is created.</remarks>
        /// <param name="projectId">The project identifier.</param>
        /// <param name="projectStartedEventArgs">The <see cref="ProjectStartedEventArgs"/> instance containing the event data.</param>
        /// <param name="parentProject">The parent project, if any.</param>
        /// <returns>Project object</returns>
        public Project GetOrAddProject(int projectId, ProjectStartedEventArgs projectStartedEventArgs = null, Project parentProject = null)
        {
            Project result = _projectIdToProjectMap.GetOrAdd(projectId,
                id =>
                    new Project(id, projectStartedEventArgs,
                        parentProject == null ? Properties : parentProject.Properties));

            if (projectStartedEventArgs != null)
            {
                result.TryUpdate(projectStartedEventArgs);
            }

            return result;
        }

        /// <summary>
        /// Completes the target.
        /// </summary>
        /// <param name="targetFinishedEventArgs">The <see cref="TargetFinishedEventArgs" /> instance containing the event data.</param>
        public void CompleteTarget(TargetFinishedEventArgs targetFinishedEventArgs)
        {
            var project = GetOrAddProject(targetFinishedEventArgs.BuildEventContext.ProjectContextId);
            var target = project.GetTargetById(targetFinishedEventArgs.BuildEventContext.TargetId);

            target.EndTime = targetFinishedEventArgs.Timestamp;
        }

        /// <summary>
        /// Completes the project.
        /// </summary>
        /// <param name="projectFinishedEventArgs">The <see cref="ProjectFinishedEventArgs"/> instance containing the event data.</param>
        public void CompleteProject(ProjectFinishedEventArgs projectFinishedEventArgs)
        {
            var project = GetOrAddProject(projectFinishedEventArgs.BuildEventContext.ProjectContextId);
            project.EndTime = projectFinishedEventArgs.Timestamp;
        }

        /// <summary>
        /// Completes the task.
        /// </summary>
        /// <param name="taskFinishedEventArgs">The <see cref="TaskFinishedEventArgs"/> instance containing the event data.</param>
        public void CompleteTask(TaskFinishedEventArgs taskFinishedEventArgs)
        {
            var project = GetOrAddProject(taskFinishedEventArgs.BuildEventContext.ProjectContextId);
            var target = project.GetTargetById(taskFinishedEventArgs.BuildEventContext.TargetId);
            var task = target.GetTaskById(taskFinishedEventArgs.BuildEventContext.TaskId);

            task.EndTime = taskFinishedEventArgs.Timestamp;
        }

        /// <summary>
        /// Sets the assembly location for a given task.
        /// </summary>
        /// <param name="taskName">Name of the task.</param>
        /// <param name="assembly">The assembly location.</param>
        public void SetTaskAssembly(string taskName, string assembly)
        {
            _taskToAssemblyMap.GetOrAdd(taskName, t => assembly);
        }

        /// <summary>
        /// Gets the task assembly.
        /// </summary>
        /// <param name="taskName">Name of the task.</param>
        /// <returns>The assembly location for the task.</returns>
        public string GetTaskAssembly(string taskName)
        {
            string assembly;
            return _taskToAssemblyMap.TryGetValue(taskName, out assembly) ? assembly : string.Empty;
        }
    }
}
