using System;
using System.Text;
using log4net.Core;
using log4net.Util;

namespace log4net.Appender
{
    /// <summary>
    /// Appends logging events to the terminal using ANSI color escape sequences.
    /// </summary>
    /// <remarks>
    /// <para>
    /// AnsiColorTerminalAppender appends log events to the standard output stream
    /// or the error output stream using a layout specified by the 
    /// user. It also allows the color of a specific level of message to be set.
    /// </para>
    /// <note>
    /// This appender expects the terminal to understand the VT100 control set 
    /// in order to interpret the color codes. If the terminal or console does not
    /// understand the control codes the behavior is not defined.
    /// </note>
    /// <para>
    /// By default, all output is written to the console's standard output stream.
    /// The <see cref="AnsiColorTerminalAppender.Target"/> property can be set to direct the output to the
    /// error stream.
    /// </para>
    /// <para>
    /// NOTE: This appender writes each message to the <c>System.Console.Out</c> or 
    /// <c>System.Console.Error</c> that is set at the time the event is appended.
    /// Therefore it is possible to programmatically redirect the output of this appender 
    /// (for example NUnit does this to capture program output). While this is the desired
    /// behavior of this appender it may have security implications in your application. 
    /// </para>
    /// <para>
    /// When configuring the ANSI colored terminal appender, a mapping should be
    /// specified to map a logging level to a color. For example:
    /// </para>
    /// <code lang="XML" escaped="true">
    /// <mapping>
    /// 	<level value="ERROR" />
    /// 	<foreColor value="White" />
    /// 	<backColor value="Red" />
    ///     <attributes value="Bright,Underscore" />
    /// </mapping>
    /// <mapping>
    /// 	<level value="DEBUG" />
    /// 	<backColor value="Green" />
    /// </mapping>
    /// </code>
    /// <para>
    /// The Level is the standard log4net logging level and ForeColor and BackColor can be any
    /// of the following values:
    /// <list type="bullet">
    /// <item><term>Blue</term><description></description></item>
    /// <item><term>Green</term><description></description></item>
    /// <item><term>Red</term><description></description></item>
    /// <item><term>White</term><description></description></item>
    /// <item><term>Yellow</term><description></description></item>
    /// <item><term>Purple</term><description></description></item>
    /// <item><term>Cyan</term><description></description></item>
    /// </list>
    /// These color values cannot be combined together to make new colors.
    /// </para>
    /// <para>
    /// The attributes can be any combination of the following:
    /// <list type="bullet">
    /// <item><term>Bright</term><description>foreground is brighter</description></item>
    /// <item><term>Dim</term><description>foreground is dimmer</description></item>
    /// <item><term>Underscore</term><description>message is underlined</description></item>
    /// <item><term>Blink</term><description>foreground is blinking (does not work on all terminals)</description></item>
    /// <item><term>Reverse</term><description>foreground and background are reversed</description></item>
    /// <item><term>Hidden</term><description>output is hidden</description></item>
    /// <item><term>Strikethrough</term><description>message has a line through it</description></item>
    /// </list>
    /// While any of these attributes may be combined together not all combinations
    /// work well together, for example setting both <i>Bright</i> and <i>Dim</i> attributes makes
    /// no sense.
    /// </para>
    /// </remarks>
    /// <author>Patrick Wagstrom</author>
    /// <author>Nicko Cadell</author>
    public class AnsiColorForwardingAppender : AnsiColorTerminalAppender, IAppenderAttachable, IDisposable
    {
        //#region Public Instance Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="AnsiColorTerminalAppender" /> class.
        /// </summary>
        /// <remarks>
        /// The instance of the <see cref="AnsiColorTerminalAppender" /> class is set up to write 
        /// to the standard output stream.
        /// </remarks>
        public AnsiColorForwardingAppender()
        {
        }

        //#endregion Public Instance Constructors

        #region Public Instance Properties

