"""Tauon Music Box - Basic Drawing and Text Drawing Functions Module"""

# Copyright © 2015-2019, Taiko2k captain(dot)gxj(at)gmail.com

#     This file is part of Tauon Music Box.
#
#     Tauon Music Box 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 3 of the License, or
#     (at your option) any later version.
#
#     Tauon Music Box 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 General Public License
#     along with Tauon Music Box.  If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations

import ctypes
import io
import logging
import math
from collections import OrderedDict
from ctypes import byref, c_bool, c_float, c_size_t
from typing import TYPE_CHECKING

import sdl3
from PIL import Image

from tauon.t_modules.t_extra import ColourRGBA, Timer, alpha_blend, coll_rect

if TYPE_CHECKING:
	from io import BytesIO

	from tauon.t_modules.t_main import Tauon

try:
	from jxlpy import JXLImagePlugin

	logging.info("Found jxlpy for JPEG XL support")
except ModuleNotFoundError:
	logging.warning("Unable to import jxlpy, JPEG XL support will be disabled.")
except Exception:
	logging.exception("Unknown error trying to import jxlpy, JPEG XL support will be disabled.")


import sys

if sys.platform == "win32":
	import os

	os.environ["PANGOCAIRO_BACKEND"] = "fc"

import cairo
import gi

gi.require_version("Pango", "1.0")
gi.require_version("PangoCairo", "1.0")
from gi.repository import Pango, PangoCairo


class QuickThumbnail:
	def __init__(self, tauon: Tauon) -> None:
		self.ddt = tauon.ddt
		self.renderer = tauon.renderer
		self.items: list[QuickThumbnail] = []
		self.queue: list[QuickThumbnail] = []
		self.rect = sdl3.SDL_FRect(0.0, 0.0)
		self.texture = None
		self.surface = None
		self.size = 50
		self.alive = False
		self.url = None

	def destruct(self) -> None:
		if self.surface:
			sdl3.SDL_DestroySurface(self.surface)
			self.surface = None
		if self.texture:
			sdl3.SDL_DestroyTexture(self.texture)
			self.texture = None
		self.alive = False

	def read_and_thumbnail(self, f: str, width: int, height: int) -> None:
		g = io.BytesIO()
		g.seek(0)
		im = Image.open(f)
		im.thumbnail((width, height), Image.Resampling.LANCZOS)
		im.save(g, "PNG")
		g.seek(0)
		self.surface = self.ddt.load_image(g)
		# self.items.append(self)
		self.alive = True

	def prime(self) -> None:
		texture = sdl3.SDL_CreateTextureFromSurface(self.renderer, self.surface)
		sdl3.SDL_DestroySurface(self.surface)
		self.surface = None
		tex_w = c_float(0.0)
		tex_h = c_float(0.0)
		sdl3.SDL_GetTextureSize(texture, byref(tex_w), byref(tex_h))
		self.rect.w = int(tex_w.value)
		self.rect.h = int(tex_h.value)
		self.texture = texture

	def draw(self, x: int, y: int) -> bool | None:
		if len(self.items) > 30:
			img = self.items[0]
			img.destruct()
			self.items.remove(img)
		if not self.alive:
			if self not in self.queue:
				self.queue.append(self)
			return False
		if not self.texture:
			self.prime()
		self.rect.x = round(x)
		self.rect.y = round(y)
		sdl3.SDL_RenderTexture(self.renderer, self.texture, None, self.rect)

		return True


perf = Timer()


