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

namespace log4net.Repository.Hierarchy;

/// <summary>
/// Implementation of <see cref="ILogger"/> used by <see cref="Hierarchy"/>
/// </summary>
/// <param name="name">The name of the <see cref="Logger" />.</param>
/// <remarks>
/// <para>
/// Internal class used to provide implementation of <see cref="ILogger"/>
/// interface. Applications should use <see cref="LogManager"/> to get
/// logger instances.
/// </para>
/// <para>
/// This is one of the central classes in the log4net implementation. One of the
/// distinctive features of log4net are hierarchical loggers and their
/// evaluation. The <see cref="Hierarchy"/> organizes the <see cref="Logger"/>
/// instances into a rooted tree hierarchy.
/// </para>
/// <para>
/// The <see cref="Logger"/> class is abstract. Only concrete subclasses of
/// <see cref="Logger"/> can be created. The <see cref="ILoggerFactory"/>
/// is used to create instances of this type for the <see cref="Hierarchy"/>.
/// </para>
/// </remarks>
/// <author>Nicko Cadell</author>
/// <author>Gert Driesen</author>
/// <author>Aspi Havewala</author>
/// <author>Douglas de la Torre</author>
public abstract class Logger(string name) : IHierarchyNode, IAppenderAttachable, ILogger
{
  /// <summary>
  /// The fully qualified type of the Logger class.
  /// </summary>
  private static readonly Type _declaringType = typeof(Logger);

  /// <summary>
  /// The parent of this logger.
  /// </summary>
  /// <remarks>
  /// <para>
  /// All loggers have at least one ancestor which is the root logger.
  /// </para>
  /// </remarks>
  private Logger? _parent;

  /// <summary>
  /// Loggers need to know what Hierarchy they are in.
  /// </summary>
  private Hierarchy? _hierarchy;

  /// <summary>
  /// Helper implementation of the <see cref="IAppenderAttachable"/> interface
  /// </summary>
  private AppenderAttachedImpl? _appenderAttachedImpl;

  /// <summary>
  /// Lock to protect AppenderAttachedImpl variable appenderAttachedImpl
  /// </summary>
  private readonly ReaderWriterLock _appenderLock = new();

  /// <summary>
  /// Gets or sets the parent logger in the hierarchy.
  /// </summary>
  /// <value>
  /// The parent logger in the hierarchy.
  /// </value>
  /// <remarks>
  /// <para>
  /// Part of the Composite pattern that makes the hierarchy.
  /// The hierarchy is parent linked rather than child linked.
  /// </para>
  /// </remarks>
  public virtual Logger? Parent
  {
    get => _parent;
    set => _parent = value;
  }

  /// <summary>
  /// Gets or sets a value indicating if child loggers inherit their parent's appenders.
  /// </summary>
  /// <value>
  /// <see langword="true"/> if child loggers inherit their parent's appenders.
  /// </value>
  /// <remarks>
  /// <para>
  /// Additivity is set to <see langword="true"/> by default, that is children inherit
  /// the appenders of their ancestors by default. If this variable is
  /// set to <see langword="false"/> then the appenders found in the
  /// ancestors of this logger are not used. However, the children
  /// of this logger will inherit its appenders, unless the children
  /// have their additivity flag set to <see langword="false"/> too. See
  /// the user manual for more details.
  /// </para>
  /// </remarks>
  public virtual bool Additivity { get; set; } = true;

  /// <summary>
  /// Gets the effective level for this logger.
  /// </summary>
  /// <returns>The nearest level in the logger hierarchy.</returns>
  /// <remarks>
  /// <para>
  /// Starting from this logger, searches the logger hierarchy for a
  /// non-null level and returns it. Otherwise, returns the level of the
  /// root logger.
  /// </para>
  /// <para>The Logger class is designed so that this method executes as
  /// quickly as possible.</para>
  /// </remarks>
  public virtual Level EffectiveLevel
  {
    get
    {
      for (Logger? c = this; c is not null; c = c._parent)
      {
        if (c.Level is Level level)
        {
          return level;
        }
      }
      return null!; // If reached will cause an NullPointerException.
    }
  }

  /// <summary>
  /// Gets or sets the <see cref="Hierarchy"/> where this <see cref="Logger"/> instance is attached to.
  /// </summary>
  public virtual Hierarchy? Hierarchy
  {
    get => _hierarchy;
    set => _hierarchy = value;
  }

  /// <summary>
  /// Gets or sets the assigned <see cref="Level"/> for this Logger.  
  /// </summary>
  public virtual Level? Level { get; set; }

