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

#nullable disable

namespace Microsoft.Build.Utilities
{
    /// <summary>
    /// This is a multiplexing logger. The purpose of this logger is to allow the registration and deregistration of
    /// multiple loggers during the build. This is to support the VS IDE scenario where loggers are registered and unregistered
    /// for each project system's build request. This means one physical build may have multiple logical builds
    /// each with their own set of loggers.
    ///
    /// The Mux logger will register itself with the build manager as a regular central /l style logger.
    /// It will be responsible for receiving messages from the build manager and route them to the correct
    /// logger based on the logical build the message came from.
    ///
    /// Requirements:
    ///     1) Multiplexing logger will be registered at the beginning of the build manager's Begin build
    ///         Any loggers registered before the build manager actually started building will get the build started event at the same time as the MUX logger
    ///         Any loggers registered after the build manager starts the build will get a synthesised build started event. The event cannot be cached because the
    ///         timestamp of the build started event is determined when the event is created, caching the event would give incorrect build times in the loggers registered to the MUX.
    ///
    ///     2) The MUX logger will be initialized by the build manager.
    ///         The mux will listen to all events on the event source from the build manager and will route events correctly to the registered loggers.
    ///
    ///     3) The MUX logger will be shutdown when the build is finished in end build . At this time it will un-register any loggers attached to it.
    ///
    ///     4) The MUX logger will log the build finished event when the project finished event for the first project started event is seen for each logger.
    ///
    /// Registering Loggers:
    ///
    /// The multiplexing logger will function in the following way:
    ///     A logger will be passed to the MUX Register logger method with a submission ID which will be used to route a the message to the correct logger.
    ///     A new event source will be created so that the logger passed in can be registered to that event source
    ///     If the build started event has already been logged the MUX logger will create a new BuildStartedEvent and send that to the event source.
    ///
    /// UnregisterLoggers:
    ///     When a build submission is completed the UnregisterLoggers method will be called with the submission ID.
    ///     At this point we will look up the success state of the project finished event for the submission ID and log a build finished event to the logger.
    ///     The event source will be cleaned up.  This may be interesting because the unregister will come from a thread other than what is doing the logging.
    ///     This may create a Synchronization issue, if unregister is called while events are being logged.
    /// </summary>
    //
    // UNDONE: If we can use ErrorUtilities, replace all InvalidOperation and Argument exceptions with the appropriate calls.
    //
    public class MuxLogger : INodeLogger
    {
        /// <summary>
        /// The mapping of submission IDs to the submission record.
        /// </summary>
        private readonly Dictionary<int, SubmissionRecord> _submissionRecords = new Dictionary<int, SubmissionRecord>();

        /// <summary>
        ///  Keep the build started event if it has been seen, we need the message off it.
        /// </summary>
        private BuildStartedEventArgs _buildStartedEvent;

        /// <summary>
        /// Event source which events from the build manager will be raised on.
        /// </summary>
        private IEventSource _eventSourceForBuild;

        /// <summary>
        /// The handler for the build started event
        /// </summary>
        private readonly BuildStartedEventHandler _buildStartedEventHandler;

        /// <summary>
        /// The handler for the build finished event.
        /// </summary>
        private readonly BuildFinishedEventHandler _buildFinishedEventHandler;

        /// <summary>
        /// The handler for the project started event.
        /// </summary>
        private readonly ProjectStartedEventHandler _projectStartedEventHandler;

        /// <summary>
        /// The handler for the project finished event.
        /// </summary>
        private readonly ProjectFinishedEventHandler _projectFinishedEventHandler;

        /// <summary>
        /// Dictionary mapping submission id to projects in progress.
        /// </summary>
        private readonly Dictionary<int, int> _submissionProjectsInProgress = new Dictionary<int, int>();

        /// <summary>
        /// The maximum node count as specified in the call to Initialize()
        /// </summary>
        private int _maxNodeCount = 1;

        /// <summary>
        /// Constructor.
        /// </summary>
        public MuxLogger()
        {
            _buildStartedEventHandler = BuildStarted;
            _buildFinishedEventHandler = BuildFinished;
            _projectStartedEventHandler = ProjectStarted;
            _projectFinishedEventHandler = ProjectFinished;
        }

