use std::f32::consts::PI;

use macroquad::prelude::*;
use macroquad::miniquad::{BlendState, BlendFactor, BlendValue, Equation};
use crate::{math::BoundingBox, render_commands::{CornerRadii, RenderCommand, RenderCommandConfig}, shaders::{ShaderConfig, ShaderUniformValue}, elements::BorderPosition};

#[cfg(feature = "text-styling")]
use crate::text_styling::{render_styled_text, StyledSegment};
#[cfg(feature = "text-styling")]
use rustc_hash::FxHashMap;

const PIXELS_PER_POINT: f32 = 2.0;

/// On Android, the APK asset root is the `assets/` directory,
/// so paths like `"assets/fonts/x.ttf"` need the prefix stripped.
fn resolve_asset_path(path: &str) -> &str {
    #[cfg(target_os = "android")]
    if let Some(stripped) = path.strip_prefix("assets/") {
        return stripped;
    }
    path
}

#[cfg(feature = "text-styling")]
static ANIMATION_TRACKER: std::sync::LazyLock<std::sync::Mutex<FxHashMap<String, (usize, f64)>>> = std::sync::LazyLock::new(|| std::sync::Mutex::new(FxHashMap::default()));

/// Represents an asset that can be loaded as a texture. This can be either a file path or embedded bytes.
#[derive(Debug)]
pub enum GraphicAsset {
    Path(&'static str), // For external assets
    Bytes{file_name: &'static str, data: &'static [u8]}, // For embedded assets
}
impl GraphicAsset {
    pub fn get_name(&self) -> &str {
        match self {
            GraphicAsset::Path(path) => path,
            GraphicAsset::Bytes { file_name, .. } => file_name,
        }
    }
}

/// Represents the source of image data for an element. Accepts static assets,
/// runtime GPU textures, or procedural TinyVG scene graphs.
#[derive(Debug, Clone)]
pub enum ImageSource {
    /// Static asset: file path or embedded bytes (existing behavior).
    Asset(&'static GraphicAsset),
    /// Pre-existing GPU texture handle (lightweight, Copy).
    Texture(Texture2D),
    /// Procedural TinyVG scene graph, rasterized at the element's layout size each frame.
    #[cfg(feature = "tinyvg")]
    TinyVg(tinyvg::format::Image),
}

impl ImageSource {
    /// Returns a human-readable name for debug/logging purposes.
    pub fn get_name(&self) -> &str {
        match self {
            ImageSource::Asset(ga) => ga.get_name(),
            ImageSource::Texture(_) => "[Texture2D]",
            #[cfg(feature = "tinyvg")]
            ImageSource::TinyVg(_) => "[TinyVG procedural]",
        }
    }
}

impl From<&'static GraphicAsset> for ImageSource {
    fn from(asset: &'static GraphicAsset) -> Self {
        ImageSource::Asset(asset)
    }
}

impl From<Texture2D> for ImageSource {
    fn from(tex: Texture2D) -> Self {
        ImageSource::Texture(tex)
    }
}

#[cfg(feature = "tinyvg")]
impl From<tinyvg::format::Image> for ImageSource {
    fn from(img: tinyvg::format::Image) -> Self {
        ImageSource::TinyVg(img)
    }
}

/// Represents a font asset that can be loaded. This can be either a file path or embedded bytes.
#[derive(Debug)]
pub enum FontAsset {
    /// A file path to a `.ttf` font file (e.g. `"assets/fonts/lexend.ttf"`).
    Path(&'static str),
    /// Embedded font bytes, typically via `include_bytes!`.
    Bytes {
        file_name: &'static str,
        data: &'static [u8],
    },
}

impl FontAsset {
    /// Returns a unique key string for this asset (the path or file name).
    pub fn key(&self) -> &'static str {
        match self {
            FontAsset::Path(path) => path,
            FontAsset::Bytes { file_name, .. } => file_name,
        }
    }
}

/// Global FontManager. Manages font loading, caching, and eviction.
pub static FONT_MANAGER: std::sync::LazyLock<std::sync::Mutex<FontManager>> =
    std::sync::LazyLock::new(|| std::sync::Mutex::new(FontManager::new()));

/// Manages fonts, loading and caching them as needed.
pub struct FontManager {
    fonts: rustc_hash::FxHashMap<&'static str, FontData>,
    default_font: Option<DefaultFont>,
    pub max_frames_not_used: usize,
    font_metrics: rustc_hash::FxHashMap<(u16, usize), FontMetrics>,
}
struct DefaultFont {
    key: &'static str,
    font: Font,
}
#[derive(Clone, Copy)]
struct FontMetrics {
    pub height: f32,
    pub baseline_offset: f32,
}
struct FontData {
    pub frames_not_used: usize,
    pub font: Font,
}
impl FontManager {
    pub fn new() -> Self {
        Self {
            fonts: rustc_hash::FxHashMap::default(),
            default_font: None,
            max_frames_not_used: 60,
            font_metrics: rustc_hash::FxHashMap::default(),
        }
    }

    /// Get a cached font by its asset key.
    pub fn get(&mut self, asset: &'static FontAsset) -> Option<&Font> {
        let key = asset.key();
        if let Some(data) = self.fonts.get_mut(key) {
            return Some(&data.font);
        }
        // Fall back to the default font if the key matches
        self.default_font.as_ref()
            .filter(|d| d.key == key)
            .map(|d| &d.font)
    }

    /// Get the default font (set via [`load_default`](FontManager::load_default)).
    /// Returns `None` if no default font has been set.
    pub fn get_default(&self) -> Option<&Font> {
        self.default_font.as_ref().map(|d| &d.font)
    }

    fn metrics(&mut self, font_size: u16, font_asset: Option<&'static FontAsset>) -> FontMetrics {
        let font_ptr = font_asset.map_or(0usize, |a| a as *const _ as usize);
        let key = (font_size, font_ptr);
        if let Some(&m) = self.font_metrics.get(&key) {
            return m;
        }
        let (font, found) = if let Some(a) = font_asset {
            let k = a.key();
            if let Some(data) = self.fonts.get(k) {
                (Some(&data.font), true)
            } else if self.default_font.as_ref().map(|d| d.key) == Some(k) {
                (self.default_font.as_ref().map(|d| &d.font), true)
            } else {
                (self.default_font.as_ref().map(|d| &d.font), false)
            }
        } else {
            (self.default_font.as_ref().map(|d| &d.font), true)
        };
        let ref_dims = macroquad::text::measure_text("Xig", font, font_size, 1.0);
        let m = FontMetrics {
            height: ref_dims.height,
            baseline_offset: ref_dims.offset_y,
        };
        if found {
            self.font_metrics.insert(key, m);
        }
        m
    }

    /// Load the default font. Stored outside the cache and never evicted.
    pub async fn load_default(asset: &'static FontAsset) {
        let font = match asset {
            FontAsset::Bytes { data, .. } => {
                macroquad::text::load_ttf_font_from_bytes(data)
                    .expect("Failed to load font from bytes")
            }
            FontAsset::Path(path) => {
                let resolved = resolve_asset_path(path);
                macroquad::text::load_ttf_font(resolved).await
                    .unwrap_or_else(|e| panic!("Failed to load font '{}': {:?}", path, e))
            }
        };
        let mut fm = FONT_MANAGER.lock().unwrap();
        fm.default_font = Some(DefaultFont { key: asset.key(), font });
    }

    /// Ensure a font is loaded (no-op if already cached).
    pub async fn ensure(asset: &'static FontAsset) {
        // Check if already loaded (quick lock)
        {
            let mut fm = FONT_MANAGER.lock().unwrap();
            // Already the default font?
            if fm.default_font.as_ref().map(|d| d.key) == Some(asset.key()) {
                return;
            }
            // Already in cache?
            if let Some(data) = fm.fonts.get_mut(asset.key()) {
                data.frames_not_used = 0;
                return;
            }
        }

        // Load outside the lock
        let font = match asset {
            FontAsset::Bytes { data, .. } => {
                macroquad::text::load_ttf_font_from_bytes(data)
                    .expect("Failed to load font from bytes")
            }
            FontAsset::Path(path) => {
                let resolved = resolve_asset_path(path);
                macroquad::text::load_ttf_font(resolved).await
                    .unwrap_or_else(|e| panic!("Failed to load font '{}': {:?}", path, e))
            }
        };

        // Insert with lock
        let mut fm = FONT_MANAGER.lock().unwrap();
        let key = asset.key();
        fm.fonts.entry(key).or_insert(FontData { frames_not_used: 0, font });
    }

    pub fn clean(&mut self) {
        self.fonts.retain(|_, data| data.frames_not_used <= self.max_frames_not_used);
        for (_, data) in self.fonts.iter_mut() {
            data.frames_not_used += 1;
        }
    }

    pub fn is_loaded(asset: &'static FontAsset) -> bool {
        let fm = FONT_MANAGER.lock().unwrap();
        let key = asset.key();
        if fm.default_font.as_ref().map(|d| d.key) == Some(key) {
            return true;
        }
        fm.fonts.contains_key(key)
    }

    /// Returns the number of currently loaded fonts.
    pub fn size(&self) -> usize {
        self.fonts.len()
    }
}

/// Global TextureManager. Can also be used outside the renderer to manage your own macroquad textures.
pub static TEXTURE_MANAGER: std::sync::LazyLock<std::sync::Mutex<TextureManager>> = std::sync::LazyLock::new(|| std::sync::Mutex::new(TextureManager::new()));

/// Manages textures, loading and unloading them as needed. No manual management needed.
/// 
/// You can adjust `max_frames_not_used` to control how many frames a texture can go unused before being unloaded.
pub struct TextureManager {
    textures: rustc_hash::FxHashMap<String, CacheEntry>,
    pub max_frames_not_used: usize,
}
struct CacheEntry {
    frames_not_used: usize,
    owner: TextureOwner,
}
enum TextureOwner {
    Standalone(Texture2D),
    RenderTarget(RenderTarget),
}

impl TextureOwner {
    pub fn texture(&self) -> &Texture2D {
        match self {
            TextureOwner::Standalone(tex) => tex,
            TextureOwner::RenderTarget(rt) => &rt.texture,
        }
    }
}

impl From<Texture2D> for TextureOwner {
    fn from(tex: Texture2D) -> Self {
        TextureOwner::Standalone(tex)
    }
}

impl From<RenderTarget> for TextureOwner {
    fn from(rt: RenderTarget) -> Self {
        TextureOwner::RenderTarget(rt)
    }
}

impl TextureManager {
    pub fn new() -> Self {
        Self {
            textures: rustc_hash::FxHashMap::default(),
            max_frames_not_used: 1,
        }
    }

    /// Get a cached texture by its key.
    pub fn get(&mut self, path: &str) -> Option<&Texture2D> {
        if let Some(entry) = self.textures.get_mut(path) {
            entry.frames_not_used = 0;
            Some(entry.owner.texture())
        } else {
            None
        }
    }

    /// Get the cached texture by its key, or load from a file path and cache it.
    pub async fn get_or_load(&mut self, path: &'static str) -> &Texture2D {
        if !self.textures.contains_key(path) {
            let texture = load_texture(resolve_asset_path(path)).await.unwrap();
            self.textures.insert(path.to_owned(), CacheEntry { frames_not_used: 0, owner: texture.into() });
        }
        let entry = self.textures.get_mut(path).unwrap();
        entry.frames_not_used = 0;
        entry.owner.texture()
    }

