// 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.IO;
using System.Runtime.Serialization;
using System.Text;
using Microsoft.Build.Shared;

namespace Microsoft.Build.Framework
{
    /// <summary>
    /// This class encapsulates the default data associated with build events. 
    /// It is intended to be extended/sub-classed.
    /// </summary>
    // WARNING: marking a type [Serializable] without implementing
    // ISerializable imposes a serialization contract -- it is a
    // promise to never change the type's fields i.e. the type is
    // immutable; adding new fields in the next version of the type
    // without following certain special FX guidelines, can break both
    // forward and backward compatibility
    [Serializable]
    public abstract class BuildEventArgs : EventArgs
    {
        /// <summary>
        /// Message. Volatile because it may be updated lock-free after construction.
        /// </summary>
        private volatile string? message;

        /// <summary>
        /// Help keyword
        /// </summary>
        private string? helpKeyword;

        /// <summary>
        /// Sender name
        /// </summary>
        private string? senderName;

        /// <summary>
        /// Timestamp
        /// </summary>
        private DateTime timestamp;

        [NonSerialized]
        private DateTime? _localTimestamp;

        /// <summary>
        /// Thread id
        /// </summary>
        private int threadId;

        /// <summary>
        /// Build event context
        /// </summary>
        [OptionalField(VersionAdded = 2)]
        private BuildEventContext? buildEventContext;

        /// <summary>
        /// Default constructor
        /// </summary>
        protected BuildEventArgs()
            : this(null, null, null, DateTime.UtcNow)
        {
        }

        /// <summary>
        /// This constructor allows all event data to be initialized
        /// </summary>
        /// <param name="message">text message</param>
        /// <param name="helpKeyword">help keyword </param>
        /// <param name="senderName">name of event sender</param>
        protected BuildEventArgs(string? message, string? helpKeyword, string? senderName)
            : this(message, helpKeyword, senderName, DateTime.UtcNow)
        {
        }

        /// <summary>
        /// This constructor allows all event data to be initialized while providing a custom timestamp.
        /// </summary>
        /// <param name="message">text message</param>
        /// <param name="helpKeyword">help keyword </param>
        /// <param name="senderName">name of event sender</param>
        /// <param name="eventTimestamp">TimeStamp of when the event was created</param>
        protected BuildEventArgs(string? message, string? helpKeyword, string? senderName, DateTime eventTimestamp)
        {
            this.message = message;
            this.helpKeyword = helpKeyword;
            this.senderName = senderName;
            timestamp = eventTimestamp;
            threadId = System.Threading.Thread.CurrentThread.GetHashCode();
        }

        /// <summary>
        /// The time when event was raised.
        /// </summary>
        public DateTime Timestamp
        {
            get
            {
                // Rather than storing dates in Local time all the time, we store in UTC type, and only
                // convert to Local when the user requests access to this field.  This lets us avoid the
                // expensive conversion to Local time unless it's absolutely necessary.
                if (!_localTimestamp.HasValue)
                {
                    _localTimestamp = timestamp.Kind == DateTimeKind.Utc || timestamp.Kind == DateTimeKind.Unspecified
                        ? timestamp.ToLocalTime()
                        : timestamp;
                }

                return _localTimestamp.Value;
            }
        }

        /// <summary>
        /// Exposes the private <see cref="timestamp"/> field to derived types.
        /// Used for serialization. Avoids the side effects of calling the
        /// <see cref="Timestamp"/> getter.
        /// </summary>
        protected internal DateTime RawTimestamp
        {
            get => timestamp;
            set => timestamp = value;
        }

        /// <summary>
        /// The thread that raised event.  
        /// </summary>
        public int ThreadId => threadId;

        /// <summary>
        /// Text of event.
        /// </summary>
        public virtual string? Message
        {
            get => message;
            protected set => message = value;
        }

        /// <summary>
        /// Exposes the underlying message field without side-effects.
        /// Used for serialization.
        /// </summary>
        protected internal string? RawMessage
        {
            get => FormattedMessage;
            set => message = value;
        }

        /// <summary>
        /// Like <see cref="RawMessage"/> but returns a formatted message string if available.
        /// Used for serialization.
        /// </summary>
        private protected virtual string? FormattedMessage
        {
            get => message;
        }

        /// <summary>
        /// Custom help keyword associated with event.
        /// </summary>
        public string? HelpKeyword => helpKeyword;

        /// <summary>
        /// Name of the object sending this event.
        /// </summary>
        public string? SenderName => senderName;

        /// <summary>
        /// Event contextual information for the build event argument
        /// </summary>
        public BuildEventContext? BuildEventContext
        {
            get => buildEventContext;
            set => buildEventContext = value;
        }

