/*
 * Ibis - IRCv3 Library
 * Copyright (C) 2022-2024 Ibis Developers
 *
 * Ibis is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this library; if not, see <https://www.gnu.org/licenses/>.
 */

#include "ibisformatting.h"

#define IBIS_FORMAT_BOLD (0x02)
#define IBIS_FORMAT_COLOR (0x03)
#define IBIS_FORMAT_HEX_COLOR (0x04)
#define IBIS_FORMAT_ITALIC (0x1d)
#define IBIS_FORMAT_MONOSPACE (0x11)
#define IBIS_FORMAT_RESET (0x0f)
#define IBIS_FORMAT_REVERSE (0x16)
#define IBIS_FORMAT_STRIKETHROUGH (0x1e)
#define IBIS_FORMAT_UNDERLINE (0x1f)

/* These colors are copied from https://modern.ircdocs.horse/formatting#colors
 * and https://modern.ircdocs.horse/formatting#colors-16-98.
 */
static PangoColor ibis_formatting_palette[] = {
	{ 0xffff, 0xffff, 0xffff }, /* 00 white */
	{ 0x0000, 0x0000, 0x0000 }, /* 01 black */
	{ 0x0000, 0x0000, 0x7f7f }, /* 02 blue */
	{ 0x0000, 0x9393, 0x0000 }, /* 03 green */
	{ 0xffff, 0x0000, 0x0000 }, /* 04 red */
	{ 0x7f7f, 0x0000, 0x0000 }, /* 05 brown? */
	{ 0x9c9c, 0x0000, 0x9c9c }, /* 06 magenta */
	{ 0xfcfc, 0x7f7f, 0x0000 }, /* 07 orange */
	{ 0xffff, 0xffff, 0x0000 }, /* 08 yellow */
	{ 0x0000, 0xfcfc, 0x0000 }, /* 09 light green */
	{ 0x0000, 0x9393, 0x9393 }, /* 10 cyan */
	{ 0x0000, 0xffff, 0xffff }, /* 11 light cyan */
	{ 0x0000, 0x0000, 0xfcfc }, /* 12 light blue */
	{ 0xffff, 0x0000, 0xffff }, /* 13 pink */
	{ 0x7f7f, 0x7f7f, 0x7f7f }, /* 14 grey */
	{ 0xd2d2, 0xd2d2, 0xd2d2 }, /* 15 light grey */
	{ 0x4747, 0x0000, 0x0000 }, /* 16 */
	{ 0x4747, 0x2121, 0x0000 }, /* 17 */
	{ 0x4747, 0x4747, 0x0000 }, /* 18 */
	{ 0x3232, 0x4747, 0x0000 }, /* 19 */
	{ 0x0000, 0x4747, 0x0000 }, /* 20 */
	{ 0x0000, 0x4747, 0x2c2c }, /* 21 */
	{ 0x0000, 0x4747, 0x4747 }, /* 22 */
	{ 0x0000, 0x2727, 0x4747 }, /* 23 */
	{ 0x0000, 0x0000, 0x4747 }, /* 24 */
	{ 0x2e2e, 0x0000, 0x4747 }, /* 25 */
	{ 0x4747, 0x0000, 0x4747 }, /* 26 */
	{ 0x4747, 0x0000, 0x2a2a }, /* 27 */
	{ 0x7474, 0x0000, 0x0000 }, /* 28 */
	{ 0x7474, 0x3a3a, 0x0000 }, /* 29 */
	{ 0x7474, 0x7474, 0x0000 }, /* 30 */
	{ 0x5151, 0x7474, 0x0000 }, /* 31 */
	{ 0x0000, 0x7474, 0x0000 }, /* 32 */
	{ 0x0000, 0x7474, 0x4949 }, /* 33 */
	{ 0x0000, 0x7474, 0x7474 }, /* 34 */
	{ 0x0000, 0x4040, 0x7474 }, /* 35 */
	{ 0x0000, 0x0000, 0x7474 }, /* 36 */
	{ 0x4b4b, 0x0000, 0x7474 }, /* 37 */
	{ 0x7474, 0x0000, 0x7474 }, /* 38 */
	{ 0x7474, 0x0000, 0x4545 }, /* 39 */
	{ 0xb5b5, 0x0000, 0x0000 }, /* 40 */
	{ 0xb5b5, 0x6363, 0x0000 }, /* 41 */
	{ 0xb5b5, 0xb5b5, 0x0000 }, /* 42 */
	{ 0x7d7d, 0xb5b5, 0x0000 }, /* 43 */
	{ 0x0000, 0xb5b5, 0x0000 }, /* 44 */
	{ 0x0000, 0xb5b5, 0x7171 }, /* 45 */
	{ 0x0000, 0xb5b5, 0xb5b5 }, /* 46 */
	{ 0x0000, 0x6363, 0xb5b5 }, /* 47 */
	{ 0x0000, 0x0000, 0xb5b5 }, /* 48 */
	{ 0x7575, 0x0000, 0xb5b5 }, /* 49 */
	{ 0xb5b5, 0x0000, 0xb5b5 }, /* 50 */
	{ 0xb5b5, 0x0000, 0x6b6b }, /* 51 */
	{ 0xffff, 0x0000, 0x0000 }, /* 52 */
	{ 0xffff, 0x8c8c, 0x0000 }, /* 53 */
	{ 0xffff, 0xffff, 0x0000 }, /* 54 */
	{ 0xb2b2, 0xffff, 0x0000 }, /* 55 */
	{ 0x0000, 0xffff, 0x0000 }, /* 56 */
	{ 0x0000, 0xffff, 0xa0a0 }, /* 57 */
	{ 0x0000, 0xffff, 0xffff }, /* 58 */
	{ 0x0000, 0x8c8c, 0xffff }, /* 59 */
	{ 0x0000, 0x0000, 0xffff }, /* 60 */
	{ 0xa5a5, 0x0000, 0xffff }, /* 61 */
	{ 0xffff, 0x0000, 0xffff }, /* 62 */
	{ 0xffff, 0x0000, 0x9898 }, /* 63 */
	{ 0xffff, 0x5959, 0x5959 }, /* 64 */
	{ 0xffff, 0xb4b4, 0x5959 }, /* 65 */
	{ 0xffff, 0xffff, 0x7171 }, /* 66 */
	{ 0xcfcf, 0xffff, 0x6060 }, /* 67 */
	{ 0x6f6f, 0xffff, 0x6f6f }, /* 68 */
	{ 0x6565, 0xffff, 0xc9c9 }, /* 69 */
	{ 0x6d6d, 0xffff, 0xffff }, /* 70 */
	{ 0x5959, 0xb4b4, 0xffff }, /* 71 */
	{ 0x5959, 0x5959, 0xffff }, /* 72 */
	{ 0xc4c4, 0x5959, 0xffff }, /* 73 */
	{ 0xffff, 0x6666, 0xffff }, /* 74 */
	{ 0xffff, 0x5959, 0xbcbc }, /* 75 */
	{ 0xffff, 0x9c9c, 0x9c9c }, /* 76 */
	{ 0xffff, 0xd3d3, 0x9c9c }, /* 77 */
	{ 0xffff, 0xffff, 0x9c9c }, /* 78 */
	{ 0xe2e2, 0xffff, 0x9c9c }, /* 79 */
	{ 0x9c9c, 0xffff, 0x9c9c }, /* 80 */
	{ 0x9c9c, 0xffff, 0xdbdb }, /* 81 */
	{ 0x9c9c, 0xffff, 0xffff }, /* 82 */
	{ 0x9c9c, 0xd3d3, 0xffff }, /* 83 */
	{ 0x9c9c, 0x9c9c, 0xffff }, /* 84 */
	{ 0xdcdc, 0x9c9c, 0xffff }, /* 85 */
	{ 0xffff, 0x9c9c, 0xffff }, /* 86 */
	{ 0xffff, 0x9494, 0xd3d3 }, /* 87 */
	{ 0x0000, 0x0000, 0x0000 }, /* 88 */
	{ 0x1313, 0x1313, 0x1313 }, /* 89 */
	{ 0x2828, 0x2828, 0x2828 }, /* 90 */
	{ 0x3636, 0x3636, 0x3636 }, /* 91 */
	{ 0x4d4d, 0x4d4d, 0x4d4d }, /* 92 */
	{ 0x6565, 0x6565, 0x6565 }, /* 93 */
	{ 0x8181, 0x8181, 0x8181 }, /* 94 */
	{ 0x9f9f, 0x9f9f, 0x9f9f }, /* 95 */
	{ 0xbcbc, 0xbcbc, 0xbcbc }, /* 96 */
	{ 0xe2e2, 0xe2e2, 0xe2e2 }, /* 97 */
	{ 0xffff, 0xffff, 0xffff }, /* 98 */
};

