/*
 * Portions of this file are copyright Rebirth contributors and licensed as
 * described in COPYING.txt.
 * Portions of this file are copyright Parallax Software and licensed
 * according to the Parallax license below.
 * See COPYING.txt for license details.

THE COMPUTER CODE CONTAINED HEREIN IS THE SOLE PROPERTY OF PARALLAX
SOFTWARE CORPORATION ("PARALLAX").  PARALLAX, IN DISTRIBUTING THE CODE TO
END-USERS, AND SUBJECT TO ALL OF THE TERMS AND CONDITIONS HEREIN, GRANTS A
ROYALTY-FREE, PERPETUAL LICENSE TO SUCH END-USERS FOR USE BY SUCH END-USERS
IN USING, DISPLAYING,  AND CREATING DERIVATIVE WORKS THEREOF, SO LONG AS
SUCH USE, DISPLAY OR CREATION IS FOR NON-COMMERCIAL, ROYALTY OR REVENUE
FREE PURPOSES.  IN NO EVENT SHALL THE END-USER USE THE COMPUTER CODE
CONTAINED HEREIN FOR REVENUE-BEARING PURPOSES.  THE END-USER UNDERSTANDS
AND AGREES TO THE TERMS HEREIN AND ACCEPTS THE SAME BY USE OF THIS FILE.
COPYRIGHT 1993-1999 PARALLAX SOFTWARE CORPORATION.  ALL RIGHTS RESERVED.
*/

/*
 *
 * Graphical routines for drawing fonts.
 *
 */

#include <algorithm>
#include <memory>
#include <stdexcept>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef macintosh
#include <fcntl.h>
#endif

#include "u_mem.h"
#include "gr.h"
#include "dxxerror.h"
#include "common/2d/bitmap.h"
#include "physfsx.h"
#include "gamefont.h"
#include "byteutil.h"
#include "console.h"
#include "config.h"
#if DXX_USE_OGL
#include "ogl_init.h"
#endif

#include "compiler-range_for.h"
#include "partial_range.h"
#include "d_range.h"
#include <array>
#include <memory>