    /// Get the cached texture by its key, or create it using the provided function and cache it.
    pub fn get_or_create<F>(&mut self, key: String, create_fn: F) -> &Texture2D
    where F: FnOnce() -> Texture2D
    {
        if !self.textures.contains_key(&key) {
            let texture = create_fn();
            self.textures.insert(key.clone(), CacheEntry { frames_not_used: 0, owner: texture.into() });
        }
        let entry = self.textures.get_mut(&key).unwrap();
        entry.frames_not_used = 0;
        entry.owner.texture()
    }

    pub async fn get_or_create_async<F, Fut>(&mut self, key: String, create_fn: F) -> &Texture2D
    where F: FnOnce() -> Fut,
          Fut: std::future::Future<Output = Texture2D>
    {
        if !self.textures.contains_key(&key) {
            let texture = create_fn().await;
            self.textures.insert(key.clone(), CacheEntry { frames_not_used: 0, owner: texture.into() });
        }
        let entry = self.textures.get_mut(&key).unwrap();
        entry.frames_not_used = 0;
        entry.owner.texture()
    }

    /// Cache a value with the given key. Accepts `Texture2D` or `RenderTarget`.
    #[allow(private_bounds)]
    pub fn cache(&mut self, key: String, value: impl Into<TextureOwner>) -> &Texture2D {
        self.textures.insert(key.clone(), CacheEntry { frames_not_used: 0, owner: value.into() });
        self.textures.get(&key).unwrap().owner.texture()
    }

    pub fn clean(&mut self) {
        self.textures.retain(|_, entry| entry.frames_not_used <= self.max_frames_not_used);
        for (_, entry) in self.textures.iter_mut() {
            entry.frames_not_used += 1;
        }
    }

    pub fn size(&self) -> usize {
        self.textures.len()
    }
}

/// Default passthrough vertex shader used for all shader effects.
const DEFAULT_VERTEX_SHADER: &str = "#version 100
attribute vec3 position;
attribute vec2 texcoord;
attribute vec4 color0;
varying lowp vec2 uv;
varying lowp vec4 color;
uniform mat4 Model;
uniform mat4 Projection;
void main() {
    gl_Position = Projection * Model * vec4(position, 1);
    color = color0 / 255.0;
    uv = texcoord;
}
";

/// Default fragment shader as fallback.
pub const DEFAULT_FRAGMENT_SHADER: &str = "#version 100
precision lowp float;
varying vec2 uv;
varying vec4 color;
uniform sampler2D Texture;
void main() {
    gl_FragColor = color;
}
";

/// Global MaterialManager for caching compiled shader materials.
pub static MATERIAL_MANAGER: std::sync::LazyLock<std::sync::Mutex<MaterialManager>> =
    std::sync::LazyLock::new(|| std::sync::Mutex::new(MaterialManager::new()));

/// Manages compiled GPU materials (shaders), caching them by fragment source.
///
/// Equivalent to `TextureManager` but for materials. The renderer creates and uses
/// this to avoid recompiling shaders every frame.
///
/// Also holds a runtime shader storage (`name → source`) for [`ShaderAsset::Stored`]
/// shaders. Update stored sources with [`set_source`](Self::set_source); the old
/// compiled material is evicted automatically when the source changes.
pub struct MaterialManager {
    materials: rustc_hash::FxHashMap<std::borrow::Cow<'static, str>, MaterialData>,
    /// Runtime shader storage: name → fragment source.
    shader_storage: rustc_hash::FxHashMap<String, String>,
    /// How many frames a material can go unused before being evicted.
    pub max_frames_not_used: usize,
}

struct MaterialData {
    pub frames_not_used: usize,
    pub material: Material,
}

impl MaterialManager {
    pub fn new() -> Self {
        Self {
            materials: rustc_hash::FxHashMap::default(),
            shader_storage: rustc_hash::FxHashMap::default(),
            max_frames_not_used: 60, // Keep materials longer than textures
        }
    }

    /// Get or create a material for the given shader config.
    /// The material is cached by fragment source string.
    pub fn get_or_create(&mut self, config: &ShaderConfig) -> &Material {
        let key: &str = &config.fragment;
        if !self.materials.contains_key(key) {
            // Derive uniform declarations from the config
            let mut uniform_decls: Vec<UniformDesc> = vec![
                // Auto-uniforms
                UniformDesc::new("u_resolution", UniformType::Float2),
                UniformDesc::new("u_position", UniformType::Float2),
            ];
            for u in &config.uniforms {
                let utype = match &u.value {
                    ShaderUniformValue::Float(_) => UniformType::Float1,
                    ShaderUniformValue::Vec2(_) => UniformType::Float2,
                    ShaderUniformValue::Vec3(_) => UniformType::Float3,
                    ShaderUniformValue::Vec4(_) => UniformType::Float4,
                    ShaderUniformValue::Int(_) => UniformType::Int1,
                    ShaderUniformValue::Mat4(_) => UniformType::Mat4,
                };
                uniform_decls.push(UniformDesc::new(&u.name, utype));
            }

            let blend_pipeline_params = PipelineParams {
                color_blend: Some(BlendState::new(
                    Equation::Add,
                    BlendFactor::Value(BlendValue::SourceAlpha),
                    BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
                )),
                alpha_blend: Some(BlendState::new(
                    Equation::Add,
                    BlendFactor::Value(BlendValue::SourceAlpha),
                    BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
                )),
                ..Default::default()
            };

            let material = load_material(
                ShaderSource::Glsl {
                    vertex: DEFAULT_VERTEX_SHADER,
                    fragment: &config.fragment,
                },
                MaterialParams {
                    pipeline_params: blend_pipeline_params,
                    uniforms: uniform_decls,
                    ..Default::default()
                },
            )
            .unwrap_or_else(|e| {
                eprintln!("Failed to compile shader material: {:?}", e);
                // Fall back to default material 
                load_material(
                    ShaderSource::Glsl {
                        vertex: DEFAULT_VERTEX_SHADER,
                        fragment: DEFAULT_FRAGMENT_SHADER,
                    },
                    MaterialParams::default(),
                )
                .unwrap()
            });

            self.materials.insert(config.fragment.clone(), MaterialData {
                frames_not_used: 0,
                material,
            });
        }

        let entry = self.materials.get_mut(key).unwrap();
        entry.frames_not_used = 0;
        &entry.material
    }

    /// Evict materials that haven't been used recently.
    pub fn clean(&mut self) {
        self.materials.retain(|_, data| data.frames_not_used <= self.max_frames_not_used);
        for (_, data) in self.materials.iter_mut() {
            data.frames_not_used += 1;
        }
    }

    /// Store or update a named shader source.
    ///
    /// If the source changed compared to the previously stored value, the old
    /// compiled material is evicted from the cache so the next render pass
    /// recompiles automatically. No-ops if the source is unchanged.
    pub fn set_source(&mut self, name: &str, fragment: &str) {
        if let Some(old_source) = self.shader_storage.get(name) {
            if old_source == fragment {
                return; // Unchanged — nothing to do
            }
            // Evict stale material keyed by the old source
            self.materials.remove(old_source.as_str());
        }
        self.shader_storage.insert(name.to_string(), fragment.to_string());
    }