/******************************************************************************
 * Helpers
 *****************************************************************************/
static inline gboolean
ibis_formatting_get_color(PangoColor *color, guint index) {
	if(index > G_N_ELEMENTS(ibis_formatting_palette)) {
		return FALSE;
	}

	*color = ibis_formatting_palette[index];

	return TRUE;
}

static inline gboolean
ibis_formatting_is_color(const char *text, guint *bytes, PangoColor *color) {
	char buffer[3];
	guint length = 0;

	if(text == NULL || text[0] == '\0') {
		return FALSE;
	}

	/* If the first byte is not a digit we can exit early. */
	if(!g_ascii_isdigit(text[0])) {
		return FALSE;
	}

	/* Check if the second byte is a digit as well. */
	if(g_ascii_isdigit(text[1])) {
		length = 2;
	} else {
		length = 1;
	}

	/* If the user gave us a place to put the value, do it. */
	if(color != NULL) {
		int index = 0;

		memset(buffer, 0, sizeof(buffer));
		strncpy(buffer, text, length);
		index = atoi(buffer);

		if(!ibis_formatting_get_color(color, index)) {
			return FALSE;
		}
	}

	/* Set bytes to the length of the color. */
	if(bytes != NULL) {
		*bytes = length;
	}

	return TRUE;
}