        /// <summary>
        /// Required for ILogger interface
        /// </summary>
        public LoggerVerbosity Verbosity { get; set; }

        /// <summary>
        /// Required for the ILoggerInterface
        /// </summary>
        public string Parameters { get; set; }

        /// <summary>
        /// Should evaluation events include generated metaprojects?
        /// </summary>
        public bool IncludeEvaluationMetaprojects { get; set; }

        /// <summary>
        /// Should evaluation events include profiling information?
        /// </summary>
        public bool IncludeEvaluationProfiles { get; set; }

        /// <summary>
        /// Should task events include task inputs?
        /// </summary>
        public bool IncludeTaskInputs { get; set; }

        /// <summary>
        /// Should properties and items be logged on <see cref="ProjectEvaluationFinishedEventArgs"/>
        /// instead of <see cref="ProjectStartedEventArgs"/>?
        /// </summary>
        public bool IncludeEvaluationPropertiesAndItems { get; set; }

        /// <summary>
        /// Initialize the logger.
        /// </summary>
        public void Initialize(IEventSource eventSource) => Initialize(eventSource, 1);

        /// <summary>
        /// Initialize the logger.
        /// </summary>
        public void Initialize(IEventSource eventSource, int maxNodeCount)
        {
            if (_eventSourceForBuild != null)
            {
                throw new InvalidOperationException("MuxLogger already initialized.");
            }

            _eventSourceForBuild = eventSource;
            _maxNodeCount = maxNodeCount;

            _eventSourceForBuild.BuildStarted += _buildStartedEventHandler;
            _eventSourceForBuild.BuildFinished += _buildFinishedEventHandler;
            _eventSourceForBuild.ProjectStarted += _projectStartedEventHandler;
            _eventSourceForBuild.ProjectFinished += _projectFinishedEventHandler;

            if (_eventSourceForBuild is IEventSource3 eventSource3)
            {
                if (IncludeEvaluationMetaprojects)
                {
                    eventSource3.IncludeEvaluationMetaprojects();
                }

                if (IncludeEvaluationProfiles)
                {
                    eventSource3.IncludeEvaluationProfiles();
                }

                if (IncludeTaskInputs)
                {
                    eventSource3.IncludeTaskInputs();
                }
            }

            if (_eventSourceForBuild is IEventSource4 eventSource4)
            {
                if (IncludeEvaluationPropertiesAndItems)
                {
                    eventSource4.IncludeEvaluationPropertiesAndItems();
                }
            }
        }

        /// <summary>
        /// Shutdown the mux logger and clear out any state
        /// </summary>
        public void Shutdown()
        {
            if (_eventSourceForBuild == null)
            {
                throw new InvalidOperationException("MuxLogger not initialized.");
            }

            // Go through ALL loggers and shutdown any which remain.
            List<SubmissionRecord> recordsToShutdown;
            lock (_submissionRecords)
            {
                recordsToShutdown = new List<SubmissionRecord>(_submissionRecords.Values);
                _submissionRecords.Clear();
            }

            foreach (SubmissionRecord record in recordsToShutdown)
            {
                record.Shutdown();
            }

            _eventSourceForBuild.ProjectStarted -= _projectStartedEventHandler;
            _eventSourceForBuild.ProjectFinished -= _projectFinishedEventHandler;
            _eventSourceForBuild.BuildStarted -= _buildStartedEventHandler;
            _eventSourceForBuild.BuildFinished -= _buildFinishedEventHandler;

            _submissionProjectsInProgress.Clear();
            _buildStartedEvent = null;
            _eventSourceForBuild = null;
        }

        /// <summary>
        /// This method will register a logger on the MUX logger and then raise a build started event if the build started event has already been logged
        /// </summary>
        public void RegisterLogger(int submissionId, ILogger logger)
        {
            if (logger == null)
            {
                throw new ArgumentNullException(nameof(logger));
            }

            if (_eventSourceForBuild == null)
            {
                throw new InvalidOperationException("Cannot register a logger before the MuxLogger has been initialized.");
            }

            if (_submissionProjectsInProgress.ContainsKey(submissionId))
            {
                throw new InvalidOperationException("Cannot register a logger for a submission once it has started.");
            }

            // See if another logger has been registered already with the same submission ID
            SubmissionRecord record;
            lock (_submissionRecords)
            {
                if (!_submissionRecords.TryGetValue(submissionId, out record))
                {
                    record = new SubmissionRecord(submissionId, _eventSourceForBuild, _buildStartedEvent, _maxNodeCount);
                    _submissionRecords.Add(submissionId, record);
                }
            }

            record.AddLogger(logger);
        }

