/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2016-2025 Matthias Klumpp <matthias@tenstral.net>
 *
 * Licensed under the GNU Lesser General Public License Version 2.1
 *
 * This library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * SECTION:asc-utils-fonts
 * @short_description: Helper functions for generating metadata from fonts
 * @include: appstream-compose.h
 */

#include "config.h"
#include "asc-utils-fonts.h"

#include "as-utils-private.h"
#include "asc-globals.h"
#include "asc-font.h"
#include "asc-canvas.h"
#include "asc-canvas-private.h"

struct {
	gint width;
	gint height;
} font_screenshot_sizes[] = {
	{ 1560, 878 },
	{ 752,  423 },
	{ 624,  351 },
	{ 0,    0   }
};

/**
 * asc_render_font_screenshots:
 *
 * Render a "screenshot" sample for this font.
 */
static gboolean
asc_render_font_screenshots (AscResult *cres,
			     GPtrArray *fonts,
			     const gchar *cpt_screenshots_path,
			     AsComponent *cpt)
{
	gboolean first = TRUE;
	g_mkdir_with_parents (cpt_screenshots_path, 0755);

	for (guint i = 0; i < fonts->len; i++) {
		const gchar *font_id = NULL;
		const gchar *custom_sample_text = NULL;
		const gchar *bg_letter = NULL;
		g_autofree gchar *scr_caption = NULL;
		g_autofree gchar *scr_url_root = NULL;
		g_autoptr(AsScreenshot) scr = NULL;
		AscFont *font = ASC_FONT (g_ptr_array_index (fonts, i));

		font_id = asc_font_get_id (font);
		if (as_is_empty (font_id)) {
			g_warning ("%s: Ignored font for screenshot rendering due to missing ID.",
				   as_component_get_id (cpt));
			continue;
		}

		scr = as_screenshot_new ();
		if (first) {
			as_screenshot_set_kind (scr, AS_SCREENSHOT_KIND_DEFAULT);
			first = FALSE;
		} else {
			as_screenshot_set_kind (scr, AS_SCREENSHOT_KIND_EXTRA);
		}
		scr_caption = g_strconcat (asc_font_get_family (font),
					   " ",
					   asc_font_get_style (font),
					   NULL);
		as_screenshot_set_caption (scr, scr_caption, "C");

		/* check if we have a custom sample text value (useful for symbolic fonts)
		 * we set this value for every fonr in the font-bundle, there is no way for this
		 * hack to select which font face should have the sample text.
		 * Since this hack only affects very few exotic fonts and should generally not
		 * be used, this should not be an issue. */
		custom_sample_text = as_component_get_custom_value (cpt, "FontSampleText");
		if (!as_is_empty (custom_sample_text))
			asc_font_set_sample_text (font, custom_sample_text);

		scr_url_root = g_build_filename (asc_result_gcid_for_component (cres, cpt),
						 "screenshots",
						 NULL);

		if (as_str_equal0 (asc_font_get_preferred_language (font), "en"))
			bg_letter = "a";
		else
			bg_letter = asc_font_get_sample_icon_text (font);

		for (guint j = 0; font_screenshot_sizes[j].width > 0; j++) {
			g_autofree gchar *img_name = NULL;
			g_autofree gchar *img_filename = NULL;
			g_autofree gchar *img_url = NULL;
			g_autoptr(AsImage) img = NULL;
			g_autoptr(AscCanvas) cv = NULL;
			g_autoptr(GError) tmp_error = NULL;
			gboolean ret;

			img_name = g_strdup_printf ("image-%s_%i.png",
						    font_id,
						    font_screenshot_sizes[j].width);
			img_filename = g_build_filename (cpt_screenshots_path, img_name, NULL);
			img_url = g_build_filename (scr_url_root, img_name, NULL);

			/* we didn't create a screenshot image yet - let's render it! */
			cv = asc_canvas_new (font_screenshot_sizes[j].width,
					     font_screenshot_sizes[j].height);

			ret = asc_canvas_draw_font_card (cv,
							 font,
							 NULL, /* default info label */
							 asc_font_get_sample_text (font),
							 bg_letter,
							 -1, /* default border width */
							 &tmp_error);
			if (!ret) {
				asc_result_add_hint (cres,
						     cpt,
						     "font-render-error",
						     "name",
						     asc_font_get_fullname (font),
						     "error",
						     tmp_error->message,
						     NULL);
				continue;
			}

			g_debug ("Saving font screenshot image: %s", img_name);
			ret = asc_canvas_save_png (cv, img_filename, &tmp_error);
			if (!ret) {
				asc_result_add_hint (cres,
						     cpt,
						     "font-render-error",
						     "name",
						     asc_font_get_fullname (font),
						     "error",
						     tmp_error->message,
						     NULL);
				continue;
			}

			img = as_image_new ();
			as_image_set_kind (img, AS_IMAGE_KIND_THUMBNAIL);
			as_image_set_width (img, asc_canvas_get_width (cv));
			as_image_set_height (img, asc_canvas_get_height (cv));
			as_image_set_url (img, img_url);

			as_screenshot_add_image (scr, img);
		}

		if (as_screenshot_is_valid (scr))
			as_component_add_screenshot (cpt, scr);
	}

	return TRUE;
}