    /// Look up a stored shader source by name.
    pub fn get_source(&self, name: &str) -> Option<&str> {
        self.shader_storage.get(name).map(String::as_str)
    }
}

/// Update a named shader source in the global shader storage.
///
/// When the source changes, the previously compiled material is evicted
/// and will be recompiled on the next render pass. No-ops if unchanged.
///
/// Use with [`ShaderAsset::Stored`](crate::shaders::ShaderAsset::Stored) to reference
/// the stored source by name.
///
/// # Example
/// ```rust,ignore
/// set_shader_source("live_shader", &editor.text);
///
/// const LIVE: ShaderAsset = ShaderAsset::Stored("live_shader");
/// ui.element()
///     .effect(&LIVE, |s| s.uniform("u_time", get_time() as f32))
///     .build();
/// ```
pub fn set_shader_source(name: &str, fragment: &str) {
    MATERIAL_MANAGER.lock().unwrap().set_source(name, fragment);
}

/// Apply shader uniforms to a material, including auto-uniforms.
fn apply_shader_uniforms(material: &Material, config: &ShaderConfig, bb: &BoundingBox) {
    // Auto-uniforms
    material.set_uniform("u_resolution", (bb.width, bb.height));
    material.set_uniform("u_position", (bb.x, bb.y));

    // User-defined uniforms
    for u in &config.uniforms {
        match &u.value {
            ShaderUniformValue::Float(v) => material.set_uniform(&u.name, *v),
            ShaderUniformValue::Vec2(v) => material.set_uniform(&u.name, *v),
            ShaderUniformValue::Vec3(v) => material.set_uniform(&u.name, *v),
            ShaderUniformValue::Vec4(v) => material.set_uniform(&u.name, *v),
            ShaderUniformValue::Int(v) => material.set_uniform(&u.name, *v),
            ShaderUniformValue::Mat4(v) => material.set_uniform(&u.name, *v),
        }
    }
}

fn ply_to_macroquad_color(ply_color: &crate::color::Color) -> Color {
    Color {
        r: ply_color.r / 255.0,
        g: ply_color.g / 255.0,
        b: ply_color.b / 255.0,
        a: ply_color.a / 255.0,
    }
}

/// Draws a rounded rectangle as a single triangle-fan mesh.
/// This avoids the visual artifacts of multi-shape rendering and handles alpha correctly.
fn draw_good_rounded_rectangle(x: f32, y: f32, w: f32, h: f32, cr: &CornerRadii, color: Color) {
    use std::f32::consts::{FRAC_PI_2, PI};

    if cr.top_left == 0.0 && cr.top_right == 0.0 && cr.bottom_left == 0.0 && cr.bottom_right == 0.0 {
        draw_rectangle(x, y, w, h, color);
        return;
    }

    // Generate outline vertices for the rounded rectangle
    // Pre-allocate: each corner produces ~(FRAC_PI_2 * radius / PIXELS_PER_POINT).max(6) + 1 vertices
    let est_verts = [cr.top_left, cr.top_right, cr.bottom_left, cr.bottom_right]
        .iter()
        .map(|&r| if r <= 0.0 { 1 } else { ((FRAC_PI_2 * r) / PIXELS_PER_POINT).max(6.0) as usize + 1 })
        .sum::<usize>();
    let mut outline: Vec<Vec2> = Vec::with_capacity(est_verts);

    let add_arc = |outline: &mut Vec<Vec2>, cx: f32, cy: f32, radius: f32, start_angle: f32, end_angle: f32| {
        if radius <= 0.0 {
            outline.push(Vec2::new(cx, cy));
            return;
        }
        let sides = ((FRAC_PI_2 * radius) / PIXELS_PER_POINT).max(6.0) as usize;
        // Use incremental rotation to avoid per-point cos/sin
        let step = (end_angle - start_angle) / sides as f32;
        let step_cos = step.cos();
        let step_sin = step.sin();
        let mut dx = start_angle.cos() * radius;
        let mut dy = start_angle.sin() * radius;
        for _ in 0..=sides {
            outline.push(Vec2::new(cx + dx, cy + dy));
            let new_dx = dx * step_cos - dy * step_sin;
            let new_dy = dx * step_sin + dy * step_cos;
            dx = new_dx;
            dy = new_dy;
        }
    };

    // Top-left corner: arc from π to 3π/2
    add_arc(&mut outline, x + cr.top_left, y + cr.top_left, cr.top_left,
            PI, 3.0 * FRAC_PI_2);
    // Top-right corner: arc from 3π/2 to 2π
    add_arc(&mut outline, x + w - cr.top_right, y + cr.top_right, cr.top_right,
            3.0 * FRAC_PI_2, 2.0 * PI);
    // Bottom-right corner: arc from 0 to π/2
    add_arc(&mut outline, x + w - cr.bottom_right, y + h - cr.bottom_right, cr.bottom_right,
            0.0, FRAC_PI_2);
    // Bottom-left corner: arc from π/2 to π
    add_arc(&mut outline, x + cr.bottom_left, y + h - cr.bottom_left, cr.bottom_left,
            FRAC_PI_2, PI);

    let n = outline.len();
    if n < 3 { return; }

    let color_bytes = [
        (color.r * 255.0) as u8,
        (color.g * 255.0) as u8,
        (color.b * 255.0) as u8,
        (color.a * 255.0) as u8,
    ];

    let cx = x + w / 2.0;
    let cy = y + h / 2.0;

    let mut vertices = Vec::with_capacity(n + 1);
    // Center vertex (index 0)
    vertices.push(Vertex {
        position: Vec3::new(cx, cy, 0.0),
        uv: Vec2::new(0.5, 0.5),
        color: color_bytes,
        normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
    });
    // Outline vertices (indices 1..=n)
    for p in &outline {
        vertices.push(Vertex {
            position: Vec3::new(p.x, p.y, 0.0),
            uv: Vec2::new((p.x - x) / w, (p.y - y) / h),
            color: color_bytes,
            normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
        });
    }

    let mut indices = Vec::with_capacity(n * 3);
    for i in 0..n {
        indices.push(0u16); // center
        indices.push((i + 1) as u16);
        indices.push(((i + 1) % n + 1) as u16);
    }

    let mesh = Mesh {
        vertices,
        indices,
        texture: None,
    };
    draw_mesh(&mesh);
}

/// Draws a rounded rectangle rotated by `rotation_radians` around its center.
/// All outline vertices are rotated before building the triangle fan mesh.
/// `(x, y, w, h)` is the *original* (unrotated) bounding box — the centre of
/// rotation is `(x + w/2, y + h/2)`.
fn draw_good_rotated_rounded_rectangle(
    x: f32,
    y: f32,
    w: f32,
    h: f32,
    cr: &CornerRadii,
    color: Color,
    rotation_radians: f32,
    flip_x: bool,
    flip_y: bool,
) {
    use std::f32::consts::{FRAC_PI_2, PI};

    let cx = x + w / 2.0;
    let cy = y + h / 2.0;

    let cos_r = rotation_radians.cos();
    let sin_r = rotation_radians.sin();

    // Rotate a point around (cx, cy)
    let rotate_point = |px: f32, py: f32| -> Vec2 {
        // Apply flips relative to centre first
        let mut dx = px - cx;
        let mut dy = py - cy;
        if flip_x { dx = -dx; }
        if flip_y { dy = -dy; }
        let rx = dx * cos_r - dy * sin_r;
        let ry = dx * sin_r + dy * cos_r;
        Vec2::new(cx + rx, cy + ry)
    };

    // Build outline in local (unrotated) space, then rotate
    // Pre-allocate based on expected corner vertex count
    let est_verts = if cr.top_left == 0.0 && cr.top_right == 0.0 && cr.bottom_left == 0.0 && cr.bottom_right == 0.0 {
        4
    } else {
        [cr.top_left, cr.top_right, cr.bottom_left, cr.bottom_right]
            .iter()
            .map(|&r| if r <= 0.0 { 1 } else { ((FRAC_PI_2 * r) / PIXELS_PER_POINT).max(6.0) as usize + 1 })
            .sum::<usize>()
    };
    let mut outline: Vec<Vec2> = Vec::with_capacity(est_verts);

    let add_arc = |outline: &mut Vec<Vec2>, arc_cx: f32, arc_cy: f32, radius: f32, start_angle: f32, end_angle: f32| {
        if radius <= 0.0 {
            outline.push(rotate_point(arc_cx, arc_cy));
            return;
        }
        let sides = ((FRAC_PI_2 * radius) / PIXELS_PER_POINT).max(6.0) as usize;
        // Use incremental rotation to avoid per-point cos/sin
        let step = (end_angle - start_angle) / sides as f32;
        let step_cos = step.cos();
        let step_sin = step.sin();
        let mut dx = start_angle.cos() * radius;
        let mut dy = start_angle.sin() * radius;
        for _ in 0..=sides {
            outline.push(rotate_point(arc_cx + dx, arc_cy + dy));
            let new_dx = dx * step_cos - dy * step_sin;
            let new_dy = dx * step_sin + dy * step_cos;
            dx = new_dx;
            dy = new_dy;
        }
    };

    if cr.top_left == 0.0 && cr.top_right == 0.0 && cr.bottom_left == 0.0 && cr.bottom_right == 0.0 {
        // Sharp rectangle — just rotate 4 corners
        outline.push(rotate_point(x, y));
        outline.push(rotate_point(x + w, y));
        outline.push(rotate_point(x + w, y + h));
        outline.push(rotate_point(x, y + h));
    } else {
        add_arc(&mut outline, x + cr.top_left, y + cr.top_left, cr.top_left,
                PI, 3.0 * FRAC_PI_2);
        add_arc(&mut outline, x + w - cr.top_right, y + cr.top_right, cr.top_right,
                3.0 * FRAC_PI_2, 2.0 * PI);
        add_arc(&mut outline, x + w - cr.bottom_right, y + h - cr.bottom_right, cr.bottom_right,
                0.0, FRAC_PI_2);
        add_arc(&mut outline, x + cr.bottom_left, y + h - cr.bottom_left, cr.bottom_left,
                FRAC_PI_2, PI);
    }

    let n = outline.len();
    if n < 3 { return; }

    let color_bytes = [
        (color.r * 255.0) as u8,
        (color.g * 255.0) as u8,
        (color.b * 255.0) as u8,
        (color.a * 255.0) as u8,
    ];

    let center_rot = Vec2::new(cx, cy);

    let mut vertices = Vec::with_capacity(n + 1);
    vertices.push(Vertex {
        position: Vec3::new(center_rot.x, center_rot.y, 0.0),
        uv: Vec2::new(0.5, 0.5),
        color: color_bytes,
        normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
    });
    for p in &outline {
        vertices.push(Vertex {
            position: Vec3::new(p.x, p.y, 0.0),
            uv: Vec2::new((p.x - x) / w, (p.y - y) / h),
            color: color_bytes,
            normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
        });
    }

    let mut indices = Vec::with_capacity(n * 3);
    for i in 0..n {
        indices.push(0u16);
        indices.push((i + 1) as u16);
        indices.push(((i + 1) % n + 1) as u16);
    }

    draw_mesh(&Mesh { vertices, indices, texture: None });
}

/// Remap corner radii for a 90° clockwise rotation.
fn rotate_corner_radii_90(cr: &CornerRadii) -> CornerRadii {
    CornerRadii {
        top_left: cr.bottom_left,
        top_right: cr.top_left,
        bottom_right: cr.top_right,
        bottom_left: cr.bottom_right,
    }
}

/// Remap corner radii for a 180° rotation.
fn rotate_corner_radii_180(cr: &CornerRadii) -> CornerRadii {
    CornerRadii {
        top_left: cr.bottom_right,
        top_right: cr.bottom_left,
        bottom_right: cr.top_left,
        bottom_left: cr.top_right,
    }
}

/// Remap corner radii for a 270° clockwise rotation.
fn rotate_corner_radii_270(cr: &CornerRadii) -> CornerRadii {
    CornerRadii {
        top_left: cr.top_right,
        top_right: cr.bottom_right,
        bottom_right: cr.bottom_left,
        bottom_left: cr.top_left,
    }
}

/// Apply flip_x and flip_y to corner radii (before rotation).
fn flip_corner_radii(cr: &CornerRadii, flip_x: bool, flip_y: bool) -> CornerRadii {
    let mut result = cr.clone();
    if flip_x {
        std::mem::swap(&mut result.top_left, &mut result.top_right);
        std::mem::swap(&mut result.bottom_left, &mut result.bottom_right);
    }
    if flip_y {
        std::mem::swap(&mut result.top_left, &mut result.bottom_left);
        std::mem::swap(&mut result.top_right, &mut result.bottom_right);
    }
    result
}

struct RenderState {
    clip: Option<(i32, i32, i32, i32)>,
    /// Render target stack for group effects (shaders and/or visual rotation).
    rt_stack: Vec<(RenderTarget, Option<crate::shaders::ShaderConfig>, Option<crate::engine::VisualRotationConfig>, BoundingBox)>,
    #[cfg(feature = "text-styling")]
    style_stack: Vec<String>,
    #[cfg(feature = "text-styling")]
    total_char_index: usize,
}

impl RenderState {
    fn new() -> Self {
        Self {
            clip: None,
            rt_stack: Vec::new(),
            #[cfg(feature = "text-styling")]
            style_stack: Vec::new(),
            #[cfg(feature = "text-styling")]
            total_char_index: 0,
        }
    }
}

/// Render custom content to a [`Texture2D`]
///
/// Sets up a render target, points a camera at it, calls your closure, then
/// restores the default camera and returns the resulting texture.
/// The coordinate system inside the closure runs from `(0, 0)` at the top-left
/// to `(width, height)` at the bottom-right.
///
/// Call this before the layout pass, then hand the texture to an element with `.image(tex)`.
///
/// # Example
/// ```rust,ignore
/// let tex = render_to_texture(200.0, 100.0, || {
///     clear_background(BLANK);
///     draw_circle(w / 2.0, h / 2.0, 40.0, RED);
/// });
/// ```
pub fn render_to_texture(width: f32, height: f32, draw: impl FnOnce()) -> Texture2D {
    let render_target = render_target_msaa(width as u32, height as u32);
    render_target.texture.set_filter(FilterMode::Linear);
    let mut cam = Camera2D::from_display_rect(Rect::new(0.0, 0.0, width, height));
    cam.render_target = Some(render_target.clone());
    set_camera(&cam);

    draw();

    set_default_camera();
    render_target.texture
}

fn rounded_rectangle_texture(cr: &CornerRadii, bb: &BoundingBox, clip: &Option<(i32, i32, i32, i32)>) -> Texture2D {
    let render_target = render_target_msaa(bb.width as u32, bb.height as u32);
    render_target.texture.set_filter(FilterMode::Linear);
    let mut cam = Camera2D::from_display_rect(Rect::new(0.0, 0.0, bb.width, bb.height));
    cam.render_target = Some(render_target.clone());
    set_camera(&cam);
    unsafe {
        get_internal_gl().quad_gl.scissor(None);
    };

    draw_good_rounded_rectangle(0.0, 0.0, bb.width, bb.height, cr, WHITE);

    set_default_camera();
    unsafe {
        get_internal_gl().quad_gl.scissor(*clip);
    }
    render_target.texture
}

/// Render a TinyVG image to a RenderTarget, scaled to fit the given dimensions.
/// Decodes from raw bytes, then delegates to `render_tinyvg_image`.
#[cfg(feature = "tinyvg")]
fn render_tinyvg_texture(
    tvg_data: &[u8],
    dest_width: f32,
    dest_height: f32,
    clip: &Option<(i32, i32, i32, i32)>,
) -> Option<RenderTarget> {
    use tinyvg::Decoder;
    let decoder = Decoder::new(std::io::Cursor::new(tvg_data));
    let image = match decoder.decode() {
        Ok(img) => img,
        Err(_) => return None,
    };
    render_tinyvg_image(&image, dest_width, dest_height, clip)
}

/// Render a decoded `tinyvg::format::Image` to a RenderTarget, scaled to fit the given dimensions.
#[cfg(feature = "tinyvg")]
fn render_tinyvg_image(
    image: &tinyvg::format::Image,
    dest_width: f32,
    dest_height: f32,
    clip: &Option<(i32, i32, i32, i32)>,
) -> Option<RenderTarget> {
    use tinyvg::format::{Command, Style, Segment, SegmentCommandKind, Point as TvgPoint, Color as TvgColor};
    use kurbo::{BezPath, Point as KurboPoint, Vec2 as KurboVec2, ParamCurve, SvgArc, Arc as KurboArc, PathEl};
    use lyon::tessellation::{FillTessellator, FillOptions, VertexBuffers, BuffersBuilder, FillVertex, FillRule};
    use lyon::path::Path as LyonPath;
    use lyon::math::point as lyon_point;
    
    fn tvg_to_kurbo(p: TvgPoint) -> KurboPoint {
        KurboPoint::new(p.x, p.y)
    }
    
    let tvg_width = image.header.width as f32;
    let tvg_height = image.header.height as f32;
    let scale_x = dest_width / tvg_width;
    let scale_y = dest_height / tvg_height;
    
    let render_target = render_target_msaa(dest_width as u32, dest_height as u32);
    render_target.texture.set_filter(FilterMode::Linear);
    let mut cam = Camera2D::from_display_rect(Rect::new(0.0, 0.0, dest_width, dest_height));
    cam.render_target = Some(render_target.clone());
    set_camera(&cam);
    unsafe {
        get_internal_gl().quad_gl.scissor(None);
    }
    
    let tvg_to_mq_color = |c: &TvgColor| -> Color {
        let (r, g, b, a) = c.as_rgba();
        Color::new(r as f32, g as f32, b as f32, a as f32)
    };
    
    let style_to_color = |style: &Style, color_table: &[TvgColor]| -> Color {
        match style {
            Style::FlatColor { color_index } => {
                color_table.get(*color_index).map(|c| tvg_to_mq_color(c)).unwrap_or(WHITE)
            }
            Style::LinearGradient { color_index_0, .. } |
            Style::RadialGradient { color_index_0, .. } => {
                color_table.get(*color_index_0).map(|c| tvg_to_mq_color(c)).unwrap_or(WHITE)
            }
        }
    };
    
    let draw_filled_path_lyon = |bezpath: &BezPath, color: Color| {
        let mut builder = LyonPath::builder();
        let mut subpath_started = false;
        
        for el in bezpath.iter() {
            match el {
                PathEl::MoveTo(p) => {
                    if subpath_started {
                        builder.end(false);
                    }
                    builder.begin(lyon_point((p.x * scale_x as f64) as f32, (p.y * scale_y as f64) as f32));
                    subpath_started = true;
                }
                PathEl::LineTo(p) => {
                    builder.line_to(lyon_point((p.x * scale_x as f64) as f32, (p.y * scale_y as f64) as f32));
                }
                PathEl::QuadTo(c, p) => {
                    builder.quadratic_bezier_to(
                        lyon_point((c.x * scale_x as f64) as f32, (c.y * scale_y as f64) as f32),
                        lyon_point((p.x * scale_x as f64) as f32, (p.y * scale_y as f64) as f32),
                    );
                }
                PathEl::CurveTo(c1, c2, p) => {
                    builder.cubic_bezier_to(
                        lyon_point((c1.x * scale_x as f64) as f32, (c1.y * scale_y as f64) as f32),
                        lyon_point((c2.x * scale_x as f64) as f32, (c2.y * scale_y as f64) as f32),
                        lyon_point((p.x * scale_x as f64) as f32, (p.y * scale_y as f64) as f32),
                    );
                }
                PathEl::ClosePath => {
                    builder.end(true);
                    subpath_started = false;
                }
            }
        }
        
        if subpath_started {
            builder.end(true);
        }
        
        let lyon_path = builder.build();
        
        let mut geometry: VertexBuffers<[f32; 2], u16> = VertexBuffers::new();
        let mut tessellator = FillTessellator::new();
        
        let fill_options = FillOptions::default().with_fill_rule(FillRule::NonZero);
        
        let result = tessellator.tessellate_path(
            &lyon_path,
            &fill_options,
            &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
                vertex.position().to_array()
            }),
        );
        