        #region CustomSerializationToStream
        /// <summary>
        /// Serializes to a stream through a binary writer
        /// </summary>
        /// <param name="writer">Binary writer which is attached to the stream the event will be serialized into</param>
        /// <param name="messageToWrite">The message to write to the stream.</param>
        private protected void WriteToStreamWithExplicitMessage(BinaryWriter writer, string? messageToWrite)
        {
            writer.WriteOptionalString(messageToWrite);
            writer.WriteOptionalString(helpKeyword);
            writer.WriteOptionalString(senderName);
            writer.WriteTimestamp(timestamp);
            writer.Write(threadId);
            writer.WriteOptionalBuildEventContext(buildEventContext);
        }

        /// <summary>
        /// Serializes to a stream through a binary writer
        /// </summary>
        /// <param name="writer">Binary writer which is attached to the stream the event will be serialized into</param>
        internal virtual void WriteToStream(BinaryWriter writer)
        {
            WriteToStreamWithExplicitMessage(writer, RawMessage);
        }

        /// <summary>
        /// Deserializes from a stream through a binary reader
        /// </summary>
        /// <param name="reader">Binary reader which is attached to the stream the event will be deserialized from</param>
        /// <param name="version">The version of the runtime the message packet was created from</param>
        internal virtual void CreateFromStream(BinaryReader reader, int version)
        {
            message = reader.ReadOptionalString();
            helpKeyword = reader.ReadOptionalString();
            senderName = reader.ReadOptionalString();

            long timestampTicks = reader.ReadInt64();

            if (version > 20)
            {
                DateTimeKind kind = (DateTimeKind)reader.ReadInt32();
                timestamp = new DateTime(timestampTicks, kind);
            }
            else
            {
                timestamp = new DateTime(timestampTicks);
            }

            threadId = reader.ReadInt32();

            if (reader.ReadByte() == 0)
            {
                buildEventContext = null;
            }
            else
            {
                int nodeId = reader.ReadInt32();
                int projectContextId = reader.ReadInt32();
                int targetId = reader.ReadInt32();
                int taskId = reader.ReadInt32();

                if (version > 20)
                {
                    int submissionId = reader.ReadInt32();
                    int projectInstanceId = reader.ReadInt32();
                    int evaluationId = reader.ReadInt32();
                    buildEventContext = new BuildEventContext(submissionId, nodeId, evaluationId, projectInstanceId, projectContextId, targetId, taskId);
                }
                else
                {
                    buildEventContext = new BuildEventContext(nodeId, targetId, projectContextId, taskId);
                }
            }
        }
        #endregion

        #region SetSerializationDefaults
        /// <summary>
        /// Run before the object has been deserialized
        /// UNDONE (Logging.)  Can this and the next function go away, and instead return a BuildEventContext.Invalid from
        /// the property if the buildEventContext field is null?
        /// </summary>
        [OnDeserializing]
        private void SetBuildEventContextDefaultBeforeSerialization(StreamingContext sc)
        {
            // Don't want to create a new one here as default all the time as that would be a lot of 
            // possibly useless allocations
            buildEventContext = null;
        }

        /// <summary>
        /// Run after the object has been deserialized
        /// </summary>
        [OnDeserialized]
        private void SetBuildEventContextDefaultAfterSerialization(StreamingContext sc)
        {
            if (buildEventContext == null)
            {
                buildEventContext = BuildEventContext.Invalid;
            }
        }
        #endregion

        /// <summary>
        /// This is the default stub implementation, only here as a safeguard.
        /// Actual logic is injected from Microsoft.Build.dll to replace this.
        /// This is used by the Message property overrides to reconstruct the
        /// message lazily on demand.
        /// </summary>
        internal static Func<string, string?[], string> ResourceStringFormatter = (string resourceName, string?[] arguments) =>
        {
            var sb = new StringBuilder();
            sb.Append(resourceName);
            sb.Append('(');

            bool notFirst = false;
            foreach (var argument in arguments)
            {
                if (notFirst)
                {
                    sb.Append(',');
                }
                else
                {
                    notFirst = true;
                }

                sb.Append(argument);
            }

            sb.Append(')');
            return sb.ToString();
        };

        /// <summary>
        /// Shortcut method to mimic the original logic of creating the formatted strings.
        /// </summary>
        /// <param name="resourceName">Name of the resource string.</param>
        /// <param name="arguments">Optional list of arguments to pass to the formatted string.</param>
        /// <returns>The concatenated formatted string.</returns>
        internal static string FormatResourceStringIgnoreCodeAndKeyword(string resourceName, params string?[] arguments)
        {
            return ResourceStringFormatter(resourceName, arguments);
        }
    }
}