  /// <summary>
  /// Add <paramref name="appender"/> to the list of appenders of this
  /// Logger instance.
  /// </summary>
  /// <param name="appender">An appender to add to this logger</param>
  /// <remarks>
  /// <para>
  /// If <paramref name="appender"/> is already in the list of
  /// appenders, then it won't be added again.
  /// </para>
  /// </remarks>
  public virtual void AddAppender(IAppender appender)
  {
    appender.EnsureNotNull();

    _appenderLock.AcquireWriterLock();
    try
    {
      _appenderAttachedImpl ??= new();
      _appenderAttachedImpl.AddAppender(appender);
    }
    finally
    {
      _appenderLock.ReleaseWriterLock();
    }
  }

  /// <summary>
  /// Get the appenders contained in this logger as an 
  /// <see cref="System.Collections.ICollection"/>.
  /// </summary>
  /// <returns>
  /// A collection of the appenders in this logger. If no appenders 
  /// can be found, then a <see cref="EmptyCollection"/> is returned.
  /// </returns>
  public virtual AppenderCollection Appenders
  {
    get
    {
      _appenderLock.AcquireReaderLock();
      try
      {
        if (_appenderAttachedImpl is null)
        {
          return AppenderCollection.EmptyCollection;
        }
        return _appenderAttachedImpl.Appenders;
      }
      finally
      {
        _appenderLock.ReleaseReaderLock();
      }
    }
  }

  /// <summary>
  /// Look for the appender named as <paramref name="name"/>
  /// </summary>
  /// <param name="name">The name of the appender to lookup</param>
  /// <returns>The appender with the name specified, or <see langword="null"/>.</returns>
  public virtual IAppender? GetAppender(string? name)
  {
    _appenderLock.AcquireReaderLock();
    try
    {
      if (_appenderAttachedImpl is null || name is null)
      {
        return null;
      }

      return _appenderAttachedImpl.GetAppender(name);
    }
    finally
    {
      _appenderLock.ReleaseReaderLock();
    }
  }

  /// <summary>
  /// Removes all previously added appenders from this Logger instance.
  /// </summary>
  /// <remarks>
  /// <para>
  /// This is useful when re-reading configuration information.
  /// </para>
  /// </remarks>
  public virtual void RemoveAllAppenders()
  {
    _appenderLock.AcquireWriterLock();
    try
    {
      if (_appenderAttachedImpl is not null)
      {
        _appenderAttachedImpl.RemoveAllAppenders();
        _appenderAttachedImpl = null;
      }
    }
    finally
    {
      _appenderLock.ReleaseWriterLock();
    }
  }

  /// <summary>
  /// Remove the appender passed as parameter form the list of appenders.
  /// </summary>
  /// <param name="appender">The appender to remove</param>
  /// <returns>The appender removed from the list</returns>
  /// <remarks>
  /// <para>
  /// The appender removed is not closed.
  /// If you are discarding the appender you must call
  /// <see cref="IAppender.Close"/> on the appender removed.
  /// </para>
  /// </remarks>
  public virtual IAppender? RemoveAppender(IAppender? appender)
  {
    _appenderLock.AcquireWriterLock();
    try
    {
      if (appender is not null && _appenderAttachedImpl is not null)
      {
        return _appenderAttachedImpl.RemoveAppender(appender);
      }
    }
    finally
    {
      _appenderLock.ReleaseWriterLock();
    }
    return null;
  }

  /// <summary>
  /// Remove the appender passed as parameter form the list of appenders.
  /// </summary>
  /// <param name="name">The name of the appender to remove</param>
  /// <returns>The appender removed from the list</returns>
  /// <remarks>
  /// <para>
  /// The appender removed is not closed.
  /// If you are discarding the appender you must call
  /// <see cref="IAppender.Close"/> on the appender removed.
  /// </para>
  /// </remarks>
  public virtual IAppender? RemoveAppender(string? name)
  {
    _appenderLock.AcquireWriterLock();
    try
    {
      if (name is not null && _appenderAttachedImpl is not null)
      {
        return _appenderAttachedImpl.RemoveAppender(name);
      }
    }
    finally
    {
      _appenderLock.ReleaseWriterLock();
    }
    return null;
  }

  /// <summary>
  /// Gets the logger name.
  /// </summary>
  public virtual string Name { get; } = string.Intern(name);

