#region Copyright & License
//
// Copyright 2004-2005 The Apache Software Foundation
//
// Licensed 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.Text;
using System.Globalization;

using log4net.Core;
using log4net.Layout;
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="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 AnsiColorTerminalAppender : AppenderSkeleton
	{
		#region Colors Enum

		/// <summary>
		/// The enum of possible display attributes
		/// </summary>
		/// <remarks>
		/// <para>
		/// The following flags can be combined together to
		/// form the ANSI color attributes.
		/// </para>
		/// </remarks>
		/// <seealso cref="AnsiColorTerminalAppender" />
		[Flags]
		public enum AnsiAttributes : int
		{
			/// <summary>
			/// text is bright
			/// </summary>
			Bright			= 1,
			/// <summary>
			/// text is dim
			/// </summary>
			Dim				= 2,

			/// <summary>
			/// text is underlined
			/// </summary>
			Underscore		= 4,

			/// <summary>
			/// text is blinking
			/// </summary>
			/// <remarks>
			/// Not all terminals support this attribute
			/// </remarks>
			Blink			= 8,

			/// <summary>
			/// text and background colors are reversed
			/// </summary>
			Reverse			= 16,

			/// <summary>
			/// text is hidden
			/// </summary>
			Hidden			= 32,

			/// <summary>
			/// text is displayed with a strikethrough
			/// </summary>
			Strikethrough	= 64
		}

		/// <summary>
		/// The enum of possible foreground or background color values for 
		/// use with the color mapping method
		/// </summary>
		/// <remarks>
		/// <para>
		/// The output can be in one for the following ANSI colors.
		/// </para>
		/// </remarks>
		/// <seealso cref="AnsiColorTerminalAppender" />
		public enum AnsiColor : int
		{
			/// <summary>
			/// color is black
			/// </summary>
			Black	= 0,

			/// <summary>
			/// color is red
			/// </summary>
			Red		= 1,

			/// <summary>
			/// color is green
			/// </summary>
			Green	= 2,

			/// <summary>
			/// color is yellow
			/// </summary>
			Yellow	= 3,

			/// <summary>
			/// color is blue
			/// </summary>
			Blue	= 4,

			/// <summary>
			/// color is magenta
			/// </summary>
			Magenta	= 5,

			/// <summary>
			/// color is cyan
			/// </summary>
			Cyan	= 6,

			/// <summary>
			/// color is white
			/// </summary>
			White	= 7
		}

		#endregion

		#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 AnsiColorTerminalAppender() 
		{
		}

		#endregion Public Instance Constructors

		#region Public Instance Properties

		/// <summary>
		/// Target is the value of the console output stream.
		/// </summary>
		/// <value>
		/// Target is the value of the console output stream.
		/// This is either <c>"Console.Out"</c> or <c>"Console.Error"</c>.
		/// </value>
		/// <remarks>
		/// <para>
		/// Target is the value of the console output stream.
		/// This is either <c>"Console.Out"</c> or <c>"Console.Error"</c>.
		/// </para>
		/// </remarks>
		virtual public string Target
		{
			get { return m_writeToErrorStream ? ConsoleError : ConsoleOut; }
			set
			{
				string trimmedTargetName = value.Trim();
				
				if (string.Compare(ConsoleError, trimmedTargetName, true, CultureInfo.InvariantCulture) == 0) 
				{
					m_writeToErrorStream = true;
				} 
				else 
				{
					m_writeToErrorStream = false;
				}
			}
		}

		/// <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>
		/// 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(log4net.Core.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;
				}
			}

#if NETCF
			// Write to the output stream
			Console.Write(loggingMessage);
#else
			if (m_writeToErrorStream)
			{
				// Write to the error stream
				Console.Error.Write(loggingMessage);
			}
			else
			{
				// Write to the output stream
				Console.Write(loggingMessage);
			}
#endif
		
		}

		/// <summary>
		/// This appender requires a <see cref="Layout"/> to be set.
		/// </summary>
		/// <value><c>true</c></value>
		/// <remarks>
		/// <para>
		/// This appender requires a <see cref="Layout"/> to be set.
		/// </para>
		/// </remarks>
		override protected bool RequiresLayout
		{
			get { return true; }
		}

		/// <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()
		{
			base.ActivateOptions();
			m_levelMapping.ActivateOptions();
		}

		#endregion Override implementation of AppenderSkeleton

		#region Public Static Fields

		/// <summary>
		/// The <see cref="AnsiColorTerminalAppender.Target"/> to use when writing to the Console 
		/// standard output stream.
		/// </summary>
		/// <remarks>
		/// <para>
		/// The <see cref="AnsiColorTerminalAppender.Target"/> to use when writing to the Console 
		/// standard output stream.
		/// </para>
		/// </remarks>
		public const string ConsoleOut = "Console.Out";

		/// <summary>
		/// The <see cref="AnsiColorTerminalAppender.Target"/> to use when writing to the Console 
		/// standard error output stream.
		/// </summary>
		/// <remarks>
		/// <para>
		/// The <see cref="AnsiColorTerminalAppender.Target"/> to use when writing to the Console 
		/// standard error output stream.
		/// </para>
		/// </remarks>
		public const string ConsoleError = "Console.Error";

		#endregion Public Static Fields

		#region Private Instances Fields

		/// <summary>
		/// Flag to write output to the error stream rather than the standard output stream
		/// </summary>
		private bool m_writeToErrorStream = false;

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

		/// <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>
		public class LevelColors : LevelMappingEntry
		{
			private AnsiColor m_foreColor;
			private AnsiColor m_backColor;
			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>
			internal string CombinedColor
			{
				get { return m_combinedColor; }
			}
		}

		#endregion // LevelColors LevelMapping Entry
	}
}
