// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Platform.Linux.Rendering;
using Microsoft.Maui.Platform.Linux.Services;
using SkiaSharp;

namespace Microsoft.Maui.Platform;

/// <summary>
/// Skia-rendered label control matching the .NET MAUI Label API.
/// </summary>
public class SkiaLabel : SkiaView
{
    #region BindableProperties

    /// <summary>
    /// Bindable property for Text.
    /// </summary>
    public static readonly BindableProperty TextProperty = BindableProperty.Create(
        nameof(Text),
        typeof(string),
        typeof(SkiaLabel),
        string.Empty,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());

    /// <summary>
    /// Bindable property for TextColor.
    /// Default is null to match MAUI Label.TextColor (falls back to platform default).
    /// </summary>
    public static readonly BindableProperty TextColorProperty = BindableProperty.Create(
        nameof(TextColor),
        typeof(Color),
        typeof(SkiaLabel),
        null,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());

    /// <summary>
    /// Bindable property for FontFamily.
    /// </summary>
    public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(
        nameof(FontFamily),
        typeof(string),
        typeof(SkiaLabel),
        string.Empty,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged());

    /// <summary>
    /// Bindable property for FontSize.
    /// </summary>
    public static readonly BindableProperty FontSizeProperty = BindableProperty.Create(
        nameof(FontSize),
        typeof(double),
        typeof(SkiaLabel),
        14.0,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged());

    /// <summary>
    /// Bindable property for FontAttributes.
    /// </summary>
    public static readonly BindableProperty FontAttributesProperty = BindableProperty.Create(
        nameof(FontAttributes),
        typeof(FontAttributes),
        typeof(SkiaLabel),
        FontAttributes.None,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged());

    /// <summary>
    /// Bindable property for FontAutoScalingEnabled.
    /// </summary>
    public static readonly BindableProperty FontAutoScalingEnabledProperty = BindableProperty.Create(
        nameof(FontAutoScalingEnabled),
        typeof(bool),
        typeof(SkiaLabel),
        true,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFontChanged());

