/**
	Handles all the console output of the Dub package manager, by providing useful
	methods for handling colored text. The module also disables colors when stdout
	and stderr are not a TTY in order to avoid ASCII escape sequences in piped
	output. The module can auto-detect and configure itself in this regard by
	calling initLogging() at the beginning of the program. But, whether to color
	text or not can also be set manually with setLoggingColorsEnabled(bool).

	The output for the log levels error, warn and info is formatted like this:

	"			 <tag> <text>"
	 '----------'
	 fixed width

	the "tag" part can be colored (most often will be) and always has a fixed
	width, which is defined as a constant at the beginning of this module.

	The output for the log levels debug and diagnostic will be just the plain
	string.

	There are some default tag string and color values for some logging levels:
	- warn: "Warning", yellow bold
	- error: "Error", red bold

	Actually, for error and warn levels, the tag color is fixed to the ones listed
	above.

	Also, the default tag string for the info level is "" (the empty string) and
	the default color is white (usually it's manually set when calling logInfo
	with the wanted tag string, but this allows to just logInfo("text") without
	having to worry about the tag if it's not needed).

	Usage:
		After initializing the logging module with initLogging(), the functions
		logDebug(..), logDiagnostic(..), logInfo(..), logWarning(..) and logError(..)
		can be used to print log messages. Whether the messages are printed on stdout
		or stderr depends on the log level (warning and error go to stderr).
		The log(..) function can also be used. Check the signature and documentation
		of the functions for more information.

		The minimum log level to print can be configured using setLogLevel(..),
		and whether to color outputted text or not can be set with
		setLoggingColorsEnabled(..)

		The color(str, color) function can be used to color text within a log
		message, for instance like this:

		logInfo("Tag", Color.green, "My %s message", "colored".color(Color.red))

	Copyright: © 2018 Giacomo De Lazzari
	License: Subject to the terms of the MIT license, as written in the included LICENSE file.
	Authors: Giacomo De Lazzari
*/

module dub.internal.logging;

import std.stdio;
import std.array;
import std.format;
import std.string;

import dub.internal.colorize : fg, mode;

/**
	An enum listing possible colors for terminal output, useful to set the color
	of a tag. Re-exported from d-colorize in dub.internal.colorize. See the enum
	definition there for a list of possible values.
*/
public alias Color = fg;

/**
	An enum listing possible text "modes" for terminal output, useful to set the
	text to bold, underline, blinking, etc...
	Re-exported from d-colorize in dub.internal.colorize. See the enum definition
	there for a list of possible values.
*/
public alias Mode = mode;

/// Defines the current width of logging tags for justifying in chars.
/// Can be manipulated through push and pop.
struct TagWidth {
	import core.atomic;

	private shared int value = 12;
	private shared int index;
	private shared int[16] stack;

	/// Gets the tag width in chars
	public int get() {
		return value;
	}

	/// Changes the tag width for all following logging calls, until $(LREF pop) is called.
	public void push(int width) {
		int currentIndex = index;
		index.atomicOp!"+="(1);
		stack[currentIndex] = value;
		assert(index < stack.length, "too many TagWidth.push without pop");
		value = width;
	}

	/// Reverts the last $(LREF push) call.
	public void pop() {
		assert(index > 0);
		value = stack[index.atomicOp!"-="(1)];
	}
}

/// The global tag width instance used for logging.
public __gshared TagWidth tagWidth;

/// Possible log levels supported
enum LogLevel {
	debug_,
	diagnostic,
	info,
	warn,
	error,
	none
}

// The current minimum log level to be printed
private shared LogLevel _minLevel = LogLevel.info;

/*
	Whether to print text with colors or not, defaults to true but will be set
	to false in initLogging() if stdout or stderr are not a TTY (which means the
	output is probably being piped and we don't want ASCII escape chars in it)
*/
private shared bool _printColors = true;

/// Ditto
public bool hasColors () @trusted nothrow @nogc { return _printColors; }

