#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.Collections.Generic;
using System.Text;
using System.Xml;

using log4net.Core;
using log4net.Layout.Internal;
using log4net.Util;

namespace log4net.Layout;

/// <summary>
/// Layout that formats the log events as XML elements.
/// </summary>
/// <remarks>
/// <para>
/// The output of the <see cref="XmlLayout" /> consists of a series of 
/// log4net:event elements. It does not output a complete well-formed XML 
/// file. The output is designed to be included as an <em>external entity</em>
/// in a separate file to form a correct XML file.
/// </para>
/// <para>
/// For example, if <c>abc</c> is the name of the file where
/// the <see cref="XmlLayout" /> output goes, then a well-formed XML file would 
/// be:
/// </para>
/// <code lang="XML">
/// &lt;?xml version="1.0" ?&gt;
/// 
/// &lt;!DOCTYPE log4net:events SYSTEM "log4net-events.dtd" [&lt;!ENTITY data SYSTEM "abc"&gt;]&gt;
///
/// &lt;log4net:events version="1.2" xmlns:log4net="http://logging.apache.org/log4net/schemas/log4net-events-1.2&gt;
///     &amp;data;
/// &lt;/log4net:events&gt;
/// </code>
/// <para>
/// This approach enforces the independence of the <see cref="XmlLayout" /> 
/// and the appender where it is embedded.
/// </para>
/// <para>
/// The <c>version</c> attribute helps components to correctly
/// interpret output generated by <see cref="XmlLayout" />. The value of 
/// this attribute should be "1.2" for release 1.2 and later.
/// </para>
/// <para>
/// Alternatively the <c>Header</c> and <c>Footer</c> properties can be
/// configured to output the correct XML header, open tag and close tag.
/// When setting the <c>Header</c> and <c>Footer</c> properties it is essential
/// that the underlying data store not be appendable otherwise the data
/// will become invalid XML.
/// </para>
/// </remarks>
/// <author>Nicko Cadell</author>
/// <author>Gert Driesen</author>
public class XmlLayout : XmlLayoutBase
{
  /// <summary>
  /// Constructs an XmlLayout
  /// </summary>
  public XmlLayout()
  {
  }

  /// <summary>
  /// Constructs an XmlLayout.
  /// </summary>
  /// <remarks>
  /// <para>
  /// The <b>LocationInfo</b> option takes a boolean value. By
  /// default, it is set to false which means there will be no location
  /// information output by this layout. If the option is set to
  /// true, then the file name and line number of the statement
  /// at the origin of the log statement will be output. 
  /// </para>
  /// <para>
  /// If you are embedding this layout within an SmtpAppender
  /// then make sure to set the <b>LocationInfo</b> option of that 
  /// appender as well.
  /// </para>
  /// </remarks>
  public XmlLayout(bool locationInfo) : base(locationInfo)
  {
  }

  /// <summary>
  /// The prefix to use for all element names
  /// </summary>
  /// <remarks>
  /// <para>
  /// The default prefix is <b>log4net</b>. Set this property
  /// to change the prefix. If the prefix is set to an empty string
  /// then no prefix will be written.
  /// </para>
  /// </remarks>
  public string Prefix { get; set; } = DefaultPrefix;

  /// <summary>
  /// Set whether to base64 encode the message.
  /// </summary>
  /// <remarks>
  /// <para>
  /// By default the log message will be written as text to the xml
  /// output. This can cause problems when the message contains binary
  /// data. By setting this to true the contents of the message will be
  /// base64 encoded. If this is set then invalid character replacement
  /// (see <see cref="XmlLayoutBase.InvalidCharReplacement"/>) will not be performed
  /// on the log message.
  /// </para>
  /// </remarks>
  public bool Base64EncodeMessage { get; set; }