/**
 * asc_render_font_icon:
 *
 * Render an icon for this font package using one of its fonts.
 * (Since we have no better way to do this, we just pick the first font
 * at time)
 **/
static gboolean
asc_render_font_icon (AscResult *cres,
		      AscUnit *unit,
		      AscFont *font,
		      const gchar *cpt_icons_path,
		      const gchar *icons_export_dir,
		      AsComponent *cpt,
		      AscIconPolicy *icon_policy)
{
	AscIconPolicyIter iter;
	guint size;
	guint scale_factor;
	AscIconState icon_state;

	asc_icon_policy_iter_init (&iter, icon_policy);
	while (asc_icon_policy_iter_next (&iter, &size, &scale_factor, &icon_state)) {
		g_autofree gchar *size_str = NULL;
		g_autofree gchar *icon_dir = NULL;
		g_autofree gchar *icon_name = NULL;
		g_autofree gchar *icon_full_path = NULL;
		const gchar *custom_icon_text = NULL;

		/* skip icon if it should be skipped */
		if (icon_state == ASC_ICON_STATE_IGNORED)
			continue;

		size_str = (scale_factor == 1)
			       ? g_strdup_printf ("%ix%i", size, size)
			       : g_strdup_printf ("%ix%i@%i", size, size, scale_factor);
		icon_dir = g_build_filename (cpt_icons_path, size_str, NULL);
		g_mkdir_with_parents (icon_dir, 0755);

		/* check if we have a custom icon text value (useful for symbolic fonts) */
		custom_icon_text = as_component_get_custom_value (cpt, "FontIconText");
		if (!as_is_empty (custom_icon_text))
			asc_font_set_sample_icon_text (
			    font,
			    custom_icon_text); /* Font will ensure that the value does not exceed 3 chars */

		icon_name = g_strdup_printf ("%s_%s.png",
					     asc_unit_get_bundle_id_safe (unit),
					     asc_font_get_id (font));
		icon_full_path = g_build_filename (icon_dir, icon_name, NULL);

		if (!g_file_test (icon_full_path, G_FILE_TEST_EXISTS)) {
			g_autoptr(AscCanvas) cv = NULL;
			g_autoptr(GError) tmp_error = NULL;
			AscCanvasShape bg_shape;
			gboolean ret;

			/* we didn't create an icon yet - let's render it! */
			cv = asc_canvas_new (size * scale_factor, size * scale_factor);

			bg_shape = g_str_hash (asc_font_get_id (font)) % ASC_CANVAS_SHAPE_LAST;
			ret = asc_canvas_draw_shape (cv,
						     bg_shape,
						     (size * scale_factor) *
							 0.032, /* border width */
						     0.85,	/* red */
						     0.85,	/* green */
						     0.85,	/* blue */
						     &tmp_error);
			if (!ret) {
				asc_result_add_hint (cres,
						     cpt,
						     "font-render-error",
						     "name",
						     asc_font_get_fullname (font),
						     "error",
						     tmp_error->message,
						     NULL);
				continue;
			}

			ret = asc_canvas_draw_text_line (cv,
							 font,
							 asc_font_get_sample_icon_text (font),
							 (size * scale_factor) *
							     0.16, /* border width */
							 &tmp_error);
			if (!ret) {
				asc_result_add_hint (cres,
						     cpt,
						     "font-render-error",
						     "name",
						     asc_font_get_fullname (font),
						     "error",
						     tmp_error->message,
						     NULL);
				continue;
			}
			g_debug ("Saving font icon: %s/%s", size_str, icon_name);
			ret = asc_canvas_save_png (cv, icon_full_path, &tmp_error);
			if (!ret) {
				asc_result_add_hint (cres,
						     cpt,
						     "font-render-error",
						     "name",
						     asc_font_get_fullname (font),
						     "error",
						     tmp_error->message,
						     NULL);
				continue;
			}

			if (icons_export_dir != NULL) {
				g_autofree gchar *icon_export_dir = NULL;
				g_autofree gchar *icon_export_fname = NULL;

				g_debug ("Copying icon to icon export dir: %s/%s",
					 size_str,
					 icon_name);
				icon_export_dir = g_build_filename (icons_export_dir,
								    size_str,
								    NULL);
				icon_export_fname = g_build_filename (icon_export_dir,
								      icon_name,
								      NULL);

				g_mkdir_with_parents (icon_export_dir, 0755);
				if (!as_copy_file (icon_full_path, icon_export_fname, &tmp_error)) {
					g_autofree gchar *tmp_icon_fname = g_strdup_printf (
					    "%s/%s",
					    size_str,
					    icon_name);
					g_warning ("Unable to write exported icon: %s",
						   icon_export_fname);
					asc_result_add_hint (cres,
							     cpt,
							     "icon-write-error",
							     "fname",
							     tmp_icon_fname,
							     "msg",
							     tmp_error->message,
							     NULL);
					continue;
				}
			}
		}

		if (icon_state != ASC_ICON_STATE_REMOTE_ONLY) {
			g_autoptr(AsIcon) icon = as_icon_new ();
			as_icon_set_kind (icon, AS_ICON_KIND_CACHED);
			as_icon_set_width (icon, size);
			as_icon_set_height (icon, size);
			as_icon_set_scale (icon, scale_factor);
			as_icon_set_name (icon, icon_name);
			as_component_add_icon (cpt, icon);
		}

		if (icon_state != ASC_ICON_STATE_CACHED_ONLY) {
			const gchar *gcid = NULL;
			g_autofree gchar *remote_icon_url = NULL;
			g_autoptr(AsIcon) icon = as_icon_new ();

			gcid = asc_result_gcid_for_component (cres, cpt);
			if (as_is_empty (gcid)) {
				asc_result_add_hint (cres,
						     cpt,
						     "internal-error",
						     "msg",
						     "No global ID could be found for component "
						     "when processing fonts.",
						     NULL);
				return FALSE;
			}

			remote_icon_url = g_build_filename (gcid,
							    "icons",
							    size_str,
							    icon_name,
							    NULL);
			as_icon_set_kind (icon, AS_ICON_KIND_REMOTE);
			as_icon_set_width (icon, size);
			as_icon_set_height (icon, size);
			as_icon_set_scale (icon, scale_factor);
			as_icon_set_url (icon, remote_icon_url);
			as_component_add_icon (cpt, icon);
		}
	}

	return TRUE;
}

