#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 log4net.Util;
using log4net.Layout;
using log4net.Core;

namespace log4net.Appender;

/// <summary>
/// Sends logging events to a <see cref="TextWriter"/>.
/// </summary>
/// <remarks>
/// <para>
/// An Appender that writes to a <see cref="TextWriter"/>.
/// </para>
/// <para>
/// This appender may be used stand alone if initialized with an appropriate
/// writer, however it is typically used as a base class for an appender that
/// can open a <see cref="TextWriter"/> to write to.
/// </para>
/// </remarks>
/// <author>Nicko Cadell</author>
/// <author>Gert Driesen</author>
/// <author>Douglas de la Torre</author>
public class TextWriterAppender : AppenderSkeleton
{
  /// <summary>
  /// Gets or set whether the appender will flush at the end 
  /// of each append operation.
  /// </summary>
  /// <value>
  /// <para>
  /// The default behavior is to flush at the end of each 
  /// append operation.
  /// </para>
  /// <para>
  /// If this option is set to <see langword="false"/>, then the underlying 
  /// stream can defer persisting the logging event to a later 
  /// time.
  /// </para>
  /// </value>
  /// <remarks>
  /// Avoiding the flush operation at the end of each append results in
  /// a performance gain of 10 to 20 percent. However, there is a safety
  /// trade-off involved in skipping flushing. Indeed, when flushing is
  /// skipped, then it is likely that the last few log events will not
  /// be recorded on disk when the application exits. This is a high
  /// price to pay even for a 20% performance gain.
  /// </remarks>
  public bool ImmediateFlush { get; set; } = true;

  /// <summary>
  /// Sets the <see cref="TextWriter"/> where the log output will go.
  /// </summary>
  /// <remarks>
  /// <para>
  /// The specified <see cref="TextWriter"/> must be open and writable.
  /// </para>
  /// <para>
  /// The <see cref="TextWriter"/> will be closed when the appender 
  /// instance is closed.
  /// </para>
  /// <para>
  /// <b>Note:</b> Logging to an unopened <see cref="TextWriter"/> will fail.
  /// </para>
  /// </remarks>
  public virtual TextWriter? Writer
  {
    get => QuietWriter;
    set
    {
      lock (_syncRoot)
      {
        Reset();
        if (value is not null)
        {
          QuietWriter = new QuietTextWriter(value, ErrorHandler);
          WriteHeader();
        }
      }
    }
  }

  /// <summary>
  /// This method determines if there is a sense in attempting to append.
  /// </summary>
  /// <remarks>
  /// <para>
  /// This method checks if an output target has been set and if a
  /// layout has been set. 
  /// </para>
  /// </remarks>
  /// <returns><see langword="false"/> if any of the preconditions fail.</returns>
  protected override bool PreAppendCheck()
  {
    if (!base.PreAppendCheck())
    {
      return false;
    }

    if (QuietWriter is null)
    {
      // Allow subclass to lazily create the writer
      PrepareWriter();

      if (QuietWriter is null)
      {
        ErrorHandler.Error("No output stream or file set for the appender named [" + Name + "].");
        return false;
      }
    }
    if (QuietWriter.Closed)
    {
      ErrorHandler.Error("Output stream for appender named [" + Name + "] has been closed.");
      return false;
    }

    return true;
  }

  /// <summary>
  /// This method is called by the <see cref="AppenderSkeleton.DoAppend(LoggingEvent)"/>
  /// method. 
  /// </summary>
  /// <param name="loggingEvent">The event to log.</param>
  /// <remarks>
  /// <para>
  /// Writes a log statement to the output stream if the output stream exists 
  /// and is writable.  
  /// </para>
  /// <para>
  /// The format of the output will depend on the appender's layout.
  /// </para>
  /// </remarks>
  protected override void Append(LoggingEvent loggingEvent)
  {
    if (QuietWriter is null)
    {
      return;
    }

    RenderLoggingEvent(QuietWriter, loggingEvent);

    if (ImmediateFlush)
    {
      QuietWriter.Flush();
    }
  }

  /// <summary>
  /// This method is called by the <see cref="AppenderSkeleton.DoAppend(LoggingEvent[])"/>
  /// method. 
  /// </summary>
  /// <param name="loggingEvents">The array of events to log.</param>
  /// <remarks>
  /// <para>
  /// This method writes all the bulk logged events to the output writer
  /// before flushing the stream.
  /// </para>
  /// </remarks>
  protected override void Append(LoggingEvent[] loggingEvents)
  {
    if (QuietWriter is null)
    {
      return;
    }
    loggingEvents.EnsureNotNull();
    foreach (LoggingEvent loggingEvent in loggingEvents)
    {
      RenderLoggingEvent(QuietWriter, loggingEvent);
    }

    if (ImmediateFlush)
    {
      QuietWriter.Flush();
    }
  }

  /// <summary>
  /// Close this appender instance. The underlying stream or writer is also closed.
  /// </summary>
  /// <remarks>
  /// Closed appenders cannot be reused.
  /// </remarks>
  protected override void OnClose()
  {
    lock (_syncRoot)
    {
      Reset();
    }
  }

