PatternFormatterFactory.java

package io.jstach.rainbowgum.pattern.format;

import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BLACK_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BLUE_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BOLD;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.CYAN_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.DEFAULT_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.GRAY_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.GREEN_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.MAGENTA_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.RED_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.WHITE_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.YELLOW_FG;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.FAINT;

import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_BLACK;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_RED;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_GREEN;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_YELLOW;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_BLUE;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_MAGENTA;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_CYAN;
import static io.jstach.rainbowgum.pattern.format.ANSIConstants.BRIGHT_WHITE;

import java.lang.System.Logger.Level;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicLong;

import org.jspecify.annotations.Nullable;

import io.jstach.rainbowgum.LogEvent;
import io.jstach.rainbowgum.LogEvent.Caller;
import io.jstach.rainbowgum.LogFormatter;
import io.jstach.rainbowgum.LogFormatter.EventFormatter;
import io.jstach.rainbowgum.LogFormatter.ThrowableFormatter;
import io.jstach.rainbowgum.pattern.Padding;
import io.jstach.rainbowgum.pattern.PatternKeyword;
import io.jstach.rainbowgum.pattern.format.PatternFormatterFactory.CompositeFactory;
import io.jstach.rainbowgum.pattern.format.PatternFormatterFactory.KeywordFactory;

/**
 * Creates formatters from pattern keywords and are registered to a
 * {@link PatternRegistry}.
 */
public sealed interface PatternFormatterFactory {

	/**
	 * Whether this factory's formatter renders (or, like <code>%nopex</code>,
	 * deliberately suppresses) a throwable, and thus counts as the pattern having
	 * addressed exception rendering itself. {@link PatternCompiler} appends a default
	 * exception formatter to any pattern where every keyword answers {@code false} here,
	 * so a logged throwable is never silently dropped just because the pattern forgot to
	 * ask for it - see the built-in throwable keywords and Spring Boot's
	 * <code>%wEx</code>/<code>%wex</code> for the keywords that override this.
	 * @return {@code false} by default.
	 */
	default boolean isExceptionFormatter() {
		return false;
	}

	/**
	 * A composite formatter factory expects possible children. For example
	 * {@code %keyword(child) }.
	 */
	public non-sealed interface CompositeFactory extends PatternFormatterFactory {

		/**
		 * Creates a formatter from a keyword.
		 * @param config config to help create formatter.
		 * @param node current keyword info.
		 * @param child the embedded child in the keyword or null if non is provided.
		 * @return formatter.
		 */
		public LogFormatter create(PatternConfig config, PatternKeyword node, @Nullable LogFormatter child);

	}

	/**
	 * A keyword formatter factory expects keywords and option list. It is usually in the
	 * format of {@code %keyword}.
	 */
	public non-sealed interface KeywordFactory extends PatternFormatterFactory {

		/**
		 * Creates a formatter from a keyword.
		 * @param config config to help create formatter.
		 * @param node current keyword info.
		 * @return formatter.
		 */
		public LogFormatter create(PatternConfig config, PatternKeyword node);

		/**
		 * Creates a keyword factory from a formatter that only cares about padding info.
		 * @param formatter existing formatter.
		 * @return factory.
		 */
		static PatternFormatterFactory.KeywordFactory of(LogFormatter formatter) {
			return (c, n) -> PadFormatter.of(formatter, n.padding());
		}

		/**
		 * Creates a keyword factory from a formatter that only cares about padding info,
		 * additionally marking it as {@link #isExceptionFormatter()}. Lambdas cannot
		 * override default methods, hence this overload instead of a plain lambda for
		 * keywords like <code>%nopex</code> that need to.
		 * @param formatter existing formatter.
		 * @param isExceptionFormatter value to return from
		 * {@link #isExceptionFormatter()}.
		 * @return factory.
		 */
		static PatternFormatterFactory.KeywordFactory of(LogFormatter formatter, boolean isExceptionFormatter) {
			return new KeywordFactory() {
				@Override
				public LogFormatter create(PatternConfig config, PatternKeyword node) {
					return PadFormatter.of(formatter, node.padding());
				}

				@Override
				public boolean isExceptionFormatter() {
					return isExceptionFormatter;
				}
			};
		}

	}