        /// <summary>
        /// Unregisters all the loggers for a given submission id.
        /// </summary>
        public bool UnregisterLoggers(int submissionId)
        {
            SubmissionRecord record;
            lock (_submissionRecords)
            {
                if (!_submissionRecords.TryGetValue(submissionId, out record))
                {
                    return false;
                }

                _submissionRecords.Remove(submissionId);
            }

            record.Shutdown();
            return true;
        }

        /// <summary>
        /// Receives the build started event for the whole build.
        /// </summary>
        private void BuildStarted(object sender, BuildStartedEventArgs e)
        {
            _buildStartedEvent = e;
            lock (_submissionRecords)
            {
                foreach (SubmissionRecord record in _submissionRecords.Values)
                {
                    record.SetGlobalBuildStartedEvent(e);
                }
            }
        }

        /// <summary>
        /// Receives the build finished event.
        /// </summary>
        private void BuildFinished(object sender, BuildFinishedEventArgs e)
        {
            _buildStartedEvent = null;
        }

        /// <summary>
        /// Receives the project started event and records the submission as being in-progress.
        /// </summary>
        private void ProjectStarted(object sender, ProjectStartedEventArgs e)
        {
            _submissionProjectsInProgress.TryGetValue(e.BuildEventContext.SubmissionId, out int value);
            _submissionProjectsInProgress[e.BuildEventContext.SubmissionId] = value + 1;
        }

        /// <summary>
        /// Receives the project finished event.
        /// </summary>
        private void ProjectFinished(object sender, ProjectFinishedEventArgs e)
        {
            int value = _submissionProjectsInProgress[e.BuildEventContext.SubmissionId];

            if (value == 1)
            {
                _submissionProjectsInProgress.Remove(e.BuildEventContext.SubmissionId);
                lock (_submissionRecords)
                {
                    _submissionRecords.Remove(e.BuildEventContext.SubmissionId);
                }
            }
            else
            {
                _submissionProjectsInProgress[e.BuildEventContext.SubmissionId] = value - 1;
            }
        }