static gint
asc_font_cmp (gconstpointer a, gconstpointer b)
{
	AscFont *f1 = ASC_FONT (*((AscFont **) a));
	AscFont *f2 = ASC_FONT (*((AscFont **) b));
	return g_strcmp0 (asc_font_get_id (f1), asc_font_get_id (f2));
}

/**
 * process_font_data_for_component:
 */
static void
process_font_data_for_component (AscResult *cres,
				 AscUnit *unit,
				 AsComponent *cpt,
				 GHashTable *all_fonts,
				 const gchar *media_export_root,
				 const gchar *icons_export_dir,
				 AscIconPolicy *icon_policy,
				 AscComposeFlags flags)
{
	const gchar *gcid = NULL;
	AsProvided *provided = NULL;
	g_autoptr(GPtrArray) font_hints = NULL;
	g_autoptr(GPtrArray) selected_fonts = NULL;
	g_autofree gchar *cpt_icons_path = NULL;
	g_autofree gchar *cpt_screenshots_path = NULL;
	g_autoptr(GList) cpt_languages = NULL;
	gboolean has_icon = FALSE;

	gcid = asc_result_gcid_for_component (cres, cpt);
	if (as_is_empty (gcid)) {
		asc_result_add_hint (
		    cres,
		    cpt,
		    "internal-error",
		    "msg",
		    "No global ID could be found for component when processing fonts.",
		    NULL);
		return;
	}

	font_hints = g_ptr_array_new_with_free_func (g_free);
	provided = as_component_get_provided_for_kind (cpt, AS_PROVIDED_KIND_FONT);
	if (provided != NULL) {
		GPtrArray *items = as_provided_get_items (provided);
		for (guint i = 0; i < items->len; i++) {
			const gchar *font_full_name = g_ptr_array_index (items, i);
			g_ptr_array_add (font_hints, g_utf8_strdown (font_full_name, -1));
		}
	}

	/* data export paths */
	if (media_export_root == NULL)
		cpt_icons_path = g_build_filename (asc_globals_get_tmp_dir (), gcid, NULL);
	else
		cpt_icons_path = g_build_filename (media_export_root, gcid, "icons", NULL);

	if (media_export_root != NULL)
		cpt_screenshots_path = g_build_filename (media_export_root,
							 gcid,
							 "screenshots",
							 NULL);

	/* if we have no fonts hints, we simply process all the fonts
	 * that we found in this unit. */
	if (font_hints->len == 0) {
		gboolean regular_found = FALSE;
		g_autoptr(GPtrArray) tmp_array = NULL;
		GHashTableIter ht_iter;
		gpointer ht_value;

		selected_fonts = g_ptr_array_new_full (g_hash_table_size (all_fonts),
						       g_object_unref);

		tmp_array = g_ptr_array_new_full (g_hash_table_size (all_fonts), g_object_unref);
		g_hash_table_iter_init (&ht_iter, all_fonts);
		while (g_hash_table_iter_next (&ht_iter, NULL, &ht_value))
			g_ptr_array_add (tmp_array, g_object_ref (ASC_FONT (ht_value)));

		/* prepend fonts that contain "regular" so we prefer the regular
		 * font face for rendering samples over the other styles
		 * also ensure that the font style list is sorted for more
		 * deterministic results. */
		g_ptr_array_sort (tmp_array, asc_font_cmp);
		for (guint i = 0; i < tmp_array->len; i++) {
			g_autofree gchar *font_style_id = NULL;
			AscFont *font = ASC_FONT (g_ptr_array_index (tmp_array, i));

			font_style_id = g_utf8_strdown (asc_font_get_style (font), -1);
			if (!regular_found && g_strstr_len (font_style_id, -1, "regular") != NULL) {
				g_ptr_array_insert (selected_fonts, 0, g_object_ref (font));
				/* if we found a font which has a style that equals "regular",
				 * we can stop searching for the preferred font */
				if (g_strcmp0 (font_style_id, "regular") == 0)
					regular_found = TRUE;
			} else {
				g_ptr_array_add (selected_fonts, g_object_ref (font));
			}
		}
	} else {
		selected_fonts = g_ptr_array_new_full (font_hints->len, g_object_unref);

		/* Find fonts based on the hints we have.
		 * The hint as well as the dictionary keys are all lowercased, so we
		 * can do case-insensitive matching here. */
		for (guint i = 0; i < font_hints->len; i++) {
			AscFont *font;
			const gchar *font_hint = g_ptr_array_index (font_hints, i);

			font = g_hash_table_lookup (all_fonts, font_hint);
			if (font == NULL)
				continue;
			g_ptr_array_add (selected_fonts, g_object_ref (font));
		}
	}

	/* we have nothing to do if we did not select any font
	 * (this is a bug, since we filtered for font metainfo previously) */
	if (selected_fonts->len == 0) {
		GHashTableIter ht_iter;
		gpointer ht_value;
		g_autoptr(GString) font_names_str = NULL;

		font_names_str = g_string_new ("");
		g_hash_table_iter_init (&ht_iter, all_fonts);
		while (g_hash_table_iter_next (&ht_iter, NULL, &ht_value))
			g_string_append_printf (font_names_str,
						"%s | ",
						asc_font_get_fullname (ASC_FONT (ht_value)));
		if (font_names_str->len > 4)
			g_string_truncate (font_names_str, font_names_str->len - 3);

		asc_result_add_hint (cres,
				     cpt,
				     "font-metainfo-but-no-font",
				     "font_names",
				     font_names_str->str,
				     NULL);
		return;
	}

	/* language information of fonts is often completely wrong. In case there was a metainfo file
	 * with languages explicitly set, we take the first language and prefer that over the others. */
	cpt_languages = as_component_get_languages (cpt);
	if (cpt_languages != NULL) {
		const gchar *first_lang = cpt_languages->data;

		for (guint i = 0; i < selected_fonts->len; i++) {
			AscFont *font = ASC_FONT (g_ptr_array_index (selected_fonts, i));
			asc_font_set_preferred_language (font, first_lang);
		}

		/* add languages mentioned in the metainfo file to list of supported languages
		 * of the respective font */
		for (GList *item = cpt_languages->next; item != NULL; item = item->next) {
			for (guint i = 0; i < selected_fonts->len; i++) {
				AscFont *font = ASC_FONT (g_ptr_array_index (selected_fonts, i));
				asc_font_add_language (font, item->data);
			}
		}
	}

	g_debug ("Rendering font data for %s", gcid);

	/* try to render icon with "Regular"-style font, if we find none we just pick the first font in the list */
	has_icon = FALSE;
	for (guint i = 0; i < selected_fonts->len; i++) {
		AscFont *font = ASC_FONT (g_ptr_array_index (selected_fonts, i));

		if (!as_str_equal0 (asc_font_get_style (font), "Regular") &&
		    !as_str_equal0 (asc_font_get_style (font), "regular"))
			continue;
		g_debug ("Rendering font icon using '%s'", asc_font_get_id (font));
		has_icon = asc_render_font_icon (cres,
						 unit,
						 font,
						 cpt_icons_path,
						 icons_export_dir,
						 cpt,
						 icon_policy);
		break;
	}

	/* process font files */
	for (guint i = 0; i < selected_fonts->len; i++) {
		g_autoptr(GList) language_list = NULL;
		AscFont *font = ASC_FONT (g_ptr_array_index (selected_fonts, i));

		g_debug ("Processing font '%s'", asc_font_get_id (font));

		/* add language information */
		language_list = asc_font_get_language_list (font);
		for (GList *l = language_list; l != NULL; l = l->next) {
			/* we have no idea how well the font supports the language's script,
			 * but since it advertises support in its metadata, we just assume 100% here */
			as_component_add_language (cpt, (const gchar *) l->data, 100);
		}

		/* render an icon for our font */
		if (!has_icon)
			has_icon = asc_render_font_icon (cres,
							 unit,
							 font,
							 cpt_icons_path,
							 icons_export_dir,
							 cpt,
							 icon_policy);

		/* set additional metadata. The font metadata might be terrible, but if the data is bad
		 * it hopefully motivates people to write proper metainfo files. */
		if (as_is_empty (as_component_get_description (cpt)) &&
		    !as_is_empty (asc_font_get_description (font)))
			as_component_set_description (cpt, asc_font_get_description (font), "C");

		if (as_is_empty (as_component_get_url (cpt, AS_URL_KIND_HOMEPAGE)) &&
		    !as_is_empty (asc_font_get_homepage (font)))
			as_component_add_url (cpt,
					      AS_URL_KIND_HOMEPAGE,
					      asc_font_get_homepage (font));
	}

	/* render all sample screenshots for all font styles we have */
	if (as_flags_contains (flags, ASC_COMPOSE_FLAG_STORE_SCREENSHOTS)) {
		if (cpt_screenshots_path == NULL)
			g_warning ("Screenshot storage is enabled, but no screenshot media path "
				   "could be constructed for %s.",
				   as_component_get_id (cpt));
		else
			asc_render_font_screenshots (cres,
						     selected_fonts,
						     cpt_screenshots_path,
						     cpt);
	}
}