static inline void
ibis_formatting_parse_color(const char *text, guint *offset,
                            gboolean *fg_set, PangoColor *fg,
                            gboolean *bg_set, PangoColor *bg)
{
	guint bytes = 0;

	/* If we're at the end of the string then we are ending a previous color
	 * format.
	 */
	if(text == NULL || text[0] == '\0') {
		return;
	}

	if(ibis_formatting_is_color(text, &bytes, fg)) {
		if(fg_set != NULL) {
			*fg_set = TRUE;
		}

		if(text[bytes] == ',') {
			guint bg_bytes = 0;

			if(ibis_formatting_is_color(&text[bytes + 1], &bg_bytes, bg)) {
				if(bg_set != NULL) {
					*bg_set = TRUE;
				}

				/* Increment bytes for the length of bg_bytes and the comma. */
				bytes += bg_bytes + 1;
			}
		}
	}

	if(offset != NULL) {
		*offset = bytes;
	}
}

static inline gboolean
ibis_formatting_is_hex_color(const char *text, PangoColor *color) {
	for(int i = 0; i < 6; i++) {
		if(text[i] == '\0') {
			return FALSE;
		}

		if(!g_ascii_isxdigit(text[i])) {
			return FALSE;
		}
	}

	if(color != NULL) {
		char buffer[8];

		memset(buffer, 0, sizeof(buffer));
		buffer[0] = '#';
		strncpy(buffer + 1, text, 6);
		if(!pango_color_parse(color, buffer)) {
			return FALSE;
		}
	}

	return TRUE;
}

