// 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.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

#nullable disable

namespace Microsoft.Build.Logging.StructuredLogger
{
    /// <summary>
    /// XML File Logger class to handle, parse, and route messages form the MSBuild logging system.
    /// </summary>
    public class XmlFileLogger : Logger
    {
        public const string OutputItemsMessagePrefix = @"Output Item(s): ";
        public const string OutputPropertyMessagePrefix = @"Output Property: ";
        public const string TaskParameterMessagePrefix = @"Task Parameter:";
        public const string PropertyGroupMessagePrefix = @"Set Property: ";
        public const string ItemGroupIncludeMessagePrefix = @"Added Item(s): ";
        public const string ItemGroupRemoveMessagePrefix = @"Removed Item(s): ";

        /// <summary>
        /// The path to the log file specified by the user
        /// </summary>
        private string _logFile;

        /// <summary>
        /// The build instance set when the build starts.
        /// </summary>
        private Build _build;

        private int _errors;
        private int _warings;

        /// <summary>
        /// Initializes the logger and subscribes to the relevant events.
        /// </summary>
        /// <param name="eventSource">The available events that processEvent logger can subscribe to.</param>
        public override void Initialize(IEventSource eventSource)
        {
            ProcessParameters();

            eventSource.BuildStarted += (s, args) => _build = new Build(args);
            eventSource.BuildFinished += (o, args) => _build.CompleteBuild(args, _logFile, _errors, _warings);

            eventSource.ProjectStarted += (o, args) => TryProcessEvent(() => _build.AddProject(args));
            eventSource.ProjectFinished += (o, args) => TryProcessEvent(() => _build.CompleteProject(args));
            eventSource.TargetStarted += (o, args) => TryProcessEvent(() => _build.AddTarget(args));
            eventSource.TargetFinished += (o, args) => TryProcessEvent(() => _build.CompleteTarget(args));
            eventSource.TaskStarted += (o, args) => TryProcessEvent(() => _build.AddTask(args));
            eventSource.TaskFinished += (o, args) => TryProcessEvent(() => _build.CompleteTask(args));

            eventSource.TaskFinished += (o, args) => TryProcessEvent(() => _build.CompleteTask(args));

            eventSource.MessageRaised += HandleMessageRaised;

            eventSource.ErrorRaised += (o, args) =>
            {
                _errors++;
                _build.AddMessage(args, string.Format("Error {0}: {1}", args.Code, args.Message));
            };
            eventSource.WarningRaised += (o, args) =>
            {
                _warings++;
                _build.AddMessage(args, string.Format("Warning {0}: {1}", args.Code, args.Message));
            };
        }

        /// <summary>
        /// Tries to process an event (action). On exception, log as a build message so we don't crash.
        /// </summary>
        /// <param name="processEvent">Action/event to process.</param>
        private void TryProcessEvent(Action processEvent)
        {
            try
            {
                processEvent();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                _build.AddMessage(new Message(string.Format("XmlFileLogger Error: {0}", e), DateTime.Now));
            }
        }

        /// <summary>
        /// Handles the generic message raised event.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="messageArgs">The <see cref="BuildMessageEventArgs"/> instance containing the event data.</param>
        private void HandleMessageRaised(object sender, BuildMessageEventArgs messageArgs)
        {
            const string taskAssemblyPattern = "Using \"(?<task>.+)\" task from (assembly|the task factory) \"(?<assembly>.+)\"\\.";

            // Task Input / Outputs
            if (messageArgs.Message.StartsWith(TaskParameterMessagePrefix))
            {
                _build.AddTaskParameter(messageArgs, TaskParameterMessagePrefix);
            }
            else if (messageArgs.Message.StartsWith(OutputItemsMessagePrefix))
            {
                _build.AddTaskParameter(messageArgs, OutputItemsMessagePrefix);
            }
            else if (messageArgs.Message.StartsWith(OutputPropertyMessagePrefix))
            {
                _build.AddTaskParameter(messageArgs, OutputPropertyMessagePrefix);
            }

            // Item / Property groups
            else if (messageArgs.Message.StartsWith(PropertyGroupMessagePrefix))
            {
                _build.AddPropertyGroup(messageArgs, PropertyGroupMessagePrefix);
            }
            else if (messageArgs.Message.StartsWith(ItemGroupIncludeMessagePrefix))
            {
                _build.AddItemGroup(messageArgs, ItemGroupIncludeMessagePrefix);
            }
            else if (messageArgs.Message.StartsWith(ItemGroupRemoveMessagePrefix))
            {
                _build.AddItemGroup(messageArgs, ItemGroupRemoveMessagePrefix);
            }
            else
            {
                // This was command line arguments for processEvent task
                var args = messageArgs as TaskCommandLineEventArgs;
                if (args != null)
                {
                    _build.AddCommandLine(args);
                    return;
                }

                // A task from assembly message (parses out the task name and assembly path).
                var match = Regex.Match(messageArgs.Message, taskAssemblyPattern);
                if (match.Success)
                {
                    _build.SetTaskAssembly(match.Groups["task"].Value, match.Groups["assembly"].Value);
                }
                else
                {
                    // Just processEvent generic log message or something we currently don't handle in the object model.
                    _build.AddMessage(messageArgs, messageArgs.Message);
                }
            }
        }

        /// <summary>
        /// Processes the parameters given to the logger from MSBuild.
        /// </summary>
        /// <exception cref="LoggerException">
        /// </exception>
        private void ProcessParameters()
        {
            const string invalidParamSpecificationMessage = @"Need processEvent log file.  Specify using the following pattern: '/logger:XmlFileLogger,XmlFileLogger.dll;buildlog.xml";

            if (Parameters == null)
            {
                throw new LoggerException(invalidParamSpecificationMessage);
            }

            string[] parameters = Parameters.Split(';');

            if (parameters.Length != 1)
            {
                throw new LoggerException(invalidParamSpecificationMessage);
            }

            _logFile = parameters[0];
        }
    }
}
