// Styling control codes
const BOLD = "\x02";
const COLOR = "\x03";
const HEX_COLOR = "\x04";
const RESET = "\x0f";
const REVERSE = "\x16";
const ITALIC = "\x1d";
const UNDERLINE = "\x1f";
const STRIKETHROUGH = "\x1e";
const MONOSPACE = "\x11";

export type ParsedStyle = {
	bold?: boolean;
	textColor?: string;
	bgColor?: string;
	hexColor?: string;
	hexBgColor?: string;
	italic?: boolean;
	underline?: boolean;
	strikethrough?: boolean;
	monospace?: boolean;
	text: string;
	start: number;
	end: number;
};

// Color code matcher, with format `XX,YY` where both `XX` and `YY` are
// integers, `XX` is the text color and `YY` is an optional background color.
const colorRx = /^(\d{1,2})(?:,(\d{1,2}))?/;

// 6-char Hex color code matcher
const hexColorRx = /^([0-9a-f]{6})(?:,([0-9a-f]{6}))?/i;

// Represents all other control codes that to be ignored/filtered from the text
// This regex allows line feed character
const controlCodesRx = /[\u0000-\u0009\u000B-\u001F]/g;

// Converts a given text into an array of objects, each of them representing a
// similarly styled section of the text. Each object carries the `text`, style
// information (`bold`, `textColor`, `bgcolor`, `italic`,
// `underline`, `strikethrough`, `monospace`), and `start`/`end` cursors.
function parseStyle(text: string) {
	const result: ParsedStyle[] = [];
	let start = 0;
	let position = 0;

	// At any given time, these carry style information since last time a styling
	// control code was met.
	let colorCodes,
		bold,
		textColor,
		bgColor,
		hexColor,
		hexBgColor,
		italic,
		underline,
		strikethrough,
		monospace;

	const resetStyle = () => {
		bold = false;
		textColor = undefined;
		bgColor = undefined;
		hexColor = undefined;
		hexBgColor = undefined;
		italic = false;
		underline = false;
		strikethrough = false;
		monospace = false;
	};

	resetStyle();

	// When called, this "closes" the current fragment by adding an entry to the
	// `result` array using the styling information set last time a control code
	// was met.
	const emitFragment = () => {
		// Uses the text fragment starting from the last control code position up to
		// the current position
		const textPart = text.slice(start, position);

		// Filters out all non-style related control codes present in this text
		const processedText = textPart.replace(controlCodesRx, " ");

		if (processedText.length) {
			// Current fragment starts where the previous one ends, or at 0 if none
			const fragmentStart = result.length ? result[result.length - 1].end : 0;

			result.push({
				bold,
				textColor,
				bgColor,
				hexColor,
				hexBgColor,
				italic,
				underline,
				strikethrough,
				monospace,
				text: processedText,
				start: fragmentStart,
				end: fragmentStart + processedText.length,
			});
		}

		// Now that a fragment has been "closed", the next one will start after that
		start = position + 1;
	};

	// This loop goes through each character of the given text one by one by
	// bumping the `position` cursor. Every time a new special "styling" character
	// is met, an object gets created (with `emitFragment()`)information on text
	// encountered since the previous styling character.
	while (position < text.length) {
		switch (text[position]) {
			case RESET:
				emitFragment();
				resetStyle();
				break;

			// Meeting a BOLD character means that the ongoing text is either going to
			// be in bold or that the previous one was in bold and the following one
			// must be reset.
			// This same behavior applies to COLOR, REVERSE, ITALIC, and UNDERLINE.
			case BOLD:
				emitFragment();
				bold = !bold;
				break;

			case COLOR:
				emitFragment();

				// Go one step further to find the corresponding color
				colorCodes = text.slice(position + 1).match(colorRx);

				if (colorCodes) {
					textColor = Number(colorCodes[1]);

					if (colorCodes[2]) {
						bgColor = Number(colorCodes[2]);
					}

					// Color code length is > 1, so bump the current position cursor by as
					// much (and reset the start cursor for the current text block as well)
					position += colorCodes[0].length;
					start = position + 1;
				} else {
					// If no color codes were found, toggles back to no colors (like BOLD).
					textColor = undefined;
					bgColor = undefined;
				}

				break;

			case HEX_COLOR:
				emitFragment();

				colorCodes = text.slice(position + 1).match(hexColorRx);

				if (colorCodes) {
					hexColor = colorCodes[1].toUpperCase();

					if (colorCodes[2]) {
						hexBgColor = colorCodes[2].toUpperCase();
					}

					// Color code length is > 1, so bump the current position cursor by as
					// much (and reset the start cursor for the current text block as well)
					position += colorCodes[0].length;
					start = position + 1;
				} else {
					// If no color codes were found, toggles back to no colors (like BOLD).
					hexColor = undefined;
					hexBgColor = undefined;
				}

				break;

			case REVERSE: {
				emitFragment();
				const tmp = bgColor;
				bgColor = textColor;
				textColor = tmp;
				break;
			}

			case ITALIC:
				emitFragment();
				italic = !italic;
				break;

			case UNDERLINE:
				emitFragment();
				underline = !underline;
				break;

			case STRIKETHROUGH:
				emitFragment();
				strikethrough = !strikethrough;
				break;

			case MONOSPACE:
				emitFragment();
				monospace = !monospace;
				break;
		}

		// Evaluate the next character at the next iteration
		position += 1;
	}

	// The entire text has been parsed, so we finalize the current text fragment.
	emitFragment();

	return result;
}

const properties = [
	"bold",
	"textColor",
	"bgColor",
	"hexColor",
	"hexBgColor",
	"italic",
	"underline",
	"strikethrough",
	"monospace",
];

function prepare(text: string) {
	return (
		parseStyle(text)
			// This optimizes fragments by combining them together when all their values
			// for the properties defined above are equal.
			.reduce((prev: ParsedStyle[], curr) => {
				if (prev.length) {
					const lastEntry = prev[prev.length - 1];

					if (properties.every((key) => curr[key] === lastEntry[key])) {
						lastEntry.text += curr.text;
						lastEntry.end += curr.text.length;
						return prev;
					}
				}

				return prev.concat([curr]);
			}, [])
	);
}

export default prepare;