	/**
	 * Parses the depth (<code>full</code>/<code>short</code>/a number) and exclude regex
	 * options shared by throwable keywords such as <code>%ex</code>/<code>%xEx</code> and
	 * Spring Boot's <code>%wEx</code>.
	 * @param node keyword node holding the option list.
	 * @param packagingData whether the resulting formatter should also append packaging
	 * data (jar/module and version) after each frame; see
	 * {@link ThrowableFormatter.Builder#packagingData(boolean)}.
	 * @return throwable formatter configured from the options.
	 */
	public static ThrowableFormatter throwableFormatter(PatternKeyword node, boolean packagingData) {
		return throwableFormatter(node, packagingData, false);
	}

	/**
	 * Like {@link #throwableFormatter(PatternKeyword, boolean)} but also able to build a
	 * root-cause-first formatter for <code>%rEx</code>/<code>%rootException</code>.
	 * @param node keyword node holding the option list.
	 * @param packagingData whether the resulting formatter should also append packaging
	 * data (jar/module and version) after each frame; see
	 * {@link ThrowableFormatter.Builder#packagingData(boolean)}.
	 * @param rootCauseFirst whether to print the root cause first; see
	 * {@link ThrowableFormatter.Builder#rootCauseFirst(boolean)}.
	 * @return throwable formatter configured from the options.
	 */
	public static ThrowableFormatter throwableFormatter(PatternKeyword node, boolean packagingData,
			boolean rootCauseFirst) {
		String depth = node.optOrNull(0);
		int maxLines;
		if (depth == null || depth.equalsIgnoreCase("full")) {
			maxLines = Integer.MAX_VALUE;
		}
		else if (depth.equalsIgnoreCase("short")) {
			maxLines = 0;
		}
		else {
			maxLines = Integer.parseInt(depth);
		}
		var options = node.optionList();
		List<String> excludes = options.size() > 1 ? options.subList(1, options.size()) : List.of();
		return ThrowableFormatter.builder()
			.maxLines(maxLines)
			.excludes(excludes)
			.packagingData(packagingData)
			.rootCauseFirst(rootCauseFirst)
			.build();
	}

}

enum CallerInfoFormatter implements EventFormatter {

	METHOD() {

		@Override
		protected void format(StringBuilder output, Caller info) {
			output.append(info.methodName());
		}

	},
	CLASS {
		@Override
		protected void format(StringBuilder output, Caller info) {
			output.append(info.className());

		}
	},
	FILE {
		@Override
		protected void format(StringBuilder output, Caller info) {
			output.append(info.fileNameOrNull());

		}
	},
	LINE {
		@Override
		protected void format(StringBuilder output, Caller info) {
			output.append(info.lineNumber());

		}
	};

	@Override
	public void format(StringBuilder output, LogEvent event) {
		var info = event.callerOrNull();
		if (info != null) {
			format(output, info);
		}

	}

	protected abstract void format(StringBuilder output, Caller caller);

}

enum StandardKeywordFactory implements KeywordFactory {