        /// <summary>
        /// This class holds everything the logger needs to know about a particular submission, including the event source.
        /// </summary>
        private class SubmissionRecord :
#if FEATURE_APPDOMAIN
            MarshalByRefObject,
#endif
            IEventSource2
        {
            #region Fields
            /// <summary>
            /// Object used to synchronize access to internals.
            /// </summary>
            private readonly LockType _syncLock = new LockType();

            /// <summary>
            /// List of loggers
            /// </summary>
            private readonly List<ILogger> _loggers;

            /// <summary>
            /// The maximum node count
            /// </summary>
            private readonly int _maxNodeCount;

            /// <summary>
            /// The event source which will have events raised from the buld manager.
            /// </summary>
            private readonly IEventSource _eventSourceForBuild;

            /// <summary>
            /// The buildStartedEvent to use when synthesizing the build started event.
            /// </summary>
            private BuildStartedEventArgs _buildStartedEvent;

            /// <summary>
            /// The project build event coontext for the first project started event seen, this is the root of the submission.
            /// </summary>
            private BuildEventContext _firstProjectStartedEventContext;

            /// <summary>
            /// SubmissionId for this submission record
            /// </summary>
            private readonly int _submissionId;

            /// <summary>
            /// Has the record been shutdown yet.
            /// </summary>
            private bool _shutdown;
            #endregion

            // Keep instance of event handlers so they can be unregistered at the end of the submissionID.
            // If we wait for the entire build to finish we will leak the handlers until we unregister ALL of the handlers from the
            // event source on the build manager.
            #region RegisteredHandlers
            /// <summary>
            /// Even hander for "anyEvent" this is a handler which will be called from each of the other event handlers
            /// </summary>
            private AnyEventHandler _anyEventHandler;

            /// <summary>
            /// Handle the Build Finished event
            /// </summary>
            private BuildFinishedEventHandler _buildFinishedEventHandler;

            /// <summary>
            /// Handle the Build started event
            /// </summary>
            private BuildStartedEventHandler _buildStartedEventHandler;

            /// <summary>
            /// Handle custom build events
            /// </summary>
            private CustomBuildEventHandler _customBuildEventHandler;

            /// <summary>
            /// Handle error events
            /// </summary>
            private BuildErrorEventHandler _buildErrorEventHandler;

            /// <summary>
            /// Handle message events
            /// </summary>
            private BuildMessageEventHandler _buildMessageEventHandler;

            /// <summary>
            /// Handle project finished events
            /// </summary>
            private ProjectFinishedEventHandler _projectFinishedEventHandler;

            /// <summary>
            /// Handle project started events
            /// </summary>
            private ProjectStartedEventHandler _projectStartedEventHandler;

            /// <summary>
            /// Handle build sttus events
            /// </summary>
            private BuildStatusEventHandler _buildStatusEventHandler;

            /// <summary>
            /// Handle target finished events
            /// </summary>
            private TargetFinishedEventHandler _targetFinishedEventHandler;

            /// <summary>
            /// Handle target started events
            /// </summary>
            private TargetStartedEventHandler _targetStartedEventHandler;

            /// <summary>
            /// Handle task finished
            /// </summary>
            private TaskFinishedEventHandler _taskFinishedEventHandler;

            /// <summary>
            /// Handle task started
            /// </summary>
            private TaskStartedEventHandler _taskStartedEventHandler;

            /// <summary>
            /// Handle warning events
            /// </summary>
            private BuildWarningEventHandler _buildWarningEventHandler;

            /// <summary>
            /// Handle telemetry events.
            /// </summary>
            private TelemetryEventHandler _telemetryEventHandler;

            #endregion

            /// <summary>
            /// Constructor.
            /// </summary>
            internal SubmissionRecord(int submissionId, IEventSource buildEventSource, BuildStartedEventArgs buildStartedEvent, int maxNodeCount)
            {
                _maxNodeCount = maxNodeCount;
                _submissionId = submissionId;
                _buildStartedEvent = buildStartedEvent;
                _eventSourceForBuild = buildEventSource;
                _loggers = new List<ILogger>();
                InitializeInternalEventSource();
            }

            #region Events
            /// <summary>
            /// This event is raised to log a message.
            /// </summary>
            public event BuildMessageEventHandler MessageRaised;

            /// <summary>
            /// This event is raised to log an error.
            /// </summary>
            public event BuildErrorEventHandler ErrorRaised;

            /// <summary>
            /// This event is raised to log a warning.
            /// </summary>
            public event BuildWarningEventHandler WarningRaised;

            /// <summary>
            /// this event is raised to log the start of a build
            /// </summary>
            public event BuildStartedEventHandler BuildStarted;

            /// <summary>
            /// this event is raised to log the end of a build
            /// </summary>
            public event BuildFinishedEventHandler BuildFinished;

            /// <summary>
            /// this event is raised to log the start of a project build
            /// </summary>
            public event ProjectStartedEventHandler ProjectStarted;

            /// <summary>
            /// this event is raised to log the end of a project build
            /// </summary>
            public event ProjectFinishedEventHandler ProjectFinished;

            /// <summary>
            /// this event is raised to log the start of a target build
            /// </summary>
            public event TargetStartedEventHandler TargetStarted;

            /// <summary>
            /// this event is raised to log the end of a target build
            /// </summary>
            public event TargetFinishedEventHandler TargetFinished;

            /// <summary>
            /// this event is raised to log the start of task execution
            /// </summary>
            public event TaskStartedEventHandler TaskStarted;

            /// <summary>
            /// this event is raised to log the end of task execution
            /// </summary>
            public event TaskFinishedEventHandler TaskFinished;

            /// <summary>
            /// this event is raised to log a custom event
            /// </summary>
            public event CustomBuildEventHandler CustomEventRaised;

            /// <summary>
            /// this event is raised to log build status events, such as
            /// build/project/target/task started/stopped
            /// </summary>
            public event BuildStatusEventHandler StatusEventRaised;

            /// <summary>
            /// This event is raised to log that some event has
            /// occurred.  It is raised on every event.
            /// </summary>
            public event AnyEventHandler AnyEventRaised;

            /// <summary>
            /// This event is raised when telemetry is sent.
            /// </summary>
            public event TelemetryEventHandler TelemetryLogged;

            #endregion

            #region Internal Methods
            /// <summary>
            /// Adds the specified logger to the set of loggers for this submission.
            /// </summary>
            internal void AddLogger(ILogger logger)
            {
                lock (_syncLock)
                {
                    if (_loggers.Contains(logger))
                    {
                        throw new InvalidOperationException("Cannot register the same logger twice.");
                    }

                    // Node loggers are central /l loggers which can understand how many CPU's the build is running with, they are only different in that
                    // they can take a number of CPU
                    if (logger is INodeLogger nodeLogger)
                    {
                        nodeLogger.Initialize(this, _maxNodeCount);
                    }
                    else
                    {
                        logger.Initialize(this);
                    }

                    _loggers.Add(logger);
                }
            }

            /// <summary>
            /// Shuts down the loggers and removes them
            /// </summary>
            internal void Shutdown()
            {
                lock (_syncLock)
                {
                    if (!_shutdown)
                    {
                        _shutdown = true;
                        _firstProjectStartedEventContext = null;

                        UnregisterAllEventHandlers();

                        foreach (ILogger logger in _loggers)
                        {
                            logger.Shutdown();
                        }

                        _loggers.Clear();
                    }
                }
            }

            /// <summary>
            /// Sets the build started event for this event source if it hasn't already been set.
            /// </summary>
            internal void SetGlobalBuildStartedEvent(BuildStartedEventArgs buildStartedEvent)
            {
                lock (_syncLock)
                {
                    if (_buildStartedEvent == null)
                    {
                        _buildStartedEvent = buildStartedEvent;
                    }
                }
            }

            /// <summary>
            /// Raises a message event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">BuildMessageEventArgs</param>
            private void RaiseMessageEvent(object sender, BuildMessageEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    // If the event does not have the submissionId for our loggers then drop it.
                    if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId)
                    {
                        return;
                    }

                    if (MessageRaised != null)
                    {
                        try
                        {
                            MessageRaised(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                    }
                }
            }

            /// <summary>
            /// Raises an error event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">BuildErrorEventArgs</param>
            private void RaiseErrorEvent(object sender, BuildErrorEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    if (
                        buildEvent.BuildEventContext != null &&
                        (
                         buildEvent.BuildEventContext.SubmissionId != _submissionId && /* The build submission does not match the submissionId for this logger */
                         buildEvent.BuildEventContext.SubmissionId != BuildEventContext.InvalidSubmissionId))
                    {
                        return;
                    }

                    if (ErrorRaised != null)
                    {
                        try
                        {
                            ErrorRaised(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();
                        }
                    }
                }
            }

            /// <summary>
            /// Raises a warning event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">BuildWarningEventArgs</param>
            private void RaiseWarningEvent(object sender, BuildWarningEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    if (
                        buildEvent.BuildEventContext != null &&
                        (
                         buildEvent.BuildEventContext.SubmissionId != _submissionId && /* The build submission does not match the submissionId for this logger */
                         buildEvent.BuildEventContext.SubmissionId != BuildEventContext.InvalidSubmissionId))
                    {
                        return;
                    }

                    if (WarningRaised != null)
                    {
                        try
                        {
                            WarningRaised(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();
                        }
                    }
                }
            }

            /// <summary>
            /// Raises a "build started" event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">BuildStartedEventArgs</param>
            private void RaiseBuildStartedEvent(object sender, BuildStartedEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    // If we receive a REAL build started event, ignore it.  We only want the one we get as a result of the project started event
                    if (_firstProjectStartedEventContext == null)
                    {
                        return;
                    }

                    if (BuildStarted != null)
                    {
                        try
                        {
                            BuildStarted(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                    }

                    RaiseStatusEvent(sender, buildEvent, true /* cascade to AnyEvent */);
                }
            }

            /// <summary>
            /// Raises a "build finished" event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">BuildFinishedEventArgs</param>
            private void RaiseBuildFinishedEvent(object sender, BuildFinishedEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    // If we already did the build finished event (synthesized from project finished), we don't want to do it again if we happen
                    // to still be registered when the REAL build finished event comes through.
                    if (_firstProjectStartedEventContext == null)
                    {
                        return;
                    }

                    if (BuildFinished != null)
                    {
                        try
                        {
                            BuildFinished(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                    }

                    RaiseStatusEvent(sender, buildEvent, true /* cascade to AnyEvent */);
                }
            }

            /// <summary>
            /// Raises a "project build started" event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">ProjectStartedEventArgs</param>
            private void RaiseProjectStartedEvent(object sender, ProjectStartedEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    // If the event does not have the submissionId for our loggers then drop it.
                    if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId)
                    {
                        return;
                    }

                    if (_firstProjectStartedEventContext == null)
                    {
                        // Capture the build event context for the first project started event so we can make sure we know when to fire the
                        // build finished event (in the case of loggers on the mux logger this is on the last project finished event for the submission
                        _firstProjectStartedEventContext = buildEvent.BuildEventContext;

                        // We've never seen a project started event, so raise the build started event and save this project started event.
                        BuildStartedEventArgs startedEvent =
                            new BuildStartedEventArgs(_buildStartedEvent.Message,
                            _buildStartedEvent.HelpKeyword,
                            Traits.LogAllEnvironmentVariables ? _buildStartedEvent.BuildEnvironment : _buildStartedEvent.BuildEnvironment?.Where(kvp => EnvironmentUtilities.IsWellKnownEnvironmentDerivedProperty(kvp.Key)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value));
                        RaiseBuildStartedEvent(sender, startedEvent);
                    }

                    if (ProjectStarted != null)
                    {
                        try
                        {
                            ProjectStarted(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                    }
                }
            }

            /// <summary>
            /// Raises a "project build finished" event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">ProjectFinishedEventArgs</param>
            private void RaiseProjectFinishedEvent(object sender, ProjectFinishedEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    // If the event does not have the submissionId for our loggers then drop it.
                    if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId)
                    {
                        return;
                    }

                    if (ProjectFinished != null)
                    {
                        try
                        {
                            ProjectFinished(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                    }
                }
            }

            /// <summary>
            /// Raises a "target build started" event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">TargetStartedEventArgs</param>
            private void RaiseTargetStartedEvent(object sender, TargetStartedEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    // If the event does not have the submissionId for our loggers then drop it.
                    if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId)
                    {
                        return;
                    }

                    if (TargetStarted != null)
                    {
                        try
                        {
                            TargetStarted(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                    }
                }
            }

            /// <summary>
            /// Raises a "target build finished" event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">TargetFinishedEventArgs</param>
            private void RaiseTargetFinishedEvent(object sender, TargetFinishedEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    // If the event does not have the submissionId for our loggers then drop it.
                    if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId)
                    {
                        return;
                    }

                    if (TargetFinished != null)
                    {
                        try
                        {
                            TargetFinished(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                    }
                }
            }

            /// <summary>
            /// Raises a "task execution started" event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">TaskStartedEventArgs</param>
            private void RaiseTaskStartedEvent(object sender, TaskStartedEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    // If the event does not have the submissionId for our loggers then drop it.
                    if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId)
                    {
                        return;
                    }

                    if (TaskStarted != null)
                    {
                        try
                        {
                            TaskStarted(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                    }
                }
            }

            /// <summary>
            /// Raises a "task finished executing" event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">TaskFinishedEventArgs</param>
            private void RaiseTaskFinishedEvent(object sender, TaskFinishedEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    // If the event does not have the submissionId for our loggers then drop it.
                    if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId)
                    {
                        return;
                    }

                    if (TaskFinished != null)
                    {
                        try
                        {
                            TaskFinished(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                    }
                }
            }

            /// <summary>
            /// Raises a custom event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">CustomBuildEventArgs</param>
            private void RaiseCustomEvent(object sender, CustomBuildEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    // If the event does not have the submissionId for our loggers then drop it.
                    if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId)
                    {
                        return;
                    }

                    if (CustomEventRaised != null)
                    {
                        try
                        {
                            CustomEventRaised(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                    }
                }
            }

            /// <summary>
            /// Raises a catch-all build status event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">BuildStatusEventArgs</param>
            private void RaiseStatusEvent(object sender, BuildStatusEventArgs buildEvent) => RaiseStatusEvent(sender, buildEvent, false);

            /// <summary>
            /// Raises a status event, optionally cascading to an any event.
            /// </summary>
            private void RaiseStatusEvent(object sender, BuildStatusEventArgs buildEvent, bool cascade)
            {
                lock (_syncLock)
                {
                    // If the event does not have the submissionId for our loggers then drop it.
                    if (buildEvent.BuildEventContext != null && buildEvent.BuildEventContext.SubmissionId != _submissionId)
                    {
                        return;
                    }

                    if (StatusEventRaised != null)
                    {
                        try
                        {
                            StatusEventRaised(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                    }

                    if (cascade)
                    {
                        RaiseAnyEvent(sender, buildEvent);
                    }
                }
            }

            /// <summary>
            /// Raises a catch-all build event to all registered loggers.
            /// </summary>
            /// <param name="sender">sender of the event</param>
            /// <param name="buildEvent">Build EventArgs</param>
            private void RaiseAnyEvent(object sender, BuildEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    bool eventIsErrorOrWarning = (buildEvent is BuildWarningEventArgs) || (buildEvent is BuildErrorEventArgs);

                    if (
                        buildEvent.BuildEventContext != null &&
                        (
                         buildEvent.BuildEventContext.SubmissionId != _submissionId && /* The build submission does not match the submissionId for this logger */
                         !( /* We do not have a build submissionid this can happen if the event comes from the nodeloggingcontext -- but we only want to raise it if it was an error or warning */
                           buildEvent.BuildEventContext.SubmissionId == BuildEventContext.InvalidSubmissionId && eventIsErrorOrWarning)))
                    {
                        return;
                    }

                    // If we receive a REAL build started or finished event, ignore it.  We only want the one we get as a result of the project started and finished events
                    if (_firstProjectStartedEventContext == null && (buildEvent is BuildStartedEventArgs || buildEvent is BuildFinishedEventArgs))
                    {
                        return;
                    }

                    if (AnyEventRaised != null)
                    {
                        try
                        {
                            AnyEventRaised(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                    }

                    // If this project finished event matches our first project started event, then send build finished.
                    // Because of the way the event source works, we actually have to process this here rather than in project finished because if the
                    // logger is registered without a ProjectFinished handler, but does have an Any handler (as the mock logger does) then we would end up
                    // sending the BuildFinished event before the ProjectFinished event got processed in the Any handler.
                    ProjectFinishedEventArgs projectFinishedEvent = buildEvent as ProjectFinishedEventArgs;
                    if (projectFinishedEvent != null && buildEvent.BuildEventContext?.Equals(_firstProjectStartedEventContext) == true)
                    {
                        string message = projectFinishedEvent.Succeeded ? ResourceUtilities.GetResourceString("MuxLogger_BuildFinishedSuccess") : ResourceUtilities.GetResourceString("MuxLogger_BuildFinishedFailure");
                        RaiseBuildFinishedEvent(sender, new BuildFinishedEventArgs(message, null, projectFinishedEvent.Succeeded));
                        Shutdown();
                    }
                }
            }

            /// <summary>
            /// Raises a telemetry event to all registered loggers.
            /// </summary>
            private void RaiseTelemetryEvent(object sender, TelemetryEventArgs buildEvent)
            {
                lock (_syncLock)
                {
                    if (TelemetryLogged != null)
                    {
                        try
                        {
                            TelemetryLogged(sender, buildEvent);
                        }
                        catch (LoggerException)
                        {
                            // if a logger has failed politely, abort immediately
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();

                            throw;
                        }
                        catch (Exception)
                        {
                            // first unregister all loggers, since other loggers may receive remaining events in unexpected orderings
                            // if a fellow logger is throwing in an event handler.
                            UnregisterAllEventHandlers();
                        }
                    }
                }
            }
            #endregion

            #region private methods
            /// <summary>
            /// Initialize the internal event source which is used to raise events on loggers registered to this submission
            /// </summary>
            private void InitializeInternalEventSource()
            {
                _anyEventHandler = RaiseAnyEvent;
                _buildFinishedEventHandler = RaiseBuildFinishedEvent;
                _buildStartedEventHandler = RaiseBuildStartedEvent;
                _customBuildEventHandler = RaiseCustomEvent;
                _buildErrorEventHandler = RaiseErrorEvent;
                _buildMessageEventHandler = RaiseMessageEvent;
                _projectFinishedEventHandler = RaiseProjectFinishedEvent;
                _projectStartedEventHandler = RaiseProjectStartedEvent;
                _buildStatusEventHandler = RaiseStatusEvent;
                _targetFinishedEventHandler = RaiseTargetFinishedEvent;
                _targetStartedEventHandler = RaiseTargetStartedEvent;
                _taskFinishedEventHandler = RaiseTaskFinishedEvent;
                _taskStartedEventHandler = RaiseTaskStartedEvent;
                _buildWarningEventHandler = RaiseWarningEvent;
                _telemetryEventHandler = RaiseTelemetryEvent;

                _eventSourceForBuild.AnyEventRaised += _anyEventHandler;
                _eventSourceForBuild.BuildFinished += _buildFinishedEventHandler;
                _eventSourceForBuild.BuildStarted += _buildStartedEventHandler;
                _eventSourceForBuild.CustomEventRaised += _customBuildEventHandler;
                _eventSourceForBuild.ErrorRaised += _buildErrorEventHandler;
                _eventSourceForBuild.MessageRaised += _buildMessageEventHandler;
                _eventSourceForBuild.ProjectFinished += _projectFinishedEventHandler;
                _eventSourceForBuild.ProjectStarted += _projectStartedEventHandler;
                _eventSourceForBuild.StatusEventRaised += _buildStatusEventHandler;
                _eventSourceForBuild.TargetFinished += _targetFinishedEventHandler;
                _eventSourceForBuild.TargetStarted += _targetStartedEventHandler;
                _eventSourceForBuild.TaskFinished += _taskFinishedEventHandler;
                _eventSourceForBuild.TaskStarted += _taskStartedEventHandler;
                _eventSourceForBuild.WarningRaised += _buildWarningEventHandler;

                if (_eventSourceForBuild is IEventSource2 eventSource2)
                {
                    eventSource2.TelemetryLogged += _telemetryEventHandler;
                }
            }

            /// <summary>
            /// Clears out all events.
            /// </summary>
            private void UnregisterAllEventHandlers()
            {
                _eventSourceForBuild.AnyEventRaised -= _anyEventHandler;
                _eventSourceForBuild.BuildFinished -= _buildFinishedEventHandler;
                _eventSourceForBuild.BuildStarted -= _buildStartedEventHandler;
                _eventSourceForBuild.CustomEventRaised -= _customBuildEventHandler;
                _eventSourceForBuild.ErrorRaised -= _buildErrorEventHandler;
                _eventSourceForBuild.MessageRaised -= _buildMessageEventHandler;
                _eventSourceForBuild.ProjectFinished -= _projectFinishedEventHandler;
                _eventSourceForBuild.ProjectStarted -= _projectStartedEventHandler;
                _eventSourceForBuild.StatusEventRaised -= _buildStatusEventHandler;
                _eventSourceForBuild.TargetFinished -= _targetFinishedEventHandler;
                _eventSourceForBuild.TargetStarted -= _targetStartedEventHandler;
                _eventSourceForBuild.TaskFinished -= _taskFinishedEventHandler;
                _eventSourceForBuild.TaskStarted -= _taskStartedEventHandler;
                _eventSourceForBuild.WarningRaised -= _buildWarningEventHandler;

                if (_eventSourceForBuild is IEventSource2 eventSource2)
                {
                    eventSource2.TelemetryLogged -= _telemetryEventHandler;
                }

                MessageRaised = null;
                ErrorRaised = null;
                WarningRaised = null;
                BuildStarted = null;
                BuildFinished = null;
                ProjectStarted = null;
                ProjectFinished = null;
                TargetStarted = null;
                TargetFinished = null;
                TaskStarted = null;
                TaskFinished = null;
                CustomEventRaised = null;
                StatusEventRaised = null;
                AnyEventRaised = null;
                TelemetryLogged = null;
            }
            #endregion
        }
    }
}
