// 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.Globalization;
using System.IO;

namespace Microsoft.Build.Framework
{
    /// <summary>
    /// Stores strings for parts of a message delaying the formatting until it needs to be shown
    /// </summary>
    [Serializable]
    public class LazyFormattedBuildEventArgs : BuildEventArgs
    {
        /// <summary>
        /// Stores the message arguments.
        /// </summary>
        private volatile object? argumentsOrFormattedMessage;

        /// <summary>
        /// Exposes the underlying arguments field to serializers.
        /// </summary>
        internal object[]? RawArguments
        {
            get => (argumentsOrFormattedMessage is object[] arguments) ? arguments : null;
        }

        /// <summary>
        /// Exposes the formatted message string to serializers.
        /// </summary>
        private protected override string? FormattedMessage
        {
            get => (argumentsOrFormattedMessage is string formattedMessage) ? formattedMessage : base.FormattedMessage;
        }

        /// <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>
        public LazyFormattedBuildEventArgs(
            string? message,
            string? helpKeyword,
            string? senderName)
            : this(message, helpKeyword, senderName, DateTime.Now, null)
        {
        }

        /// <summary>
        /// This constructor that allows message arguments that are lazily formatted.
        /// </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 when event was created.</param>
        /// <param name="messageArgs">Message arguments.</param>
        public LazyFormattedBuildEventArgs(
            string? message,
            string? helpKeyword,
            string? senderName,
            DateTime eventTimestamp,
            params object[]? messageArgs)
            : base(message, helpKeyword, senderName, eventTimestamp)
        {
            argumentsOrFormattedMessage = messageArgs;
        }

        /// <summary>
        /// Default constructor.
        /// </summary>
        protected LazyFormattedBuildEventArgs()
            : base()
        {
        }

        /// <summary>
        /// Gets the formatted message.
        /// </summary>
        public override string? Message
        {
            get
            {
                object? argsOrMessage = argumentsOrFormattedMessage;
                if (argsOrMessage is string formattedMessage)
                {
                    return formattedMessage;
                }

                if (argsOrMessage is object[] arguments && arguments.Length > 0 && base.Message is not null)
                {
                    formattedMessage = FormatString(base.Message, arguments);
                    argumentsOrFormattedMessage = formattedMessage;
                    return formattedMessage;
                }

                return base.Message;
            }
        }

        /// <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 override void WriteToStream(BinaryWriter writer)
        {
            object? argsOrMessage = argumentsOrFormattedMessage;
            if (argsOrMessage is object[] arguments && arguments.Length > 0)
            {
                base.WriteToStreamWithExplicitMessage(writer, base.Message);
                writer.Write(arguments.Length);

                foreach (object argument in arguments)
                {
                    // Arguments may be ints, etc, so explicitly convert
                    // Convert.ToString returns String.Empty when it cannot convert, rather than throwing
                    // It returns null if the input is null.
                    writer.Write(Convert.ToString(argument, CultureInfo.CurrentCulture) ?? "");
                }
            }
            else
            {
                base.WriteToStreamWithExplicitMessage(writer, (argsOrMessage is string formattedMessage) ? formattedMessage : base.Message);
                writer.Write(-1);
            }
        }

        /// <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 override void CreateFromStream(BinaryReader reader, Int32 version)
        {
            base.CreateFromStream(reader, version);

            if (version > 20)
            {
                string[]? messageArgs = null;
                int numArguments = reader.ReadInt32();

                if (numArguments >= 0)
                {
                    messageArgs = new string[numArguments];

                    for (int numRead = 0; numRead < numArguments; numRead++)
                    {
                        messageArgs[numRead] = reader.ReadString();
                    }
                }

                argumentsOrFormattedMessage = messageArgs;
            }
        }

        /// <summary>
        /// Formats the given string using the variable arguments passed in.
        /// 
        /// PERF WARNING: calling a method that takes a variable number of arguments is expensive, because memory is allocated for
        /// the array of arguments -- do not call this method repeatedly in performance-critical scenarios
        /// </summary>
        /// <remarks>This method is thread-safe.</remarks>
        /// <param name="unformatted">The string to format.</param>
        /// <param name="args">Optional arguments for formatting the given string.</param>
        /// <returns>The formatted string.</returns>
        private static string FormatString(string unformatted, params object[] args)
        {
            // Based on the one in Shared/ResourceUtilities.
            string formatted = unformatted;

            // NOTE: String.Format() does not allow a null arguments array
            if ((args?.Length > 0))
            {
#if DEBUG
                // If you accidentally pass some random type in that can't be converted to a string, 
                // FormatResourceString calls ToString() which returns the full name of the type!
                foreach (object param in args)
                {
                    // Check against a list of types that we know have
                    // overridden ToString() usefully. If you want to pass 
                    // another one, add it here.
                    if (param != null && param.ToString() == param.GetType().FullName)
                    {
                        throw new InvalidOperationException(string.Format("Invalid type for message formatting argument, was {0}", param.GetType().FullName));
                    }
                }
#endif
                // Format the string, using the variable arguments passed in.
                // NOTE: all String methods are thread-safe
                try
                {
                    formatted = string.Format(unformatted, args);
                }
                catch (FormatException ex)
                {
                    // User task may have logged something with too many format parameters
                    // We don't have resources in this assembly, and we generally log stack for task failures so they can be fixed by the owner
                    // However, we don't want to crash the logger and stop the build.
                    // Error will look like this (it's OK to not localize subcategory). It's not too bad, although there's no file.
                    // 
                    //       Task "Crash"
                    //          (16,14):  error : "This message logged from a task {1} has too few formatting parameters."
                    //             at System.Text.StringBuilder.AppendFormat(IFormatProvider provider, String format, Object[] args)
                    //             at System.String.Format(String format, Object[] args)
                    //             at Microsoft.Build.Framework.LazyFormattedBuildEventArgs.FormatString(String unformatted, Object[] args) in d:\W8T_Refactor\src\vsproject\xmake\Framework\LazyFormattedBuildEventArgs.cs:line 263
                    //          Done executing task "Crash".
                    //
                    // T
                    formatted = string.Format("\"{0}\"\n{1}", unformatted, ex.ToString());
                }
            }

            return formatted;
        }
    }
}