  /// <summary>
  /// Gets or set the <see cref="IErrorHandler"/> and the underlying 
  /// <see cref="QuietTextWriter"/>, if any, for this appender. 
  /// </summary>
  /// <value>
  /// The <see cref="IErrorHandler"/> for this appender.
  /// </value>
  public override IErrorHandler ErrorHandler
  {
    get => base.ErrorHandler;
    set
    {
      lock (_syncRoot)
      {
        if (value is null)
        {
          LogLog.Warn(_declaringType, "TextWriterAppender: You have tried to set a null error-handler.");
        }
        else
        {
          base.ErrorHandler = value;
          if (QuietWriter is not null)
          {
            QuietWriter.ErrorHandler = value;
          }
        }
      }
    }
  }

  /// <summary>
  /// This appender requires a <see cref="Layout"/> to be set.
  /// </summary>
  protected override bool RequiresLayout => true;

  /// <summary>
  /// Writes the footer and closes the underlying <see cref="TextWriter"/>.
  /// </summary>
  protected virtual void WriteFooterAndCloseWriter()
  {
    WriteFooter();
    CloseWriter();
  }

  /// <summary>
  /// Closes the underlying <see cref="TextWriter"/>.
  /// </summary>
  protected virtual void CloseWriter()
  {
    if (QuietWriter is not null)
    {
      try
      {
        QuietWriter.Close();
      }
      catch (Exception e) when (!e.IsFatal())
      {
        ErrorHandler.Error($"Could not close writer [{QuietWriter}]", e);
        // do need to invoke an error handler
        // at this late stage
      }
    }
  }

  /// <summary>
  /// Clears internal references to the underlying <see cref="TextWriter" /> 
  /// and other variables.
  /// </summary>
  /// <remarks>
  /// <para>
  /// Subclasses can override this method for an alternate closing behavior.
  /// </para>
  /// </remarks>
  protected virtual void Reset()
  {
    WriteFooterAndCloseWriter();
    QuietWriter = null;
  }

  /// <summary>
  /// Writes a footer as produced by the embedded layout's <see cref="ILayout.Footer"/> property.
  /// </summary>
  /// <remarks>
  /// <para>
  /// Writes a footer as produced by the embedded layout's <see cref="ILayout.Footer"/> property.
  /// </para>
  /// </remarks>
  protected virtual void WriteFooter()
  {
    if (Layout is not null && QuietWriter is not null && !QuietWriter.Closed)
    {
      string? f = Layout.Footer;
      if (f is not null)
      {
        QuietWriter.Write(f);
      }
    }
  }

  /// <summary>
  /// Writes a header produced by the embedded layout's <see cref="ILayout.Header"/> property.
  /// </summary>
  /// <remarks>
  /// <para>
  /// Writes a header produced by the embedded layout's <see cref="ILayout.Header"/> property.
  /// </para>
  /// </remarks>
  protected virtual void WriteHeader()
  {
    if (Layout is not null && QuietWriter is not null && !QuietWriter.Closed)
    {
      if (Layout.Header is string h)
      {
        QuietWriter.Write(h);
      }
    }
  }

  /// <summary>
  /// Called to allow a subclass to lazily initialize the writer
  /// </summary>
  /// <remarks>
  /// <para>
  /// This method is called when an event is logged and the <see cref="Writer"/> or
  /// <see cref="QuietWriter"/> have not been set. This allows a subclass to
  /// attempt to initialize the writer multiple times.
  /// </para>
  /// </remarks>
  protected virtual void PrepareWriter()
  {
  }

  /// <summary>
  /// Gets or sets the <see cref="QuietTextWriter"/> where logging events
  /// will be written to. 
  /// </summary>
  /// <value>
  /// The <see cref="QuietTextWriter"/> where logging events are written.
  /// </value>
  /// <remarks>
  /// <para>
  /// This is the <see cref="QuietTextWriter"/> where logging events
  /// will be written to. 
  /// </para>
  /// </remarks>
  protected QuietTextWriter? QuietWriter { get; set; }

  private readonly object _syncRoot = new();

  /// <summary>
  /// The fully qualified type of the TextWriterAppender class.
  /// </summary>
  /// <remarks>
  /// Used by the internal logger to record the Type of the
  /// log message.
  /// </remarks>
  private static readonly Type _declaringType = typeof(TextWriterAppender);

  /// <summary>
  /// Flushes any buffered log data.
  /// </summary>
  /// <param name="millisecondsTimeout">The maximum time to wait for logging events to be flushed.</param>
  /// <returns><see langword="true"/> if all logging events were flushed successfully, else <see langword="false"/>.</returns>
  public override bool Flush(int millisecondsTimeout)
  {
    // Nothing to do if ImmediateFlush is true
    if (ImmediateFlush)
    {
      return true;
    }

    // lock(this) will block any Appends while the buffer is flushed.
    lock (_syncRoot)
    {
      QuietWriter?.Flush();
    }

    return true;
  }
}