        if result.is_err() || geometry.indices.is_empty() {
            return;
        }
        
        let color_bytes = [(color.r * 255.0) as u8, (color.g * 255.0) as u8, (color.b * 255.0) as u8, (color.a * 255.0) as u8];
        
        let vertices: Vec<Vertex> = geometry.vertices.iter().map(|pos| {
            Vertex {
                position: Vec3::new(pos[0], pos[1], 0.0),
                uv: Vec2::ZERO,
                color: color_bytes,
                normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
            }
        }).collect();
        
        let mesh = Mesh {
            vertices,
            indices: geometry.indices,
            texture: None,
        };
        draw_mesh(&mesh);
    };
    
    let draw_filled_polygon_tvg = |points: &[TvgPoint], color: Color| {
        if points.len() < 3 {
            return;
        }
        
        let mut builder = LyonPath::builder();
        builder.begin(lyon_point(points[0].x as f32 * scale_x, points[0].y as f32 * scale_y));
        for point in &points[1..] {
            builder.line_to(lyon_point(point.x as f32 * scale_x, point.y as f32 * scale_y));
        }
        builder.end(true);
        let lyon_path = builder.build();
        
        let mut geometry: VertexBuffers<[f32; 2], u16> = VertexBuffers::new();
        let mut tessellator = FillTessellator::new();
        
        let result = tessellator.tessellate_path(
            &lyon_path,
            &FillOptions::default(),
            &mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
                vertex.position().to_array()
            }),
        );
        
        if result.is_err() || geometry.indices.is_empty() {
            return;
        }
        
        let color_bytes = [(color.r * 255.0) as u8, (color.g * 255.0) as u8, (color.b * 255.0) as u8, (color.a * 255.0) as u8];
        
        let vertices: Vec<Vertex> = geometry.vertices.iter().map(|pos| {
            Vertex {
                position: Vec3::new(pos[0], pos[1], 0.0),
                uv: Vec2::ZERO,
                color: color_bytes,
                normal: Vec4::new(0.0, 0.0, 1.0, 0.0),
            }
        }).collect();
        
        let mesh = Mesh {
            vertices,
            indices: geometry.indices,
            texture: None,
        };
        draw_mesh(&mesh);
    };
    
    let build_bezpath = |segments: &[Segment]| -> BezPath {
        let mut bezier = BezPath::new();
        for segment in segments {
            let start = tvg_to_kurbo(segment.start);
            let mut pen = start;
            bezier.move_to(pen);
            
            for cmd in &segment.commands {
                match &cmd.kind {
                    SegmentCommandKind::Line { end } => {
                        let end_k = tvg_to_kurbo(*end);
                        bezier.line_to(end_k);
                        pen = end_k;
                    }
                    SegmentCommandKind::HorizontalLine { x } => {
                        let end = KurboPoint::new(*x, pen.y);
                        bezier.line_to(end);
                        pen = end;
                    }
                    SegmentCommandKind::VerticalLine { y } => {
                        let end = KurboPoint::new(pen.x, *y);
                        bezier.line_to(end);
                        pen = end;
                    }
                    SegmentCommandKind::CubicBezier { control_0, control_1, point_1 } => {
                        let c0 = tvg_to_kurbo(*control_0);
                        let c1 = tvg_to_kurbo(*control_1);
                        let p1 = tvg_to_kurbo(*point_1);
                        bezier.curve_to(c0, c1, p1);
                        pen = p1;
                    }
                    SegmentCommandKind::QuadraticBezier { control, point_1 } => {
                        let c = tvg_to_kurbo(*control);
                        let p1 = tvg_to_kurbo(*point_1);
                        bezier.quad_to(c, p1);
                        pen = p1;
                    }
                    SegmentCommandKind::ArcEllipse { large, sweep, radius_x, radius_y, rotation, target } => {
                        let target_k = tvg_to_kurbo(*target);
                        let svg_arc = SvgArc {
                            from: pen,
                            to: target_k,
                            radii: KurboVec2::new(*radius_x, *radius_y),
                            x_rotation: *rotation,
                            large_arc: *large,
                            sweep: *sweep,
                        };
                        if let Some(arc) = KurboArc::from_svg_arc(&svg_arc) {
                            for seg in arc.append_iter(0.2) {
                                bezier.push(seg);
                            }
                        }
                        pen = target_k;
                    }
                    SegmentCommandKind::ClosePath => {
                        bezier.close_path();
                        pen = start;
                    }
                }
            }
        }
        bezier
    };
    
    let line_scale = (scale_x + scale_y) / 2.0;
    
    for cmd in &image.commands {
        match cmd {
            Command::FillPath { fill_style, path, outline } => {
                let fill_color = style_to_color(fill_style, &image.color_table);
                let bezpath = build_bezpath(path);
                draw_filled_path_lyon(&bezpath, fill_color);
                
                if let Some(outline_style) = outline {
                    let line_color = style_to_color(&outline_style.line_style, &image.color_table);
                    let line_width = outline_style.line_width as f32 * line_scale;
                    for segment in path {
                        let start = segment.start;
                        let mut pen = start;
                        for cmd in &segment.commands {
                            match &cmd.kind {
                                SegmentCommandKind::Line { end } => {
                                    draw_line(
                                        pen.x as f32 * scale_x, pen.y as f32 * scale_y,
                                        end.x as f32 * scale_x, end.y as f32 * scale_y,
                                        line_width, line_color
                                    );
                                    pen = *end;
                                }
                                SegmentCommandKind::HorizontalLine { x } => {
                                    let end = TvgPoint { x: *x, y: pen.y };
                                    draw_line(
                                        pen.x as f32 * scale_x, pen.y as f32 * scale_y,
                                        end.x as f32 * scale_x, end.y as f32 * scale_y,
                                        line_width, line_color
                                    );
                                    pen = end;
                                }
                                SegmentCommandKind::VerticalLine { y } => {
                                    let end = TvgPoint { x: pen.x, y: *y };
                                    draw_line(
                                        pen.x as f32 * scale_x, pen.y as f32 * scale_y,
                                        end.x as f32 * scale_x, end.y as f32 * scale_y,
                                        line_width, line_color
                                    );
                                    pen = end;
                                }
                                SegmentCommandKind::ClosePath => {
                                    draw_line(
                                        pen.x as f32 * scale_x, pen.y as f32 * scale_y,
                                        start.x as f32 * scale_x, start.y as f32 * scale_y,
                                        line_width, line_color
                                    );
                                    pen = start;
                                }
                                SegmentCommandKind::CubicBezier { control_0, control_1, point_1 } => {
                                    let c0 = tvg_to_kurbo(*control_0);
                                    let c1 = tvg_to_kurbo(*control_1);
                                    let p1 = tvg_to_kurbo(*point_1);
                                    let p0 = tvg_to_kurbo(pen);
                                    let cubic = kurbo::CubicBez::new(p0, c0, c1, p1);
                                    let steps = 16usize;
                                    let mut prev = p0;
                                    for i in 1..=steps {
                                        let t = i as f64 / steps as f64;
                                        let next = cubic.eval(t);
                                        draw_line(
                                            prev.x as f32 * scale_x, prev.y as f32 * scale_y,
                                            next.x as f32 * scale_x, next.y as f32 * scale_y,
                                            line_width, line_color
                                        );
                                        prev = next;
                                    }
                                    pen = *point_1;
                                }
                                SegmentCommandKind::QuadraticBezier { control, point_1 } => {
                                    let c = tvg_to_kurbo(*control);
                                    let p1 = tvg_to_kurbo(*point_1);
                                    let p0 = tvg_to_kurbo(pen);
                                    let quad = kurbo::QuadBez::new(p0, c, p1);
                                    let steps = 12usize;
                                    let mut prev = p0;
                                    for i in 1..=steps {
                                        let t = i as f64 / steps as f64;
                                        let next = quad.eval(t);
                                        draw_line(
                                            prev.x as f32 * scale_x, prev.y as f32 * scale_y,
                                            next.x as f32 * scale_x, next.y as f32 * scale_y,
                                            line_width, line_color
                                        );
                                        prev = next;
                                    }
                                    pen = *point_1;
                                }
                                SegmentCommandKind::ArcEllipse { large, sweep, radius_x, radius_y, rotation, target } => {
                                    let target_k = tvg_to_kurbo(*target);
                                    let p0 = tvg_to_kurbo(pen);
                                    let svg_arc = SvgArc {
                                        from: p0,
                                        to: target_k,
                                        radii: KurboVec2::new(*radius_x, *radius_y),
                                        x_rotation: *rotation,
                                        large_arc: *large,
                                        sweep: *sweep,
                                    };
                                    if let Some(arc) = KurboArc::from_svg_arc(&svg_arc) {
                                        let mut prev = p0;
                                        for seg in arc.append_iter(0.2) {
                                            match seg {
                                                PathEl::LineTo(p) | PathEl::MoveTo(p) => {
                                                    draw_line(
                                                        prev.x as f32 * scale_x, prev.y as f32 * scale_y,
                                                        p.x as f32 * scale_x, p.y as f32 * scale_y,
                                                        line_width, line_color
                                                    );
                                                    prev = p;
                                                }
                                                PathEl::CurveTo(c0, c1, p) => {
                                                    // Flatten the curve
                                                    let cubic = kurbo::CubicBez::new(prev, c0, c1, p);
                                                    let steps = 8usize;
                                                    let mut prev_pt = prev;
                                                    for j in 1..=steps {
                                                        let t = j as f64 / steps as f64;
                                                        let next = cubic.eval(t);
                                                        draw_line(
                                                            prev_pt.x as f32 * scale_x, prev_pt.y as f32 * scale_y,
                                                            next.x as f32 * scale_x, next.y as f32 * scale_y,
                                                            line_width, line_color
                                                        );
                                                        prev_pt = next;
                                                    }
                                                    prev = p;
                                                }
                                                _ => {}
                                            }
                                        }
                                    }
                                    pen = *target;
                                }
                            }
                        }
                    }
                }
            }
            Command::FillRectangles { fill_style, rectangles, outline } => {
                let fill_color = style_to_color(fill_style, &image.color_table);
                for rect in rectangles {
                    draw_rectangle(
                        rect.x0 as f32 * scale_x,
                        rect.y0 as f32 * scale_y,
                        rect.width() as f32 * scale_x,
                        rect.height() as f32 * scale_y,
                        fill_color
                    );
                }
                
                if let Some(outline_style) = outline {
                    let line_color = style_to_color(&outline_style.line_style, &image.color_table);
                    let line_width = outline_style.line_width as f32 * line_scale;
                    for rect in rectangles {
                        draw_rectangle_lines(
                            rect.x0 as f32 * scale_x,
                            rect.y0 as f32 * scale_y,
                            rect.width() as f32 * scale_x,
                            rect.height() as f32 * scale_y,
                            line_width, line_color
                        );
                    }
                }
            }
            Command::FillPolygon { fill_style, polygon, outline } => {
                let fill_color = style_to_color(fill_style, &image.color_table);
                draw_filled_polygon_tvg(polygon, fill_color);
                
                if let Some(outline_style) = outline {
                    let line_color = style_to_color(&outline_style.line_style, &image.color_table);
                    let line_width = outline_style.line_width as f32 * line_scale;
                    for i in 0..polygon.len() {
                        let next = (i + 1) % polygon.len();
                        draw_line(
                            polygon[i].x as f32 * scale_x, polygon[i].y as f32 * scale_y,
                            polygon[next].x as f32 * scale_x, polygon[next].y as f32 * scale_y,
                            line_width, line_color
                        );
                    }
                }
            }
            Command::DrawLines { line_style, line_width, lines } => {
                let line_color = style_to_color(line_style, &image.color_table);
                for line in lines {
                    draw_line(
                        line.p0.x as f32 * scale_x, line.p0.y as f32 * scale_y,
                        line.p1.x as f32 * scale_x, line.p1.y as f32 * scale_y,
                        *line_width as f32 * line_scale, line_color
                    );
                }
            }
            Command::DrawLineLoop { line_style, line_width, close_path, points } => {
                let line_color = style_to_color(line_style, &image.color_table);
                for i in 0..points.len().saturating_sub(1) {
                    draw_line(
                        points[i].x as f32 * scale_x, points[i].y as f32 * scale_y,
                        points[i+1].x as f32 * scale_x, points[i+1].y as f32 * scale_y,
                        *line_width as f32 * line_scale, line_color
                    );
                }
                if *close_path && points.len() >= 2 {
                    let last = points.len() - 1;
                    draw_line(
                        points[last].x as f32 * scale_x, points[last].y as f32 * scale_y,
                        points[0].x as f32 * scale_x, points[0].y as f32 * scale_y,
                        *line_width as f32 * line_scale, line_color
                    );
                }
            }
            Command::DrawLinePath { line_style, line_width, path } => {
                let line_color = style_to_color(line_style, &image.color_table);
                let scaled_line_width = *line_width as f32 * line_scale;
                // Draw line path by tracing segments directly
                for segment in path {
                    let start = segment.start;
                    let mut pen = start;
                    for cmd in &segment.commands {
                        match &cmd.kind {
                            SegmentCommandKind::Line { end } => {
                                draw_line(
                                    pen.x as f32 * scale_x, pen.y as f32 * scale_y,
                                    end.x as f32 * scale_x, end.y as f32 * scale_y,
                                    scaled_line_width, line_color
                                );
                                pen = *end;
                            }
                            SegmentCommandKind::HorizontalLine { x } => {
                                let end = TvgPoint { x: *x, y: pen.y };
                                draw_line(
                                    pen.x as f32 * scale_x, pen.y as f32 * scale_y,
                                    end.x as f32 * scale_x, end.y as f32 * scale_y,
                                    scaled_line_width, line_color
                                );
                                pen = end;
                            }
                            SegmentCommandKind::VerticalLine { y } => {
                                let end = TvgPoint { x: pen.x, y: *y };
                                draw_line(
                                    pen.x as f32 * scale_x, pen.y as f32 * scale_y,
                                    end.x as f32 * scale_x, end.y as f32 * scale_y,
                                    scaled_line_width, line_color
                                );
                                pen = end;
                            }
                            SegmentCommandKind::ClosePath => {
                                draw_line(
                                    pen.x as f32 * scale_x, pen.y as f32 * scale_y,
                                    start.x as f32 * scale_x, start.y as f32 * scale_y,
                                    scaled_line_width, line_color
                                );
                                pen = start;
                            }
                            // For curves, we need to flatten them for line drawing
                            SegmentCommandKind::CubicBezier { control_0, control_1, point_1 } => {
                                let c0 = tvg_to_kurbo(*control_0);
                                let c1 = tvg_to_kurbo(*control_1);
                                let p1 = tvg_to_kurbo(*point_1);
                                let p0 = tvg_to_kurbo(pen);
                                let cubic = kurbo::CubicBez::new(p0, c0, c1, p1);
                                let steps = 16usize;
                                let mut prev = p0;
                                for i in 1..=steps {
                                    let t = i as f64 / steps as f64;
                                    let next = cubic.eval(t);
                                    draw_line(
                                        prev.x as f32 * scale_x, prev.y as f32 * scale_y,
                                        next.x as f32 * scale_x, next.y as f32 * scale_y,
                                        scaled_line_width, line_color
                                    );
                                    prev = next;
                                }
                                pen = *point_1;
                            }
                            SegmentCommandKind::QuadraticBezier { control, point_1 } => {
                                let c = tvg_to_kurbo(*control);
                                let p1 = tvg_to_kurbo(*point_1);
                                let p0 = tvg_to_kurbo(pen);
                                let quad = kurbo::QuadBez::new(p0, c, p1);
                                let steps = 12usize;
                                let mut prev = p0;
                                for i in 1..=steps {
                                    let t = i as f64 / steps as f64;
                                    let next = quad.eval(t);
                                    draw_line(
                                        prev.x as f32 * scale_x, prev.y as f32 * scale_y,
                                        next.x as f32 * scale_x, next.y as f32 * scale_y,
                                        scaled_line_width, line_color
                                    );
                                    prev = next;
                                }
                                pen = *point_1;
                            }
                            SegmentCommandKind::ArcEllipse { large, sweep, radius_x, radius_y, rotation, target } => {
                                let target_k = tvg_to_kurbo(*target);
                                let p0 = tvg_to_kurbo(pen);
                                let svg_arc = SvgArc {
                                    from: p0,
                                    to: target_k,
                                    radii: KurboVec2::new(*radius_x, *radius_y),
                                    x_rotation: *rotation,
                                    large_arc: *large,
                                    sweep: *sweep,
                                };
                                if let Some(arc) = KurboArc::from_svg_arc(&svg_arc) {
                                    let mut prev = p0;
                                    for seg in arc.append_iter(0.2) {
                                        match seg {
                                            PathEl::LineTo(p) | PathEl::MoveTo(p) => {
                                                draw_line(
                                                    prev.x as f32 * scale_x, prev.y as f32 * scale_y,
                                                    p.x as f32 * scale_x, p.y as f32 * scale_y,
                                                    scaled_line_width, line_color
                                                );
                                                prev = p;
                                            }
                                            PathEl::CurveTo(c0, c1, p) => {
                                                // Flatten the curve
                                                let cubic = kurbo::CubicBez::new(prev, c0, c1, p);
                                                let steps = 8usize;
                                                let mut prev_pt = prev;
                                                for j in 1..=steps {
                                                    let t = j as f64 / steps as f64;
                                                    let next = cubic.eval(t);
                                                    draw_line(
                                                        prev_pt.x as f32 * scale_x, prev_pt.y as f32 * scale_y,
                                                        next.x as f32 * scale_x, next.y as f32 * scale_y,
                                                        scaled_line_width, line_color
                                                    );
                                                    prev_pt = next;
                                                }
                                                prev = p;
                                            }
                                            _ => {}
                                        }
                                    }
                                }
                                pen = *target;
                            }
                        }
                    }
                }
            }
        }
    }
    
    set_default_camera();
    unsafe {
        get_internal_gl().quad_gl.scissor(*clip);
    }
    
    Some(render_target)
}

