#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; } = PREFIX;
    
    /// <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)
      {
        m_elmEvent = Prefix + ":" + ELM_EVENT;
        m_elmMessage = Prefix + ":" + ELM_MESSAGE;
        m_elmProperties = Prefix + ":" + ELM_PROPERTIES;
        m_elmData = Prefix + ":" + ELM_DATA;
        m_elmException = Prefix + ":" + ELM_EXCEPTION;
        m_elmLocation = Prefix + ":" + ELM_LOCATION;
      }
    }

    /// <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.WriteStartElement(m_elmEvent, Prefix, ELM_EVENT, Prefix);
      writer.WriteAttributeString(ATTR_LOGGER, loggingEvent.LoggerName!);

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

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

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

      if (loggingEvent.Domain is not null && loggingEvent.Domain.Length > 0)
      {
        writer.WriteAttributeString(ATTR_DOMAIN, loggingEvent.Domain);
      }
      if (loggingEvent.Identity is not null && loggingEvent.Identity.Length > 0)
      {
        writer.WriteAttributeString(ATTR_IDENTITY, loggingEvent.Identity);
      }
      if (loggingEvent.UserName.Length > 0)
      {
        writer.WriteAttributeString(ATTR_USERNAME, loggingEvent.UserName);
      }

      // Append the message text
      if (loggingEvent.RenderedMessage is not null)
      {
        writer.WriteStartElement(m_elmMessage, Prefix, ELM_MESSAGE, 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(m_elmProperties, Prefix, ELM_PROPERTIES, Prefix);
        foreach (KeyValuePair<string, object?> entry in properties)
        {
          writer.WriteStartElement(m_elmData, Prefix, ELM_DATA, Prefix);
          writer.WriteAttributeString(ATTR_NAME, 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(ATTR_VALUE, valueStr);
          }

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

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

      if (LocationInfo)
      {
        if (loggingEvent.LocationInformation is LocationInfo locationInfo)
        {
          writer.WriteStartElement(m_elmLocation, Prefix, ELM_LOCATION, Prefix);
          writer.WriteAttributeString(ATTR_CLASS, locationInfo.ClassName);
          writer.WriteAttributeString(ATTR_METHOD, locationInfo.MethodName);
          writer.WriteAttributeString(ATTR_FILE, locationInfo.FileName);
          writer.WriteAttributeString(ATTR_LINE, locationInfo.LineNumber);
          writer.WriteEndElement();
        }
      }

      writer.WriteEndElement();
    }

    private string m_elmEvent = ELM_EVENT;
    private string m_elmMessage = ELM_MESSAGE;
    private string m_elmData = ELM_DATA;
    private string m_elmProperties = ELM_PROPERTIES;
    private string m_elmException = ELM_EXCEPTION;
    private string m_elmLocation = ELM_LOCATION;

    private const string PREFIX = "log4net";

    private const string ELM_EVENT = "event";
    private const string ELM_MESSAGE = "message";
    private const string ELM_PROPERTIES = "properties";
    private const string ELM_DATA = "data";
    private const string ELM_EXCEPTION = "exception";
    private const string ELM_LOCATION = "locationInfo";

    private const string ATTR_LOGGER = "logger";
    private const string ATTR_TIMESTAMP = "timestamp";
    private const string ATTR_LEVEL = "level";
    private const string ATTR_THREAD = "thread";
    private const string ATTR_DOMAIN = "domain";
    private const string ATTR_IDENTITY = "identity";
    private const string ATTR_USERNAME = "username";
    private const string ATTR_CLASS = "class";
    private const string ATTR_METHOD = "method";
    private const string ATTR_FILE = "file";
    private const string ATTR_LINE = "line";
    private const string ATTR_NAME = "name";
    private const string ATTR_VALUE = "value";
  }
}