	DATE() {
		@Override
		protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
			String pattern = node.opt(0, ISO8601_PATTERN);
			if (pattern.equals(ISO8601_STR)) {
				pattern = ISO8601_PATTERN;
			}
			ZoneId zoneId = node.opt(1, config.zoneId(), s -> ZoneId.of(s));
			@Nullable Locale locale = node.optOrNull(2, s -> Locale.forLanguageTag(s));
			var dtf = DateTimeFormatter.ofPattern(pattern).withZone(zoneId);
			if (locale != null) {
				dtf = dtf.withLocale(locale);
			}
			return LogFormatter.TimestampFormatter.of(dtf, isMillisOrCoarserPrecision(pattern));
		}
	},
	/**
	 * <code>%r</code>, <code>%relative</code>: milliseconds elapsed since
	 * {@link PatternConfig#startTime()}.
	 */
	RELATIVE() {

		@Override
		protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
			return new RelativeTimeFormatter(config.startTime());
		}

	},
	/**
	 * <code>%lsn</code>: a counter local to this compiled keyword occurrence, starting at
	 * {@link PatternConfig#sequenceNumberStart()} and incrementing once per event
	 * formatted.
	 */
	LOCAL_SEQUENCE_NUMBER() {

		@Override
		protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
			return new LocalSequenceNumberFormatter(new AtomicLong(config.sequenceNumberStart()));
		}

	},
	LOGGER() {

		@Override
		protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
			Integer length = node.optOrNull(0, Integer::parseInt);
			if (length == null) {
				return LogFormatter.builder().loggerName().build();
			}
			Abbreviator abbreviator = Abbreviator.of(length);
			if (config.abbreviatorCache() == PatternConfig.CacheType.BASIC) {
				abbreviator = Abbreviator.cache(abbreviator);
			}
			return new LoggerFormatter(abbreviator);
		}

	},
	/**
	 * Logback/Log4j2 <code>%X</code>/<code>%mdc</code> style: with no key every key value
	 * as comma space separated <code>key=value</code> pairs, and with a key see
	 * {@link LogFormatter.Builder#keyValue(String, String)}. For RainbowGum's own
	 * percent-encoded style instead, see {@link #ENCODED_MDC}.
	 */
	MDC() {

		@Override
		protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
			String key = node.optOrNull(0);
			if (key == null) {
				return LogbackKeyValuesFormatter.INSTANCE;
			}
			var kf = keyAndFallback(key);
			return LogFormatter.builder().keyValue(kf.key(), kf.fallback()).build();
		}

	},
	/**
	 * RainbowGum's percent-encoded (RFC 3986 URI query) MDC style - see
	 * {@link LogFormatter.Builder#keyValues()} and
	 * {@link LogFormatter.Builder#encodedKeyValue(String, String)} for the exact format.
	 * For Logback/Log4j2's <code>%X</code> style instead, see {@link #MDC}.
	 */
	ENCODED_MDC() {

		@Override
		protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
			String key = node.optOrNull(0);
			if (key == null) {
				return LogFormatter.builder().keyValues().build();
			}
			var kf = keyAndFallback(key);
			return LogFormatter.builder().encodedKeyValue(kf.key(), kf.fallback()).build();
		}

	},
	PROPERTY() {

		@Override
		protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
			String key = node.optOrNull(0);
			if (key == null) {
				return LogFormatter.builder().text("Property_HAS_NO_KEY").build();
			}
			var f = config.propertyFunction();
			var v = f.apply(key);
			if (v == null) {
				return LogFormatter.noop();
			}
			return LogFormatter.builder().text(v).build();
		}

	},
	/**
	 * This comes from Spring Boot %esb
	 */
	ENCLOSED_SQUARE_BRACKET() {

		@Override
		protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
			String key = node.optOrNull(0);
			if (key == null) {
				return LogFormatter.noop();
			}
			var f = config.propertyFunction();
			var v = f.apply(key);
			if (v == null || v.isEmpty()) {
				return LogFormatter.noop();
			}
			return LogFormatter.builder().text("[").text(v).text("]").build();
		}

	},
	/**
	 * <code>%ex</code>, <code>%ex{full}</code>, <code>%ex{short}</code>,
	 * <code>%ex{N}</code>, <code>%ex{N, regex1, regex2, ...}</code>.
	 */
	THROWABLE() {

		@Override
		protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
			return PatternFormatterFactory.throwableFormatter(node, false);
		}

		@Override
		public boolean isExceptionFormatter() {
			return true;
		}

	},
	/**
	 * <code>%xEx</code>, <code>%xEx{full}</code>, <code>%xEx{short}</code>,
	 * <code>%xEx{N}</code>, <code>%xEx{N, regex1, regex2, ...}</code>. Same options as
	 * {@link #THROWABLE} but also appends packaging data (jar/module and version) after
	 * each frame, similar to logback's <code>ExtendedThrowableProxyConverter</code>.
	 */
	EXTENDED_THROWABLE() {

		@Override
		protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
			return PatternFormatterFactory.throwableFormatter(node, true);
		}

		@Override
		public boolean isExceptionFormatter() {
			return true;
		}

	},
	/**
	 * <code>%rEx</code>, <code>%rEx{full}</code>, <code>%rEx{short}</code>,
	 * <code>%rEx{N}</code>, <code>%rEx{N, regex1, regex2, ...}</code>. Same options as
	 * {@link #EXTENDED_THROWABLE} (including packaging data) but the root cause prints
	 * first and each wrapper prints afterward, similar to logback's
	 * <code>RootCauseFirstThrowableProxyConverter</code>.
	 */
	ROOT_EXCEPTION() {

		@Override
		protected LogFormatter _create(PatternConfig config, PatternKeyword node) {
			return PatternFormatterFactory.throwableFormatter(node, true, true);
		}

		@Override
		public boolean isExceptionFormatter() {
			return true;
		}

	};

	static final String ISO8601_STR = "ISO8601";

	static final String ISO8601_PATTERN = "yyyy-MM-dd HH:mm:ss,SSS";

	record KeyAndFallback(String key, @Nullable String fallback) {
	}

	/**
	 * Splits a <code>%X{key}</code>/<code>%mdc{key}</code> option on <code>:-</code>,
	 * Logback's syntax for a default value (e.g. <code>%X{requestId:-none}</code>).
	 */
	static KeyAndFallback keyAndFallback(String option) {
		String[] s = option.split(":-", 0);
		if (s.length == 2) {
			return new KeyAndFallback(s[0], s[1]);
		}
		return new KeyAndFallback(option, null);
	}

	/**
	 * Whether a {@link DateTimeFormatter} pattern's finest resolution is milliseconds or
	 * coarser, i.e. it never renders sub-millisecond digits. Used to decide whether the
	 * formatted string can be safely cached and reused for events landing in the same
	 * millisecond (see
	 * {@link LogFormatter.TimestampFormatter#of(DateTimeFormatter, boolean)}).
	 */
	static boolean isMillisOrCoarserPrecision(String pattern) {
		boolean literal = false;
		int consecutiveS = 0;
		for (int i = 0; i < pattern.length(); i++) {
			char c = pattern.charAt(i);
			if (c == '\'') {
				literal = !literal;
				consecutiveS = 0;
				continue;
			}
			if (literal) {
				continue;
			}
			if (c == 'n' || c == 'N') {
				return false;
			}
			if (c == 'S') {
				consecutiveS++;
				if (consecutiveS > 3) {
					return false;
				}
			}
			else {
				consecutiveS = 0;
			}
		}
		return true;
	}

	// MICROS, //
	// THREAD, //
	// LEVEL, //
	// LOGGER, //
	// MESSAGE, //
	// MDC, //
	// THROWABLE, //
	// LINESEP;

	@Override
	public LogFormatter create(PatternConfig config, PatternKeyword node) {
		var formatter = _create(config, node);
		var formatInfo = node.padding();
		return PadFormatter.of(formatter, formatInfo);
	}

	protected abstract LogFormatter _create(PatternConfig config, PatternKeyword node);

	record LoggerFormatter(Abbreviator abbreviator) implements LogFormatter.EventFormatter {

		@Override
		public void format(StringBuilder output, LogEvent event) {
			String out = abbreviator.abbreviate(event.loggerName());
			output.append(out);

		}

	}

	record RelativeTimeFormatter(Instant startTime) implements LogFormatter.EventFormatter {

		@Override
		public void format(StringBuilder output, LogEvent event) {
			long millis = Duration.between(startTime, event.timestamp()).toMillis();
			output.append(millis);
		}

	}

	record LocalSequenceNumberFormatter(AtomicLong counter) implements LogFormatter.EventFormatter {

		@Override
		public void format(StringBuilder output, LogEvent event) {
			output.append(counter.getAndIncrement());
		}

	}

}