fn resize(texture: &Texture2D, height: f32, width: f32, clip: &Option<(i32, i32, i32, i32)>) -> Texture2D {
    let render_target = render_target_msaa(width as u32, height as u32);
    render_target.texture.set_filter(FilterMode::Linear);
    let mut cam = Camera2D::from_display_rect(Rect::new(0.0, 0.0, width, height));
    cam.render_target = Some(render_target.clone());
    set_camera(&cam);
    unsafe {
        get_internal_gl().quad_gl.scissor(None);
    };
    draw_texture_ex(
        texture,
        0.0,
        0.0,
        WHITE,
        DrawTextureParams {
            dest_size: Some(Vec2::new(width, height)),
            flip_y: true,
            ..Default::default()
        },
    );
    set_default_camera();
    unsafe {
        get_internal_gl().quad_gl.scissor(*clip);
    }
    render_target.texture
}

/// Draws all render commands to the screen using macroquad.
pub async fn render<CustomElementData: Clone + Default + std::fmt::Debug>(
    commands: Vec<RenderCommand<CustomElementData>>,
    handle_custom_command: impl Fn(&RenderCommand<CustomElementData>),
) {
    let mut state = RenderState::new();
    for command in commands {
        match &command.config {
            RenderCommandConfig::Image(image) => {
                let bb = command.bounding_box;
                let cr = &image.corner_radii;
                let mut tint = ply_to_macroquad_color(&image.background_color);
                if tint == Color::new(0.0, 0.0, 0.0, 0.0) {
                    tint = Color::new(1.0, 1.0, 1.0, 1.0);
                }

                match &image.data {
                    ImageSource::Texture(tex) => {
                        // Direct GPU texture — draw immediately, no TextureManager
                        let has_corner_radii = cr.top_left > 0.0 || cr.top_right > 0.0 || cr.bottom_left > 0.0 || cr.bottom_right > 0.0;
                        if !has_corner_radii {
                            draw_texture_ex(
                                tex,
                                bb.x,
                                bb.y,
                                tint,
                                DrawTextureParams {
                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
                                    ..Default::default()
                                },
                            );
                        } else {
                            let mut manager = TEXTURE_MANAGER.lock().unwrap();
                            // Use texture raw pointer as a unique key for the corner-radii variant
                            let key = format!(
                                "tex-proc:{:?}:{}:{}:{}:{}:{}:{}:{:?}",
                                tex.raw_miniquad_id(),
                                bb.width, bb.height,
                                cr.top_left, cr.top_right, cr.bottom_left, cr.bottom_right,
                                state.clip
                            );
                            let texture = manager.get_or_create(key, || {
                                let mut resized_image: Image = resize(tex, bb.height, bb.width, &state.clip).get_texture_data();
                                let rounded_rect: Image = rounded_rectangle_texture(cr, &bb, &state.clip).get_texture_data();
                                for i in 0..resized_image.bytes.len()/4 {
                                    let this_alpha = resized_image.bytes[i * 4 + 3] as f32 / 255.0;
                                    let mask_alpha = rounded_rect.bytes[i * 4 + 3] as f32 / 255.0;
                                    resized_image.bytes[i * 4 + 3] = (this_alpha * mask_alpha * 255.0) as u8;
                                }
                                Texture2D::from_image(&resized_image)
                            });
                            draw_texture_ex(
                                texture,
                                bb.x,
                                bb.y,
                                tint,
                                DrawTextureParams {
                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
                                    ..Default::default()
                                },
                            );
                        }
                    }
                    #[cfg(feature = "tinyvg")]
                    ImageSource::TinyVg(tvg_image) => {
                        // Procedural TinyVG — rasterize every frame (no caching, content may change)
                        let has_corner_radii = cr.top_left > 0.0 || cr.top_right > 0.0 || cr.bottom_left > 0.0 || cr.bottom_right > 0.0;
                        if let Some(tvg_rt) = render_tinyvg_image(tvg_image, bb.width, bb.height, &state.clip) {
                            let final_texture = if has_corner_radii {
                                let mut tvg_img: Image = tvg_rt.texture.get_texture_data();
                                let rounded_rect: Image = rounded_rectangle_texture(cr, &bb, &state.clip).get_texture_data();
                                for i in 0..tvg_img.bytes.len()/4 {
                                    let this_alpha = tvg_img.bytes[i * 4 + 3] as f32 / 255.0;
                                    let mask_alpha = rounded_rect.bytes[i * 4 + 3] as f32 / 255.0;
                                    tvg_img.bytes[i * 4 + 3] = (this_alpha * mask_alpha * 255.0) as u8;
                                }
                                Texture2D::from_image(&tvg_img)
                            } else {
                                tvg_rt.texture.clone()
                            };
                            draw_texture_ex(
                                &final_texture,
                                bb.x,
                                bb.y,
                                tint,
                                DrawTextureParams {
                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
                                    flip_y: true,
                                    ..Default::default()
                                },
                            );
                        }
                    }
                    ImageSource::Asset(ga) => {
                        // Static asset — existing behavior
                        let mut manager = TEXTURE_MANAGER.lock().unwrap();

                        #[cfg(feature = "tinyvg")]
                        let is_tvg = ga.get_name().to_lowercase().ends_with(".tvg");
                        #[cfg(not(feature = "tinyvg"))]
                        let is_tvg = false;

                        #[cfg(feature = "tinyvg")]
                        if is_tvg {
                            let key = format!(
                                "tvg:{}:{}:{}:{}:{}:{}:{}:{:?}",
                                ga.get_name(),
                                bb.width, bb.height,
                                cr.top_left, cr.top_right, cr.bottom_left, cr.bottom_right,
                                state.clip
                            );
                            let has_corner_radii = cr.top_left > 0.0 || cr.top_right > 0.0 || cr.bottom_left > 0.0 || cr.bottom_right > 0.0;
                            let texture = if !has_corner_radii {
                                // No corner radii — cache the render target to keep its GL texture alive
                                if let Some(cached) = manager.get(&key) {
                                    cached
                                } else {
                                    match ga {
                                        GraphicAsset::Path(path) => {
                                            match load_file(resolve_asset_path(path)).await {
                                                Ok(tvg_bytes) => {
                                                    if let Some(tvg_rt) = render_tinyvg_texture(&tvg_bytes, bb.width, bb.height, &state.clip) {
                                                        manager.cache(key.clone(), tvg_rt)
                                                    } else {
                                                        warn!("Failed to load TinyVG image: {}", path);
                                                        manager.cache(key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
                                                    }
                                                }
                                                Err(error) => {
                                                    warn!("Failed to load TinyVG file: {}. Error: {}", path, error);
                                                    manager.cache(key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
                                                }
                                            }
                                        }
                                        GraphicAsset::Bytes { file_name, data: tvg_bytes } => {
                                            if let Some(tvg_rt) = render_tinyvg_texture(tvg_bytes, bb.width, bb.height, &state.clip) {
                                                manager.cache(key.clone(), tvg_rt)
                                            } else {
                                                warn!("Failed to load TinyVG image: {}", file_name);
                                                manager.cache(key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
                                            }
                                        }
                                    }
                                }
                            } else {
                                let zerocr_key = format!(
                                    "tvg:{}:{}:{}:{}:{}:{}:{}:{:?}",
                                    ga.get_name(),
                                    bb.width, bb.height,
                                    0.0, 0.0, 0.0, 0.0,
                                    state.clip
                                );
                                let base_texture = if let Some(cached) = manager.get(&zerocr_key) {
                                    cached
                                } else {
                                    match ga {
                                        GraphicAsset::Path(path) => {
                                            match load_file(resolve_asset_path(path)).await {
                                                Ok(tvg_bytes) => {
                                                    if let Some(tvg_rt) = render_tinyvg_texture(&tvg_bytes, bb.width, bb.height, &state.clip) {
                                                        manager.cache(zerocr_key.clone(), tvg_rt)
                                                    } else {
                                                        warn!("Failed to load TinyVG image: {}", path);
                                                        manager.cache(zerocr_key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
                                                    }
                                                }
                                                Err(error) => {
                                                    warn!("Failed to load TinyVG file: {}. Error: {}", path, error);
                                                    manager.cache(zerocr_key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
                                                }
                                            }
                                        }
                                        GraphicAsset::Bytes { file_name, data: tvg_bytes } => {
                                            if let Some(tvg_rt) = render_tinyvg_texture(tvg_bytes, bb.width, bb.height, &state.clip) {
                                                manager.cache(zerocr_key.clone(), tvg_rt)
                                            } else {
                                                warn!("Failed to load TinyVG image: {}", file_name);
                                                manager.cache(zerocr_key.clone(), Texture2D::from_rgba8(1, 1, &[0, 0, 0, 0]))
                                            }
                                        }
                                    }
                                }.clone();
                                manager.get_or_create(key, || {
                                    let mut tvg_image: Image = base_texture.get_texture_data();
                                    let rounded_rect: Image = rounded_rectangle_texture(cr, &bb, &state.clip).get_texture_data();
                                    for i in 0..tvg_image.bytes.len()/4 {
                                        let this_alpha = tvg_image.bytes[i * 4 + 3] as f32 / 255.0;
                                        let mask_alpha = rounded_rect.bytes[i * 4 + 3] as f32 / 255.0;
                                        tvg_image.bytes[i * 4 + 3] = (this_alpha * mask_alpha * 255.0) as u8;
                                    }
                                    Texture2D::from_image(&tvg_image)
                                })
                            };
                            draw_texture_ex(
                                texture,
                                bb.x,
                                bb.y,
                                tint,
                                DrawTextureParams {
                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
                                    flip_y: true,
                                    ..Default::default()
                                },
                            );
                            continue;
                        }

                        if !is_tvg && cr.top_left == 0.0 && cr.top_right == 0.0 && cr.bottom_left == 0.0 && cr.bottom_right == 0.0 {
                            let texture = match ga {
                                GraphicAsset::Path(path) => manager.get_or_load(path).await,
                                GraphicAsset::Bytes { file_name, data } => {
                                    manager.get_or_create(file_name.to_string(), || {
                                        Texture2D::from_file_with_format(data, None)
                                    })
                                }
                            };
                            draw_texture_ex(
                                texture,
                                bb.x,
                                bb.y,
                                tint,
                                DrawTextureParams {
                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
                                    ..Default::default()
                                },
                            );
                        } else {
                            let source_texture = match ga {
                                GraphicAsset::Path(path) => manager.get_or_load(path).await.clone(),
                                GraphicAsset::Bytes { file_name, data } => {
                                    manager.get_or_create(file_name.to_string(), || {
                                        Texture2D::from_file_with_format(data, None)
                                    }).clone()
                                }
                            };
                            let key = format!(
                                "image:{}:{}:{}:{}:{}:{}:{}:{:?}",
                                ga.get_name(),
                                bb.width, bb.height,
                                cr.top_left, cr.top_right, cr.bottom_left, cr.bottom_right,
                                state.clip
                            );
                            let texture = manager.get_or_create(key, || {
                                let mut resized_image: Image = resize(&source_texture, bb.height, bb.width, &state.clip).get_texture_data();
                                let rounded_rect: Image = rounded_rectangle_texture(cr, &bb, &state.clip).get_texture_data();
                                for i in 0..resized_image.bytes.len()/4 {
                                    let this_alpha = resized_image.bytes[i * 4 + 3] as f32 / 255.0;
                                    let mask_alpha = rounded_rect.bytes[i * 4 + 3] as f32 / 255.0;
                                    resized_image.bytes[i * 4 + 3] = (this_alpha * mask_alpha * 255.0) as u8;
                                }
                                Texture2D::from_image(&resized_image)
                            });
                            draw_texture_ex(
                                texture,
                                bb.x,
                                bb.y,
                                tint,
                                DrawTextureParams {
                                    dest_size: Some(Vec2::new(bb.width, bb.height)),
                                    ..Default::default()
                                },
                            );
                        }
                    }
                }
            }
            RenderCommandConfig::Rectangle(config) => {
                let bb = command.bounding_box;
                let color = ply_to_macroquad_color(&config.color);
                let cr = &config.corner_radii;

                // Activate effect material if present (Phase 1: single effect only)
                let has_effect = !command.effects.is_empty();
                if has_effect {
                    let effect = &command.effects[0];
                    let mut mat_mgr = MATERIAL_MANAGER.lock().unwrap();
                    let material = mat_mgr.get_or_create(effect);
                    apply_shader_uniforms(material, effect, &bb);
                    gl_use_material(material);
                }

                if let Some(ref sr) = command.shape_rotation {
                    use crate::math::{classify_angle, AngleType};
                    let flip_x = sr.flip_x;
                    let flip_y = sr.flip_y;
                    match classify_angle(sr.rotation_radians) {
                        AngleType::Zero => {
                            // Flips only — remap corner radii
                            let cr = flip_corner_radii(cr, flip_x, flip_y);
                            draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, &cr, color);
                        }
                        AngleType::Right90 => {
                            let cr = rotate_corner_radii_90(&flip_corner_radii(cr, flip_x, flip_y));
                            draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, &cr, color);
                        }
                        AngleType::Straight180 => {
                            let cr = rotate_corner_radii_180(&flip_corner_radii(cr, flip_x, flip_y));
                            draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, &cr, color);
                        }
                        AngleType::Right270 => {
                            let cr = rotate_corner_radii_270(&flip_corner_radii(cr, flip_x, flip_y));
                            draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, &cr, color);
                        }
                        AngleType::Arbitrary(theta) => {
                            draw_good_rotated_rounded_rectangle(
                                bb.x, bb.y, bb.width, bb.height,
                                cr, color, theta, flip_x, flip_y,
                            );
                        }
                    }
                } else if cr.top_left == 0.0 && cr.top_right == 0.0 && cr.bottom_left == 0.0 && cr.bottom_right == 0.0 {
                    draw_rectangle(
                        bb.x,
                        bb.y,
                        bb.width,
                        bb.height,
                        color
                    );
                } else {
                    draw_good_rounded_rectangle(bb.x, bb.y, bb.width, bb.height, cr, color);
                }

                // Deactivate effect material
                if has_effect {
                    gl_use_default_material();
                }
            }
            #[cfg(feature = "text-styling")]
            RenderCommandConfig::Text(config) => {
                let bb = command.bounding_box;
                let font_size = config.font_size as f32;
                // Ensure font is loaded
                if let Some(asset) = config.font_asset {
                    FontManager::ensure(asset).await;
                }
                // Hold the FM lock for the duration of text rendering — no clone needed
                let mut fm = FONT_MANAGER.lock().unwrap();
                let baseline_y = bb.y + fm.metrics(config.font_size, config.font_asset).baseline_offset;
                let font = if let Some(asset) = config.font_asset {
                    fm.get(asset)
                } else {
                    fm.get_default()
                };
                let default_color = ply_to_macroquad_color(&config.color);

                // Activate effect material if present
                let has_effect = !command.effects.is_empty();
                if has_effect {
                    let effect = &command.effects[0];
                    let mut mat_mgr = MATERIAL_MANAGER.lock().unwrap();
                    let material = mat_mgr.get_or_create(effect);
                    apply_shader_uniforms(material, effect, &bb);
                    gl_use_material(material);
                }

                let normal_render = || {
                    let x_scale = compute_letter_spacing_x_scale(
                        bb.width,
                        count_visible_chars(&config.text),
                        config.letter_spacing,
                    );
                    draw_text_ex(
                        &config.text,
                        bb.x,
                        baseline_y,
                        TextParams {
                            font_size: config.font_size as u16,
                            font,
                            font_scale: 1.0,
                            font_scale_aspect: x_scale,
                            rotation: 0.0,
                            color: default_color
                        }
                    );
                };
                
                let mut in_style_def = false;
                let mut escaped = false;
                let mut failed = false;
                
                let mut text_buffer = String::new();
                let mut style_buffer = String::new();

                let line = config.text.to_string();
                let mut segments: Vec<StyledSegment> = Vec::new();

                for c in line.chars() {
                    if escaped {
                        if in_style_def {
                            style_buffer.push(c);
                        } else {
                            text_buffer.push(c);
                        }
                        escaped = false;
                        continue;
                    }

                    match c {
                        '\\' => {
                            escaped = true;
                        }
                        '{' => {
                            if in_style_def {
                                style_buffer.push(c); 
                            } else {
                                if !text_buffer.is_empty() {
                                    segments.push(StyledSegment {
                                        text: text_buffer.clone(),
                                        styles: state.style_stack.clone(),
                                    });
                                    text_buffer.clear();
                                }
                                in_style_def = true;
                            }
                        }
                        '|' => {
                            if in_style_def {
                                state.style_stack.push(style_buffer.clone());
                                style_buffer.clear();
                                in_style_def = false;
                            } else {
                                text_buffer.push(c);
                            }
                        }
                        '}' => {
                            if in_style_def {
                                style_buffer.push(c);
                            } else {
                                if !text_buffer.is_empty() {
                                    segments.push(StyledSegment {
                                        text: text_buffer.clone(),
                                        styles: state.style_stack.clone(),
                                    });
                                    text_buffer.clear();
                                }
                                
                                if state.style_stack.pop().is_none() {
                                    failed = true;
                                    break;
                                }
                            }
                        }
                        _ => {
                            if in_style_def {
                                style_buffer.push(c);
                            } else {
                                text_buffer.push(c);
                            }
                        }
                    }
                }
                if !(failed || in_style_def) {
                    if !text_buffer.is_empty() {
                        segments.push(StyledSegment {
                            text: text_buffer.clone(),
                            styles: state.style_stack.clone(),
                        });
                    }
                    
                    let time = get_time();
                    
                    let cursor_x = std::cell::Cell::new(bb.x);
                    let cursor_y = baseline_y;
                    let mut pending_renders = Vec::new();
                    
                    let x_scale = compute_letter_spacing_x_scale(
                        bb.width,
                        count_visible_chars(&config.text),
                        config.letter_spacing,
                    );
                    {
                        let mut tracker = ANIMATION_TRACKER.lock().unwrap();
                        let ts_default = crate::color::Color::rgba(
                            config.color.r,
                            config.color.g,
                            config.color.b,
                            config.color.a,
                        );
                        render_styled_text(
                            &segments,
                            time,
                            font_size,
                            ts_default,
                            &mut *tracker,
                            &mut state.total_char_index,
                            |text, tr, style_color| {
                                let text_string = text.to_string();
                                let text_width = measure_text(&text_string, font, config.font_size as u16, 1.0).width;
                                
                                let color = Color::new(style_color.r / 255.0, style_color.g / 255.0, style_color.b / 255.0, style_color.a / 255.0);
                                let x = cursor_x.get();
                                
                                pending_renders.push((x, text_string, tr, color));
                                
                                cursor_x.set(x + text_width*x_scale);
                            },
                            |text, tr, style_color| {
                                let text_string = text.to_string();
                                let color = Color::new(style_color.r / 255.0, style_color.g / 255.0, style_color.b / 255.0, style_color.a / 255.0);
                                let x = cursor_x.get();
                                
                                draw_text_ex(
                                    &text_string,
                                    x + tr.x*x_scale,
                                    cursor_y + tr.y,
                                    TextParams {
                                        font_size: config.font_size as u16,
                                        font,
                                        font_scale: tr.scale_y.max(0.01),
                                        font_scale_aspect: if tr.scale_y > 0.01 { tr.scale_x / tr.scale_y * x_scale } else { x_scale },
                                        rotation: tr.rotation.to_radians(),
                                        color
                                    }
                                );
                            }
                        );
                    }
                    for (x, text_string, tr, color) in pending_renders {
                        draw_text_ex(
                            &text_string,
                            x + tr.x*x_scale,
                            cursor_y + tr.y,
                            TextParams {
                                font_size: config.font_size as u16,
                                font,
                                font_scale: tr.scale_y.max(0.01),
                                font_scale_aspect: if tr.scale_y > 0.01 { tr.scale_x / tr.scale_y * x_scale } else { x_scale },
                                rotation: tr.rotation.to_radians(),
                                color
                            }
                        );
                    }
                } else {
                    if in_style_def {
                        warn!("Style definition didn't end! Here is what we tried to render: {}", config.text);
                    } else if failed {
                        warn!("Encountered }} without opened style! Make sure to escape curly braces with \\. Here is what we tried to render: {}", config.text);
                    }
                    normal_render();
                }

                // Deactivate effect material
                if has_effect {
                    gl_use_default_material();
                }
            }
            #[cfg(not(feature = "text-styling"))]
            RenderCommandConfig::Text(config) => {
                let bb = command.bounding_box;
                let color = ply_to_macroquad_color(&config.color);
                // Ensure font is loaded
                if let Some(asset) = config.font_asset {
                    FontManager::ensure(asset).await;
                }
                // Hold the FM lock for the duration of text rendering — no clone needed
                let mut fm = FONT_MANAGER.lock().unwrap();
                let baseline_y = bb.y + fm.metrics(config.font_size, config.font_asset).baseline_offset;
                let font = if let Some(asset) = config.font_asset {
                    fm.get(asset)
                } else {
                    fm.get_default()
                };

                // Activate effect material if present
                let has_effect = !command.effects.is_empty();
                if has_effect {
                    let effect = &command.effects[0];
                    let mut mat_mgr = MATERIAL_MANAGER.lock().unwrap();
                    let material = mat_mgr.get_or_create(effect);
                    apply_shader_uniforms(material, effect, &bb);
                    gl_use_material(material);
                }

                let x_scale = compute_letter_spacing_x_scale(
                    bb.width,
                    config.text.chars().count(),
                    config.letter_spacing,
                );
                draw_text_ex(
                    &config.text,
                    bb.x,
                    baseline_y,
                    TextParams {
                        font_size: config.font_size as u16,
                        font,
                        font_scale: 1.0,
                        font_scale_aspect: x_scale,
                        rotation: 0.0,
                        color
                    }
                );

                // Deactivate effect material
                if has_effect {
                    gl_use_default_material();
                }
            }
            RenderCommandConfig::Border(config) => {
                let bb = command.bounding_box;
                let bw = &config.width;
                let cr = &config.corner_radii;
                let color = ply_to_macroquad_color(&config.color);
                let s = match config.position {
                    BorderPosition::Outside => 1.,
                    BorderPosition::Middle => 0.5,
                    BorderPosition::Inside => 0.0,
                };

                let get_sides = |corner: f32| {
                    (std::f32::consts::PI * corner / (2.0 * PIXELS_PER_POINT)).max(5.0) as usize
                };
                let v = |x: f32, y: f32| Vertex::new(x, y, 0., 0., 0., color);

                let top = bw.top as f32;
                let left = bw.left as f32;
                let bottom = bw.bottom as f32;
                let right = bw.right as f32;
                let tl_r = cr.top_left;
                let tr_r = cr.top_right;
                let bl_r = cr.bottom_left;
                let br_r = cr.bottom_right;

                let ox1 = bb.x - left * s;
                let ox2 = bb.x + bb.width + right * s;
                let oy1 = bb.y - top * s;
                let oy2 = bb.y + bb.height + bottom * s;
                let ix1 = bb.x + left * (1.0 - s);
                let ix2 = bb.x + bb.width - right * (1.0 - s);
                let iy1 = bb.y + top * (1.0 - s);
                let iy2 = bb.y + bb.height - bottom * (1.0 - s);

                let o_tl_rx = tl_r + left * s;
                let o_tl_ry = tl_r + top * s;
                let o_tr_rx = tr_r + right * s;
                let o_tr_ry = tr_r + top * s;
                let o_bl_rx = bl_r + left * s;
                let o_bl_ry = bl_r + bottom * s;
                let o_br_rx = br_r + right * s;
                let o_br_ry = br_r + bottom * s;
                let i_tl_rx = (tl_r - left * (1.0 - s)).max(0.0);
                let i_tl_ry = (tl_r - top * (1.0 - s)).max(0.0);
                let i_tr_rx = (tr_r - right * (1.0 - s)).max(0.0);
                let i_tr_ry = (tr_r - top * (1.0 - s)).max(0.0);
                let i_bl_rx = (bl_r - left * (1.0 - s)).max(0.0);
                let i_bl_ry = (bl_r - bottom * (1.0 - s)).max(0.0);
                let i_br_rx = (br_r - right * (1.0 - s)).max(0.0);
                let i_br_ry = (br_r - bottom * (1.0 - s)).max(0.0);

                let tl_sides = get_sides(o_tl_rx.max(o_tl_ry).max(i_tl_rx).max(i_tl_ry));
                let tr_sides = get_sides(o_tr_rx.max(o_tr_ry).max(i_tr_rx).max(i_tr_ry));
                let bl_sides = get_sides(o_bl_rx.max(o_bl_ry).max(i_bl_rx).max(i_bl_ry));
                let br_sides = get_sides(o_br_rx.max(o_br_ry).max(i_br_rx).max(i_br_ry));
                let side_count = tl_sides + tr_sides + bl_sides + br_sides;

                let mut vertices = Vec::<Vertex>::with_capacity(16 + side_count * 4);
                let mut indices = Vec::<u16>::with_capacity(24 + side_count * 6);

                // 4 quads
                vertices.extend([
                    // Top edge
                    v(ox1 + o_tl_rx, oy1),
                    v(ox2 - o_tr_rx, oy1),
                    v(ix1 + i_tl_rx, iy1),
                    v(ix2 - i_tr_rx, iy1),
                    // Bottom edge
                    v(ox1 + o_bl_rx, oy2),
                    v(ox2 - o_br_rx, oy2),
                    v(ix1 + i_bl_rx, iy2),
                    v(ix2 - i_br_rx, iy2),
                    // Left edge
                    v(ox1, oy1 + o_tl_ry),
                    v(ox1, oy2 - o_bl_ry),
                    v(ix1, iy1 + i_tl_ry),
                    v(ix1, iy2 - i_bl_ry),
                    // Right edge
                    v(ox2, oy1 + o_tr_ry),
                    v(ox2, oy2 - o_br_ry),
                    v(ix2, iy1 + i_tr_ry),
                    v(ix2, iy2 - i_br_ry),
                ]);
                for l in [0, 4, 8, 12] {
                    indices.extend([
                        l, l + 1, l + 2,
                        l + 1, l + 3, l + 2
                    ]);
                }

                let corners = [
                    (
                        tl_sides,
                        PI,
                        ox1 + o_tl_rx,
                        oy1 + o_tl_ry,
                        ix1 + i_tl_rx,
                        iy1 + i_tl_ry,
                        o_tl_rx,
                        o_tl_ry,
                        i_tl_rx,
                        i_tl_ry,
                    ),
                    (
                        tr_sides,
                        PI * 1.5,
                        ox2 - o_tr_rx,
                        oy1 + o_tr_ry,
                        ix2 - i_tr_rx,
                        iy1 + i_tr_ry,
                        o_tr_rx,
                        o_tr_ry,
                        i_tr_rx,
                        i_tr_ry,
                    ),
                    (
                        bl_sides,
                        PI * 0.5,
                        ox1 + o_bl_rx,
                        oy2 - o_bl_ry,
                        ix1 + i_bl_rx,
                        iy2 - i_bl_ry,
                        o_bl_rx,
                        o_bl_ry,
                        i_bl_rx,
                        i_bl_ry,
                    ),
                    (
                        br_sides,
                        0.,
                        ox2 - o_br_rx,
                        oy2 - o_br_ry,
                        ix2 - i_br_rx,
                        iy2 - i_br_ry,
                        o_br_rx,
                        o_br_ry,
                        i_br_rx,
                        i_br_ry,
                    ),
                ];

                for (sides, start, ocx, ocy, icx, icy, o_rx, o_ry, i_rx, i_ry) in corners {
                    let step = (PI / 2.) / (sides as f32);

                    for i in 0..sides {
                        let i = i as f32;
                        let a1 = start + i * step;
                        let a2 = a1 + step;
                        let l = vertices.len() as u16;

                        // quad
                        vertices.extend([
                            v(ocx + a1.cos() * o_rx, ocy + a1.sin() * o_ry),
                            v(ocx + a2.cos() * o_rx, ocy + a2.sin() * o_ry),
                            v(icx + a1.cos() * i_rx, icy + a1.sin() * i_ry),
                            v(icx + a2.cos() * i_rx, icy + a2.sin() * i_ry),
                        ]);
                        indices.extend([
                            l, l + 1, l + 2,
                            l + 1, l + 3, l + 2
                        ]);
                    }
                }

                draw_mesh(&Mesh { vertices, indices, texture: None });
            }
            RenderCommandConfig::ScissorStart() => {
                let bb = command.bounding_box;
                // Layout coordinates are in logical pixels, but macroquad's
                // quad_gl.scissor() passes values to glScissor which operates
                // in physical (framebuffer) pixels.  Scale by DPI so the
                // scissor rectangle matches on high-DPI displays (e.g. WASM).
                let dpi = miniquad::window::dpi_scale();
                state.clip = Some((
                    (bb.x * dpi) as i32,
                    (bb.y * dpi) as i32,
                    (bb.width * dpi) as i32,
                    (bb.height * dpi) as i32,
                ));
                unsafe {
                    get_internal_gl().quad_gl.scissor(state.clip);
                }
            }
            RenderCommandConfig::ScissorEnd() => {
                state.clip = None;
                unsafe {
                    get_internal_gl().quad_gl.scissor(None);
                }
            }
            RenderCommandConfig::Custom(_) => {
                handle_custom_command(&command);
            }
            RenderCommandConfig::GroupBegin { ref shader, ref visual_rotation } => {
                let bb = command.bounding_box;
                let rt = render_target_msaa(bb.width as u32, bb.height as u32);
                rt.texture.set_filter(FilterMode::Linear);
                let cam = Camera2D {
                    render_target: Some(rt.clone()),
                    ..Camera2D::from_display_rect(Rect::new(
                        bb.x, bb.y, bb.width, bb.height,
                    ))
                };
                set_camera(&cam);
                clear_background(Color::new(0.0, 0.0, 0.0, 0.0));
                state.rt_stack.push((rt, shader.clone(), *visual_rotation, bb));
            }
            RenderCommandConfig::GroupEnd => {
                if let Some((rt, shader_config, visual_rotation, bb)) = state.rt_stack.pop() {
                    // Restore previous camera
                    if let Some((prev_rt, _, _, prev_bb)) = state.rt_stack.last() {
                        let cam = Camera2D {
                            render_target: Some(prev_rt.clone()),
                            ..Camera2D::from_display_rect(Rect::new(
                                prev_bb.x, prev_bb.y, prev_bb.width, prev_bb.height,
                            ))
                        };
                        set_camera(&cam);
                    } else {
                        set_default_camera();
                    }

                    // Apply the shader material if present
                    if let Some(ref config) = shader_config {
                        let mut mat_mgr = MATERIAL_MANAGER.lock().unwrap();
                        let material = mat_mgr.get_or_create(config);
                        apply_shader_uniforms(material, config, &bb);
                        gl_use_material(material);
                    }

                    // Compute draw params — apply visual rotation if present
                    let (rotation, flip_x, flip_y, pivot) = match &visual_rotation {
                        Some(rot) => {
                            let pivot_screen = Vec2::new(
                                bb.x + rot.pivot_x * bb.width,
                                bb.y + rot.pivot_y * bb.height,
                            );
                            // flip_y is inverted because render targets are flipped in OpenGL
                            (rot.rotation_radians, rot.flip_x, !rot.flip_y, Some(pivot_screen))
                        }
                        None => (0.0, false, true, None),
                    };

                    draw_texture_ex(
                        &rt.texture,
                        bb.x,
                        bb.y,
                        WHITE,
                        DrawTextureParams {
                            dest_size: Some(Vec2::new(bb.width, bb.height)),
                            rotation,
                            flip_x,
                            flip_y,
                            pivot,
                            ..Default::default()
                        },
                    );

                    if shader_config.is_some() {
                        gl_use_default_material();
                    }
                }
            }
            RenderCommandConfig::None() => {}
        }
    }
    TEXTURE_MANAGER.lock().unwrap().clean();
    MATERIAL_MANAGER.lock().unwrap().clean();
    FONT_MANAGER.lock().unwrap().clean();
}

pub fn create_measure_text_function(
) -> impl Fn(&str, &crate::TextConfig) -> crate::Dimensions + 'static {
    move |text: &str, config: &crate::TextConfig| {
        #[cfg(feature = "text-styling")]
        let cleaned_text = {
            // Remove macroquad_text_styling tags, handling escapes
            let mut result = String::new();
            let mut in_style_def = false;
            let mut escaped = false;
            for c in text.chars() {
                if escaped {
                    result.push(c);
                    escaped = false;
                    continue;
                }
                match c {
                    '\\' => {
                        escaped = true;
                    }
                    '{' => {
                        in_style_def = true;
                    }
                    '|' => {
                        if in_style_def {
                            in_style_def = false;
                        } else {
                            result.push(c);
                        }
                    }
                    '}' => {
                        // Nothing
                    }
                    _ => {
                        if !in_style_def {
                            result.push(c);
                        }
                    }
                }
            }
            if in_style_def {
                warn!("Ended inside a style definition while cleaning text for measurement! Make sure to escape curly braces with \\. Here is what we tried to measure: {}", text);
            }
            result
        };
        #[cfg(not(feature = "text-styling"))]
        let cleaned_text = text.to_string();
        let mut fm = FONT_MANAGER.lock().unwrap();
        // Resolve font: use asset font if available, otherwise default
        let font = if let Some(asset) = config.font_asset {
            fm.get(asset)
        } else {
            fm.get_default()
        };
        let measured = macroquad::text::measure_text(
            &cleaned_text,
            font,
            config.font_size,
            1.0,
        );
        let metrics = fm.metrics(config.font_size as u16, config.font_asset);
        let added_space = (cleaned_text.chars().count().max(1) - 1) as f32 * config.letter_spacing as f32;
        crate::Dimensions::new(measured.width + added_space, metrics.height)
    }
}