static inline gboolean
ibis_formatting_parse_hex_color(const char *text, guint *offset,
                                gboolean *fg_set, PangoColor *fg,
                                gboolean *bg_set, PangoColor *bg)
{
	guint bytes = 0;

	if(text == NULL || text[0] == '\0') {
		return FALSE;
	}

	if(ibis_formatting_is_hex_color(text, fg)) {
		if(fg_set != NULL) {
			*fg_set = TRUE;
		}

		bytes = 6;

		if(text[bytes] == ',' &&
		   ibis_formatting_is_hex_color(&text[bytes + 1], bg))
		{
			if(bg_set != NULL) {
				*bg_set = TRUE;
			}

			bytes += 7;
		}
	}

	if(offset != NULL) {
		*offset = bytes;
	}

	return TRUE;
}

/* The following macros have been created to make the format parser easier to
 * read. There's not much to them, but they help avoid a lot of duplicated
 * code in the parsing function.
 */
#define IBIS_FORMATTING_START_ATTRIBUTE(tag, ctor) G_STMT_START { \
	tag = ctor; \
	tag->start_index = position; \
	pango_attr_list_insert(real_attrs, tag); \
} G_STMT_END

#define IBIS_FORMATTING_END_ATTRIBUTE(tag) G_STMT_START { \
	if(tag != NULL) { \
		tag->end_index = position; \
		tag = NULL; \
	} \
} G_STMT_END

#define IBIS_FORMATTING_TOGGLE_ATTRIBUTE(tag, ctor) G_STMT_START { \
	if(tag == NULL) { \
		IBIS_FORMATTING_START_ATTRIBUTE(tag, ctor); \
	} else { \
		IBIS_FORMATTING_END_ATTRIBUTE(tag); \
	} \
} G_STMT_END

/******************************************************************************
 * Public API
 *****************************************************************************/