namespace dcx {

namespace {

static font_x_scale_float FONTSCALE_X()
{
	return font_x_scale_float(FNTScaleX.operator float());
}

static auto FONTSCALE_Y(const int &y)
{
	return font_y_scaled_float(FNTScaleY * y);
}

#define MAX_OPEN_FONTS	50

constexpr std::integral_constant<uint8_t, 255> kerndata_terminator{};

//list of open fonts, for use (for now) for palette remapping
static std::array<grs_font *, MAX_OPEN_FONTS> open_font;

#define BITS_TO_BYTES(x)    (((x)+7)>>3)

static int gr_internal_string_clipped(grs_canvas &, const grs_font &cv_font, int x, int y, const char *s);
static int gr_internal_string_clipped_m(grs_canvas &, const grs_font &cv_font, int x, int y, const char *s);

static const uint8_t *find_kern_entry(const grs_font &font, const uint8_t first, const uint8_t second)
{
	auto p = font.ft_kerndata;

	while (*p != kerndata_terminator)
		if (p[0]==first && p[1]==second)
			return p;
		else p+=3;
	return NULL;
}

//takes the character AFTER being offset into font

class font_character_extent
{
	const unsigned r;
public:
	font_character_extent(const grs_font &cv_font) :
		r(cv_font.ft_maxchar - cv_font.ft_minchar)
	{
	}
	bool operator()(const unsigned c) const
	{
		return c <= r;
	}
};

template <typename T>
struct get_char_width_result
{
	T width, spacing;
};

/* Floating form never uses width.  This specialization allows the
 * compiler to recognize width as dead, shortening
 * get_char_width<float>.
 */
template <>
struct get_char_width_result<float>
{
	float spacing;
	get_char_width_result(float, float s) :
		spacing(s)
	{
	}
};

//takes the character BEFORE being offset into current font
template <typename T>
static get_char_width_result<T> get_char_width(const grs_font &cv_font, const uint8_t c, const uint8_t c2)
{
	const unsigned letter = c - cv_font.ft_minchar;
	const auto ft_flags = cv_font.ft_flags;
	const auto proportional = ft_flags & FT_PROPORTIONAL;

	const auto &&fontscale_x = FONTSCALE_X();
	const auto &&INFONT = font_character_extent(cv_font);
	if (!INFONT(letter)) {				//not in font, draw as space
		return {0, static_cast<T>(proportional ? fontscale_x(cv_font.ft_w) / 2 : cv_font.ft_w)};
	}
	const T width = proportional ? fontscale_x(cv_font.ft_widths[letter]).operator float() : cv_font.ft_w;
	if (ft_flags & FT_KERNED) 
	{
		if (!(c2==0 || c2=='\n')) {
			const unsigned letter2 = c2 - cv_font.ft_minchar;

			if (INFONT(letter2)) {
				const auto p = find_kern_entry(cv_font, letter, letter2);
				if (p)
					return {width, static_cast<T>(fontscale_x(p[2]))};
			}
		}
	}
	return {width, width};
}

static int get_centered_x(const grs_canvas &canvas, const grs_font &cv_font, const char *s)
{
	float w;
	for (w = 0.; const char c = *s; ++s)
	{
		if (c == '\n')
			break;
		if (c <= 0x06)
		{
			if (c <= 0x03)
				s++;
			continue;//skip color codes.
		}
		w += get_char_width<float>(cv_font, c, s[1]).spacing;
	}

	return (canvas.cv_bitmap.bm_w - w) / 2;
}

//hack to allow color codes to be embedded in strings -MPM
//note we subtract one from color, since 255 is "transparent" so it'll never be used, and 0 would otherwise end the string.
//function must already have orig_color var set (or they could be passed as args...)
//perhaps some sort of recursive orig_color type thing would be better, but that would be way too much trouble for little gain
constexpr std::integral_constant<int, 1> gr_message_color_level{};

struct per_character_row_state
{
	uint8_t draw_full_width_as_fg_color = 0;
};

#define CHECK_EMBEDDED_COLORS() if (const char control_code = *text_ptr; control_code >= 0x01 && control_code <= 0x02) { \
		text_ptr++; \
		if (*text_ptr){ \
			if (gr_message_color_level >= control_code) \
				canvas.cv_font_fg_color = *text_ptr; \
			text_ptr++; \
		} \
	} \
	else if (control_code == 0x03) \
	{ \
		state.draw_full_width_as_fg_color = 1; \
		text_ptr++; \
	} \
	else if (control_code >= 0x04 && control_code <= 0x06) { \
		if (gr_message_color_level >= control_code - 3) \
			canvas.cv_font_fg_color= orig_color; \
		text_ptr++; \
	}

template <bool masked_draws_background>
static int gr_internal_string0_template(grs_canvas &canvas, const grs_font &cv_font, const int x, int y, const char *const s)
{
	const auto &&INFONT = font_character_extent(cv_font);
	const auto ft_flags = cv_font.ft_flags;
	const auto proportional = ft_flags & FT_PROPORTIONAL;
	const auto cv_font_bg_color = canvas.cv_font_bg_color;
	int	skip_lines = 0;

	unsigned int VideoOffset1;

	//to allow easy reseting to default string color with colored strings -MPM
	const auto orig_color = canvas.cv_font_fg_color;
	VideoOffset1 = y * canvas.cv_bitmap.bm_rowsize + x;
	for (auto next_row = s; next_row;)
	{
		const auto text_ptr1 = std::exchange(next_row, nullptr);

		if (x==0x8000) {			//centered
			int xx = get_centered_x(canvas, cv_font, text_ptr1);
			VideoOffset1 = y * canvas.cv_bitmap.bm_rowsize + xx;
		}

		for (int r=0; r < cv_font.ft_h; ++r)
		{
			auto text_ptr = text_ptr1;

			unsigned VideoOffset = VideoOffset1;

			for (; const uint8_t c0 = *text_ptr; ++text_ptr)
			{
				if (c0 == '\n')
				{
					next_row = &text_ptr[1];
					break;
				}

				if (c0 == CC_COLOR)
				{
					canvas.cv_font_fg_color = static_cast<uint8_t>(*++text_ptr);
					continue;
				}

				if (c0 == CC_LSPACING)
				{
					skip_lines = *++text_ptr - '0';
					continue;
				}

				per_character_row_state state{
					(unlikely(c0 == CC_UNDERLINE)
						? ++text_ptr, r == cv_font.ft_baseline + 2 || r == cv_font.ft_baseline + 3
						: false),
				};

				const uint8_t c = *text_ptr;
				const auto &result = get_char_width<int>(cv_font, c, text_ptr[1]);
				const auto &width = result.width;
				const auto &spacing = result.spacing;

				const unsigned letter = c - cv_font.ft_minchar;

				if constexpr (masked_draws_background)
				{
					(void)orig_color;
					if (!INFONT(letter)) {	//not in font, draw as space
						VideoOffset += spacing;
						text_ptr++;
						continue;
					}
				}
				else
				{
					if (!INFONT(letter) || c <= 0x06)	//not in font, draw as space
					{
						CHECK_EMBEDDED_COLORS() else{
							VideoOffset += spacing;
							text_ptr++;
						}
						continue;
					}
				}

				if (width)
				{
					auto data = &canvas.cv_bitmap.get_bitmap_data()[VideoOffset];
					const auto cv_font_fg_color = canvas.cv_font_fg_color;
					if (state.draw_full_width_as_fg_color)
				{
					std::fill_n(data, width, cv_font_fg_color);
				}
				else
				{
					auto fp = proportional ? cv_font.ft_chars[letter] : &cv_font.ft_data[letter * BITS_TO_BYTES(width) * cv_font.ft_h];
					fp += BITS_TO_BYTES(width)*r;

					/* Setting bits=0 is a dead store, but is necessary to
					 * prevent -Og -Wuninitialized from issuing a bogus
					 * warning.  -Og does not see that bits_remaining=0
					 * guarantees that bits will be initialized from *fp before
					 * it is read.
					 */
					uint8_t bits_remaining = 0, bits = 0;
					for (uint_fast32_t i = width; i--; ++data, --bits_remaining)
					{
						if (!bits_remaining)
						{
							bits = *fp++;
							bits_remaining = 8;
						}

						const auto bit_enabled = (bits & 0x80);
						bits <<= 1;
						if constexpr (!masked_draws_background)
						{
							if (!bit_enabled)
								continue;
						}
						*data = bit_enabled ? cv_font_fg_color : cv_font_bg_color;
					}
				}
				}
				VideoOffset += spacing;
			}
			VideoOffset1 += canvas.cv_bitmap.bm_rowsize;
			y++;
		}
		y += skip_lines;
		VideoOffset1 += canvas.cv_bitmap.bm_rowsize * skip_lines;
		skip_lines = 0;
	}
	return 0;
}

static int gr_internal_string0(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
{
	return gr_internal_string0_template<true>(canvas, cv_font, x, y, s);
}

static int gr_internal_string0m(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
{
	return gr_internal_string0_template<false>(canvas, cv_font, x, y, s);
}

#if !DXX_USE_OGL
static void gr_internal_color_string(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
{
//a bitmap for the character
	grs_bitmap char_bm = {};
	char_bm.set_type(bm_mode::linear);
	char_bm.set_flags(BM_FLAG_TRANSPARENT);
	const char *text_ptr, *next_row, *text_ptr1;
	int letter;
	int xx,yy;

	const auto &&INFONT = font_character_extent(cv_font);
	char_bm.bm_h = cv_font.ft_h;		//set height for chars of this font

	next_row = s;

	yy = y;

	const auto &&fspacy1 = FSPACY(1);
	while (next_row != NULL)
	{
		text_ptr1 = next_row;
		next_row = NULL;

		text_ptr = text_ptr1;

		xx = x;

		if (xx==0x8000)			//centered
			xx = get_centered_x(canvas, cv_font, text_ptr);

		while (*text_ptr)
		{
			if (*text_ptr == '\n' )
			{
				next_row = &text_ptr[1];
				yy += cv_font.ft_h + fspacy1;
				break;
			}

			letter = static_cast<uint8_t>(*text_ptr) - cv_font.ft_minchar;

			const auto &result = get_char_width<int>(cv_font, text_ptr[0], text_ptr[1]);
			const auto &width = result.width;
			const auto &spacing = result.spacing;

			if (!INFONT(letter)) {	//not in font, draw as space
				xx += spacing;
				text_ptr++;
				continue;
			}

			const auto fp = (cv_font.ft_flags & FT_PROPORTIONAL)
				? cv_font.ft_chars[letter]
				: &cv_font.ft_data[letter * BITS_TO_BYTES(width) * cv_font.ft_h];

			/* Cast away const-ness on the font pointer.  The function
			 * must take the parameter as non-const because some callers
			 * provide an uninitialized pointer and fill it later.
			 */
			gr_init_bitmap(char_bm, bm_mode::linear, 0, 0, width, cv_font.ft_h, width, const_cast<color_palette_index *>(fp));
			gr_bitmapm(canvas, xx, yy, char_bm);

			xx += spacing;

			text_ptr++;
		}

	}
}

#else //OGL

static int get_font_total_width(const grs_font &font)
{
	if (font.ft_flags & FT_PROPORTIONAL)
	{
		int w=0;
		range_for (const auto v, unchecked_partial_range(font.ft_widths, static_cast<unsigned>(font.ft_maxchar - font.ft_minchar) + 1))
		{
			if (v < 0)
				throw std::underflow_error("negative width");
			w += v;
		}
		return w;
	}else{
		return font.ft_w*(font.ft_maxchar-font.ft_minchar+1);
	}
}

static std::pair<int, int> ogl_font_choose_size(grs_font * font, const int gap)
{
	int	nchars = font->ft_maxchar-font->ft_minchar+1;
	int r,x,y,nc=0,smallest=999999,smallr=-1,tries;
	int smallprop=10000;
	int w;
	int rw = INT_MIN, rh = INT_MIN;
	for (int h=32;h<=256;h*=2){
//		h=pow2ize(font->ft_h*rows+gap*(rows-1));
		if (font->ft_h>h)continue;
		r=(h/(font->ft_h+gap));
		w=pow2ize((get_font_total_width(*font)+(nchars-r)*gap)/r);
		tries=0;
		do {
			if (tries)
				w=pow2ize(w+1);
			if(tries>3){
				break;
			}
			nc=0;
			y=0;
			while(y+font->ft_h<=h){
				x=0;
				while (x<w){
					if (nc==nchars)
						break;
					if (font->ft_flags & FT_PROPORTIONAL){
						if (x+font->ft_widths[nc]+gap>w)break;
						x+=font->ft_widths[nc++]+gap;
					}else{
						if (x+font->ft_w+gap>w)break;
						x+=font->ft_w+gap;
						nc++;
					}
				}
				if (nc==nchars)
					break;
				y+=font->ft_h+gap;
			}
			
			tries++;
		}while(nc!=nchars);
		if (nc!=nchars)
			continue;

		if (w*h==smallest){//this gives squarer sizes priority (ie, 128x128 would be better than 512*32)
			if (w>=h){
				if (w/h<smallprop){
					smallprop=w/h;
					smallest++;//hack
				}
			}else{
				if (h/w<smallprop){
					smallprop=h/w;
					smallest++;//hack
				}
			}
		}
		if (w*h<smallest){
			smallr=1;
			smallest=w*h;
			rw = w;
			rh = h;
		}
	}
	if (smallr<=0)
		Error("Could not fit font?\n");
	return {rw, rh};
}

static void ogl_init_font(grs_font * font)
{
	int oglflags = OGL_FLAG_ALPHA;
	const unsigned nchars = font->ft_maxchar - font->ft_minchar + 1;
	int curx=0,cury=0;
	int gap=1; // x/y offset between the chars so we can filter

	const auto &&[tw, th] = ogl_font_choose_size(font, gap);
	{
		RAIIdmem<uint8_t[]> data;
		const unsigned length = tw * th;
		MALLOC(data, uint8_t[], length);
		std::fill_n(data.get(), length, TRANSPARENCY_COLOR); // map the whole data with transparency so we won't have borders if using gap
		gr_init_main_bitmap(font->ft_parent_bitmap, bm_mode::linear, 0, 0, tw, th, tw, std::move(data));
	}
	gr_set_transparent(font->ft_parent_bitmap, 1);

	if (!(font->ft_flags & FT_COLOR))
		oglflags |= OGL_FLAG_NOCOLOR;
	ogl_init_texture(*(font->ft_parent_bitmap.gltexture = ogl_get_free_texture()), tw, th, oglflags); // have to init the gltexture here so the subbitmaps will find it.

	font->ft_bitmaps = std::make_unique<grs_bitmap[]>(nchars);
	const unsigned h = font->ft_h;

	for (const auto i : xrange(nchars))
	{
		const unsigned w = (font->ft_flags & FT_PROPORTIONAL)
			? font->ft_widths[i]
			: font->ft_w;

		if (w<1 || w>256)
			continue;

		if (curx+w+gap>tw)
		{
			cury+=h+gap;
			curx=0;
		}

		if (cury+h>th)
			Error("font doesn't really fit (%i/%i)?\n",i,nchars);

		if (font->ft_flags & FT_COLOR)
		{
			const auto fp = (font->ft_flags & FT_PROPORTIONAL)
				? font->ft_chars[i]
				: font->ft_data + i * w*h;
			for (const auto y : xrange(h))
			{
				for (const auto x : xrange(w))
					font->ft_parent_bitmap.get_bitmap_data()[curx+x+(cury+y)*tw] = fp[x+y*w];
					// Let's call this a HACK:
					// If we filter the fonts, the sliders will be messed up as the border pixels will have an
					// alpha value while filtering. So the slider bitmaps will not look "connected".
					// To prevent this, duplicate the first/last pixel-row with a 1-pixel offset.
				if (gap && i >= 99 && i <= 102)
				{
						// See which bitmaps need left/right shifts:
						// 99  = SLIDER_LEFT - shift RIGHT
						// 100 = SLIDER_RIGHT - shift LEFT
						// 101 = SLIDER_MIDDLE - shift LEFT+RIGHT
						// 102 = SLIDER_MARKER - shift RIGHT

						// shift left border
					std::size_t oi, ii;
					if (i != 99 && i != 102)
					{
						oi = (curx + (cury + y) * tw) - 1;
						ii = y * w;
					}
						// shift right border
					else if (i != 100)
					{
						oi = (curx + (w - 1) + (cury + y) * tw) + 1;
						ii = (w - 1) + y * w;
					}
					else
						continue;
					font->ft_parent_bitmap.get_bitmap_data()[oi] = fp[ii];
				}
			}
		}
		else
		{
			auto white = gr_find_closest_color(63, 63, 63);
			auto fp = (font->ft_flags & FT_PROPORTIONAL)
				? font->ft_chars[i]
				: font->ft_data + i * BITS_TO_BYTES(w)*h;
			for (const auto y : xrange(h))
			{
				uint8_t BitMask = 0;
				uint8_t bits = 0;
				for (const auto x : xrange(w))
				{
					if (BitMask==0) {
						bits = *fp++;
						BitMask = 0x80;
					}

					font->ft_parent_bitmap.get_bitmap_data()[curx+x+(cury+y)*tw] = (bits & BitMask)
						? white
						: 255;
					BitMask >>= 1;
				}
			}
		}
		gr_init_sub_bitmap(font->ft_bitmaps[i],font->ft_parent_bitmap,curx,cury,w,h);
		curx+=w+gap;
	}
	ogl_loadbmtexture_f(font->ft_parent_bitmap, CGameCfg.TexFilt, 0, 0);
}

static void ogl_internal_string(grs_canvas &canvas, const grs_font &cv_font, const int entry_x, int yy, const char *const s)
{
	auto orig_color = canvas.cv_font_fg_color;//to allow easy reseting to default string color with colored strings -MPM

	if (grd_curscreen->sc_canvas.cv_bitmap.get_type() != bm_mode::ogl)
		Error("carp.\n");
	const auto &&fspacy1 = FSPACY(1);
	const auto &&INFONT = font_character_extent(cv_font);
	const auto &&fontscale_x = FONTSCALE_X();
	const auto &&FONTSCALE_Y_ft_h = FONTSCALE_Y(cv_font.ft_h);
	ogl_colors colors;
	for (auto next_row = s; next_row;)
	{
		auto text_ptr = std::exchange(next_row, nullptr);
		auto line_x = entry_x == 0x8000
			? get_centered_x(canvas, cv_font, text_ptr)
			: entry_x;

		for (; const auto c0 = *text_ptr;)
		{
			if (c0 == '\n')
			{
				next_row = &text_ptr[1];
				yy += FONTSCALE_Y_ft_h + fspacy1;
				break;
			}

			const auto letter = c0 - cv_font.ft_minchar;

			const auto &result = get_char_width<int>(cv_font, c0, text_ptr[1]);
			const auto &spacing = result.spacing;

			per_character_row_state state;
			if (!INFONT(letter) || c0 <= 0x06) //not in font, draw as space
			{
				CHECK_EMBEDDED_COLORS() else{
					line_x += spacing;
					text_ptr++;
				}
				if (state.draw_full_width_as_fg_color)
				{
					const auto color = canvas.cv_font_fg_color;
					gr_rect(canvas, line_x, yy + cv_font.ft_baseline + 2, line_x + cv_font.ft_w, yy + cv_font.ft_baseline + 3, color);
				}

				continue;
			}
			const auto ft_w = (cv_font.ft_flags & FT_PROPORTIONAL)
				? cv_font.ft_widths[letter]
				: cv_font.ft_w;

			ogl_ubitmapm_cs(canvas, line_x, yy, fontscale_x(ft_w), FONTSCALE_Y_ft_h, cv_font.ft_bitmaps[letter], (cv_font.ft_flags & FT_COLOR) ? colors.white : (canvas.cv_bitmap.get_type() == bm_mode::ogl) ? colors.init(canvas.cv_font_fg_color) : throw std::runtime_error("non-color string to non-ogl dest"));

			line_x += spacing;
			text_ptr++;
		}
	}
}

#define gr_internal_color_string ogl_internal_string
#endif //OGL

}

void gr_string(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
{
	const auto &&[w, h] = gr_get_string_size(cv_font, s);
	gr_string(canvas, cv_font, x, y, s, w, h);
}

namespace {

static void gr_ustring_mono(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
{
	switch (canvas.cv_bitmap.get_type())
	{
		case bm_mode::linear:
			if (canvas.cv_font_bg_color == TRANSPARENCY_COLOR)
				gr_internal_string0m(canvas, cv_font, x, y, s);
			else
				gr_internal_string0(canvas, cv_font, x, y, s);
			break;
		case bm_mode::ilbm:
		case bm_mode::rgb15:
#if DXX_USE_OGL
		case bm_mode::ogl:
#endif
			break;
	}
}

}

void gr_string(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s, const int w, const int h)
{
	if (y + h < 0)
		return;
	const auto bm_h = canvas.cv_bitmap.bm_h;
	if (y > bm_h)
		return;
	const auto bm_w = canvas.cv_bitmap.bm_w;
	int xw = w;
	if ( x == 0x8000 )	{
		// for x, since this will be centered, only look at
		// width.
	} else {
		if (x > bm_w)
			return;
		xw += x;
		if (xw < 0)
			return;
	}
	if (
#if DXX_USE_OGL
		canvas.cv_bitmap.get_type() == bm_mode::ogl ||
#endif
		cv_font.ft_flags & FT_COLOR)
	{
		gr_internal_color_string(canvas, cv_font, x, y, s);
		return;
	}
	// Partially clipped...
	if (!(y < 0 ||
		x < 0 ||
		xw > bm_w ||
		y + h > bm_h))
	{
		gr_ustring_mono(canvas, cv_font, x, y, s);
		return;
	}

	if (canvas.cv_font_bg_color == TRANSPARENCY_COLOR)
	{
		gr_internal_string_clipped_m(canvas, cv_font, x, y, s);
		return;
	}
	gr_internal_string_clipped(canvas, cv_font, x, y, s);
}

void gr_ustring(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
{
#if DXX_USE_OGL
	if (canvas.cv_bitmap.get_type() == bm_mode::ogl)
	{
		ogl_internal_string(canvas, cv_font, x, y, s);
		return;
	}
#endif
	
	if (cv_font.ft_flags & FT_COLOR)
		gr_internal_color_string(canvas, cv_font, x, y, s);
	else
		gr_ustring_mono(canvas, cv_font, x, y, s);
}

unsigned gr_get_string_height(const grs_font &cv_font, const unsigned lines)
{
	const auto fontscale_y = FONTSCALE_Y(cv_font.ft_h);
	return static_cast<unsigned>(fontscale_y + (lines * (fontscale_y + FSPACY(1))));
}

gr_string_size gr_get_string_size(const grs_font &cv_font, const char *s)
{
	return gr_get_string_size(cv_font, s, UINT_MAX);
}

gr_string_size gr_get_string_size(const grs_font &cv_font, const char *s, const unsigned max_chars_per_line)
{
	float longest_width=0.0,string_width_f=0.0;
	unsigned lines = 0;
	if (s)
	{
		unsigned remaining_chars_this_line = max_chars_per_line;
		while (*s)
		{
			if (*s == '\n')
			{
				if (longest_width < string_width_f)
					longest_width = string_width_f;
				string_width_f = 0;
				const auto os = s;
				while (*++s == '\n')
				{
				}
				lines += s - os;
				if (!*s)
					break;
				remaining_chars_this_line = max_chars_per_line;
			}

			const auto &result = get_char_width<float>(cv_font, s[0], s[1]);
			const auto &spacing = result.spacing;
			string_width_f += spacing;
			s++;
			if (!--remaining_chars_this_line)
				break;
		}
	}
	return {static_cast<unsigned long>(std::max(longest_width, string_width_f)), gr_get_string_height(cv_font, lines)};
}

std::pair<const char *, unsigned> gr_get_string_wrap(const grs_font &cv_font, const char *s, unsigned limit)
{
	assert(s);
	float string_width_f=0.0;
	const float limit_f = limit;
	for (uint8_t c; (c = *s); ++s)
	{
		const auto &&spacing = get_char_width<float>(cv_font, c, s[1]).spacing;
		const float next_string_width = string_width_f + spacing;
		if (next_string_width >= limit_f)
			break;
		string_width_f = next_string_width;
	}
	return {s, static_cast<unsigned>(string_width_f)};
}

template <void (&F)(grs_canvas &, const grs_font &, int, int, const char *)>
void (gr_printt)(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const format, ...)
{
	char buffer[1000];
	va_list args;

	va_start(args, format );
	vsnprintf(buffer, sizeof(buffer), format, args);
	va_end(args);
	F(canvas, cv_font, x, y, buffer);
}

template void gr_printt<gr_string>(grs_canvas &, const grs_font &, int, int, const char *, ...);
template void gr_printt<gr_ustring>(grs_canvas &, const grs_font &, int, int, const char *, ...);

void gr_close_font(std::unique_ptr<grs_font> font)
{
	if (font)
	{
		//find font in list
		if (!(font->ft_flags & FT_COLOR))
			return;
		auto e = end(open_font);
		auto i = std::find(begin(open_font), e, font.get());
		if (i == e)
			throw std::logic_error("closing non-open font");
		*i = nullptr;
	}
}

//remap a font, re-reading its data & palette
static void gr_remap_font(grs_font *font, const char *fontname);

//remap (by re-reading) all the color fonts
void gr_remap_color_fonts()
{
	range_for (auto font, open_font)
	{
		if (font)
			gr_remap_font(font, &font->ft_filename[0]);
	}
}

namespace {

/*
 * reads a grs_font structure from a PHYSFS_File
 */
static void grs_font_read(grs_font *gf, PHYSFS_File *fp)
{
	gf->ft_w = PHYSFSX_readShort(fp);
	gf->ft_h = PHYSFSX_readShort(fp);
	gf->ft_flags = PHYSFSX_readShort(fp);
	gf->ft_baseline = PHYSFSX_readShort(fp);
	gf->ft_minchar = PHYSFSX_readByte(fp);
	gf->ft_maxchar = PHYSFSX_readByte(fp);
	PHYSFSX_readShort(fp);
	gf->ft_data = reinterpret_cast<uint8_t *>(static_cast<uintptr_t>(PHYSFSX_readInt(fp)) - GRS_FONT_SIZE);
	PHYSFSX_readInt(fp);
	gf->ft_widths = reinterpret_cast<int16_t *>(static_cast<uintptr_t>(PHYSFSX_readInt(fp)) - GRS_FONT_SIZE);
	gf->ft_kerndata = reinterpret_cast<uint8_t *>(static_cast<uintptr_t>(PHYSFSX_readInt(fp)) - GRS_FONT_SIZE);
}

static std::unique_ptr<grs_font> gr_internal_init_font(const char *fontname)
{
	const uint8_t *ptr;
	color_palette_index *ft_data;
	struct {
		std::array<char, 4> magic;
		unsigned datasize;	//size up to (but not including) palette
	} file_header;

	auto &&[fontfile, physfserr] = PHYSFSX_openReadBuffered(fontname);

	if (!fontfile) {
		con_printf(CON_VERBOSE, "Failed to open font file \"%s\": %s", fontname, PHYSFS_getErrorByCode(physfserr));
		return {};
	}

	static_assert(sizeof(file_header) == 8, "file header size error");
	if (PHYSFS_read(fontfile, &file_header, sizeof(file_header), 1) != 1 ||
		memcmp(file_header.magic.data(), "PSFN", 4) ||
		(file_header.datasize = INTEL_INT(file_header.datasize)) < GRS_FONT_SIZE)
	{
		con_printf(CON_NORMAL, "Invalid header in font file %s", fontname);
		return {};
	}

	file_header.datasize -= GRS_FONT_SIZE; // subtract the size of the header.
	const auto &datasize = file_header.datasize;

	auto font = std::make_unique<grs_font>();
	grs_font_read(font.get(), fontfile);

	const unsigned nchars = font->ft_maxchar - font->ft_minchar + 1;
	std::size_t ft_chars_storage = (font->ft_flags & FT_PROPORTIONAL)
		? sizeof(uint8_t *) * nchars
		: 0;

	auto ft_allocdata = std::make_unique<color_palette_index[]>(datasize + ft_chars_storage);
	const auto font_data = &ft_allocdata[ft_chars_storage];
	if (PHYSFS_read(fontfile, font_data, 1, datasize) != datasize)
	{
		con_printf(CON_URGENT, "Insufficient data in font file %s", fontname);
		return {};
	}

	if (font->ft_flags & FT_PROPORTIONAL) {
		const auto offset_widths = reinterpret_cast<uintptr_t>(font->ft_widths);
		auto w = reinterpret_cast<short *>(&font_data[offset_widths]);
		if (offset_widths >= datasize || offset_widths + (nchars * sizeof(*w)) >= datasize)
		{
			con_printf(CON_URGENT, "Missing widths in font file %s", fontname);
			return {};
		}
		font->ft_widths = w;
		const auto offset_data = reinterpret_cast<uintptr_t>(font->ft_data);
		if (offset_data >= datasize)
		{
			con_printf(CON_URGENT, "Missing data in font file %s", fontname);
			return {};
		}
		font->ft_data = ptr = ft_data = &font_data[offset_data];
		const auto ft_chars = reinterpret_cast<const uint8_t **>(ft_allocdata.get());
		font->ft_chars = reinterpret_cast<const uint8_t *const *>(ft_chars);

		const unsigned is_color = font->ft_flags & FT_COLOR;
		const unsigned ft_h = font->ft_h;
		std::generate_n(ft_chars, nchars, [is_color, ft_h, &w, &ptr]{
			const unsigned s = INTEL_SHORT(*w);
			if constexpr (words_bigendian)
				*w = static_cast<uint16_t>(s);
			++w;
			const auto r = ptr;
			ptr += ft_h * (is_color ? s : BITS_TO_BYTES(s));
			return r;
		});
	} else  {

		font->ft_data   = ft_data = font_data;
		font->ft_widths = NULL;

		ptr = font->ft_data + (nchars * font->ft_w * font->ft_h);
	}

	if (font->ft_flags & FT_KERNED)
	{
		const auto offset_kerndata = reinterpret_cast<uintptr_t>(font->ft_kerndata);
		if (datasize <= offset_kerndata)
		{
			con_printf(CON_URGENT, "Missing kerndata in font file %s", fontname);
			return {};
		}
		const auto begin_kerndata = reinterpret_cast<const unsigned char *>(&font_data[offset_kerndata]);
		const auto end_font_data = &font_data[datasize - ((datasize - offset_kerndata + 2) % 3)];
		for (auto cur_kerndata = begin_kerndata;; cur_kerndata += 3)
		{
			if (cur_kerndata == end_font_data)
			{
				con_printf(CON_URGENT, "Unterminated kerndata in font file %s", fontname);
				return {};
			}
			if (*cur_kerndata == kerndata_terminator)
				break;
		}
		font->ft_kerndata = begin_kerndata;
	}
	else
		font->ft_kerndata = nullptr;

	if (font->ft_flags & FT_COLOR) {		//remap palette
		palette_array_t palette;
		std::array<color_palette_index, 256> colormap;
		/* `freq` exists so that decode_data can write to it, but it is
		 * otherwise unused.  `decode_data` is not guaranteed to write
		 * to every element, but the bitset constructor will initialize
		 * the storage, so reading unwritten fields will always return
		 * false.
		 */
		std::bitset<256> freq;

		PHYSFS_read(fontfile,&palette[0],sizeof(palette[0]),palette.size());		//read the palette

		build_colormap_good(palette, colormap);

		colormap[TRANSPARENCY_COLOR] = TRANSPARENCY_COLOR;              // changed from colormap[255] = 255 to this for macintosh

		decode_data(std::span{ft_data, ptr}, colormap, freq);
	}
	fontfile.reset();
	//set curcanv vars

	auto &ft_filename = font->ft_filename;
	font->ft_allocdata = move(ft_allocdata);
	strncpy(&ft_filename[0], fontname, ft_filename.size());
	return font;
}

}

grs_font_ptr gr_init_font(grs_canvas &canvas, const char *fontname)
{
	auto font = gr_internal_init_font(fontname);
	if (!font)
		return {};

	canvas.cv_font        = font.get();
	canvas.cv_font_fg_color    = 0;
	canvas.cv_font_bg_color    = 0;
	if (font->ft_flags & FT_COLOR)
	{
		auto e = end(open_font);
		auto i = std::find(begin(open_font), e, static_cast<grs_font *>(nullptr));
		if (i == e)
			throw std::logic_error("too many open fonts");
		*i = font.get();
	}
#if DXX_USE_OGL
	ogl_init_font(font.get());
#endif
	return grs_font_ptr(font.release());
}

//remap a font by re-reading its data & palette
void gr_remap_font(grs_font *font, const char *fontname)
{
	auto n = gr_internal_init_font(fontname);
	if (!n)
		return;
	if (!(n->ft_flags & FT_COLOR))
		return;
	*font = std::move(*n.get());
#if DXX_USE_OGL
	ogl_init_font(font);
#endif
}

void gr_set_curfont(grs_canvas &canvas, const grs_font &n)
{
	canvas.cv_font = &n;
}

namespace {

template <bool masked_draws_background>
static int gr_internal_string_clipped_template(grs_canvas &canvas, const grs_font &cv_font, int x, int y, const char *const s)
{
	int letter;
	int x1 = x, last_x;

	const auto &&INFONT = font_character_extent(cv_font);
	const auto ft_flags = cv_font.ft_flags;
	const auto proportional = ft_flags & FT_PROPORTIONAL;
	const auto cv_font_bg_color = canvas.cv_font_bg_color;

	for (auto next_row = s; next_row;)
	{
		const auto text_ptr1 = next_row;
		next_row = NULL;

		x = x1;
		if (x==0x8000)			//centered
			x = get_centered_x(canvas, cv_font, text_ptr1);

		last_x = x;

		for (const auto r : xrange(cv_font.ft_h))
		{
			auto text_ptr = text_ptr1;
			x = last_x;

			for (; const uint8_t c0 = *text_ptr; ++text_ptr)
			{
				if (c0 == '\n')
				{
					next_row = &text_ptr[1];
					break;
				}
				if (c0 == CC_COLOR)
				{
					canvas.cv_font_fg_color = static_cast<uint8_t>(*++text_ptr);
					continue;
				}

				if (c0 == CC_LSPACING)
				{
					Int3();	//	Warning: skip lines not supported for clipped strings.
					text_ptr += 1;
					continue;
				}

				per_character_row_state state{
					(unlikely(c0 == CC_UNDERLINE)
						? ++text_ptr, r == cv_font.ft_baseline + 2 || r == cv_font.ft_baseline + 3
						: false),
				};
				const uint8_t c = *text_ptr;
				const auto &result = get_char_width<int>(cv_font, c, text_ptr[1]);
				const auto &width = result.width;
				const auto &spacing = result.spacing;

				letter = c - cv_font.ft_minchar;

				if (!INFONT(letter)) {	//not in font, draw as space
					x += spacing;
					continue;
				}
				const auto cv_font_fg_color = canvas.cv_font_fg_color;
				auto color = cv_font_fg_color;
				if (width)
				{
					if (state.draw_full_width_as_fg_color)	{
					for (uint_fast32_t i = width; i--;)
					{
						gr_pixel(canvas.cv_bitmap, x++, y, color);
					}
				} else {
					auto fp = proportional ? cv_font.ft_chars[letter] : cv_font.ft_data + letter * BITS_TO_BYTES(width) * cv_font.ft_h;
					fp += BITS_TO_BYTES(width)*r;

					/* Setting bits=0 is a dead store, but is necessary to
					 * prevent -Og -Wuninitialized from issuing a bogus
					 * warning.  -Og does not see that bits_remaining=0
					 * guarantees that bits will be initialized from *fp before
					 * it is read.
					 */
					uint8_t bits_remaining = 0, bits = 0;

					for (uint_fast32_t i = width; i--; ++x, --bits_remaining)
					{
						if (!bits_remaining)
						{
							bits = *fp++;
							bits_remaining = 8;
						}
						const auto bit_enabled = (bits & 0x80);
						bits <<= 1;
						if constexpr (masked_draws_background)
							color = bit_enabled ? cv_font_fg_color : cv_font_bg_color;
						else
						{
							if (!bit_enabled)
								continue;
						}
						gr_pixel(canvas.cv_bitmap, x, y, color);
					}
				}
				}
				x += spacing-width;		//for kerning
			}
			y++;
		}
	}
	return 0;
}

static int gr_internal_string_clipped_m(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
{
	return gr_internal_string_clipped_template<true>(canvas, cv_font, x, y, s);
}

static int gr_internal_string_clipped(grs_canvas &canvas, const grs_font &cv_font, const int x, const int y, const char *const s)
{
	return gr_internal_string_clipped_template<false>(canvas, cv_font, x, y, s);
}

}

}