    /// <summary>
    /// Bindable property for CharacterSpacing.
    /// </summary>
    public static readonly BindableProperty CharacterSpacingProperty = BindableProperty.Create(
        nameof(CharacterSpacing),
        typeof(double),
        typeof(SkiaLabel),
        0.0,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).InvalidateMeasure());

    /// <summary>
    /// Bindable property for TextDecorations.
    /// </summary>
    public static readonly BindableProperty TextDecorationsProperty = BindableProperty.Create(
        nameof(TextDecorations),
        typeof(TextDecorations),
        typeof(SkiaLabel),
        TextDecorations.None,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());

    /// <summary>
    /// Bindable property for HorizontalTextAlignment.
    /// </summary>
    public static readonly BindableProperty HorizontalTextAlignmentProperty = BindableProperty.Create(
        nameof(HorizontalTextAlignment),
        typeof(TextAlignment),
        typeof(SkiaLabel),
        TextAlignment.Start,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());

    /// <summary>
    /// Bindable property for VerticalTextAlignment.
    /// Default is Start to match MAUI Label.VerticalTextAlignment.
    /// </summary>
    public static readonly BindableProperty VerticalTextAlignmentProperty = BindableProperty.Create(
        nameof(VerticalTextAlignment),
        typeof(TextAlignment),
        typeof(SkiaLabel),
        TextAlignment.Start,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());

    /// <summary>
    /// Bindable property for LineBreakMode.
    /// </summary>
    public static readonly BindableProperty LineBreakModeProperty = BindableProperty.Create(
        nameof(LineBreakMode),
        typeof(LineBreakMode),
        typeof(SkiaLabel),
        LineBreakMode.TailTruncation,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());

    /// <summary>
    /// Bindable property for MaxLines.
    /// </summary>
    public static readonly BindableProperty MaxLinesProperty = BindableProperty.Create(
        nameof(MaxLines),
        typeof(int),
        typeof(SkiaLabel),
        0,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());

    /// <summary>
    /// Bindable property for LineHeight.
    /// Default is -1 to match MAUI Label.LineHeight (platform default).
    /// </summary>
    public static readonly BindableProperty LineHeightProperty = BindableProperty.Create(
        nameof(LineHeight),
        typeof(double),
        typeof(SkiaLabel),
        -1.0,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());

    /// <summary>
    /// Bindable property for TextTransform.
    /// </summary>
    public static readonly BindableProperty TextTransformProperty = BindableProperty.Create(
        nameof(TextTransform),
        typeof(TextTransform),
        typeof(SkiaLabel),
        TextTransform.Default,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).Invalidate());

    /// <summary>
    /// Bindable property for TextType.
    /// </summary>
    public static readonly BindableProperty TextTypeProperty = BindableProperty.Create(
        nameof(TextType),
        typeof(TextType),
        typeof(SkiaLabel),
        TextType.Text,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).OnTextChanged());

    /// <summary>
    /// Bindable property for Padding.
    /// </summary>
    public static new readonly BindableProperty PaddingProperty = BindableProperty.Create(
        nameof(Padding),
        typeof(Thickness),
        typeof(SkiaLabel),
        new Thickness(0),
        propertyChanged: (b, o, n) => ((SkiaLabel)b).InvalidateMeasure());

    /// <summary>
    /// Bindable property for FormattedText.
    /// </summary>
    public static readonly BindableProperty FormattedTextProperty = BindableProperty.Create(
        nameof(FormattedText),
        typeof(FormattedString),
        typeof(SkiaLabel),
        null,
        propertyChanged: (b, o, n) => ((SkiaLabel)b).OnFormattedTextChanged((FormattedString?)o, (FormattedString?)n));

    #endregion

    #region Properties

    /// <summary>
    /// Gets or sets the text content.
    /// </summary>
    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetValue(TextProperty, value);
    }

    /// <summary>
    /// Gets or sets the text color.
    /// Null means use platform default (black on Linux).
    /// </summary>
    public Color? TextColor
    {
        get => (Color?)GetValue(TextColorProperty);
        set => SetValue(TextColorProperty, value);
    }

    /// <summary>
    /// Gets or sets the font family.
    /// </summary>
    public string FontFamily
    {
        get => (string)GetValue(FontFamilyProperty);
        set => SetValue(FontFamilyProperty, value);
    }

    /// <summary>
    /// Gets or sets the font size.
    /// </summary>
    public double FontSize
    {
        get => (double)GetValue(FontSizeProperty);
        set => SetValue(FontSizeProperty, value);
    }

    /// <summary>
    /// Gets or sets the font attributes.
    /// </summary>
    public FontAttributes FontAttributes
    {
        get => (FontAttributes)GetValue(FontAttributesProperty);
        set => SetValue(FontAttributesProperty, value);
    }

    /// <summary>
    /// Gets or sets whether font auto-scaling is enabled.
    /// </summary>
    public bool FontAutoScalingEnabled
    {
        get => (bool)GetValue(FontAutoScalingEnabledProperty);
        set => SetValue(FontAutoScalingEnabledProperty, value);
    }

    /// <summary>
    /// Gets or sets the character spacing.
    /// </summary>
    public double CharacterSpacing
    {
        get => (double)GetValue(CharacterSpacingProperty);
        set => SetValue(CharacterSpacingProperty, value);
    }

    /// <summary>
    /// Gets or sets the text decorations.
    /// </summary>
    public TextDecorations TextDecorations
    {
        get => (TextDecorations)GetValue(TextDecorationsProperty);
        set => SetValue(TextDecorationsProperty, value);
    }

    /// <summary>
    /// Gets or sets the horizontal text alignment.
    /// </summary>
    public TextAlignment HorizontalTextAlignment
    {
        get => (TextAlignment)GetValue(HorizontalTextAlignmentProperty);
        set => SetValue(HorizontalTextAlignmentProperty, value);
    }

    /// <summary>
    /// Gets or sets the vertical text alignment.
    /// </summary>
    public TextAlignment VerticalTextAlignment
    {
        get => (TextAlignment)GetValue(VerticalTextAlignmentProperty);
        set => SetValue(VerticalTextAlignmentProperty, value);
    }

    /// <summary>
    /// Gets or sets the line break mode.
    /// </summary>
    public LineBreakMode LineBreakMode
    {
        get => (LineBreakMode)GetValue(LineBreakModeProperty);
        set => SetValue(LineBreakModeProperty, value);
    }

    /// <summary>
    /// Gets or sets the maximum number of lines.
    /// </summary>
    public int MaxLines
    {
        get => (int)GetValue(MaxLinesProperty);
        set => SetValue(MaxLinesProperty, value);
    }

    /// <summary>
    /// Gets or sets the line height multiplier.
    /// </summary>
    public double LineHeight
    {
        get => (double)GetValue(LineHeightProperty);
        set => SetValue(LineHeightProperty, value);
    }

    /// <summary>
    /// Gets or sets the text transform.
    /// </summary>
    public TextTransform TextTransform
    {
        get => (TextTransform)GetValue(TextTransformProperty);
        set => SetValue(TextTransformProperty, value);
    }

    /// <summary>
    /// Gets or sets the text type.
    /// </summary>
    public TextType TextType
    {
        get => (TextType)GetValue(TextTypeProperty);
        set => SetValue(TextTypeProperty, value);
    }

    /// <summary>
    /// Gets or sets the padding.
    /// </summary>
    public new Thickness Padding
    {
        get => (Thickness)GetValue(PaddingProperty);
        set => SetValue(PaddingProperty, value);
    }

    /// <summary>
    /// Gets or sets the formatted text.
    /// </summary>
    public FormattedString? FormattedText
    {
        get => (FormattedString?)GetValue(FormattedTextProperty);
        set => SetValue(FormattedTextProperty, value);
    }

    #endregion

    #region Selection State

    private int _selectionStart = -1;
    private int _selectionLength = 0;
    private bool _isSelecting = false;
    private DateTime _lastClickTime = DateTime.MinValue;
    private float _lastClickX;
    private const double DoubleClickThresholdMs = 400;

    /// <summary>
    /// Gets or sets whether text selection is enabled.
    /// </summary>
    public bool IsTextSelectionEnabled { get; set; } = true;

    /// <summary>
    /// Gets the currently selected text.
    /// </summary>
    public string SelectedText
    {
        get
        {
            if (_selectionStart < 0 || _selectionLength == 0) return string.Empty;
            var text = GetDisplayText();
            var start = Math.Min(_selectionStart, _selectionStart + _selectionLength);
            var length = Math.Abs(_selectionLength);
            if (start < 0 || start >= text.Length) return string.Empty;
            return text.Substring(start, Math.Min(length, text.Length - start));
        }
    }

    #endregion

    #region Events

    /// <summary>
    /// Occurs when the label is tapped.
    /// </summary>
    public event EventHandler? Tapped;

    /// <summary>
    /// Raises the Tapped event.
    /// </summary>
    protected virtual void OnTapped()
    {
        Tapped?.Invoke(this, EventArgs.Empty);
    }

    public override void OnPointerPressed(PointerEventArgs e)
    {
        base.OnPointerPressed(e);

        if (!IsTextSelectionEnabled || string.IsNullOrEmpty(Text)) return;

        var text = GetDisplayText();
        if (string.IsNullOrEmpty(text)) return;

        // Calculate character position from click
        var screenBounds = ScreenBounds;
        var clickX = e.X - (float)screenBounds.Left - (float)Padding.Left;
        var charIndex = GetCharacterIndexAtX(clickX);

        // Check for double-click (select word)
        var now = DateTime.UtcNow;
        var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds;
        var distanceFromLastClick = Math.Abs(e.X - _lastClickX);

        if (timeSinceLastClick < DoubleClickThresholdMs && distanceFromLastClick < 10)
        {
            // Double-click: select word
            SelectWordAt(charIndex);
            _lastClickTime = DateTime.MinValue;
            _isSelecting = false;
        }
        else
        {
            // Single click: start selection
            _selectionStart = charIndex;
            _selectionLength = 0;
            _isSelecting = true;
            _lastClickTime = now;
            _lastClickX = e.X;
        }

        Invalidate();
    }

    public override void OnPointerMoved(PointerEventArgs e)
    {
        base.OnPointerMoved(e);

        if (!IsTextSelectionEnabled || !_isSelecting) return;

        var text = GetDisplayText();
        if (string.IsNullOrEmpty(text)) return;

        var screenBounds = ScreenBounds;
        var clickX = e.X - (float)screenBounds.Left - (float)Padding.Left;
        var charIndex = GetCharacterIndexAtX(clickX);

        _selectionLength = charIndex - _selectionStart;
        Invalidate();
    }

    public override void OnPointerReleased(PointerEventArgs e)
    {
        base.OnPointerReleased(e);

        if (_isSelecting && _selectionLength == 0)
        {
            // No drag happened, it's a tap
            OnTapped();
        }

        _isSelecting = false;
    }

    public override void OnKeyDown(KeyEventArgs e)
    {
        base.OnKeyDown(e);

        if (!IsTextSelectionEnabled) return;

        // Ctrl+A: Select All
        if (e.Key == Key.A && e.Modifiers.HasFlag(KeyModifiers.Control))
        {
            SelectAll();
            e.Handled = true;
        }
        // Ctrl+C: Copy
        else if (e.Key == Key.C && e.Modifiers.HasFlag(KeyModifiers.Control))
        {
            CopyToClipboard();
            e.Handled = true;
        }
    }

    /// <summary>
    /// Selects all text in the label.
    /// </summary>
    public void SelectAll()
    {
        var text = GetDisplayText();
        _selectionStart = 0;
        _selectionLength = text.Length;
        Invalidate();
    }

    /// <summary>
    /// Clears the current selection.
    /// </summary>
    public void ClearSelection()
    {
        _selectionStart = -1;
        _selectionLength = 0;
        Invalidate();
    }

    private void SelectWordAt(int charIndex)
    {
        var text = GetDisplayText();
        if (string.IsNullOrEmpty(text) || charIndex < 0 || charIndex >= text.Length) return;

        int start = charIndex;
        int end = charIndex;

        // Move start backwards to beginning of word
        while (start > 0 && IsWordChar(text[start - 1]))
            start--;

        // Move end forwards to end of word
        while (end < text.Length && IsWordChar(text[end]))
            end++;

        _selectionStart = start;
        _selectionLength = end - start;
    }

    private static bool IsWordChar(char c)
    {
        return char.IsLetterOrDigit(c) || c == '_';
    }

    private int GetCharacterIndexAtX(float x)
    {
        var text = GetDisplayText();
        if (string.IsNullOrEmpty(text)) return 0;

        float fontSize = FontSize > 0 ? (float)FontSize : 14f;
        var fontFamily = string.IsNullOrEmpty(FontFamily) ? "Sans" : FontFamily;

        using var font = new SKFont(
            SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(fontFamily, GetFontStyle()) ?? SKTypeface.Default,
            fontSize);
        using var paint = new SKPaint(font);

        for (int i = 0; i <= text.Length; i++)
        {
            var substring = text.Substring(0, i);
            var width = paint.MeasureText(substring);
            if (CharacterSpacing != 0 && i > 0)
            {
                width += (float)(CharacterSpacing * i);
            }
            if (width > x)
            {
                return i > 0 ? i - 1 : 0;
            }
        }
        return text.Length;
    }

    private void CopyToClipboard()
    {
        var selectedText = SelectedText;
        if (!string.IsNullOrEmpty(selectedText))
        {
            SystemClipboard.SetText(selectedText);
        }
    }

    #endregion

    #region Private Methods

    private void OnTextChanged()
    {
        InvalidateMeasure();
        Invalidate();
    }

    private void OnFontChanged()
    {
        InvalidateMeasure();
        Invalidate();
    }

    private void OnFormattedTextChanged(FormattedString? oldValue, FormattedString? newValue)
    {
        if (oldValue != null)
        {
            oldValue.PropertyChanged -= OnFormattedTextPropertyChanged;
        }
        if (newValue != null)
        {
            newValue.PropertyChanged += OnFormattedTextPropertyChanged;
        }
        OnTextChanged();
    }

    private void OnFormattedTextPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        OnTextChanged();
    }

    private SKColor ToSKColor(Color? color) => TextRenderingHelper.ToSKColor(color, SkiaTheme.TextPrimarySK);

    private string GetDisplayText()
    {
        var text = Text ?? string.Empty;

        // Handle TextType.Html by stripping tags (basic implementation)
        if (TextType == TextType.Html)
        {
            text = System.Text.RegularExpressions.Regex.Replace(text, "<[^>]*>", "");
        }

        // Apply text transform
        return TextTransform switch
        {
            TextTransform.Uppercase => text.ToUpperInvariant(),
            TextTransform.Lowercase => text.ToLowerInvariant(),
            _ => text
        };
    }

    private SKFontStyle GetFontStyle() => TextRenderingHelper.GetFontStyle(FontAttributes);

    /// <summary>
    /// Determines if text should be rendered right-to-left based on FlowDirection.
    /// </summary>
    private bool IsRightToLeft()
    {
        return FlowDirection == FlowDirection.RightToLeft;
    }

    /// <summary>
    /// Gets the effective horizontal alignment for the given alignment,
    /// accounting for FlowDirection (RTL flips Start/End).
    /// </summary>
    private float GetHorizontalPosition(TextAlignment alignment, float boundsLeft, float boundsRight, float textWidth)
    {
        bool isRtl = IsRightToLeft();

        return alignment switch
        {
            TextAlignment.Start => isRtl ? boundsRight - textWidth : boundsLeft,
            TextAlignment.Center => (boundsLeft + boundsRight) / 2 - textWidth / 2,
            TextAlignment.End => isRtl ? boundsLeft : boundsRight - textWidth,
            _ => isRtl ? boundsRight - textWidth : boundsLeft
        };
    }

    #endregion

    #region Drawing

    protected override void OnDraw(SKCanvas canvas, SKRect bounds)
    {
        var padding = Padding;
        var contentBounds = new SKRect(
            bounds.Left + (float)padding.Left,
            bounds.Top + (float)padding.Top,
            bounds.Right - (float)padding.Right,
            bounds.Bottom - (float)padding.Bottom);

        // If we have FormattedText, draw that instead
        if (FormattedText != null && FormattedText.Spans.Count > 0)
        {
            DrawFormattedText(canvas, contentBounds);
            return;
        }

        string displayText = GetDisplayText();
        if (string.IsNullOrEmpty(displayText)) return;

        float fontSize = FontSize > 0 ? (float)FontSize : 14f;
        var fontFamily = string.IsNullOrEmpty(FontFamily) ? "Sans" : FontFamily;

        using var font = new SKFont(
            SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(fontFamily, GetFontStyle()) ?? SKTypeface.Default,
            fontSize);

        using var paint = new SKPaint(font)
        {
            Color = ToSKColor(TextColor),
            IsAntialias = true
        };

        // Check if we need multi-line rendering
        bool needsMultiLine = LineBreakMode == LineBreakMode.WordWrap ||
                             LineBreakMode == LineBreakMode.CharacterWrap ||
                             MaxLines > 1 ||
                             displayText.Contains('\n');

        if (needsMultiLine)
        {
            DrawMultiLineText(canvas, paint, font, contentBounds, displayText);
        }
        else
        {
            DrawSingleLineText(canvas, paint, contentBounds, displayText);
        }
    }

    private void DrawSingleLineText(SKCanvas canvas, SKPaint paint, SKRect bounds, string text)
    {
        var textBounds = new SKRect();
        paint.MeasureText(text, ref textBounds);

        // Apply truncation if needed
        string displayText = text;
        float availableWidth = bounds.Width;

        if (textBounds.Width > availableWidth && LineBreakMode != LineBreakMode.NoWrap)
        {
            displayText = TruncateText(text, paint, availableWidth, LineBreakMode);
            paint.MeasureText(displayText, ref textBounds);
        }

        // Account for character spacing in measurement
        float textWidth = textBounds.Width;
        if (CharacterSpacing != 0 && displayText.Length > 1)
        {
            textWidth += (float)(CharacterSpacing * (displayText.Length - 1));
        }

        // Calculate position based on alignment and FlowDirection
        float x = GetHorizontalPosition(HorizontalTextAlignment, bounds.Left, bounds.Right, textWidth);

        float y = VerticalTextAlignment switch
        {
            TextAlignment.Start => bounds.Top - textBounds.Top,
            TextAlignment.Center => bounds.MidY - textBounds.MidY,
            TextAlignment.End => bounds.Bottom - textBounds.Bottom,
            _ => bounds.MidY - textBounds.MidY
        };

        // Draw selection highlight if applicable
        if (_selectionStart >= 0 && _selectionLength != 0)
        {
            DrawSelectionHighlight(canvas, paint, x, y, displayText, textBounds);
        }

        DrawTextWithSpacing(canvas, displayText, x, y, paint);
        DrawTextDecorations(canvas, paint, x, y, textBounds);
    }

    private void DrawSelectionHighlight(SKCanvas canvas, SKPaint paint, float x, float y, string text, SKRect textBounds)
    {
        var selStart = Math.Min(_selectionStart, _selectionStart + _selectionLength);
        var selEnd = Math.Max(_selectionStart, _selectionStart + _selectionLength);

        // Clamp to text length
        selStart = Math.Max(0, Math.Min(selStart, text.Length));
        selEnd = Math.Max(0, Math.Min(selEnd, text.Length));

        if (selStart >= selEnd) return;

        var textToStart = text.Substring(0, selStart);
        var textToEnd = text.Substring(0, selEnd);

        float startX = x + paint.MeasureText(textToStart);
        float endX = x + paint.MeasureText(textToEnd);

        if (CharacterSpacing != 0)
        {
            startX += (float)(CharacterSpacing * selStart);
            endX += (float)(CharacterSpacing * selEnd);
        }

        using var selectionPaint = new SKPaint
        {
            Color = SkiaTheme.PrimaryLightSK,
            Style = SKPaintStyle.Fill
        };

        float selectionTop = y + textBounds.Top;
        float selectionBottom = y + textBounds.Bottom;
        canvas.DrawRect(new SKRect(startX, selectionTop, endX, selectionBottom), selectionPaint);
    }

    private void DrawMultiLineText(SKCanvas canvas, SKPaint paint, SKFont font, SKRect bounds, string text)
    {
        // LineHeight -1 means platform default (use 1.0 multiplier)
        double effectiveLineHeight = LineHeight < 0 ? 1.0 : LineHeight;
        float lineHeight = (float)(FontSize * effectiveLineHeight);
        float y = bounds.Top;
        int lineCount = 0;

        var lines = WrapText(text, paint, bounds.Width);

        foreach (var line in lines)
        {
            if (MaxLines > 0 && lineCount >= MaxLines) break;
            if (y + lineHeight > bounds.Bottom && MaxLines == 0) break;

            var textBounds = new SKRect();
            paint.MeasureText(line, ref textBounds);

            float textWidth = textBounds.Width;
            if (CharacterSpacing != 0 && line.Length > 1)
            {
                textWidth += (float)(CharacterSpacing * (line.Length - 1));
            }

            // Use FlowDirection-aware positioning
            float x = GetHorizontalPosition(HorizontalTextAlignment, bounds.Left, bounds.Right, textWidth);

            float textY = y - textBounds.Top;
            DrawTextWithSpacing(canvas, line, x, textY, paint);
            DrawTextDecorations(canvas, paint, x, textY, textBounds);

            y += lineHeight;
            lineCount++;
        }
    }

    private void DrawTextWithSpacing(SKCanvas canvas, string text, float x, float y, SKPaint paint)
    {
        if (string.IsNullOrEmpty(text)) return;

        // Get the preferred typeface from the current paint
        var fontFamily = string.IsNullOrEmpty(FontFamily) ? "Sans" : FontFamily;
        var preferredTypeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(fontFamily, GetFontStyle())
                               ?? SKTypeface.Default;

        if (CharacterSpacing == 0 || text.Length <= 1)
        {
            // No character spacing - use font fallback for the whole string
            DrawTextWithFallback(canvas, text, x, y, paint, preferredTypeface);
            return;
        }

        // With character spacing, we need to draw character by character with fallback
        float currentX = x;
        float fontSize = FontSize > 0 ? (float)FontSize : 14f;

        // Use font fallback to get runs for proper glyph coverage
        var runs = FontFallbackManager.Instance.ShapeTextWithFallback(text, preferredTypeface);

        foreach (var run in runs)
        {
            // Draw each character in the run with spacing
            foreach (char c in run.Text)
            {
                string charStr = c.ToString();
                using var charFont = new SKFont(run.Typeface, fontSize);
                using var charPaint = new SKPaint(charFont)
                {
                    Color = paint.Color,
                    IsAntialias = true
                };

                canvas.DrawText(charStr, currentX, y, charPaint);
                currentX += charPaint.MeasureText(charStr) + (float)CharacterSpacing;
            }
        }
    }

    /// <summary>
    /// Draws text with font fallback for emoji, CJK, and other scripts.
    /// </summary>
    private void DrawTextWithFallback(SKCanvas canvas, string text, float x, float y, SKPaint paint, SKTypeface preferredTypeface)
        => TextRenderingHelper.DrawTextWithFallback(canvas, text, x, y, paint, preferredTypeface, FontSize > 0 ? (float)FontSize : 14f);

    /// <summary>
    /// Draws formatted span text with font fallback for emoji, CJK, and other scripts.
    /// </summary>
    private void DrawFormattedSpanWithFallback(SKCanvas canvas, string text, float x, float y, SKPaint paint, SKTypeface preferredTypeface, float fontSize)
    {
        if (string.IsNullOrEmpty(text))
        {
            return;
        }

        // Use FontFallbackManager for mixed-script text
        var runs = FontFallbackManager.Instance.ShapeTextWithFallback(text, preferredTypeface);

        if (runs.Count <= 1)
        {
            // Single run or no fallback needed - draw directly
            canvas.DrawText(text, x, y, paint);
            return;
        }

        // Multiple runs with different fonts
        float currentX = x;

        foreach (var run in runs)
        {
            using var runFont = new SKFont(run.Typeface, fontSize);
            using var runPaint = new SKPaint(runFont)
            {
                Color = paint.Color,
                IsAntialias = true
            };

            canvas.DrawText(run.Text, currentX, y, runPaint);
            currentX += runPaint.MeasureText(run.Text);
        }
    }

    private void DrawTextDecorations(SKCanvas canvas, SKPaint paint, float x, float y, SKRect textBounds)
    {
        if (TextDecorations == TextDecorations.None) return;

        using var linePaint = new SKPaint
        {
            Color = paint.Color,
            StrokeWidth = 1,
            IsAntialias = true
        };

        float textWidth = textBounds.Width;
        if (CharacterSpacing != 0)
        {
            // Approximate width adjustment for decorations
            textWidth += (float)(CharacterSpacing * Math.Max(0, Text?.Length - 1 ?? 0));
        }

        if (TextDecorations.HasFlag(TextDecorations.Underline))
        {
            float underlineY = y + 2;
            canvas.DrawLine(x, underlineY, x + textWidth, underlineY, linePaint);
        }

        if (TextDecorations.HasFlag(TextDecorations.Strikethrough))
        {
            float strikeY = y - textBounds.Height / 3;
            canvas.DrawLine(x, strikeY, x + textWidth, strikeY, linePaint);
        }
    }

    private void DrawFormattedText(SKCanvas canvas, SKRect bounds)
    {
        if (FormattedText == null) return;

        float x = bounds.Left;
        float y = bounds.Top;
        // LineHeight -1 means platform default (use 1.0 multiplier)
        double effectiveLineHeight = LineHeight < 0 ? 1.0 : LineHeight;
        float lineHeight = (float)(FontSize * effectiveLineHeight);
        float fontSize = FontSize > 0 ? (float)FontSize : 14f;

        // Calculate baseline for first line
        using var measureFont = new SKFont(SKTypeface.Default, fontSize);
        using var measurePaint = new SKPaint(measureFont);
        var metrics = measurePaint.FontMetrics;
        y -= metrics.Ascent;

        foreach (var span in FormattedText.Spans)
        {
            if (string.IsNullOrEmpty(span.Text)) continue;

            // Get span-specific styling
            var spanFontSize = span.FontSize > 0 ? (float)span.FontSize : fontSize;
            var spanFontFamily = !string.IsNullOrEmpty(span.FontFamily) ? span.FontFamily :
                                 (!string.IsNullOrEmpty(FontFamily) ? FontFamily : "Sans");

            bool isBold = span.FontAttributes.HasFlag(FontAttributes.Bold) ||
                         FontAttributes.HasFlag(FontAttributes.Bold);
            bool isItalic = span.FontAttributes.HasFlag(FontAttributes.Italic) ||
                           FontAttributes.HasFlag(FontAttributes.Italic);

            var fontStyle = new SKFontStyle(
                isBold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal,
                SKFontStyleWidth.Normal,
                isItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright);

            using var font = new SKFont(
                SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(spanFontFamily, fontStyle) ?? SKTypeface.Default,
                spanFontSize);

            var spanColor = span.TextColor ?? TextColor;
            using var paint = new SKPaint(font)
            {
                Color = ToSKColor(spanColor),
                IsAntialias = true
            };

            var textBounds = new SKRect();
            paint.MeasureText(span.Text, ref textBounds);

            // Check if we need to wrap to next line
            if (x + textBounds.Width > bounds.Right && x > bounds.Left)
            {
                x = bounds.Left;
                y += lineHeight;
            }

            // Use font fallback for this span
            var preferredTypeface = SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(spanFontFamily, fontStyle)
                                   ?? SKTypeface.Default;
            DrawFormattedSpanWithFallback(canvas, span.Text, x, y, paint, preferredTypeface, spanFontSize);

            // Draw span decorations
            if (span.TextDecorations != TextDecorations.None)
            {
                using var linePaint = new SKPaint { Color = paint.Color, StrokeWidth = 1, IsAntialias = true };

                if (span.TextDecorations.HasFlag(TextDecorations.Underline))
                {
                    canvas.DrawLine(x, y + 2, x + textBounds.Width, y + 2, linePaint);
                }
                if (span.TextDecorations.HasFlag(TextDecorations.Strikethrough))
                {
                    float strikeY = y - textBounds.Height / 3;
                    canvas.DrawLine(x, strikeY, x + textBounds.Width, strikeY, linePaint);
                }
            }

            x += textBounds.Width;
        }
    }

    private string TruncateText(string text, SKPaint paint, float maxWidth, LineBreakMode mode)
    {
        if (string.IsNullOrEmpty(text)) return text;

        var bounds = new SKRect();
        paint.MeasureText(text, ref bounds);
        if (bounds.Width <= maxWidth) return text;

        string ellipsis = "...";
        float ellipsisWidth = paint.MeasureText(ellipsis);

        switch (mode)
        {
            case LineBreakMode.HeadTruncation:
                for (int i = 1; i < text.Length; i++)
                {
                    string truncated = ellipsis + text.Substring(i);
                    if (paint.MeasureText(truncated) <= maxWidth)
                        return truncated;
                }
                return ellipsis;

            case LineBreakMode.MiddleTruncation:
                int half = text.Length / 2;
                for (int i = 0; i < half; i++)
                {
                    string truncated = text.Substring(0, half - i) + ellipsis + text.Substring(half + i);
                    if (paint.MeasureText(truncated) <= maxWidth)
                        return truncated;
                }
                return ellipsis;

            case LineBreakMode.TailTruncation:
            default:
                for (int i = text.Length - 1; i > 0; i--)
                {
                    string truncated = text.Substring(0, i) + ellipsis;
                    if (paint.MeasureText(truncated) <= maxWidth)
                        return truncated;
                }
                return ellipsis;
        }
    }

    private List<string> WrapText(string text, SKPaint paint, float maxWidth)
    {
        var lines = new List<string>();
        if (string.IsNullOrEmpty(text)) return lines;

        // Split by existing newlines first
        var paragraphs = text.Split('\n');

        foreach (var paragraph in paragraphs)
        {
            if (string.IsNullOrEmpty(paragraph))
            {
                lines.Add(string.Empty);
                continue;
            }

            // Check if the entire paragraph fits on one line - no need to wrap
            // Use small tolerance to account for floating point precision
            float paragraphWidth = paint.MeasureText(paragraph);
            if (paragraphWidth <= maxWidth + 1.0f)
            {
                lines.Add(paragraph);
                continue;
            }

            if (LineBreakMode == LineBreakMode.CharacterWrap)
            {
                WrapByCharacter(paragraph, paint, maxWidth, lines);
            }
            else
            {
                WrapByWord(paragraph, paint, maxWidth, lines);
            }
        }

        return lines;
    }

    private void WrapByWord(string text, SKPaint paint, float maxWidth, List<string> lines)
    {
        var words = text.Split(' ');
        string currentLine = "";

        foreach (var word in words)
        {
            string testLine = string.IsNullOrEmpty(currentLine) ? word : currentLine + " " + word;
            float width = paint.MeasureText(testLine);

            if (width > maxWidth && !string.IsNullOrEmpty(currentLine))
            {
                lines.Add(currentLine);
                currentLine = word;
            }
            else
            {
                currentLine = testLine;
            }
        }

        if (!string.IsNullOrEmpty(currentLine))
        {
            lines.Add(currentLine);
        }
    }

    private void WrapByCharacter(string text, SKPaint paint, float maxWidth, List<string> lines)
    {
        string currentLine = "";

        foreach (char c in text)
        {
            string testLine = currentLine + c;
            float width = paint.MeasureText(testLine);

            if (width > maxWidth && !string.IsNullOrEmpty(currentLine))
            {
                lines.Add(currentLine);
                currentLine = c.ToString();
            }
            else
            {
                currentLine = testLine;
            }
        }

        if (!string.IsNullOrEmpty(currentLine))
        {
            lines.Add(currentLine);
        }
    }

    #endregion

    #region Measurement

    protected override Size MeasureOverride(Size availableSize)
    {
        var padding = Padding;
        double paddingH = padding.Left + padding.Right;
        double paddingV = padding.Top + padding.Bottom;

        string displayText = GetDisplayText();
        if (string.IsNullOrEmpty(displayText) && (FormattedText == null || FormattedText.Spans.Count == 0))
        {
            return new Size(paddingH, paddingV + FontSize);
        }

        float fontSize = FontSize > 0 ? (float)FontSize : 14f;
        var fontFamily = string.IsNullOrEmpty(FontFamily) ? "Sans" : FontFamily;

        using var font = new SKFont(
            SkiaRenderingEngine.Current?.ResourceCache.GetTypeface(fontFamily, GetFontStyle()) ?? SKTypeface.Default,
            fontSize);

        using var paint = new SKPaint(font);

        double width, height;
        // LineHeight -1 means platform default (use 1.0 multiplier)
        double effectiveLineHeight = LineHeight < 0 ? 1.0 : LineHeight;

        if (FormattedText != null && FormattedText.Spans.Count > 0)
        {
            // Measure formatted text
            width = 0;
            height = fontSize * effectiveLineHeight;
            foreach (var span in FormattedText.Spans)
            {
                if (!string.IsNullOrEmpty(span.Text))
                {
                    width += paint.MeasureText(span.Text);
                }
            }
        }
        else
        {
            // Use advance width (paint.MeasureText return value) not bounding box width
            // This must match what WrapText uses for consistency
            var textBounds = new SKRect();
            paint.MeasureText(displayText, ref textBounds);
            width = paint.MeasureText(displayText);  // Advance width, not textBounds.Width
            height = textBounds.Height;

            // Account for character spacing
            if (CharacterSpacing != 0 && displayText.Length > 1)
            {
                width += CharacterSpacing * (displayText.Length - 1);
            }

            // Account for multi-line
            if (displayText.Contains('\n') || MaxLines > 1)
            {
                var lines = displayText.Split('\n');
                int lineCount = MaxLines > 0 ? Math.Min(lines.Length, MaxLines) : lines.Length;
                height = lineCount * fontSize * effectiveLineHeight;
            }
        }

        width += paddingH;
        height += paddingV;

        // Respect explicit size requests
        if (WidthRequest >= 0)
        {
            width = WidthRequest;
        }
        if (HeightRequest >= 0)
        {
            height = HeightRequest;
        }

        return new Size(Math.Max(width, 1.0), Math.Max(height, 1.0));
    }

    #endregion
}