char *
ibis_formatting_parse(const char *text, PangoAttrList **attrs) {
	GString *str = NULL;
	PangoAttrList *real_attrs = NULL;
	PangoAttribute *background = NULL;
	PangoAttribute *bold = NULL;
	PangoAttribute *foreground = NULL;
	PangoAttribute *italic = NULL;
	PangoAttribute *monospace = NULL;
	PangoAttribute *strikethrough = NULL;
	PangoAttribute *underline = NULL;
	guint offset = 0;
	guint position = 0;

	/* We don't use ibis_str_is_empty here because if we're passed an empty
	 * string, we should return a newly allocated empty string.
	 */
	if(text == NULL) {
		return NULL;
	}

	str = g_string_new("");
	real_attrs = pango_attr_list_new();

	for(guint i = 0; text[i] != '\0'; i++) {
		switch(text[i]) {
		case IBIS_FORMAT_BOLD:
			IBIS_FORMATTING_TOGGLE_ATTRIBUTE(bold, pango_attr_weight_new(PANGO_WEIGHT_BOLD));
			break;
		case IBIS_FORMAT_ITALIC:
			IBIS_FORMATTING_TOGGLE_ATTRIBUTE(italic, pango_attr_style_new(PANGO_STYLE_ITALIC));
			break;
		case IBIS_FORMAT_MONOSPACE:
			IBIS_FORMATTING_TOGGLE_ATTRIBUTE(monospace, pango_attr_family_new("monospace"));
			break;
		case IBIS_FORMAT_RESET:
			IBIS_FORMATTING_END_ATTRIBUTE(background);
			IBIS_FORMATTING_END_ATTRIBUTE(bold);
			IBIS_FORMATTING_END_ATTRIBUTE(foreground);
			IBIS_FORMATTING_END_ATTRIBUTE(italic);
			IBIS_FORMATTING_END_ATTRIBUTE(monospace);
			IBIS_FORMATTING_END_ATTRIBUTE(strikethrough);
			IBIS_FORMATTING_END_ATTRIBUTE(underline);
			break;
		case IBIS_FORMAT_REVERSE:
			break;
		case IBIS_FORMAT_STRIKETHROUGH:
			IBIS_FORMATTING_TOGGLE_ATTRIBUTE(strikethrough, pango_attr_strikethrough_new(TRUE));
			break;
		case IBIS_FORMAT_UNDERLINE:
			IBIS_FORMATTING_TOGGLE_ATTRIBUTE(underline,
			           pango_attr_underline_new(PANGO_UNDERLINE_SINGLE));
			break;
		case IBIS_FORMAT_COLOR: {
			PangoColor bg;
			PangoColor fg;
			gboolean bg_set = FALSE;
			gboolean fg_set = FALSE;

			ibis_formatting_parse_color(&text[i + 1], &offset,
			                            &fg_set, &fg,
			                            &bg_set, &bg);

			if(fg_set) {
				/* If we had an existing foreground, we need to end it. */
				IBIS_FORMATTING_END_ATTRIBUTE(foreground);

				IBIS_FORMATTING_START_ATTRIBUTE(foreground,
				                                pango_attr_foreground_new(fg.red,
				                                                          fg.green,
				                                                          fg.blue));
			} else {
				IBIS_FORMATTING_END_ATTRIBUTE(foreground);
			}

			if(bg_set) {
				IBIS_FORMATTING_END_ATTRIBUTE(background);

				IBIS_FORMATTING_START_ATTRIBUTE(background,
				                                pango_attr_background_new(bg.red,
				                                                          bg.green,
				                                                          bg.blue));
			} else if(!fg_set) {
				/* The background should be reset if neither the foreground or
				 * background were set.
				 */
				IBIS_FORMATTING_END_ATTRIBUTE(background);
			}

			i += offset;

			break;
		}
		case IBIS_FORMAT_HEX_COLOR:
			PangoColor bg;
			PangoColor fg;
			gboolean bg_set = FALSE;
			gboolean fg_set = FALSE;

			ibis_formatting_parse_hex_color(&text[i + 1], &offset,
			                                &fg_set, &fg,
			                                &bg_set, &bg);

			if(fg_set) {
				/* If we had an existing foreground, we need to end it. */
				IBIS_FORMATTING_END_ATTRIBUTE(foreground);

				IBIS_FORMATTING_START_ATTRIBUTE(foreground,
				                                pango_attr_foreground_new(fg.red,
				                                                          fg.green,
				                                                          fg.blue));
			} else {
				IBIS_FORMATTING_END_ATTRIBUTE(foreground);
			}

			if(bg_set) {
				IBIS_FORMATTING_END_ATTRIBUTE(background);

				IBIS_FORMATTING_START_ATTRIBUTE(background,
				                                pango_attr_background_new(bg.red,
				                                                          bg.green,
				                                                          bg.blue));
			} else if(!fg_set) {
				/* The background should be reset if neither the foreground or
				 * background were set.
				 */
				IBIS_FORMATTING_END_ATTRIBUTE(background);
			}

			i += offset;

			break;
		default:
			g_string_append_c(str, text[i]);
			position += 1;
			break;
		}
	}

	IBIS_FORMATTING_END_ATTRIBUTE(background);
	IBIS_FORMATTING_END_ATTRIBUTE(bold);
	IBIS_FORMATTING_END_ATTRIBUTE(foreground);
	IBIS_FORMATTING_END_ATTRIBUTE(italic);
	IBIS_FORMATTING_END_ATTRIBUTE(monospace);
	IBIS_FORMATTING_END_ATTRIBUTE(strikethrough);
	IBIS_FORMATTING_END_ATTRIBUTE(underline);

	if(attrs != NULL) {
		*attrs = real_attrs;
	} else {
		pango_attr_list_unref(real_attrs);
	}

	return g_string_free_and_steal(str);
}

char *
ibis_formatting_strip(const char *text) {
	return ibis_formatting_parse(text, NULL);
}