// isatty() is used in initLogging() to detect whether or not we are on a TTY
extern (C) int isatty(int);

/**
	This function must be called at the beginning for the program, before any
	logging occurs. It will detect whether or not stdout/stderr are a console/TTY
	and will consequently disable colored output if needed. Also, if a NO_COLOR
	environment variable is defined, colors are disabled (https://no-color.org/).

	Forgetting to call the function will result in ASCII escape sequences in the
	piped output, probably an undesirable thing.
*/
void initLogging()
{
	import std.process : environment;
	import core.stdc.stdio;

	_printColors = environment.get("NO_COLOR") == "";
	version (Windows)
	{
		version (CRuntime_DigitalMars)
		{
			if (!isatty(core.stdc.stdio.stdout._file) ||
					!isatty(core.stdc.stdio.stderr._file))
				_printColors = false;
		}
		else version (CRuntime_Microsoft)
		{
			if (!isatty(fileno(core.stdc.stdio.stdout)) ||
					!isatty(fileno(core.stdc.stdio.stderr)))
				_printColors = false;
		}
		else
			_printColors = false;
	}
	else version (Posix)
	{
		import core.sys.posix.unistd;

		if (!isatty(STDERR_FILENO) || !isatty(STDOUT_FILENO))
			_printColors = false;
	}
}

/// Sets the minimum log level to be printed
void setLogLevel(LogLevel level) nothrow
{
	_minLevel = level;
}

/// Gets the minimum log level to be printed
LogLevel getLogLevel()
{
	return _minLevel;
}

/// Set whether to print colors or not
void setLoggingColorsEnabled(bool enabled)
{
	_printColors = enabled;
}

/**
	Shorthand function to log a message with debug/diagnostic level, no tag string
	or tag color required (since there will be no tag).

	Params:
		fmt = See http://dlang.org/phobos/std_format.html#format-string
		args = Arguments matching the format string
*/
void logDebug(T...)(string fmt, lazy T args) nothrow
{
	log(LogLevel.debug_, false, "", Color.init, fmt, args);
}

/// ditto
void logDiagnostic(T...)(string fmt, lazy T args) nothrow
{
	log(LogLevel.diagnostic, false, "", Color.init, fmt, args);
}

/**
	Shorthand function to log a message with info level, with custom tag string
	and tag color.

	Params:
		tag = The string the tag at the beginning of the line should contain
		tagColor = The color the tag string should have
		fmt = See http://dlang.org/phobos/std_format.html#format-string
		args = Arguments matching the format string
*/
void logInfo(T...)(string tag, Color tagColor, string fmt, lazy T args) nothrow
{
	log(LogLevel.info, false, tag, tagColor, fmt, args);
}

/**
	Shorthand function to log a message with info level, this version prints an
	empty tag automatically (which is different from not having a tag - in this
	case there will be an indentation of tagWidth chars on the left anyway).

	Params:
		fmt = See http://dlang.org/phobos/std_format.html#format-string
		args = Arguments matching the format string
*/
void logInfo(T...)(string fmt, lazy T args) nothrow if (!is(T[0] : Color))
{
	log(LogLevel.info, false, "", Color.init, fmt, args);
}

/**
	Shorthand function to log a message with info level, this version doesn't
	print a tag at all, it effectively just prints the given string.

	Params:
		fmt = See http://dlang.org/phobos/std_format.html#format-string
		args = Arguments matching the format string
*/
void logInfoNoTag(T...)(string fmt, lazy T args) nothrow if (!is(T[0] : Color))
{
	log(LogLevel.info, true, "", Color.init, fmt, args);
}

/**
	Shorthand function to log a message with warning level, with custom tag string.
	The tag color is fixed to yellow.

	Params:
		tag = The string the tag at the beginning of the line should contain
		fmt = See http://dlang.org/phobos/std_format.html#format-string
		args = Arguments matching the format string
*/
void logWarnTag(T...)(string tag, string fmt, lazy T args) nothrow
{
	log(LogLevel.warn, false, tag, Color.yellow, fmt, args);
}