  /// <summary>
  /// Generates a logging event for the specified <paramref name="level"/> using
  /// the <paramref name="message"/> and <paramref name="exception"/>.
  /// </summary>
  /// <param name="callerStackBoundaryDeclaringType">The declaring type of the method that is
  /// the stack boundary into the logging system for this call.</param>
  /// <param name="level">The level of the message to be logged.</param>
  /// <param name="message">The message object to log.</param>
  /// <param name="exception">The exception to log, including its stack trace.</param>
  /// <remarks>
  /// <para>
  /// This generic form is intended to be used by wrappers.
  /// </para>
  /// <para>
  /// This method must not throw any exception to the caller.
  /// </para>
  /// </remarks>
  public virtual void Log(Type? callerStackBoundaryDeclaringType, Level? level, object? message, Exception? exception)
  {
    try
    {
      if (IsEnabledFor(level))
      {
        ForcedLog(callerStackBoundaryDeclaringType ?? _declaringType, level, message, exception);
      }
    }
    catch (Exception e) when (!e.IsFatal())
    {
      LogLog.Error(_declaringType, "Exception while logging", e);
    }
  }

  /// <summary>
  /// Logs the specified logging event through this logger.
  /// </summary>
  /// <param name="logEvent">The event being logged.</param>
  /// <remarks>
  /// <para>
  /// This is the most generic printing method that is intended to be used 
  /// by wrappers.
  /// </para>
  /// <para>
  /// This method must not throw any exception to the caller.
  /// </para>
  /// </remarks>
  public virtual void Log(LoggingEvent? logEvent)
  {
    try
    {
      if (logEvent is not null)
      {
        if (IsEnabledFor(logEvent.Level))
        {
          ForcedLog(logEvent);
        }
      }
    }
    catch (Exception e) when (!e.IsFatal())
    {
      LogLog.Error(_declaringType, "Exception while logging", e);
    }
  }

  /// <summary>
  /// Checks if this logger is enabled for a given <see cref="Level"/> passed as parameter.
  /// </summary>
  /// <param name="level">The level to check.</param>
  /// <returns>
  /// <see langword="true"/> if this logger is enabled for <paramref name="level"/>,
  /// otherwise <see langword="false"/>.
  /// </returns>
  /// <remarks>
  /// <para>
  /// This method must not throw any exception to the caller.
  /// </para>
  /// </remarks>
  public virtual bool IsEnabledFor(Level? level)
  {
    try
    {
      if (level is not null)
      {
        if (_hierarchy is not null && _hierarchy.IsDisabled(level))
        {
          return false;
        }
        return level >= EffectiveLevel;
      }
    }
    catch (Exception e) when (!e.IsFatal())
    {
      LogLog.Error(_declaringType, "Exception while logging", e);
    }
    return false;
  }

  /// <summary>
  /// Gets the <see cref="ILoggerRepository"/> where this 
  /// <see cref="Logger"/> instance is attached to.
  /// </summary>
  public ILoggerRepository? Repository => _hierarchy;

  /// <summary>
  /// Deliver the <see cref="LoggingEvent"/> to the attached appenders.
  /// </summary>
  /// <param name="loggingEvent">The event to log.</param>
  /// <remarks>
  /// <para>
  /// Call the appenders in the hierarchy starting at <see langword="this"/>.
  /// If no appenders could be found, emit a warning.
  /// </para>
  /// <para>
  /// This method calls all the appenders inherited from the
  /// hierarchy circumventing any evaluation of whether to log or not
  /// to log the particular log request.
  /// </para>
  /// </remarks>
  protected virtual void CallAppenders(LoggingEvent loggingEvent)
  {
    loggingEvent.EnsureNotNull();

    int writes = 0;

    for (Logger? c = this; c is not null; c = c._parent)
    {
      if (c._appenderAttachedImpl is not null)
      {
        // Protected against simultaneous call to addAppender, removeAppender,...
        c._appenderLock.AcquireReaderLock();
        try
        {
          if (c._appenderAttachedImpl is not null)
          {
            writes += c._appenderAttachedImpl.AppendLoopOnAppenders(loggingEvent);
          }
        }
        finally
        {
          c._appenderLock.ReleaseReaderLock();
        }
      }

      if (!c.Additivity)
      {
        break;
      }
    }

    // No appenders in hierarchy, warn user only once.
    //
    // Note that by including the AppDomain values for the currently running
    // thread, it becomes much easier to see which application the warning
    // is from, which is especially helpful in a multi-AppDomain environment
    // (like IIS with multiple VDIRS).  Without this, it can be difficult
    // or impossible to determine which .config file is missing appender
    // definitions.
    //
    if (_hierarchy is not null && !_hierarchy.EmittedNoAppenderWarning && writes == 0)
    {
      _hierarchy.EmittedNoAppenderWarning = true;
      LogLog.Debug(_declaringType, $"No appenders could be found for logger [{Name}] repository [{Repository?.Name}]");
      LogLog.Debug(_declaringType, "Please initialize the log4net system properly.");
      try
      {
        LogLog.Debug(_declaringType, "    Current AppDomain context information: ");
        LogLog.Debug(_declaringType, "       BaseDirectory   : " + SystemInfo.ApplicationBaseDirectory);
        LogLog.Debug(_declaringType, "       FriendlyName    : " + AppDomain.CurrentDomain.FriendlyName);
        LogLog.Debug(_declaringType, "       DynamicDirectory: " + AppDomain.CurrentDomain.DynamicDirectory);
      }
      catch (System.Security.SecurityException)
      {
        // Insufficient permissions to display info from the AppDomain
      }
    }
  }