        /// <summary>
        /// Add a mapping of level to color
        /// </summary>
        /// <param name="mapping">The mapping to add</param>
        /// <remarks>
        /// <para>
        /// Add a <see cref="LevelColors"/> mapping to this appender.
        /// Each mapping defines the foreground and background colours
        /// for a level.
        /// </para>
        /// </remarks>
        public void AddMapping(LevelColors mapping)
        {
            m_levelMapping.Add(mapping);
        }

        #endregion Public Instance Properties

        #region Override implementation of AppenderSkeleton

        /// <summary>
        /// Closes the appender and releases resources.
        /// </summary>
        /// <remarks>
        /// <para>
        /// Releases any resources allocated within the appender such as file handles, 
        /// network connections, etc.
        /// </para>
        /// <para>
        /// It is a programming error to append to a closed appender.
        /// </para>
        /// </remarks>
        override protected void OnClose()
        {
            // Remove all the attached appenders
            lock (this)
            {
                if (m_appenderAttachedImpl != null)
                {
                    m_appenderAttachedImpl.RemoveAllAppenders();
                }
            }
        }

        /// <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 the event to the console.
        /// </para>
        /// <para>
        /// The format of the output will depend on the appender's layout.
        /// </para>
        /// </remarks>
        override protected void Append(LoggingEvent loggingEvent)
        {
            string loggingMessage = RenderLoggingEvent(loggingEvent);

            // see if there is a specified lookup.
            LevelColors levelColors = m_levelMapping.Lookup(loggingEvent.Level) as LevelColors;
            if (levelColors != null)
            {
                // Prepend the Ansi Color code
                loggingMessage = levelColors.CombinedColor + loggingMessage;
            }

            // on most terminals there are weird effects if we don't clear the background color
            // before the new line.  This checks to see if it ends with a newline, and if
            // so, inserts the clear codes before the newline, otherwise the clear codes
            // are inserted afterwards.
            if (loggingMessage.Length > 1)
            {
                if (loggingMessage.EndsWith("\r\n") || loggingMessage.EndsWith("\n\r"))
                {
                    loggingMessage = loggingMessage.Insert(loggingMessage.Length - 2, PostEventCodes);
                }
                else if (loggingMessage.EndsWith("\n") || loggingMessage.EndsWith("\r"))
                {
                    loggingMessage = loggingMessage.Insert(loggingMessage.Length - 1, PostEventCodes);
                }
                else
                {
                    loggingMessage = loggingMessage + PostEventCodes;
                }
            }
            else
            {
                if (loggingMessage[0] == '\n' || loggingMessage[0] == '\r')
                {
                    loggingMessage = PostEventCodes + loggingMessage;
                }
                else
                {
                    loggingMessage = loggingMessage + PostEventCodes;
                }
            }

            // Pass the logging event on the the attached appenders
            LoggingEventData data=new LoggingEventData();
            data.Domain = loggingEvent.Domain;
            // ExceptionString isn't necessary in nested event
            //data.ExceptionString = loggingEvent.GetExceptionString();
            data.Identity = loggingEvent.Identity;
            data.Level = loggingEvent.Level;
            data.LocationInfo = loggingEvent.LocationInformation;
            data.LoggerName = loggingEvent.LoggerName;
            data.Message = loggingMessage;
            data.Properties = loggingEvent.Properties;
            data.ThreadName = loggingEvent.ThreadName;
            data.TimeStamp = loggingEvent.TimeStamp;
            data.UserName = loggingEvent.UserName;
            if (m_appenderAttachedImpl != null)
            {
                m_appenderAttachedImpl.AppendLoopOnAppenders(new LoggingEvent(data));
            }
        }

        #region Implementation of IAppenderAttachable