enum BareCompositeFactory implements CompositeFactory {

	BARE() {
		@Override
		public LogFormatter create(PatternConfig config, PatternKeyword node, @Nullable LogFormatter child) {
			if (child == null) {
				child = new LogFormatter.StaticFormatter("");
			}
			return PadFormatter.of(child, node.padding());
		}
	};

}

final class ANSIConstants {

	final static String ESC_START = "\033[";

	final static String ESC_END = "m";

	final static String BOLD = "1;";

	final static String FAINT = "2;";

	final static String RESET = "0;";

	final static String BLACK_FG = "30";

	final static String RED_FG = "31";

	final static String GREEN_FG = "32";

	final static String YELLOW_FG = "33";

	final static String BLUE_FG = "34";

	final static String MAGENTA_FG = "35";

	final static String CYAN_FG = "36";

	final static String WHITE_FG = "37";

	final static String DEFAULT_FG = "39";

	final static String GRAY_FG = BOLD + BLACK_FG; // Logback is a little weird about gray

	final static String SET_DEFAULT_COLOR = ESC_START + RESET + DEFAULT_FG + ESC_END;

	/*
	 * Spring Boot colors
	 */
	final static String BRIGHT_BLACK = "90";

	final static String BRIGHT_RED = "91";

	final static String BRIGHT_GREEN = "92";

