#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.IO;
using System.Collections;

using log4net.Util;

namespace log4net.ObjectRenderer;

/// <summary>
/// The default object Renderer.
/// </summary>
/// <remarks>
/// <para>
/// The default renderer supports rendering objects and collections to strings.
/// </para>
/// <para>
/// See the <see cref="RenderObject"/> method for details of the output.
/// </para>
/// </remarks>
/// <author>Nicko Cadell</author>
/// <author>Gert Driesen</author>
public sealed class DefaultRenderer : IObjectRenderer
{
  /// <summary>
  /// Renders the object <paramref name="obj"/> to a string.
  /// </summary>
  /// <param name="rendererMap">The map used to lookup renderers</param>
  /// <param name="obj">The object to render</param>
  /// <param name="writer">The writer to render to</param>
  /// <remarks>
  /// <para>
  /// Render the object <paramref name="obj"/> to a string.
  /// </para>
  /// <para>
  /// The <paramref name="rendererMap"/> parameter is
  /// provided to lookup and render other objects. This is
  /// very useful where <paramref name="obj"/> contains
  /// nested objects of unknown type. The <see cref="RendererMap.FindAndRender(object)"/>
  /// method can be used to render these objects.
  /// </para>
  /// <para>
  /// The default renderer supports rendering objects to strings as follows:
  /// </para>
  /// <list type="table">
  ///    <listheader>
  ///      <term>Value</term>
  ///      <description>Rendered String</description>
  ///    </listheader>
  ///    <item>
  ///      <term><c>null</c></term>
  ///      <description>
  ///      <para>"(null)"</para>
  ///      </description>
  ///    </item>
  ///    <item>
  ///      <term><see cref="Array"/></term>
  ///      <description>
  ///      <para>
  ///      For a one dimensional array this is the
  ///      array type name, an open brace, followed by a comma
  ///      separated list of the elements (using the appropriate
  ///      renderer), followed by a close brace. 
  ///      </para>
  ///      <para>
  ///      For example: <c>int[] {1, 2, 3}</c>.
  ///      </para>
  ///      <para>
  ///      If the array is not one dimensional the 
  ///      <c>Array.ToString()</c> is returned.
  ///      </para>
  ///      </description>
  ///    </item>
  ///    <item>
  ///      <term><see cref="IEnumerable"/>, <see cref="ICollection"/> &amp; <see cref="IEnumerator"/></term>
  ///      <description>
  ///      <para>
  ///      Rendered as an open brace, followed by a comma
  ///      separated list of the elements (using the appropriate
  ///      renderer), followed by a close brace.
  ///      </para>
  ///      <para>
  ///      For example: <c>{a, b, c}</c>.
  ///      </para>
  ///      <para>
  ///      All collection classes that implement <see cref="ICollection"/> its subclasses, 
  ///      or generic equivalents all implement the <see cref="IEnumerable"/> interface.
  ///      </para>
  ///      </description>
  ///    </item>    
  ///    <item>
  ///      <term><see cref="DictionaryEntry"/></term>
  ///      <description>
  ///      <para>
  ///      Rendered as the key, an equals sign ('='), and the value (using the appropriate
  ///      renderer). 
  ///      </para>
  ///      <para>
  ///      For example: <c>key=value</c>.
  ///      </para>
  ///      </description>
  ///    </item>    
  ///    <item>
  ///      <term>other</term>
  ///      <description>
  ///      <para><c>Object.ToString()</c></para>
  ///      </description>
  ///    </item>
  /// </list>
  /// </remarks>
  public void RenderObject(RendererMap rendererMap, object? obj, TextWriter writer)
  {
    rendererMap.EnsureNotNull();
    writer.EnsureNotNull();

    if (obj is null)
    {
      writer.Write(SystemInfo.NullText);
      return;
    }

    if (obj is Array objArray)
    {
      RenderArray(rendererMap, objArray, writer);
      return;
    }

    // Test if we are dealing with some form of collection object
    if (obj is IEnumerable objEnumerable)
    {
      // Get a collection interface if we can as its .Count property may be more
      // performant than getting the IEnumerator object and trying to advance it.
      if (obj is ICollection objCollection && objCollection.Count == 0)
      {
        writer.Write("{}");
        return;
      }

      // This is a special check to allow us to get the enumerator from the IDictionary
      // interface as this guarantees us DictionaryEntry objects. Note that in .NET 2.0
      // the generic IDictionary<> interface enumerates KeyValuePair objects rather than
      // DictionaryEntry ones. However the implementation of the plain IDictionary 
      // interface on the generic Dictionary<> still returns DictionaryEntry objects.
      if (obj is IDictionary objDictionary)
      {
        RenderEnumerator(rendererMap, objDictionary.GetEnumerator(), writer);
        return;
      }

      RenderEnumerator(rendererMap, objEnumerable.GetEnumerator(), writer);
      return;
    }

    if (obj is IEnumerator objEnumerator)
    {
      RenderEnumerator(rendererMap, objEnumerator, writer);
      return;
    }

    if (obj is DictionaryEntry entry)
    {
      RenderDictionaryEntry(rendererMap, entry, writer);
      return;
    }

    writer.Write(obj.ToString() ?? SystemInfo.NullText);
  }