  /// <summary>
  /// Set whether to base64 encode the property values.
  /// </summary>
  /// <remarks>
  /// <para>
  /// By default the properties will be written as text to the xml
  /// output. This can cause problems when one or more properties contain
  /// binary data. By setting this to true the values of the properties
  /// will be base64 encoded. If this is set then invalid character replacement
  /// (see <see cref="XmlLayoutBase.InvalidCharReplacement"/>) will not be performed
  /// on the property values.
  /// </para>
  /// </remarks>
  public bool Base64EncodeProperties { get; set; }

  /// <summary>
  /// Initialize layout options
  /// </summary>
  /// <remarks>
  /// <para>
  /// This is part of the <see cref="IOptionHandler"/> delayed object
  /// activation scheme. The <see cref="ActivateOptions"/> method must 
  /// be called on this object after the configuration properties have
  /// been set. Until <see cref="ActivateOptions"/> is called this
  /// object is in an undefined state and must not be used. 
  /// </para>
  /// <para>
  /// If any of the configuration properties are modified then 
  /// <see cref="ActivateOptions"/> must be called again.
  /// </para>
  /// <para>
  /// Builds a cache of the element names
  /// </para>
  /// </remarks>
  public override void ActivateOptions()
  {
    base.ActivateOptions();

    // Cache the full element names including the prefix
    if (Prefix.Length > 0)
    {
      _eventElementName = Prefix + ":" + DefaultEventElementName;
      _messageElementName = Prefix + ":" + DefaultMessageElementName;
      _propertiesElementName = Prefix + ":" + DefaultPropertiesElementName;
      _dataElementName = Prefix + ":" + DefaultDataElementName;
      _exceptionElementName = Prefix + ":" + DefaultExceptionElementName;
      _locationElementName = Prefix + ":" + DefaultLocationElementName;
    }
  }