/// Count visible characters in text, skipping style tag markup.
/// This handles `{style_name|` openers, `}` closers, and `\` escapes.
#[cfg(feature = "text-styling")]
fn count_visible_chars(text: &str) -> usize {
    let mut count = 0;
    let mut in_style_def = false;
    let mut escaped = false;
    for c in text.chars() {
        if escaped { count += 1; escaped = false; continue; }
        match c {
            '\\' => { escaped = true; }
            '{' => { in_style_def = true; }
            '|' => { if in_style_def { in_style_def = false; } else { count += 1; } }
            '}' => { }
            _ => { if !in_style_def { count += 1; } }
        }
    }
    count
}

/// Compute the horizontal scale factor needed to visually apply letter-spacing.
///
/// The bounding-box width already includes the total letter-spacing contribution
/// (`(visible_chars - 1) * letter_spacing`). By dividing out that contribution we
/// recover the raw text width, and the ratio `bb_width / raw_width` gives the
/// scale factor that macroquad should use to stretch each glyph.
fn compute_letter_spacing_x_scale(bb_width: f32, visible_char_count: usize, letter_spacing: u16) -> f32 {
    if letter_spacing == 0 || visible_char_count <= 1 {
        return 1.0;
    }
    let total_spacing = (visible_char_count as f32 - 1.0) * letter_spacing as f32;
    let raw_width = bb_width - total_spacing;
    if raw_width > 0.0 {
        bb_width / raw_width
    } else {
        1.0
    }
}