  /// <summary>
  /// Render the array argument into a string
  /// </summary>
  /// <param name="rendererMap">The map used to lookup renderers</param>
  /// <param name="array">the array to render</param>
  /// <param name="writer">The writer to render to</param>
  /// <remarks>
  /// <para>
  /// For a one dimensional array this is the
  ///  array type name, an open brace, followed by a comma
  ///  separated list of the elements (using the appropriate
  ///  renderer), followed by a close brace. For example:
  ///  <c>int[] {1, 2, 3}</c>.
  ///  </para>
  ///  <para>
  ///  If the array is not one dimensional the 
  ///  <c>Array.ToString()</c> is returned.
  ///  </para>
  /// </remarks>
  private static void RenderArray(RendererMap rendererMap, Array array, TextWriter writer)
  {
    if (array.Rank != 1)
    {
      writer.Write(array.ToString());
    }
    else
    {
      writer.Write(array.GetType().Name + " {");
      int len = array.Length;

      if (len > 0)
      {
        rendererMap.FindAndRender(array.GetValue(0), writer);
        for (int i = 1; i < len; i++)
        {
          writer.Write(", ");
          rendererMap.FindAndRender(array.GetValue(i), writer);
        }
      }
      writer.Write("}");
    }
  }

  /// <summary>
  /// Render the enumerator argument into a string
  /// </summary>
  /// <param name="rendererMap">The map used to lookup renderers</param>
  /// <param name="enumerator">the enumerator to render</param>
  /// <param name="writer">The writer to render to</param>
  /// <remarks>
  /// <para>
  /// Rendered as an open brace, followed by a comma
  ///  separated list of the elements (using the appropriate
  ///  renderer), followed by a close brace. For example:
  ///  <c>{a, b, c}</c>.
  ///  </para>
  /// </remarks>
  private static void RenderEnumerator(RendererMap rendererMap, IEnumerator? enumerator, TextWriter writer)
  {
    writer.Write("{");

    if (enumerator is not null && enumerator.MoveNext())
    {
      rendererMap.FindAndRender(enumerator.Current, writer);

      while (enumerator.MoveNext())
      {
        writer.Write(", ");
        rendererMap.FindAndRender(enumerator.Current, writer);
      }
    }

    writer.Write("}");
  }

  /// <summary>
  /// Renders the DictionaryEntry argument into a string.
  /// </summary>
  /// <param name="rendererMap">The map used to lookup renderers</param>
  /// <param name="entry">the DictionaryEntry to render</param>
  /// <param name="writer">The writer to render to</param>
  /// <remarks>
  /// <para>
  /// Render the key, an equals sign ('='), and the value (using the appropriate
  ///  renderer). For example: <c>key=value</c>.
  ///  </para>
  /// </remarks>
  private static void RenderDictionaryEntry(RendererMap rendererMap, DictionaryEntry entry, TextWriter writer)
  {
    rendererMap.FindAndRender(entry.Key, writer);
    writer.Write("=");
    rendererMap.FindAndRender(entry.Value, writer);
  }
}