  /// <summary>
  /// Does the actual writing of the XML.
  /// </summary>
  /// <param name="writer">The writer to use to output the event to.</param>
  /// <param name="loggingEvent">The event to write.</param>
  /// <remarks>
  /// <para>
  /// Override the base class <see cref="XmlLayoutBase.FormatXml"/> method
  /// to write the <see cref="LoggingEvent"/> to the <see cref="XmlWriter"/>.
  /// </para>
  /// </remarks>
  protected override void FormatXml(XmlWriter writer, LoggingEvent loggingEvent)
  {
    writer.EnsureNotNull().WriteStartElement(_eventElementName, Prefix, DefaultEventElementName, Prefix);
    writer.WriteAttributeString(LoggerAttributeName, loggingEvent.EnsureNotNull().LoggerName!);

    writer.WriteAttributeString(TimestampAttributeName, XmlConvert.ToString(loggingEvent.TimeStamp, XmlDateTimeSerializationMode.Local));

    if (loggingEvent.Level is not null)
    {
      writer.WriteAttributeString(LevelAttributeName, loggingEvent.Level.DisplayName);
    }

    writer.WriteAttributeString(ThreadAttributeName, loggingEvent.ThreadName!);

    if (loggingEvent.Domain is not null && loggingEvent.Domain.Length > 0)
    {
      writer.WriteAttributeString(DomainAttributeName, loggingEvent.Domain);
    }
    if (loggingEvent.Identity is not null && loggingEvent.Identity.Length > 0)
    {
      writer.WriteAttributeString(IdentityAttributeName, loggingEvent.Identity);
    }
    if (loggingEvent.UserName.Length > 0)
    {
      writer.WriteAttributeString(UsernameAttributeName, loggingEvent.UserName);
    }

    // Append the message text
    if (loggingEvent.RenderedMessage is not null)
    {
      writer.WriteStartElement(_messageElementName, Prefix, DefaultMessageElementName, Prefix);
      if (!Base64EncodeMessage)
      {
        Transform.WriteEscapedXmlString(writer, loggingEvent.RenderedMessage, InvalidCharReplacement);
      }
      else
      {
        byte[] messageBytes = Encoding.UTF8.GetBytes(loggingEvent.RenderedMessage);
        string base64Message = Convert.ToBase64String(messageBytes, 0, messageBytes.Length);
        Transform.WriteEscapedXmlString(writer, base64Message, InvalidCharReplacement);
      }
      writer.WriteEndElement();
    }

    PropertiesDictionary properties = loggingEvent.GetProperties();

    // Append the properties text
    if (properties.Count > 0)
    {
      writer.WriteStartElement(_propertiesElementName, Prefix, DefaultPropertiesElementName, Prefix);
      foreach (KeyValuePair<string, object?> entry in properties)
      {
        writer.WriteStartElement(_dataElementName, Prefix, DefaultDataElementName, Prefix);
        writer.WriteAttributeString(NameAttributeName, Transform.MaskXmlInvalidCharacters(entry.Key, InvalidCharReplacement));

        // Use an ObjectRenderer to convert the object to a string
        if (loggingEvent.Repository is not null)
        {
          string valueStr;
          if (!Base64EncodeProperties)
          {
            valueStr = Transform.MaskXmlInvalidCharacters(loggingEvent.Repository.RendererMap.FindAndRender(entry.Value), InvalidCharReplacement);
          }
          else
          {
            byte[] propertyValueBytes = Encoding.UTF8.GetBytes(loggingEvent.Repository.RendererMap.FindAndRender(entry.Value));
            valueStr = Convert.ToBase64String(propertyValueBytes, 0, propertyValueBytes.Length);
          }
          writer.WriteAttributeString(ValueAttributeName, valueStr);
        }

        writer.WriteEndElement();
      }
      writer.WriteEndElement();
    }

    string? exceptionStr = loggingEvent.GetExceptionString();
    if (exceptionStr is not null && exceptionStr.Length > 0)
    {
      // Append the stack trace line
      writer.WriteStartElement(_exceptionElementName, Prefix, DefaultExceptionElementName, Prefix);
      Transform.WriteEscapedXmlString(writer, exceptionStr, InvalidCharReplacement);
      writer.WriteEndElement();
    }

    if (LocationInfo)
    {
      if (loggingEvent.LocationInformation is LocationInfo locationInfo)
      {
        writer.WriteStartElement(_locationElementName, Prefix, DefaultLocationElementName, Prefix);
        writer.WriteAttributeString(ClassAttributeName, locationInfo.ClassName!);
        writer.WriteAttributeString(MethodAttributeName, locationInfo.MethodName);
        writer.WriteAttributeString(FileAttributeName, locationInfo.FileName!);
        writer.WriteAttributeString(LineAttributeName, locationInfo.LineNumber);
        writer.WriteEndElement();
      }
    }

    writer.WriteEndElement();
  }

  private string _eventElementName = DefaultEventElementName;
  private string _messageElementName = DefaultMessageElementName;
  private string _dataElementName = DefaultDataElementName;
  private string _propertiesElementName = DefaultPropertiesElementName;
  private string _exceptionElementName = DefaultExceptionElementName;
  private string _locationElementName = DefaultLocationElementName;

  private const string DefaultPrefix = "log4net";

  private const string DefaultEventElementName = "event";
  private const string DefaultMessageElementName = "message";
  private const string DefaultPropertiesElementName = "properties";
  private const string DefaultDataElementName = "data";
  private const string DefaultExceptionElementName = "exception";
  private const string DefaultLocationElementName = "locationInfo";

  private const string LoggerAttributeName = "logger";
  private const string TimestampAttributeName = "timestamp";
  private const string LevelAttributeName = "level";
  private const string ThreadAttributeName = "thread";
  private const string DomainAttributeName = "domain";
  private const string IdentityAttributeName = "identity";
  private const string UsernameAttributeName = "username";
  private const string ClassAttributeName = "class";
  private const string MethodAttributeName = "method";
  private const string FileAttributeName = "file";
  private const string LineAttributeName = "line";
  private const string NameAttributeName = "name";
  private const string ValueAttributeName = "value";
}