/**
	Shorthand function to log a message with warning level, using the default
	tag "Warning". The tag color is also fixed to yellow.

	Params:
		fmt = See http://dlang.org/phobos/std_format.html#format-string
		args = Arguments matching the format string
*/
void logWarn(T...)(string fmt, lazy T args) nothrow
{
	log(LogLevel.warn, false, "Warning", Color.yellow, fmt, args);
}

/**
	Shorthand function to log a message with error level, with custom tag string.
	The tag color is fixed to red.

	Params:
		tag = The string the tag at the beginning of the line should contain
		fmt = See http://dlang.org/phobos/std_format.html#format-string
		args = Arguments matching the format string
*/
void logErrorTag(T...)(string tag, string fmt, lazy T args) nothrow
{
	log(LogLevel.error, false, tag, Color.red, fmt, args);
}

/**
	Shorthand function to log a message with error level, using the default
	tag "Error". The tag color is also fixed to red.

	Params:
		fmt = See http://dlang.org/phobos/std_format.html#format-string
		args = Arguments matching the format string
*/
void logError(T...)(string fmt, lazy T args) nothrow
{
	log(LogLevel.error, false, "Error", Color.red, fmt, args);
}

/**
	Log a message with the specified log level and with the specified tag string
	and color. If the log level is debug or diagnostic, the tag is not printed
	thus the tag string and tag color will be ignored. If the log level is error
	or warning, the tag will be in bold text. Also the tag can be disabled (for
	any log level) by passing true as the second argument.

	Params:
		level = The log level for the logged message
		disableTag = Setting this to true disables the tag, no matter what
		tag = The string the tag at the beginning of the line should contain
		tagColor = The color the tag string should have
		fmt = See http://dlang.org/phobos/std_format.html#format-string
		args = Arguments matching the format string
*/
void log(T...)(
	LogLevel level,
	bool disableTag,
	string tag,
	Color tagColor,
	string fmt,
	lazy T args
) nothrow
{
	if (level < _minLevel)
		return;

	auto hasTag = true;
	if (level <= LogLevel.diagnostic)
		hasTag = false;
	if (disableTag)
		hasTag = false;

	auto boldTag = false;
	if (level >= LogLevel.warn)
		boldTag = true;

	try
	{
		string result = format(fmt, args);

		if (hasTag)
			result = tag.rightJustify(tagWidth.get, ' ').color(tagColor, boldTag ? Mode.bold : Mode.init) ~ " " ~ result;

		import dub.internal.colorize : cwrite;

		File output = (level <= LogLevel.info) ? stdout : stderr;

		if (output.isOpen)
		{
			output.cwrite(result, "\n");
			output.flush();
		}
	}
	catch (Exception e)
	{
		debug assert(false, e.msg);
	}
}

/**
	Colors the specified string with the specified color. The function is used to
	print colored text within a log message. The function also checks whether
	color output is enabled or disabled (when not outputting to a TTY) and, in the
	last case, just returns the plain string. This allows to use it like so:

	logInfo("Tag", Color.green, "My %s log message", "colored".color(Color.red));

	without worrying whether or not colored output is enabled or not.

	Also a mode can be specified, such as bold/underline/etc...

	Params:
		str = The string to color
		c = The color to apply
		m = An optional mode, such as bold/underline/etc...
*/
string color(const string str, const Color c, const Mode m = Mode.init)
{
	import dub.internal.colorize;

	if (_printColors)
		return dub.internal.colorize.color(str, c, bg.init, m);
	else
		return str;
}

/**
	This function is the same as the above one, but just accepts a mode.
	It's useful, for instance, when outputting bold text without changing the
	color.

	Params:
		str = The string to color
		m = The mode, such as bold/underline/etc...
*/
string color(const string str, const Mode m = Mode.init)
{
	import dub.internal.colorize;

	if (_printColors)
		return dub.internal.colorize.color(str, fg.init, bg.init, m);
	else
		return str;
}