        /// <summary>
        /// Adds an <see cref="IAppender" /> to the list of appenders of this
        /// instance.
        /// </summary>
        /// <param name="newAppender">The <see cref="IAppender" /> to add to this appender.</param>
        /// <remarks>
        /// <para>
        /// If the specified <see cref="IAppender" /> is already in the list of
        /// appenders, then it won't be added again.
        /// </para>
        /// </remarks>
        virtual public void AddAppender(IAppender newAppender)
        {
            if (newAppender == null)
            {
                throw new ArgumentNullException("newAppender");
            }
            lock (this)
            {
                if (m_appenderAttachedImpl == null)
                {
                    m_appenderAttachedImpl = new AppenderAttachedImpl();
                }
                m_appenderAttachedImpl.AddAppender(newAppender);
            }
        }

        /// <summary>
        /// Gets the appenders contained in this appender as an 
        /// <see cref="System.Collections.ICollection"/>.
        /// </summary>
        /// <remarks>
        /// If no appenders can be found, then an <see cref="EmptyCollection"/> 
        /// is returned.
        /// </remarks>
        /// <returns>
        /// A collection of the appenders in this appender.
        /// </returns>
        virtual public AppenderCollection Appenders
        {
            get
            {
                lock (this)
                {
                    if (m_appenderAttachedImpl == null)
                    {
                        return AppenderCollection.EmptyCollection;
                    }
                    else
                    {
                        return m_appenderAttachedImpl.Appenders;
                    }
                }
            }
        }

        /// <summary>
        /// Looks for the appender with the specified name.
        /// </summary>
        /// <param name="name">The name of the appender to lookup.</param>
        /// <returns>
        /// The appender with the specified name, or <c>null</c>.
        /// </returns>
        /// <remarks>
        /// <para>
        /// Get the named appender attached to this appender.
        /// </para>
        /// </remarks>
        virtual public IAppender GetAppender(string name)
        {
            lock (this)
            {
                if (m_appenderAttachedImpl == null || name == null)
                {
                    return null;
                }

                return m_appenderAttachedImpl.GetAppender(name);
            }
        }

        /// <summary>
        /// Removes all previously added appenders from this appender.
        /// </summary>
        /// <remarks>
        /// <para>
        /// This is useful when re-reading configuration information.
        /// </para>
        /// </remarks>
        virtual public void RemoveAllAppenders()
        {
            lock (this)
            {
                if (m_appenderAttachedImpl != null)
                {
                    m_appenderAttachedImpl.RemoveAllAppenders();
                    m_appenderAttachedImpl = null;
                }
            }
        }

        /// <summary>
        /// Removes the specified appender from the list of appenders.
        /// </summary>
        /// <param name="appender">The appender to remove.</param>
        /// <returns>The appender removed from the list</returns>
        /// <remarks>
        /// The appender removed is not closed.
        /// If you are discarding the appender you must call
        /// <see cref="IAppender.Close"/> on the appender removed.
        /// </remarks>
        virtual public IAppender RemoveAppender(IAppender appender)
        {
            lock (this)
            {
                if (appender != null && m_appenderAttachedImpl != null)
                {
                    return m_appenderAttachedImpl.RemoveAppender(appender);
                }
            }
            return null;
        }

        /// <summary>
        /// Removes the appender with the specified name from 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>
        /// The appender removed is not closed.
        /// If you are discarding the appender you must call
        /// <see cref="IAppender.Close"/> on the appender removed.
        /// </remarks>
        virtual public IAppender RemoveAppender(string name)
        {
            lock (this)
            {
                if (name != null && m_appenderAttachedImpl != null)
                {
                    return m_appenderAttachedImpl.RemoveAppender(name);
                }
            }
            return null;
        }

        #endregion Implementation of IAppenderAttachable

        /// <summary>
        /// Initialize the options for this appender
        /// </summary>
        /// <remarks>
        /// <para>
        /// Initialize the level to color mappings set on this appender.
        /// </para>
        /// </remarks>
        public override void ActivateOptions()
        {
            m_levelMapping.ActivateOptions();
        }

        #endregion Override implementation of AppenderSkeleton