  /// <summary>
  /// Closes all attached appenders implementing the <see cref="IAppenderAttachable"/> interface.
  /// </summary>
  /// <remarks>
  /// <para>
  /// Used to ensure that the appenders are correctly shutdown.
  /// </para>
  /// </remarks>
  public virtual void CloseNestedAppenders()
  {
    _appenderLock.AcquireWriterLock();
    try
    {
      if (_appenderAttachedImpl is not null)
      {
        AppenderCollection appenders = _appenderAttachedImpl.Appenders;
        foreach (IAppender appender in appenders)
        {
          if (appender is IAppenderAttachable)
          {
            appender.Close();
          }
        }
      }
    }
    finally
    {
      _appenderLock.ReleaseWriterLock();
    }
  }

  /// <summary>
  /// This is the most generic printing method. This generic form is intended to be used by wrappers
  /// </summary>
  /// <param name="level">The level of the message to be logged.</param>
  /// <param name="message">The message object to log.</param>
  /// <param name="exception">The exception to log, including its stack trace.</param>
  /// <remarks>
  /// <para>
  /// Generate a logging event for the specified <paramref name="level"/> using
  /// the <paramref name="message"/>.
  /// </para>
  /// </remarks>
  public virtual void Log(Level level, object? message, Exception? exception)
  {
    if (IsEnabledFor(level))
    {
      ForcedLog(_declaringType, level, message, exception);
    }
  }

  /// <summary>
  /// Creates a new logging event and logs the event without further checks.
  /// </summary>
  /// <param name="callerStackBoundaryDeclaringType">The declaring type of the method that is
  /// the stack boundary into the logging system for this call.</param>
  /// <param name="level">The level of the message to be logged.</param>
  /// <param name="message">The message object to log.</param>
  /// <param name="exception">The exception to log, including its stack trace.</param>
  /// <remarks>
  /// <para>
  /// Generates a logging event and delivers it to the attached
  /// appenders.
  /// </para>
  /// </remarks>
  protected virtual void ForcedLog(Type callerStackBoundaryDeclaringType, Level? level,
    object? message, Exception? exception)
  {
    CallAppenders(new LoggingEvent(callerStackBoundaryDeclaringType, Hierarchy, Name, level,
      message, exception));
  }

  /// <summary>
  /// Creates a new logging event and logs the event without further checks.
  /// </summary>
  /// <param name="logEvent">The event being logged.</param>
  /// <remarks>
  /// <para>
  /// Delivers the logging event to the attached appenders.
  /// </para>
  /// </remarks>
  protected virtual void ForcedLog(LoggingEvent logEvent)
  {
    // The logging event may not have been created by this logger
    // the Repository may not be correctly set on the event. This
    // is required for the appenders to correctly lookup renderers etc...
    logEvent.EnsureNotNull().EnsureRepository(Hierarchy);

    CallAppenders(logEvent);
  }

  /// <summary>
  /// Atomically replaces all appenders with the provided collection.
  /// </summary>
  /// <param name="appenders">The new set of appenders to attach.</param>
  /// <remarks>
  /// <para>
  /// This method removes the existing appenders and attaches all new
  /// appenders inside a single writer lock, minimizing the window
  /// during which the logger has no appenders. This reduces silent log
  /// event loss during reconfiguration.
  /// </para>
  /// </remarks>
  public virtual void ReplaceAppenders(IEnumerable<IAppender> appenders)
  {
    _appenderLock.AcquireWriterLock();
    try
    {
      _appenderAttachedImpl?.RemoveAllAppenders();
      _appenderAttachedImpl = null;

      foreach (IAppender appender in appenders.EnsureNotNull())
      {
        _appenderAttachedImpl ??= new();
        _appenderAttachedImpl.AddAppender(appender);
      }
    }
    finally
    {
      _appenderLock.ReleaseWriterLock();
    }
  }
}