	final static String BRIGHT_YELLOW = "93";

	final static String BRIGHT_BLUE = "94";

	final static String BRIGHT_MAGENTA = "95";

	final static String BRIGHT_CYAN = "96";

	final static String BRIGHT_WHITE = "97";

}

/*
 * Colors the child by the event's level. With no options the colors are Logback's: error
 * bold red, warn red, info blue, everything else the default color. Options are Log4j2
 * style LEVEL=style pairs, for example {ERROR=red bold, INFO=blue bold}, where a style is
 * a color and optionally bold or faint; levels not listed keep their default.
 */
record HighlightFormatter(@Nullable LogFormatter child, String error, String warn, String info, String debug,
		String trace) implements LogFormatter.EventFormatter {

	static final HighlightFormatter LOGBACK_COLORS = new HighlightFormatter(null, BOLD + RED_FG, RED_FG, BLUE_FG,
			DEFAULT_FG, DEFAULT_FG);

	HighlightFormatter(@Nullable LogFormatter child) {
		this(child, LOGBACK_COLORS.error, LOGBACK_COLORS.warn, LOGBACK_COLORS.info, LOGBACK_COLORS.debug,
				LOGBACK_COLORS.trace);
	}

	@Override
	public void format(StringBuilder output, LogEvent event) {
		/*
		 * TODO should a null child be a noop? Need to see what logback does for compat.
		 */
		String code = switch (event.level()) {
			case ERROR -> error;
			case WARNING -> warn;
			case INFO -> info;
			case DEBUG -> debug;
			case TRACE -> trace;
			default -> DEFAULT_FG;
		};
		output.append(ANSIConstants.ESC_START);
		output.append(code);
		output.append(ANSIConstants.ESC_END);
		if (child != null) {
			child.format(output, event);
		}
		output.append(ANSIConstants.SET_DEFAULT_COLOR);
	}

	static HighlightFormatter of(@Nullable LogFormatter child, List<String> options) {
		String error = LOGBACK_COLORS.error;
		String warn = LOGBACK_COLORS.warn;
		String info = LOGBACK_COLORS.info;
		String debug = LOGBACK_COLORS.debug;
		String trace = LOGBACK_COLORS.trace;
		for (String option : options) {
			if (option.isBlank()) {
				continue;
			}
			int eq = option.indexOf('=');
			if (eq < 0) {
				throw new IllegalArgumentException(
						"highlight option '" + option.strip() + "' should be LEVEL=style, for example ERROR=red bold");
			}
			String level = option.substring(0, eq).strip().toUpperCase(Locale.ROOT);
			String code = parseStyle(option.substring(eq + 1));
			switch (level) {
				case "ERROR" -> error = code;
				case "WARN", "WARNING" -> warn = code;
				case "INFO" -> info = code;
				case "DEBUG" -> debug = code;
				case "TRACE" -> trace = code;
				default -> throw new IllegalArgumentException("highlight option '" + option.strip()
						+ "' has an unknown level. Valid levels: ERROR, WARN, INFO, DEBUG, TRACE");
			}
		}
		return new HighlightFormatter(child, error, warn, info, debug, trace);
	}

	/*
	 * A style is space separated words: at most one color plus bold and/or faint.
	 */
	static String parseStyle(String style) {
		String attributes = "";
		String color = null;
		for (String word : words(style)) {
			String w = word.toLowerCase(Locale.ROOT);
			switch (w) {
				case "bold" -> attributes += BOLD;
				case "faint" -> attributes += FAINT;
				default -> {
					if (color != null) {
						throw new IllegalArgumentException(
								"highlight style '" + style.strip() + "' has more than one color");
					}
					color = colorCode(w, style);
				}
			}
		}
		return attributes + (color == null ? DEFAULT_FG : color);
	}

	private static List<String> words(String text) {
		var words = new java.util.ArrayList<String>();
		int start = -1;
		for (int i = 0; i <= text.length(); i++) {
			boolean space = i == text.length() || Character.isWhitespace(text.charAt(i));
			if (space && start >= 0) {
				words.add(text.substring(start, i));
				start = -1;
			}
			else if (!space && start < 0) {
				start = i;
			}
		}
		return words;
	}

	private static String colorCode(String color, String style) {
		return switch (color) {
			case "black" -> BLACK_FG;
			case "red" -> RED_FG;
			case "green" -> GREEN_FG;
			case "yellow" -> YELLOW_FG;
			case "blue" -> BLUE_FG;
			case "magenta" -> MAGENTA_FG;
			case "cyan" -> CYAN_FG;
			case "white" -> WHITE_FG;
			case "default" -> DEFAULT_FG;
			case "bright_black" -> BRIGHT_BLACK;
			case "bright_red" -> BRIGHT_RED;
			case "bright_green" -> BRIGHT_GREEN;
			case "bright_yellow" -> BRIGHT_YELLOW;
			case "bright_blue" -> BRIGHT_BLUE;
			case "bright_magenta" -> BRIGHT_MAGENTA;
			case "bright_cyan" -> BRIGHT_CYAN;
			case "bright_white" -> BRIGHT_WHITE;
			default -> throw new IllegalArgumentException("highlight style '" + style.strip()
					+ "' has an unknown word '" + color + "'. Use a color (black, red, green, yellow, blue, magenta, "
					+ "cyan, white, default, or bright_ variants) and optionally bold or faint");
		};
	}

}