/**
 * asc_process_fonts:
 *
 * Process any font data.
 */
void
asc_process_fonts (AscResult *cres,
		   AscUnit *unit,
		   const gchar *prefix,
		   const gchar *media_export_root,
		   const gchar *icons_export_dir,
		   AscIconPolicy *icon_policy,
		   AscComposeFlags flags)
{
	GPtrArray *contents = NULL;
	g_autofree gchar *fonts_dir = NULL;
	g_autoptr(GPtrArray) all_cpts = NULL;
	g_autoptr(GPtrArray) font_cpts = NULL;
	g_autoptr(GHashTable) all_fonts = NULL;

	/* collect all font components that interest us */
	font_cpts = g_ptr_array_new_with_free_func (g_object_unref);
	all_cpts = asc_result_fetch_components (cres);
	for (guint i = 0; i < all_cpts->len; i++) {
		AsComponent *cpt = AS_COMPONENT (g_ptr_array_index (all_cpts, i));
		if (as_component_get_kind (cpt) != AS_COMPONENT_KIND_FONT)
			continue;
		g_ptr_array_add (font_cpts, g_object_ref (cpt));
	}

	/* quit if we have no font component to process */
	if (font_cpts->len == 0)
		return;

	fonts_dir = g_build_filename (prefix, "share", "fonts", NULL);
	all_fonts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);

	/* create a map of all fonts that this unit contains */
	contents = asc_unit_get_contents (unit);
	for (guint i = 0; i < contents->len; i++) {
		g_autoptr(GBytes) font_bytes = NULL;
		const void *data;
		gsize data_len;
		g_autoptr(GError) tmp_error = NULL;
		g_autoptr(AscFont) font = NULL;
		g_autofree gchar *basename = NULL;
		g_autofree gchar *font_fullname_lower = NULL;
		const gchar *fname = g_ptr_array_index (contents, i);
		if (!g_str_has_prefix (fname, fonts_dir))
			continue;
		if (!g_str_has_suffix (fname, ".ttf") && !g_str_has_suffix (fname, ".otf") &&
		    !g_str_has_suffix (fname, ".ttc"))
			continue;

		basename = g_path_get_basename (fname);
		font_bytes = asc_unit_read_data (unit, fname, &tmp_error);
		if (font_bytes == NULL) {
			asc_result_add_hint (cres,
					     NULL,
					     "file-read-error",
					     "fname",
					     fname,
					     "msg",
					     tmp_error->message,
					     NULL);
			continue;
		}
		data = g_bytes_get_data (font_bytes, &data_len);

		font = asc_font_new_from_data (data, data_len, basename, &tmp_error);
		if (font == NULL) {
			asc_result_add_hint (cres,
					     NULL,
					     "font-load-error",
					     "fname",
					     fname,
					     "unit_name",
					     asc_unit_get_bundle_id (unit),
					     "error",
					     tmp_error->message,
					     NULL);
			continue;
		}

		g_debug ("Found font %s/%s", basename, asc_font_get_fullname (font));
		font_fullname_lower = g_utf8_strdown (asc_font_get_fullname (font), -1);
		g_hash_table_insert (all_fonts,
				     g_steal_pointer (&font_fullname_lower),
				     g_steal_pointer (&font));
	}

	/* process fonts for all components */
	for (guint i = 0; i < font_cpts->len; i++) {
		AsComponent *cpt = AS_COMPONENT (g_ptr_array_index (font_cpts, i));
		process_font_data_for_component (cres,
						 unit,
						 cpt,
						 all_fonts,
						 media_export_root,
						 icons_export_dir,
						 icon_policy,
						 flags);
	}
}
