#region Apache License
//
// Licensed to the Apache Software Foundation (ASF) under one or more 
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership. 
// The ASF licenses this file to you under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with 
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

using System;
using System.Globalization;
using System.Runtime.InteropServices;

namespace log4net.Util;

/// <summary>
/// Represents a Win32 native error code and message.
/// </summary>
/// <author>Nicko Cadell</author>
/// <author>Gert Driesen</author>
public sealed class NativeError
{
  /// <summary>
  /// Create an instance of the <see cref="NativeError" /> class with the specified 
  /// error number and message.
  /// </summary>
  /// <param name="number">The number of the native error.</param>
  /// <param name="message">The message of the native error.</param>
  private NativeError(int number, string? message)
  {
    Number = number;
    Message = message;
  }

  /// <summary>
  /// Gets the number of the native error.
  /// </summary>
  /// <value>
  /// The number of the native error.
  /// </value>
  /// <remarks>
  /// <para>
  /// Gets the number of the native error.
  /// </para>
  /// </remarks>
  public int Number { get; }

  /// <summary>
  /// Gets the message of the native error.
  /// </summary>
  public string? Message { get; }

  /// <summary>
  /// Creates a new instance of the <see cref="NativeError" /> class for the last Windows error.
  /// </summary>
  /// <returns>
  /// An instance of the <see cref="NativeError" /> class for the last windows error.
  /// </returns>
  /// <remarks>
  /// <para>
  /// The message for the <see cref="Marshal.GetLastWin32Error"/> error number is lookup up using the 
  /// native Win32 <c>FormatMessage</c> function.
  /// </para>
  /// </remarks>
  [System.Security.SecuritySafeCritical]
  [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand, UnmanagedCode = true)]
  public static NativeError GetLastError()
  {
    int number = Marshal.GetLastWin32Error();
    return new NativeError(number, GetErrorMessage(number));
  }

  /// <summary>
  /// Create a new instance of the <see cref="NativeError" /> class.
  /// </summary>
  /// <param name="number">the error number for the native error</param>
  /// <returns>
  /// An instance of the <see cref="NativeError" /> class for the specified 
  /// error number.
  /// </returns>
  /// <remarks>
  /// <para>
  /// The message for the specified error number is lookup up using the 
  /// native Win32 <c>FormatMessage</c> function.
  /// </para>
  /// </remarks>
  public static NativeError GetError(int number)
  {
    return new NativeError(number, GetErrorMessage(number));
  }

  /// <summary>
  /// Retrieves the message corresponding with a Win32 message identifier.
  /// </summary>
  /// <param name="messageId">Message identifier for the requested message.</param>
  /// <returns>
  /// The message corresponding with the specified message identifier.
  /// </returns>
  /// <remarks>
  /// <para>
  /// The message will be searched for in system message-table resource(s)
  /// using the native <c>FormatMessage</c> function.
  /// </para>
  /// </remarks>
  [System.Security.SecuritySafeCritical]
  [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand, UnmanagedCode = true)]
  public static string? GetErrorMessage(int messageId)
  {
    // Win32 constants
    int formatMessageAllocateBuffer = 0x00000100;  // The function should allocates a buffer large enough to hold the formatted message
    int formatMessageIgnoreInserts = 0x00000200;    // Insert sequences in the message definition are to be ignored
    int formatMessageFromSystem = 0x00001000;    // The function should search the system message-table resource(s) for the requested message

    string? msgBuf = "";        // buffer that will receive the message
    var sourcePtr = new IntPtr();  // Location of the message definition, will be ignored
    var argumentsPtr = new IntPtr();  // Pointer to array of values to insert, not supported as it requires unsafe code

    if (messageId != 0)
    {
      // If the function succeeds, the return value is the number of TCHARs stored in the output buffer, excluding the terminating null character
      int messageSize = FormatMessage(
        formatMessageAllocateBuffer | formatMessageFromSystem | formatMessageIgnoreInserts,
        ref sourcePtr,
        messageId,
        0,
        ref msgBuf,
        255,
        argumentsPtr);

      if (messageSize > 0)
      {
        // Remove trailing null-terminating characters (\r\n) from the message
        msgBuf = msgBuf.TrimEnd(_newlines);
      }
      else
      {
        // A message could not be located.
        msgBuf = null;
      }
    }
    else
    {
      msgBuf = null;
    }

    return msgBuf;
  }

  /// <summary>
  /// Return error information string
  /// </summary>
  /// <returns>error information string</returns>
  /// <remarks>
  /// <para>
  /// Return error information string
  /// </para>
  /// </remarks>
  public override string ToString()
  {
    return string.Format(CultureInfo.InvariantCulture, "0x{0:x8}", Number) + (Message is not null ? ": " + Message : string.Empty);
  }

  /// <summary>
  /// Formats a message string.
  /// </summary>
  /// <param name="dwFlags">Formatting options, and how to interpret the <paramref name="lpSource" /> parameter.</param>
  /// <param name="lpSource">Location of the message definition.</param>
  /// <param name="dwMessageId">Message identifier for the requested message.</param>
  /// <param name="dwLanguageId">Language identifier for the requested message.</param>
  /// <param name="lpBuffer">If <paramref name="dwFlags" /> includes FORMAT_MESSAGE_ALLOCATE_BUFFER, the function allocates a buffer using the <c>LocalAlloc</c> function, and places the pointer to the buffer at the address specified in <paramref name="lpBuffer" />.</param>
  /// <param name="nSize">If the FORMAT_MESSAGE_ALLOCATE_BUFFER flag is not set, this parameter specifies the maximum number of TCHARs that can be stored in the output buffer. If FORMAT_MESSAGE_ALLOCATE_BUFFER is set, this parameter specifies the minimum number of TCHARs to allocate for an output buffer.</param>
  /// <param name="arguments">Pointer to an array of values that are used as insert values in the formatted message.</param>
  /// <remarks>
  /// <para>
  /// The function requires a message definition as input. The message definition can come from a 
  /// buffer passed into the function. It can come from a message table resource in an 
  /// already-loaded module. Or the caller can ask the function to search the system's message 
  /// table resource(s) for the message definition. The function finds the message definition 
  /// in a message table resource based on a message identifier and a language identifier. 
  /// The function copies the formatted message text to an output buffer, processing any embedded 
  /// insert sequences if requested.
  /// </para>
  /// <para>
  /// To prevent the usage of unsafe code, this stub does not support inserting values in the formatted message.
  /// </para>
  /// </remarks>
  /// <returns>
  /// <para>
  /// If the function succeeds, the return value is the number of TCHARs stored in the output 
  /// buffer, excluding the terminating null character.
  /// </para>
  /// <para>
  /// If the function fails, the return value is zero. To get extended error information, 
  /// call <see cref="M:Marshal.GetLastWin32Error()" />.
  /// </para>
  /// </returns>
  [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
  [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
  private static extern int FormatMessage(
    int dwFlags,
    ref IntPtr lpSource,
    int dwMessageId,
    int dwLanguageId,
    ref string lpBuffer,
    int nSize,
    IntPtr arguments);

  private static readonly char[] _newlines = ['\r', '\n'];
}