class TDraw:
	def __init__(self, renderer: sdl3.LP_SDL_Renderer) -> None:
		# All
		self.renderer = renderer
		self.scale = 1
		self.force_subpixel_text = False

		# Drawing
		self.sdlrect = sdl3.SDL_FRect(10.0, 10.0, 10.0, 10.0)

		# Text and Fonts
		self.source_rect = sdl3.SDL_FRect(0.0, 0.0, 0.0, 0.0)
		self.dest_rect = sdl3.SDL_FRect(0.0, 0.0, 0.0, 0.0)

		self.surf = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0)
		self.context = cairo.Context(self.surf)
		# self.layout is used for measurement (get_text_wh); self.draw_layout is a
		# persistent layout for the render path, re-targeted per draw with
		# PangoCairo.update_layout instead of allocating a fresh layout on every
		# cache miss (avoids GObject alloc + GC churn while scrolling).
		self.layout = PangoCairo.create_layout(self.context)
		self.draw_layout = PangoCairo.create_layout(self.context)

		self.text_background_colour = ColourRGBA(0, 0, 0, 255)
		self.pretty_rect: tuple[int, int, int, int] | None = None
		self.real_bg: bool = False
		self.alpha_bg: bool = False
		self.force_gray: bool = False
		self.f_dict: dict[float, tuple[str, int, float]] = {}
		self.font_desc_cache: dict[float, Pango.FontDescription] = {}
		# Rendered-text textures, in LRU order (oldest first).
		self.ttc: OrderedDict[
			tuple[int, str, int, int, int, int, int, int, int, int],
			list[sdl3.SDL_FRect | sdl3.LP_SDL_Texture | int | bool],
		] = OrderedDict()
		self.text_texture_cache_bytes = 0
		# The item cap is recomputed every frame (see new_frame) to fit the text
		# actually on screen rather than a fixed guess. The main UI and the track
		# list are counted separately because the track list re-renders on its own
		# cadence (only when gui.pl_update fires); the cap is their sum plus 50%
		# headroom. max_text_texture_cache_bytes still bounds memory.
		self._frame_text_draws = 0      # non-track-list cacheable draws this frame
		self._tracklist_text_draws = 0  # cacheable draws in the last track-list render
		self._counting_tracklist = False
		self.max_text_texture_cache_items = 0
		self.max_text_texture_cache_bytes = 48 * 1024 * 1024
		self.max_text_texture_cache_item_bytes = 256 * 1024
		self.text_wh_cache: OrderedDict[tuple[str, int, int, bool], tuple[int, int]] = OrderedDict()
		self.max_text_wh_cache_items = 2048

		self.was_truncated = False

	def load_image(self, g: BytesIO) -> sdl3.LP_SDL_Surface:
		size = g.getbuffer().nbytes
		pointer = ctypes.c_void_p(ctypes.addressof(ctypes.c_char.from_buffer(g.getbuffer())))
		stream = sdl3.SDL_IOFromMem(pointer, c_size_t(size))
		return sdl3.IMG_Load_IO(stream, c_bool(True))

	def rect_s(self, rectangle: tuple[int, int, int, int], colour: ColourRGBA, thickness: int) -> None:
		sdl3.SDL_SetRenderDrawColor(self.renderer, colour.r, colour.g, colour.b, colour.a)
		x, y, w, h = (round(x) for x in rectangle)
		th = math.floor(thickness)
		self.sdlrect.x = x - th
		self.sdlrect.y = y - th
		self.sdlrect.w = th
		self.sdlrect.h = h + th
		sdl3.SDL_RenderFillRect(self.renderer, self.sdlrect)  # left
		self.sdlrect.x = x - th
		self.sdlrect.y = y + h
		self.sdlrect.w = w + th
		self.sdlrect.h = th
		sdl3.SDL_RenderFillRect(self.renderer, self.sdlrect)  # bottom
		self.sdlrect.x = x
		self.sdlrect.y = y - th
		self.sdlrect.w = w + th
		self.sdlrect.h = th
		sdl3.SDL_RenderFillRect(self.renderer, self.sdlrect)  # top
		self.sdlrect.x = x + w
		self.sdlrect.y = y
		self.sdlrect.w = th
		self.sdlrect.h = h + th
		sdl3.SDL_RenderFillRect(self.renderer, self.sdlrect)  # right

	def rect_si(self, rectangle: tuple[int, int, int, int], colour: ColourRGBA, thickness: int) -> None:
		sdl3.SDL_SetRenderDrawColor(self.renderer, colour.r, colour.g, colour.b, colour.a)
		x, y, w, h = (round(x) for x in rectangle)
		th = math.floor(thickness)
		self.sdlrect.x = x
		self.sdlrect.y = y
		self.sdlrect.w = th
		self.sdlrect.h = h
		sdl3.SDL_RenderFillRect(self.renderer, self.sdlrect)  # left
		self.sdlrect.x = x
		self.sdlrect.y = y + (h - th)
		self.sdlrect.w = w
		self.sdlrect.h = th
		sdl3.SDL_RenderFillRect(self.renderer, self.sdlrect)  # bottom
		self.sdlrect.x = x
		self.sdlrect.y = y
		self.sdlrect.w = w
		self.sdlrect.h = th
		sdl3.SDL_RenderFillRect(self.renderer, self.sdlrect)  # top
		self.sdlrect.x = x + (w - th)
		self.sdlrect.y = y
		self.sdlrect.w = th
		self.sdlrect.h = h
		sdl3.SDL_RenderFillRect(self.renderer, self.sdlrect)  # right

	def rect_a(self, location_xy: list[int], size_wh: list[int], colour: ColourRGBA) -> None:
		self.rect((location_xy[0], location_xy[1], size_wh[0], size_wh[1]), colour)

	def clear_rect(self, rectangle: tuple[int, int, int, int]) -> None:
		sdl3.SDL_SetRenderDrawBlendMode(self.renderer, sdl3.SDL_BLENDMODE_NONE)
		sdl3.SDL_SetRenderDrawColor(self.renderer, 0, 0, 0, 0)

		self.sdlrect.x = float(rectangle[0])
		self.sdlrect.y = float(rectangle[1])
		self.sdlrect.w = float(rectangle[2])
		self.sdlrect.h = float(rectangle[3])

		sdl3.SDL_RenderFillRect(self.renderer, self.sdlrect)
		sdl3.SDL_SetRenderDrawBlendMode(self.renderer, sdl3.SDL_BLENDMODE_BLEND)

	def rect(self, rectangle: tuple[int, int, int, int], colour: ColourRGBA) -> None:
		sdl3.SDL_SetRenderDrawColor(self.renderer, colour.r, colour.g, colour.b, colour.a)

		self.sdlrect.x = float(rectangle[0])
		self.sdlrect.y = float(rectangle[1])
		self.sdlrect.w = float(rectangle[2])
		self.sdlrect.h = float(rectangle[3])

		# if fill:
		sdl3.SDL_RenderFillRect(self.renderer, self.sdlrect)
		# else:
		# 	sdl3.SDL_RenderDrawRect(self.renderer, self.sdlrect)

	def bordered_rect(
		self, rectangle: tuple[int, int, int, int], fill_colour: ColourRGBA, outer_colour: ColourRGBA, border_size: int
	) -> None:
		self.sdlrect.x = round(rectangle[0]) - border_size
		self.sdlrect.y = round(rectangle[1]) - border_size
		self.sdlrect.w = round(rectangle[2]) + border_size + border_size
		self.sdlrect.h = round(rectangle[3]) + border_size + border_size
		sdl3.SDL_SetRenderDrawColor(self.renderer, outer_colour.r, outer_colour.g, outer_colour.b, outer_colour.a)
		sdl3.SDL_RenderFillRect(self.renderer, self.sdlrect)
		self.sdlrect.x = round(rectangle[0])
		self.sdlrect.y = round(rectangle[1])
		self.sdlrect.w = round(rectangle[2])
		self.sdlrect.h = round(rectangle[3])
		sdl3.SDL_SetRenderDrawColor(self.renderer, fill_colour.r, fill_colour.g, fill_colour.b, fill_colour.a)
		sdl3.SDL_RenderFillRect(self.renderer, self.sdlrect)

	def line(self, x1: int, y1: int, x2: int, y2: int, colour: ColourRGBA) -> None:
		sdl3.SDL_SetRenderDrawColor(self.renderer, colour.r, colour.g, colour.b, colour.a)
		sdl3.SDL_RenderLine(self.renderer, round(x1), round(y1), round(x2), round(y2))

	def get_text_w(self, text: str, font: int, height: bool = False) -> int:
		x, y = self.get_text_wh(text, font, 3000)
		if height:
			return y
		return x

	def clear_text_cache(self) -> None:
		for so in self.ttc.values():
			sdl3.SDL_DestroyTexture(so[1])

		self.ttc.clear()
		self.text_texture_cache_bytes = 0
		self.text_wh_cache.clear()

	def new_frame(self) -> None:
		"""Resize the text-texture cache to fit the text on screen.

		Called once at the start of each rendered frame. The cap is the number
		of cacheable text draws in the main UI last frame plus the track list's
		last render, with 50% headroom, so everything visible stays cached
		instead of thrashing against a fixed limit. The byte ceiling still
		bounds memory."""
		self.max_text_texture_cache_items = int((self._frame_text_draws + self._tracklist_text_draws) * 1.5)
		self._frame_text_draws = 0
		self._counting_tracklist = False

	def begin_tracklist_count(self) -> None:
		"""Start counting track-list text draws separately (see new_frame)."""
		self._tracklist_text_draws = 0
		self._counting_tracklist = True

	def end_tracklist_count(self) -> None:
		self._counting_tracklist = False

	def prime_font(self, name: str, size: float, user_handle: float, offset: int = 0) -> None:
		self.f_dict[user_handle] = (name + " " + str(size * self.scale), offset, size * self.scale)
		self.font_desc_cache[user_handle] = Pango.FontDescription(self.f_dict[user_handle][0])
		self.text_wh_cache.clear()

	def _font_description(self, font: int) -> Pango.FontDescription:
		cached = self.font_desc_cache.get(font)
		if cached is not None:
			return cached

		font_description = Pango.FontDescription(self.f_dict[font][0])
		self.font_desc_cache[font] = font_description
		return font_description

	def get_text_wh(self, text: str, font: int, max_x: int, wrap: bool = False) -> tuple[int, int] | None:
		cache_key = None
		if len(text) <= 256:
			cache_key = (text, font, max_x, wrap)
			cached = self.text_wh_cache.get(cache_key)
			if cached is not None:
				self.text_wh_cache.move_to_end(cache_key)
				return cached

		self.layout.set_font_description(self._font_description(font))
		self.layout.set_ellipsize(Pango.EllipsizeMode.END)
		self.layout.set_width(max_x * 1000)
		if wrap:
			self.layout.set_height(20000 * 1000)
		else:
			self.layout.set_height(0)

		try:
			self.layout.set_text(text, -1)
		except Exception:
			logging.exception(f"Exception in get_text_wh for: {text}")
			self.layout.set_text(text.encode("utf-8", "replace").decode("utf-8"), -1)

		result = self.layout.get_pixel_size()
		if cache_key is not None:
			self.text_wh_cache[cache_key] = result
			if len(self.text_wh_cache) > self.max_text_wh_cache_items:
				self.text_wh_cache.popitem(last=False)
		return result

	def get_y_offset(self, text: str, font: int, max_x: int, wrap: bool = False) -> int:
		"""HACKY"""
		self.layout.set_font_description(self._font_description(font))
		self.layout.set_ellipsize(Pango.EllipsizeMode.END)
		self.layout.set_width(max_x * 1000)
		if wrap:
			self.layout.set_height(20000 * 1000)
		else:
			self.layout.set_height(0)

		try:
			self.layout.set_text(text, -1)
		except Exception:
			logging.exception(f"Exception in get_y_offset for: {text}")
			self.layout.set_text(text.encode("utf-8", "replace").decode("utf-8"), -1)

		y_off = self.layout.get_baseline() / 1000
		y_off = round(round(y_off) - 13 * self.scale)  # 13 for compat with way text position used to work

		return y_off

	def __render_text(self, key: dict, x: int, y: int, range_top: int, range_height: int, align: int) -> None:
		sd = key

		if sd[3]:
			self.was_truncated = True

		if align == 1:
			sd[0].x = round(x) - sd[0].w

		elif align == 2:
			sd[0].x -= int(sd[0].w / 2)

		if range_height is not None and range_height < sd[0].h:
			if range_top < 0:
				range_top = 0

			if range_top > sd[0].h - range_height:
				range_top = sd[0].h - range_height

			self.source_rect.y = round(range_top)
			self.source_rect.w = sd[0].w
			self.source_rect.h = round(range_height)

			self.dest_rect.x = sd[0].x
			self.dest_rect.y = sd[0].y
			self.dest_rect.w = sd[0].w
			self.dest_rect.h = round(range_height)

			# sdl3.SDL_RenderCopyEx(self.renderer, sd[1], self.source_rect, self.dest_rect, 0, None, 0)
			sdl3.SDL_RenderTexture(self.renderer, sd[1], self.source_rect, self.dest_rect)
			return

		sdl3.SDL_RenderTexture(self.renderer, sd[1], None, sd[0])

	def __draw_text_cairo(
		self,
		location: list[int],
		text: str,
		colour: ColourRGBA,
		font: int,
		max_x: int,
		bg: ColourRGBA,
		align: int = 0,
		max_y: int | None = None,
		wrap: bool = False,
		range_top: int = 0,
		range_height: int | None = None,
		real_bg: bool = False,
		key: tuple[int, str, int, int, int, int, int, int, int, int] | None = None,
	) -> int:
		# perf.set()
		force_cache = False
		if key:
			force_cache = True

		self.was_truncated = False

		max_x += 12  # Hack
		max_x = round(max_x)

		alpha_bg = self.alpha_bg
		force_gray = self.force_gray
		# real_bg = True

		if bg.a < 200:
			alpha_bg = True
			force_gray = True

		x = round(location[0])
		y = round(location[1])

		if self.pretty_rect:
			w, h = self.get_text_wh(text, font, max_x, wrap)
			quick_box = [x, y, w, h]

			if align == 1:
				quick_box[0] = x - quick_box[2]
			elif align == 2:
				quick_box[0] -= int(quick_box[2] / 2)

			if coll_rect(self.pretty_rect, quick_box):
				# self.rect_r(quick_box, [0, 0, 0, 100], True)
				# if self.real_bg:
				# real_bg = True
				alpha_bg = True
			else:
				alpha_bg = False

		if alpha_bg:
			bg = ColourRGBA(0, 0, 0, 0)

		if max_y is not None:
			max_y = round(max_y)

		if len(text) == 0:
			return 0

		if key is None:
			key = (max_x, text, font, colour.r, colour.g, colour.b, colour.a, bg.r, bg.g, bg.b)

		if not real_bg or force_cache:
			if self._counting_tracklist:
				self._tracklist_text_draws += 1
			else:
				self._frame_text_draws += 1
			sd = self.ttc.get(key)
			if sd:
				sd[0].x = round(x)
				sd[0].y = round(y) - sd[2]

				self.__render_text(sd, x, y, range_top, range_height, align)
				self.ttc.move_to_end(key)

				if wrap:
					return sd[0].h
				return sd[0].w

		if not self.pretty_rect:  # Would have already done this if True
			w, h = self.get_text_wh(text, font, max_x, wrap)

		if w < 1:
			return 0

		h += 4  # Compensate for characters that drop past the baseline, Pango doesn't seem to allow for this

		if wrap:
			w = max_x + 1

		if font not in self.f_dict:
			logging.info(f"Font not loaded: {font!s}")
			return 10

		format = sdl3.SDL_PIXELFORMAT_ARGB8888 if alpha_bg else sdl3.SDL_PIXELFORMAT_XRGB8888
		surface = None
		surface_locked = False
		pixel_data = None
		data = None
		surf = None
		context = None
		layout = None
		c = None
		y_off = 0

		try:
			if real_bg:
				box = sdl3.SDL_Rect(x, y - self.get_y_offset(text, font, max_x, wrap), w, h)

				if align == 1:
					box.x = x - box.w
				elif align == 2:
					box.x -= int(box.w / 2)

				ssurf = sdl3.SDL_RenderReadPixels(self.renderer, box)
				if not ssurf:
					logging.warning("SDL_RenderReadPixels failed while rendering text background")
					return 0

				if ssurf.contents.format != format:
					surface = sdl3.SDL_ConvertSurface(ssurf, format)
					sdl3.SDL_DestroySurface(ssurf)
					if not surface:
						logging.warning("SDL_ConvertSurface failed while rendering text background")
						return 0
				else:
					surface = ssurf
			else:
				surface = sdl3.SDL_CreateSurface(w, h, format)
				if not surface:
					logging.warning("SDL_CreateSurface failed while rendering text")
					return 0
				ctypes.memset(surface.contents.pixels, 0, surface.contents.pitch * h)

			if not sdl3.SDL_LockSurface(surface):
				logging.warning("SDL_LockSurface failed while rendering text")
				return 0
			surface_locked = True

			pixel_size = surface.contents.pitch * h
			pixel_data = (ctypes.c_ubyte * pixel_size).from_address(surface.contents.pixels)
			data = memoryview(pixel_data)

			if alpha_bg:
				surf = cairo.ImageSurface.create_for_data(data, cairo.FORMAT_ARGB32, w, h, surface.contents.pitch)
			else:
				surf = cairo.ImageSurface.create_for_data(data, cairo.FORMAT_RGB24, w, h, surface.contents.pitch)

			context = cairo.Context(surf)

			if force_gray:
				options = context.get_font_options()
				options.set_antialias(cairo.ANTIALIAS_GRAY)
				# options.set_hint_style(cairo.HINT_STYLE_NONE)
				context.set_font_options(options)
			elif self.force_subpixel_text:
				options = context.get_font_options()
				# options.set_antialias(cairo.ANTIALIAS_NONE)
				# options.set_antialias(cairo.ANTIALIAS_GRAY)
				options.set_antialias(cairo.ANTIALIAS_SUBPIXEL)
				context.set_font_options(options)

			layout = self.draw_layout
			PangoCairo.update_layout(context, layout)
			layout.set_auto_dir(False)

			if max_y is not None:
				layout.set_ellipsize(Pango.EllipsizeMode.END)
				layout.set_width(max_x * 1000)
				layout.set_height(max_y * 1000)
			else:
				layout.set_ellipsize(Pango.EllipsizeMode.END)
				layout.set_width(max_x * 1000)

				extra = 0
				if wrap:  # Compensate for height measurement being 1-2 lines too short. Pango bug?
					extra = round(400000 * self.scale)

				layout.set_height(h * 1000 + extra)

			if not wrap and max_y is None:
				layout.set_height(-1)

			# Attributes don't seem to be implemented in gi?
			# attrs = Pango.AttrList()
			# attrs.insert(Pango.Attribute(Pango.Underline.SINGLE))
			# layout.set_attributes(attrs)

			context.rectangle(0, 0, w, h)

			if not real_bg and not alpha_bg:
				context.set_source_rgb(bg.r / 255, bg.g / 255, bg.b / 255)
				# context.set_source_rgba(0, 0, 0, 0)
				context.fill()

			context.set_source_rgb(colour.r / 255, colour.g / 255, colour.b / 255)

			# desc = Pango.FontDescription(self.f_dict[font][0])
			# desc.set_family("Arial")

			layout.set_font_description(self._font_description(font))

			try:
				layout.set_text(text, -1)
			except Exception:
				logging.exception(f"Text error on text: {text}")
				layout.set_text(text.encode("utf-8", "replace").decode("utf-8"), -1)

			# logging.info(layout.get_direction(0))

			y_off = layout.get_baseline() / 1000
			y_off = round(round(y_off) - 13 * self.scale)  # 13 for compat with way text position used to work

			PangoCairo.show_layout(context, layout)

			self.was_truncated = layout.is_ellipsized()
			surf.flush()
			surf.finish()
			surf = None
			layout = None
			context = None
			data.release()
			data = None
			pixel_data = None
			sdl3.SDL_UnlockSurface(surface)
			surface_locked = False

			# Here the background colour is keyed out allowing lines to overlap slightly
			if not real_bg and not alpha_bg:
				format_details = sdl3.SDL_GetPixelFormatDetails(format)
				ke = sdl3.SDL_MapRGB(format_details, None, bg.r, bg.g, bg.b)
				sdl3.SDL_SetSurfaceColorKey(surface, True, ke)

			c = sdl3.SDL_CreateTextureFromSurface(self.renderer, surface)
			if not c:
				logging.warning("SDL_CreateTextureFromSurface failed while rendering text")
				return 0

			sdl3.SDL_DestroySurface(surface)
			surface = None

			if alpha_bg:
				blend_mode = sdl3.SDL_ComposeCustomBlendMode(
					sdl3.SDL_BLENDFACTOR_ONE,
					sdl3.SDL_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
					sdl3.SDL_BLENDOPERATION_ADD,
					sdl3.SDL_BLENDFACTOR_ONE,
					sdl3.SDL_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
					sdl3.SDL_BLENDOPERATION_ADD,
				)
				sdl3.SDL_SetTextureBlendMode(c, blend_mode)
		finally:
			layout = None
			context = None
			if surf is not None:
				surf.finish()
			if data is not None:
				data.release()
			pixel_data = None
			if surface_locked and surface is not None:
				sdl3.SDL_UnlockSurface(surface)
			if surface is not None:
				sdl3.SDL_DestroySurface(surface)

		dst = sdl3.SDL_FRect(round(x), round(y))
		dst.w = round(w)
		dst.h = round(h)
		dst.y = round(y) - y_off

		pack = [dst, c, y_off, self.was_truncated]
		cache_item_bytes = w * h * 4
		texture_cached = False

		try:
			self.__render_text(pack, x, y, range_top, range_height, align)

			# Don't cache if using real background data
			cache_texture = not real_bg and (
				force_cache or cache_item_bytes <= self.max_text_texture_cache_item_bytes
			)
			if cache_texture:
				pack.append(cache_item_bytes)
				self.ttc[key] = pack
				self.text_texture_cache_bytes += cache_item_bytes
				texture_cached = True
				while self.ttc and (
					len(self.ttc) > self.max_text_texture_cache_items
					or self.text_texture_cache_bytes > self.max_text_texture_cache_bytes
				):
					old_key, so = self.ttc.popitem(last=False)
					sdl3.SDL_DestroyTexture(so[1])
					if len(so) > 4:
						self.text_texture_cache_bytes -= so[4]
		finally:
			if c is not None and not texture_cached:
				sdl3.SDL_DestroyTexture(c)

		if wrap:
			return dst.h
		return dst.w

	def text(
		self,
		location: list[int],
		text: str,
		colour: ColourRGBA,
		font: int,
		max_w: int = 4000,
		bg: ColourRGBA | None = None,
		range_top: int = 0,
		range_height: int | None = None,
		real_bg: bool = False,
		key: tuple[int, str, int, int, int, int, int, int, int, int] | None = None,
	) -> int | None:
		# logging.info((text, font))

		if not text:
			return 0

		max_w = max(1, max_w)

		if bg is None:
			bg = self.text_background_colour

		if colour.a != 255:
			colour = alpha_blend(colour, bg)
		align = 0
		if len(location) > 2:
			if location[2] == 1:
				align = 1
			if location[2] == 2:
				align = 2
			if location[2] == 4:
				max_h = None
				if len(location) > 4:
					max_h = location[4]

				return self.__draw_text_cairo(
					location,
					text,
					colour,
					font,
					location[3],
					bg,
					max_y=max_h,
					wrap=True,
					range_top=range_top,
					range_height=range_height,
				)

		return self.__draw_text_cairo(location, text, colour, font, max_w, bg, align, real_bg=real_bg, key=key)