        #region Private Instances Fields

        /// <summary>
        /// Mapping from level object to color value
        /// </summary>
        private LevelMapping m_levelMapping = new LevelMapping();

        private AppenderAttachedImpl m_appenderAttachedImpl;

        /// <summary>
        /// Ansi code to reset terminal
        /// </summary>
        private const string PostEventCodes = "\x1b[0m";

        #endregion Private Instances Fields

        #region LevelColors LevelMapping Entry

        /// <summary>
        /// A class to act as a mapping between the level that a logging call is made at and
        /// the color it should be displayed as.
        /// </summary>
        /// <remarks>
        /// <para>
        /// Defines the mapping between a level and the color it should be displayed in.
        /// </para>
        /// </remarks>
        new public class LevelColors : LevelMappingEntry
        {
            private AnsiColor m_foreColor=AnsiColor.White;
            private AnsiColor m_backColor=AnsiColor.Black;
            private AnsiAttributes m_attributes;
            private string m_combinedColor = "";

            /// <summary>
            /// The mapped foreground color for the specified level
            /// </summary>
            /// <remarks>
            /// <para>
            /// Required property.
            /// The mapped foreground color for the specified level
            /// </para>
            /// </remarks>
            public AnsiColor ForeColor
            {
                get { return m_foreColor; }
                set { m_foreColor = value; }
            }

            /// <summary>
            /// The mapped background color for the specified level
            /// </summary>
            /// <remarks>
            /// <para>
            /// Required property.
            /// The mapped background color for the specified level
            /// </para>
            /// </remarks>
            public AnsiColor BackColor
            {
                get { return m_backColor; }
                set { m_backColor = value; }
            }

            /// <summary>
            /// The color attributes for the specified level
            /// </summary>
            /// <remarks>
            /// <para>
            /// Required property.
            /// The color attributes for the specified level
            /// </para>
            /// </remarks>
            public AnsiAttributes Attributes
            {
                get { return m_attributes; }
                set { m_attributes = value; }
            }

            /// <summary>
            /// Initialize the options for the object
            /// </summary>
            /// <remarks>
            /// <para>
            /// Combine the <see cref="ForeColor"/> and <see cref="BackColor"/> together
            /// and append the attributes.
            /// </para>
            /// </remarks>
            public override void ActivateOptions()
            {
                base.ActivateOptions();

                StringBuilder buf = new StringBuilder();

                // Reset any existing codes
                buf.Append("\x1b[0;");

                // set the foreground color
                buf.Append(30 + (int)m_foreColor);
                buf.Append(';');

                // set the background color
                buf.Append(40 + (int)m_backColor);

                // set the attributes
                if ((m_attributes & AnsiAttributes.Bright) > 0)
                {
                    buf.Append(";1");
                }
                if ((m_attributes & AnsiAttributes.Dim) > 0)
                {
                    buf.Append(";2");
                }
                if ((m_attributes & AnsiAttributes.Underscore) > 0)
                {
                    buf.Append(";4");
                }
                if ((m_attributes & AnsiAttributes.Blink) > 0)
                {
                    buf.Append(";5");
                }
                if ((m_attributes & AnsiAttributes.Reverse) > 0)
                {
                    buf.Append(";7");
                }
                if ((m_attributes & AnsiAttributes.Hidden) > 0)
                {
                    buf.Append(";8");
                }
                if ((m_attributes & AnsiAttributes.Strikethrough) > 0)
                {
                    buf.Append(";9");
                }

                buf.Append('m');

                m_combinedColor = buf.ToString();
            }

            /// <summary>
            /// The combined <see cref="ForeColor"/>, <see cref="BackColor"/> and
            /// <see cref="Attributes"/> suitable for setting the ansi terminal color.
            /// </summary>
            protected internal string CombinedColor
            {
                get { return m_combinedColor; }
            }
        }

        #endregion

        public void Dispose()
        {
            Close();
        }
    }
}