/*
 * From Spring Boot
 */
record ClrLevelFormatter(@Nullable LogFormatter child) implements LogFormatter.EventFormatter {

	@Override
	public void format(StringBuilder output, LogEvent event) {
		/*
		 * TODO should a null child be a noop? Need to see what logback does for compat.
		 */
		var level = event.level();
		String code = levelToANSI(level);
		output.append(ANSIConstants.ESC_START);
		output.append(code);
		output.append(ANSIConstants.ESC_END);
		if (child != null) {
			child.format(output, event);
		}
		output.append(ANSIConstants.SET_DEFAULT_COLOR);
	}

	private static String levelToANSI(Level level) {
		return switch (level) {
			case ERROR -> RED_FG;
			case WARNING -> YELLOW_FG;
			case INFO -> GREEN_FG;
			case DEBUG -> GREEN_FG;
			case TRACE -> GREEN_FG;
			default -> DEFAULT_FG;
		};
	}

}

record ClrStaticFormatter(@Nullable LogFormatter child, String code) implements LogFormatter.EventFormatter {

	@Override
	public void format(StringBuilder output, LogEvent event) {
		output.append(ANSIConstants.ESC_START);
		output.append(code);
		output.append(ANSIConstants.ESC_END);
		if (child != null) {
			child.format(output, event);
		}
		output.append(ANSIConstants.SET_DEFAULT_COLOR);
	}

}

enum HighlightCompositeFactory implements CompositeFactory {

	HIGHTLIGHT() {

		@Override
		public LogFormatter create(PatternConfig config, PatternKeyword node, @Nullable LogFormatter child) {
			if (config.ansiDisabled()) {
				if (child == null) {
					return LogFormatter.noop();
				}
				return child;
			}
			return HighlightFormatter.of(child, node.optionList());
		}

	},

	/**
	 * This style come from Spring Boot.
	 */
	CLR() {

		@Override
		public LogFormatter create(PatternConfig config, PatternKeyword node, @Nullable LogFormatter child) {
			if (config.ansiDisabled()) {
				if (child == null) {
					return LogFormatter.noop();
				}
				return child;
			}
			String color = node.optOrNull(0, HighlightCompositeFactory::clrParseColor);
			if (color == null) {
				return new ClrLevelFormatter(child);
			}
			return new ClrStaticFormatter(child, color);
		}

	};

