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

namespace Microsoft.NET.Build.Tasks
{
    /// <summary>
    /// Replacement and abstraction for <see cref="TaskLoggingHelper"/> in our
    /// build tasks.
    /// </summary>
    /// <remarks>
    /// Source compatible with usual Log.LogXxx MSBuild task code. (Subset of
    /// API chosen based on actual usage in SDK, and with a deliberate goal of
    /// eliminating some of the excessive overloading in TaskLoggingHelper.
    ///
    /// <see cref="Message"/> replaces the need for overloads taking over 10
    /// arguments.
    ///
    /// Also, string[] is used instead of object[] to avoid issues like passing
    /// the importance out of order as a format argument.
    ///
    /// <see cref="Log"/> allows choosing Error/Warning/Message dynamically at a
    /// single call site.
    ///
    /// Extracts error codes from the message prefix, and enforces that all of
    /// our messages have a NETSDK code.
    ///
    /// Example:
    ///   C#
    ///     Log.LogError(Strings.SomethingIsWrong);
    ///
    ///   Strings.resx:
    ///     Resource name: SomethingIsWrong
    ///     Resource value: NETSDK1234: Something is wrong.
    ///
    /// Results in LogCore getting a Message instance with Code="NETSDK1234"
    /// and Text="Something is wrong."
    ///
    /// Pattern inspired by <se cref="TaskLoggingHelper.LogErrorWithCodeFromResources"/>,
    /// but retains completion via generated <see cref="Strings"/> instead of
    /// passing resource keys by name.
    ///
    /// All actual logging is deferred to subclass in <see cref="LogCore"/>,
    /// which allows unit tests to verify task logging while mocking a single
    /// method. <see cref="TaskBase"/> adapts that to <see
    /// cref="TaskLoggingHelper"/>.
    /// </remarks>
    internal abstract class Logger
    {
        public bool HasLoggedErrors { get; private set; }

        public void LogMessage(string format, params string[] args)
            => Log(CreateMessage(MessageLevel.NormalImportance, format, args));

        public void LogMessage(MessageImportance importance, string format, params string[] args)
            => Log(CreateMessage(importance.ToLevel(), format, args));

        public void LogWarning(string format, params string[] args)
            => Log(CreateMessage(MessageLevel.Warning, format, args));

        public void LogError(string format, params string[] args)
            => Log(CreateMessage(MessageLevel.Error, format, args));

        public void LogNonSdkError(string code, string format, params string[] args)
            => Log(new Message(MessageLevel.Error, string.Format(format, args), code));

        public void Log(in Message message)
        {
            HasLoggedErrors |= message.Level == MessageLevel.Error;
            LogCore(message);
        }

        protected abstract void LogCore(in Message message);

        private static Message CreateMessage(MessageLevel level, string format, string[] args)
        {
            string code;

            if (format.Length >= 12
                && format[0] == 'N'
                && format[1] == 'E'
                && format[2] == 'T'
                && format[3] == 'S'
                && format[4] == 'D'
                && format[5] == 'K'
                && IsAsciiDigit(format[6])
                && IsAsciiDigit(format[7])
                && IsAsciiDigit(format[8])
                && IsAsciiDigit(format[9])
                && format[10] == ':'
                && format[11] == ' ')
            {
                code = format.Substring(0, 10);
                format = format.Substring(12);
            }
            else
            {
                code = null;
            }

            DebugThrowMissingOrIncorrectCode(code, format, level);

            return new Message(
                level,
                text: string.Format(format, args),
                code: code);
        }

        [Conditional("DEBUG")]
        private static void DebugThrowMissingOrIncorrectCode(string code, string message, MessageLevel level)
        {
            // NB: This is not localized because it represents a bug in our code base, not a user error.
            //     To log message with external codes, use Log.Log(in Message, string[]) directly.
            //     It is not a Debug.Assert because it doesn't render well in unit tests.

            switch (level)
            {
                case MessageLevel.Error:
                case MessageLevel.Warning:
                    if (code == null)
                    {
                        throw new ArgumentException(
                            "Message is not prefixed with NETSDK error code or error code is formatted incorrectly: "
                            + message);
                    }
                    break;

                default:
                    if (code != null)
                    {
                       throw new ArgumentException(
                           "Message is prefixed with NETSDK error, but error codes should not be used for informational messages: "
                           + $"{code}:{message}");
                    }
                    break;
            }
        }

        private static bool IsAsciiDigit(char c)
            => c >= '0' && c <= '9';
    }
}