	static String clrParseColor(String color) {
		color = color.toLowerCase(Locale.ROOT);
		return switch (color) {
			case "black" -> BLACK_FG;
			case "white" -> WHITE_FG;
			case "blue" -> BLUE_FG;
			case "cyan" -> CYAN_FG;
			case "faint" -> FAINT + DEFAULT_FG;
			case "green" -> GREEN_FG;
			case "magenta" -> MAGENTA_FG;
			case "red" -> RED_FG;
			case "yellow" -> YELLOW_FG;
			case "bright_black" -> BRIGHT_BLACK;
			case "bright_red" -> BRIGHT_RED;
			case "bright_green" -> BRIGHT_GREEN;
			case "bright_yellow" -> BRIGHT_YELLOW;
			case "bright_blue" -> BRIGHT_BLUE;
			case "bright_magenta" -> BRIGHT_MAGENTA;
			case "bright_cyan" -> BRIGHT_CYAN;
			case "bright_white" -> BRIGHT_WHITE;
			default -> throw new IllegalArgumentException("Bad color:" + color);
		};
	}

}

/**
 * This style of colors largely comes from logback
 */
enum ColorCompositeFactory implements CompositeFactory {

	BLACK(BLACK_FG, "black"), //
	RED(RED_FG, "red"), //
	GREEN(GREEN_FG, "green"), //
	YELLOW(YELLOW_FG, "yellow"), //
	BLUE(BLUE_FG, "blue"), //
	MAGENTA(MAGENTA_FG, "magenta"), //
	CYAN(CYAN_FG, "cyan"), //
	WHITE(WHITE_FG, "white"), //
	GRAY(GRAY_FG, "gray"), //
	BOLD_RED(BOLD + RED_FG, "boldRed"), //
	BOLD_GREEN(BOLD + GREEN_FG, "boldGreen"), //
	BOLD_YELLOW(BOLD + YELLOW_FG, "boldYellow"), //
	BOLD_BLUE(BOLD + BLUE_FG, "boldBlue"), //
	BOLD_MAGENTA(BOLD + MAGENTA_FG, "boldMagenta"), //
	BOLD_CYAN(BOLD + CYAN_FG, "boldCyan"), //
	BOLD_WHITE(BOLD + WHITE_FG, "boldWhite");

	private final String fg;

	private final String name;

	private ColorCompositeFactory(String fg, String name) {
		this.fg = fg;
		this.name = name;
	}

	@Override
	public LogFormatter create(PatternConfig config, PatternKeyword node, @Nullable LogFormatter child) {
		return create(node, child, !config.ansiDisabled());
	}

	LogFormatter create(PatternKeyword node, @Nullable LogFormatter child, boolean ansi) {
		var b = LogFormatter.builder();
		if (ansi) {
			b.text(ANSIConstants.ESC_START);
			b.text(fg);
			b.text(ANSIConstants.ESC_END);
		}
		if (child != null) {
			b.add(child);
		}
		if (ansi) {
			b.text(ANSIConstants.SET_DEFAULT_COLOR);
		}
		return b.build();

	}

	public String alias() {
		return name;
	}

}

record PadFormatter(Padding padding, LogFormatter formatter) implements LogFormatter.EventFormatter {

	public static LogFormatter of(LogFormatter formatter, @Nullable Padding padding) {
		if (padding == null) {
			return formatter;
		}
		if (formatter instanceof LogFormatter.StaticFormatter sf) {
			StringBuilder b = new StringBuilder();
			padding.format(b, sf.content());
			return new LogFormatter.StaticFormatter(b.toString());
		}
		return new PadFormatter(padding, formatter);
	}

	@Override
	public void format(StringBuilder output, LogEvent event) {
		/*
		 * TODO revisit formatting without using a temp buffer
		 */
		StringBuilder buf = new StringBuilder();
		formatter.format(buf, event);
		padding.format(output, buf);
	}

}