using SkiaSharp.Views.Maui.Controls;
using Lucy.ViewModels;
using SkiaSharp;
using System.Collections.Specialized;
using System.ComponentModel;

namespace Lucy.Views.Editor
{
    /// <summary>
    /// The main editor control that displays the document pages.
    /// </summary>
    public class DocumentEditor : ContentView
    {
        public static readonly BindableProperty DocumentProperty =
            BindableProperty.Create(nameof(Document), typeof(DocumentVM), typeof(DocumentEditor), null, propertyChanged: OnDocumentChanged);

        public DocumentVM Document
        {
            get => (DocumentVM)GetValue(DocumentProperty);
            set => SetValue(DocumentProperty, value);
        }

        private StackLayout _pagesStack;

        public DocumentEditor()
        {
            ///We should change this to a CollectionView that we can then apply properties to, where each PageVM is its own collection item.
            ///Furthermore, is the viewmodel being populated by the view or is it the other way around? If viewmodel relies on the view to generate the render, that's bad.
            ///We need to figure out how sections and such will reorder themselves when pages are reordered.
            ///
            ///Page rendering needs to be made asyncronous with virtualisation, I think. Performance is shit right now when we're not making edits.
            ///We'll need to be smart about enabling WYSIWYG live editing.
            
            _pagesStack = new StackLayout
            {
                Orientation = StackOrientation.Vertical,
                Spacing = 20
            };

            Content = new ScrollView
            {
                Content = _pagesStack
            };

            SetPaperSizeToA4();
        }

        private static void OnDocumentChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var editor = (DocumentEditor)bindable;

            if (oldValue is DocumentVM oldDocument)
            {
                oldDocument.PropertyChanged -= editor.OnDocumentPropertyChanged;
                if (oldDocument.Sections is INotifyCollectionChanged oldSections)
                {
                    oldSections.CollectionChanged -= editor.OnSectionsCollectionChanged;
                }
            }

            if (newValue is DocumentVM newDocument)
            {
                newDocument.PropertyChanged += editor.OnDocumentPropertyChanged;
                if (newDocument.Sections is INotifyCollectionChanged newSections)
                {
                    newSections.CollectionChanged += editor.OnSectionsCollectionChanged;
                }
            }

            editor.GeneratePageViews();
        }

        private void OnDocumentPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            // You can add specific property checks here if needed
            GeneratePageViews();
        }

        private void OnSectionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            // Handle additions, removals, or replacements in sections
            GeneratePageViews();
        }

        /// <summary>
        /// Generates the page views for the document.
        /// </summary>
        private void GeneratePageViews()
        {
            _pagesStack.Children.Clear();

            if (Document == null)
                return;

            var renderer = new DocumentRenderer(Document, (float)PaperWidth, (float)PaperHeight);

            foreach (var page in renderer.Pages)
            {
                var canvasView = new SKCanvasView
                {
                    WidthRequest = PaperWidth,
                    HeightRequest = PaperHeight
                };
                canvasView.PaintSurface += (sender, args) =>
                {
                    var canvas = args.Surface.Canvas;
                    canvas.Clear(SKColors.Transparent);

                    var pageRenderer = new PageRenderer();
                    pageRenderer.RenderPage(canvas, page);
                };

                _pagesStack.Children.Add(canvasView);
            }

            // TODO: Implement virtualization to improve performance for large documents.
            // What Hasn't Been Implemented: Virtualization of page rendering.
            // Next Steps: Use virtualization techniques to render only visible pages.
        }

        public double PaperWidth { get; private set; }
        public double PaperHeight { get; private set; }

        private const double Dpi = 96.0;

        /// <summary>
        /// Sets the paper size to A4 dimensions.
        /// </summary>
        private void SetPaperSizeToA4()
        {
            const double A4WidthMm = 210;
            const double A4HeightMm = 297;

            double widthInches = A4WidthMm / 25.4;
            double heightInches = A4HeightMm / 25.4;

            double widthDIPs = widthInches * Dpi;
            double heightDIPs = heightInches * Dpi;

            PaperWidth = widthDIPs;
            PaperHeight = heightDIPs;
        }
    }
}
using System.Collections.Generic;
using System.Linq;
using Lucy.ViewModels;
using Lucy.ViewModels.Blocks.Base;
using SkiaSharp;
using System;
using Lucy.ViewModels.Blocks;
using Lucy.ViewModels.Internals;
using Lucy.Enumerators;
using Lucy.Models.Blocks;
using System.Collections.ObjectModel;
using Lucy.ViewModels.Editor;
using Lucy.Models.Editor;

namespace Lucy.Views.Editor
{
    /// <summary>
    /// Handles the pagination of the document into pages and layers for the editor.
    /// </summary>
    public class DocumentRenderer
    {
        /// <summary>
        /// When opening files that try to import attributes such as spacing, the UI freaks the fuck out.
        /// When relying on default values, such as with .txt files, everything is fine.
        /// Whaaat the fuck?
        /// </summary>
        
        private readonly DocumentVM _document;
        private readonly float _paperWidth;
        private readonly float _paperHeight;

        private const float MarginLeft = 50f;
        private const float MarginTop = 50f;
        private const float MarginRight = 50f;
        private const float MarginBottom = 50f;

        public List<PageVM> Pages { get; private set; } = new List<PageVM>();

        public DocumentRenderer(DocumentVM document, float paperWidth, float paperHeight)
        {
            _document = document;
            _paperWidth = paperWidth;
            _paperHeight = paperHeight;

            GeneratePages();
        }

        /// <summary>
        /// Generates pages and layers based on the content of the document.
        /// </summary>
        private void GeneratePages()
        {
            Pages.Clear();
            int pageNumber = 1;

            var currentPageM = CreateNewPage(pageNumber);
            var currentPageVM = new PageVM(currentPageM);
            Pages.Add(currentPageVM);

            var currentLayerM = new LayerM { Order = 0 };
            var currentLayerVM = new LayerVM(currentLayerM);
            currentPageVM.Layers.Add(currentLayerVM);

            float yPosition = MarginTop;
            bool isFirstBlockOnPage = true; // Flag to manage SpacingBefore

            foreach (var section in _document.Sections)
            {
                // Handle PageBreakBeforeSection
                if (_document.PageBreakBeforeSection && !isFirstBlockOnPage)
                {
                    pageNumber++;
                    currentPageM = CreateNewPage(pageNumber);
                    currentPageVM = new PageVM(currentPageM);
                    Pages.Add(currentPageVM);

                    currentLayerM = new LayerM { Order = 0 };
                    currentLayerVM = new LayerVM(currentLayerM);
                    currentPageVM.Layers.Add(currentLayerVM);

                    yPosition = MarginTop;
                    isFirstBlockOnPage = true;
                }

                // Create heading block
                var headingBlock = CreateHeadingBlock(section);
                float headingHeight = MeasureBlockHeight(headingBlock, _paperWidth - MarginLeft - MarginRight - (float)(headingBlock.LeftIndentation + headingBlock.RightIndentation));

                // Apply SpacingBefore if not the first block on the page
                if (!isFirstBlockOnPage)
                {
                    yPosition += (float)headingBlock.SpacingBefore;
                }

                // Check if heading fits on the current page
                if (yPosition + headingHeight > _paperHeight - MarginBottom)
                {
                    // Start new page
                    pageNumber++;
                    currentPageM = CreateNewPage(pageNumber);
                    currentPageVM = new PageVM(currentPageM);
                    Pages.Add(currentPageVM);

                    currentLayerM = new LayerM { Order = 0 };
                    currentLayerVM = new LayerVM(currentLayerM);
                    currentPageVM.Layers.Add(currentLayerVM);

                    yPosition = MarginTop + (float)headingBlock.SpacingBefore;
                    isFirstBlockOnPage = true; // Reset flag for new page
                }

                // Set block position
                headingBlock.X = MarginLeft + (float)headingBlock.LeftIndentation;
                headingBlock.Y = yPosition;
                currentLayerVM.Blocks.Add(headingBlock);
                yPosition += headingHeight + (float)headingBlock.SpacingAfter;
                isFirstBlockOnPage = false;

                foreach (var block in section.Blocks)
                {
                    float blockHeight = MeasureBlockHeight(block, _paperWidth - MarginLeft - MarginRight - (float)(block.LeftIndentation + block.RightIndentation));

                    // Apply SpacingBefore if not the first block on the page
                    if (!isFirstBlockOnPage)
                    {
                        yPosition += (float)block.SpacingBefore;
                    }

                    // Check if block fits on the current page
                    if (yPosition + blockHeight > _paperHeight - MarginBottom)
                    {
                        // Handle 'KeepWithNext' property
                        if (block.KeepWithNext && block != section.Blocks.Last())
                        {
                            // Start new page
                            pageNumber++;
                            currentPageM = CreateNewPage(pageNumber);
                            currentPageVM = new PageVM(currentPageM);
                            Pages.Add(currentPageVM);

                            currentLayerM = new LayerM { Order = 0 };
                            currentLayerVM = new LayerVM(currentLayerM);
                            currentPageVM.Layers.Add(currentLayerVM);

                            yPosition = MarginTop + (float)block.SpacingBefore;
                            isFirstBlockOnPage = true;
                        }
                        else
                        {
                            // Start new page
                            pageNumber++;
                            currentPageM = CreateNewPage(pageNumber);
                            currentPageVM = new PageVM(currentPageM);
                            Pages.Add(currentPageVM);

                            currentLayerM = new LayerM { Order = 0 };
                            currentLayerVM = new LayerVM(currentLayerM);
                            currentPageVM.Layers.Add(currentLayerVM);

                            yPosition = MarginTop + (float)block.SpacingBefore;
                            isFirstBlockOnPage = true;
                        }
                    }

                    // Set block position
                    block.X = MarginLeft + (float)block.LeftIndentation;
                    block.Y = yPosition;
                    currentLayerVM.Blocks.Add(block);
                    yPosition += blockHeight + (float)block.SpacingAfter;
                    isFirstBlockOnPage = false;
                }
            }
        }

        /// <summary>
        /// Creates a new page model with default margins and sizes.
        /// </summary>
        private PageM CreateNewPage(int pageNumber)
        {
            return new PageM
            {
                PageNumber = pageNumber,
                Width = _paperWidth,
                Height = _paperHeight,
                MarginLeft = MarginLeft,
                MarginTop = MarginTop,
                MarginRight = MarginRight,
                MarginBottom = MarginBottom
            };
        }

        /// <summary>
        /// Creates a heading block for a section.
        /// </summary>
        private TextBlockVM CreateHeadingBlock(SectionVM section)
        {
            var headingRun = new RunVM
            {
                Text = section.Heading,
                FontSize = 24,
                Bold = true,
                FontFamily = "Arial",
                TextColor = new Color(0, 0, 0) // Black
            };

            var headingBlockModel = new TextBlockM
            {
                SpacingBefore = 20, // Adjust as needed
                SpacingAfter = 10,
                LeftIndentation = 0,
                RightIndentation = 0,
                LineSpacing = 1.2,
                Alignment = Alignment.Left,
                FirstLineIndentation = 0,
                HangingIndentation = 0
            };

            var headingBlock = new TextBlockVM(headingBlockModel);
            headingBlock.UpdateRuns(new[] { headingRun });

            return headingBlock;
        }

        /// <summary>
        /// Measures the height of a block to determine pagination.
        /// </summary>
        private float MeasureBlockHeight(BlockVM block, float maxWidth)
        {
            if (block is TextBlockVM textBlock)
            {
                return MeasureTextBlockHeight(textBlock, maxWidth);
            }
            else
            {
                // TODO: Implement measurement for other block types (e.g., images)
                // What Hasn't Been Implemented: Measurement logic for non-text blocks.
                // Next Steps: Implement measurement logic for ImageBlockVM and other custom blocks.
                return 50f; // Placeholder
            }
        }

        /// <summary>
        /// Measures the height of a text block based on its content and styling.
        /// </summary>
        private float MeasureTextBlockHeight(TextBlockVM textBlock, float maxWidth)
        {
            if (textBlock.Runs == null || !textBlock.Runs.Any())
                return 0f;

            // Aggregate all runs into a single list of words with associated run properties
            var wordsWithRuns = new List<(string Word, RunVM Run)>();
            foreach (var run in textBlock.Runs)
            {
                if (run == null || string.IsNullOrEmpty(run.Text))
                    continue;

                var words = run.Text.Split(' ');
                foreach (var word in words)
                {
                    if (!string.IsNullOrEmpty(word))
                        wordsWithRuns.Add((word + " ", run));
                }
            }

            if (!wordsWithRuns.Any())
                return 0f;

            // Initialize measurement variables
            float totalHeight = 0f;
            float currentLineWidth = 0f;
            float currentLineHeight = 0f;

            foreach (var (Word, Run) in wordsWithRuns)
            {
                using (var paint = new SKPaint())
                {
                    paint.TextSize = (float)Run.FontSize;
                    paint.Typeface = GetTypeface(Run);
                    paint.IsAntialias = true;

                    var metrics = paint.FontMetrics;
                    float lineHeight = (metrics.Descent - metrics.Ascent + metrics.Leading) * (float)textBlock.LineSpacing;

                    float wordWidth = paint.MeasureText(Word);

                    if (currentLineWidth + wordWidth > maxWidth)
                    {
                        // Move to next line
                        totalHeight += currentLineHeight;
                        currentLineWidth = wordWidth;
                        currentLineHeight = lineHeight;
                    }
                    else
                    {
                        currentLineWidth += wordWidth;
                        currentLineHeight = Math.Max(currentLineHeight, lineHeight);
                    }
                }
            }

            // Add the height of the last line
            totalHeight += currentLineHeight;

            return totalHeight;
        }

        /// <summary>
        /// Retrieves the appropriate typeface based on the run's styling.
        /// </summary>
        private SKTypeface GetTypeface(RunVM run)
        {
            var style = new SKFontStyle(
                run.Bold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal,
                SKFontStyleWidth.Normal,
                run.Italic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright);

            return SKTypeface.FromFamilyName(run.FontFamily ?? "Arial", style);
        }
    }

    ///PLAN
    ///Since we want to do layer- and grid-based content layouts, do the following.
    ///Each page is repaginated into PageM (page model) and PageVM (page view model) lists in sequential order.
    ///Each page has a layer stack; layer stacks contain references to block objects, such as TextBlockVM, ImageBlockVM, etc., and these are shown in a nested list under each layer.
    ///Lists are ordered top to bottom, with left to right being preferred if there are any clashes.
    ///When adding document objects, such as lines to separate headers from the document's body, these are added as objects such as LineM/LineVM and so on.
    ///Make it modular to apply document- or page-wide styles for these objects, allowing for LaTeX-styled templating **AND** Word-styled WYSIWYG editing.
    ///Every inserted template object has default values and modified values, and both are stored so a user can reset back to defaults.
    ///Any changes to an object or layout forces a repagination.
    ///We can then also allow for a toolbar for objects, such as textblocks, images, lines, etc., whilst also allowing for programmatic insertion.
    ///If an image is on the same layer as text, then the text can dynamically wrap around it; else, it goes in front or behind text, depending on order in the layer stack.
    ///Enable virtualisation of each PageM/PageVM to allow for performant rendering, only rendering what we need at the time.
    ///Templates define default values, meanwhile edits define modified values.
    ///This would also allow for either per-page or per-document margins, depending on need; it would be best to have a margin UI, with drag controls, that have a toggle for either "document margins" or "page margins".
    ///To allow for caret editing, a return/new paragraph character creates a new textblock object (for user friendliness, an Enter-inserted textblock would use the layout values of the previous block as its modified values).
    ///Use the "Locked" bool in each blockvm to determine if a block's attributes can be modified, meaning only default values are used, and they can't have their layout or contents changed.
    /// 
    ///Would the editor and renderer benefit from being MVVM themselves? To allow for these objects?
    ///
    ///Also, for performance's sake, pages should repaginate from the end of the document up to the modified block if editing in the document body. Anything that comes before won't be affected by layout unless the paragraph needs to run onto the next page with the edits, surely?
}

using SkiaSharp;
using Lucy.ViewModels.Blocks;
using Lucy.ViewModels.Blocks.Base;
using Lucy.Extensions;
using System.Collections.Generic;
using System;
using System.Linq;
using Lucy.ViewModels.Internals;
using Lucy.Enumerators;
using Lucy.ViewModels.Editor;

namespace Lucy.Views.Editor
{
    /// <summary>
    /// Renders a page onto the SKCanvas, drawing all layers and blocks.
    /// </summary>
    public class PageRenderer
    {
        public void RenderPage(SKCanvas canvas, PageVM page)
        {
            DrawPageBackground(canvas, page);
            canvas.Save();

            try
            {
                SetContentClipping(canvas, page);
                DrawGridLines(canvas, page);

                foreach (var layer in page.Layers.OrderBy(l => l.Order))
                {
                    foreach (var block in layer.Blocks)
                    {
                        RenderBlock(canvas, block, page);
                    }
                }
            }
            finally
            {
                canvas.Restore();
            }

            DrawPageNumber(canvas, page);
        }

        private void RenderBlock(SKCanvas canvas, BlockVM block, PageVM page)
        {
            if (block is TextBlockVM textBlock)
            {
                RenderTextBlock(canvas, textBlock, page);
            }
            // TODO: Handle other block types (e.g., images)
            // What Hasn't Been Implemented: Rendering logic for non-text blocks.
            // Next Steps: Implement rendering methods for ImageBlockVM and other custom blocks.
        }

        private void RenderTextBlock(SKCanvas canvas, TextBlockVM textBlock, PageVM page)
        {
            float x = textBlock.X;
            float y = textBlock.Y;
            float maxWidth = page.Width - page.MarginLeft - page.MarginRight - (float)(textBlock.LeftIndentation + textBlock.RightIndentation);
            float currentX = x + (float)textBlock.FirstLineIndentation;
            float currentY = y;
            float totalHeight = MeasureTextBlockHeight(textBlock, maxWidth);

            // Background rectangle covers the entire text block height
            SKRect backgroundRect = new SKRect(
                x,
                y,
                x + maxWidth,
                y + totalHeight
            );

            // Background color
            if (textBlock.BackgroundColour != null)
            {
                using (var backgroundPaint = new SKPaint
                {
                    Color = textBlock.BackgroundColour.ToSKColor(),
                    Style = SKPaintStyle.Fill
                })
                {
                    canvas.DrawRect(backgroundRect, backgroundPaint);
                }
            }

            // Draw red bounding box around the entire text block.
            using (var borderPaint = new SKPaint
            {
                Color = SKColors.Red,
                Style = SKPaintStyle.Stroke,
                StrokeWidth = 2
            })
            {
                canvas.DrawRect(backgroundRect, borderPaint);
            }

            // Aggregate all runs into a single list of words with associated run properties
            var wordsWithRuns = new List<(string Word, RunVM Run)>();
            foreach (var run in textBlock.Runs)
            {
                if (run == null || string.IsNullOrEmpty(run.Text))
                    continue;

                var words = run.Text.Split(' ');
                foreach (var word in words)
                {
                    if (!string.IsNullOrEmpty(word))
                        wordsWithRuns.Add((word + " ", run));
                }
            }

            if (!wordsWithRuns.Any())
                return;

            // Initialize rendering variables
            currentX = x + (float)textBlock.FirstLineIndentation;
            currentY = y;
            float lineHeight = 0f;
            float runAscent = 0f;
            float runDescent = 0f;

            foreach (var (Word, Run) in wordsWithRuns)
            {
                using (var paint = new SKPaint())
                {
                    paint.TextSize = (float)Run.FontSize;
                    paint.Color = Run.TextColor.ToSKColor();
                    paint.IsAntialias = true;
                    paint.Typeface = GetTypeface(Run);

                    // Text alignment
                    switch (textBlock.Alignment)
                    {
                        case Alignment.Left:
                            paint.TextAlign = SKTextAlign.Left;
                            break;
                        case Alignment.Centre:
                            paint.TextAlign = SKTextAlign.Center;
                            break;
                        case Alignment.Right:
                            paint.TextAlign = SKTextAlign.Right;
                            break;
                            // Justify alignment can be implemented with additional logic
                    }

                    var metrics = paint.FontMetrics;
                    lineHeight = (metrics.Descent - metrics.Ascent + metrics.Leading) * (float)textBlock.LineSpacing;
                    runAscent = metrics.Ascent;
                    runDescent = metrics.Descent;

                    float wordWidth = paint.MeasureText(Word);

                    // Check if the word fits in the current line
                    if (currentX + wordWidth > x + maxWidth)
                    {
                        // Move to next line
                        currentY += lineHeight;
                        currentX = x + (float)textBlock.HangingIndentation;
                    }

                    // Adjust X position based on alignment
                    float textX = currentX;
                    if (paint.TextAlign == SKTextAlign.Center)
                    {
                        textX = x + maxWidth / 2;
                    }
                    else if (paint.TextAlign == SKTextAlign.Right)
                    {
                        textX = x + maxWidth;
                    }

                    // Draw the text at the current position
                    float baselineY = currentY - runAscent;
                    canvas.DrawText(Word, textX, baselineY, paint);

                    float textWidth = paint.MeasureText(Word);

                    /*
                     * Calculate the bounding rectangle for the run.
                     * This rectangle surrounds the currently drawn text segment.
                     */
                    float rectX = paint.TextAlign == SKTextAlign.Left ? textX :
                                  paint.TextAlign == SKTextAlign.Center ? textX - (textWidth / 2) :
                                  textX - textWidth;

                    float rectY = baselineY + runAscent;
                    float rectBottom = baselineY + runDescent;
                    float rectWidth = textWidth;
                    float rectHeight = runDescent - runAscent;

                    var runRect = new SKRect(rectX, rectY, rectX + rectWidth, rectBottom);

                    /*
                     * Draw green bounding box around the run.
                     * This helps in visually identifying the boundaries of each text run.
                     */
                    using (var runBorderPaint = new SKPaint
                    {
                        Color = SKColors.Green,
                        Style = SKPaintStyle.Stroke,
                        StrokeWidth = 1
                    })
                    {
                        canvas.DrawRect(runRect, runBorderPaint);
                    }

                    currentX += wordWidth;
                }
            }
        }

        /// <summary>
        /// Retrieves the appropriate typeface based on the run's styling.
        /// </summary>
        private SKTypeface GetTypeface(RunVM run)
        {
            var style = new SKFontStyle(
                run.Bold ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal,
                SKFontStyleWidth.Normal,
                run.Italic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright);

            return SKTypeface.FromFamilyName(run.FontFamily ?? "Arial", style);
        }

        /// <summary>
        /// Draws the background and border of the page.
        /// </summary>
        private void DrawPageBackground(SKCanvas canvas, PageVM page)
        {
            var pageRect = new SKRect(0, 0, page.Width, page.Height);

            using (var pagePaint = new SKPaint
            {
                Color = SKColors.White,
                Style = SKPaintStyle.Fill
            })
            {
                canvas.DrawRect(pageRect, pagePaint);
            }

            using (var borderPaint = new SKPaint
            {
                Color = SKColors.Black,
                Style = SKPaintStyle.Stroke,
                StrokeWidth = 2
            })
            {
                canvas.DrawRect(pageRect, borderPaint);
            }
        }

        /// <summary>
        /// Sets the clipping region to the content area within the margins.
        /// </summary>
        private void SetContentClipping(SKCanvas canvas, PageVM page)
        {
            var clipRect = new SKRect(
                page.MarginLeft,
                page.MarginTop,
                page.Width - page.MarginRight,
                page.Height - page.MarginBottom
            );

            canvas.ClipRect(clipRect, SKClipOperation.Intersect, antialias: true);
        }

        /// <summary>
        /// Draws grid lines within the content area for alignment assistance.
        /// </summary>
        private void DrawGridLines(SKCanvas canvas, PageVM page)
        {
            // Grid settings
            const int DesiredColumns = 10;
            const int DesiredRows = 10;

            float contentLeft = page.MarginLeft;
            float contentRight = page.Width - page.MarginRight;
            float contentTop = page.MarginTop;
            float contentBottom = page.Height - page.MarginBottom;

            float contentWidth = contentRight - contentLeft;
            float contentHeight = contentBottom - contentTop;

            float spacingX = contentWidth / DesiredColumns;
            float spacingY = contentHeight / DesiredRows;

            using (var gridPaint = new SKPaint
            {
                Color = SKColors.LightGray,
                StrokeWidth = 1,
                Style = SKPaintStyle.Stroke,
                IsAntialias = true
            })
            {
                // Draw vertical grid lines
                for (int i = 0; i <= DesiredColumns; i++)
                {
                    float x = contentLeft + i * spacingX;
                    canvas.DrawLine(x, contentTop, x, contentBottom, gridPaint);
                }

                // Draw horizontal grid lines
                for (int i = 0; i <= DesiredRows; i++)
                {
                    float y = contentTop + i * spacingY;
                    canvas.DrawLine(contentLeft, y, contentRight, y, gridPaint);
                }
            }
        }

        /// <summary>
        /// Draws the page number at the bottom center of the page.
        /// </summary>
        private void DrawPageNumber(SKCanvas canvas, PageVM page)
        {
            using (var paint = new SKPaint())
            {
                paint.TextSize = 14;
                paint.Color = SKColors.Gray;
                paint.IsAntialias = true;
                paint.Typeface = SKTypeface.FromFamilyName("Arial");
                paint.TextAlign = SKTextAlign.Center;

                string pageNumberText = $"Page {page.PageNumber}";
                float x = page.Width / 2;
                float y = page.Height - page.MarginBottom / 2 - paint.FontMetrics.Descent;

                canvas.DrawText(pageNumberText, x, y, paint);
            }
        }

        /// <summary>
        /// Measures the height of a text block for rendering.
        /// </summary>
        private float MeasureTextBlockHeight(TextBlockVM textBlock, float maxWidth)
        {
            if (textBlock.Runs == null || !textBlock.Runs.Any())
                return 0f;

            // Aggregate all runs into a single list of words with associated run properties
            var wordsWithRuns = new List<(string Word, RunVM Run)>();
            foreach (var run in textBlock.Runs)
            {
                if (run == null || string.IsNullOrEmpty(run.Text))
                    continue;

                var words = run.Text.Split(' ');
                foreach (var word in words)
                {
                    if (!string.IsNullOrEmpty(word))
                        wordsWithRuns.Add((word + " ", run));
                }
            }

            if (!wordsWithRuns.Any())
                return 0f;

            // Initialize measurement variables
            float totalHeight = 0f;
            float currentLineWidth = 0f;
            float currentLineHeight = 0f;

            foreach (var (Word, Run) in wordsWithRuns)
            {
                using (var paint = new SKPaint())
                {
                    paint.TextSize = (float)Run.FontSize;
                    paint.Typeface = GetTypeface(Run);
                    paint.IsAntialias = true;

                    var metrics = paint.FontMetrics;
                    float lineHeight = (metrics.Descent - metrics.Ascent + metrics.Leading) * (float)textBlock.LineSpacing;

                    float wordWidth = paint.MeasureText(Word);

                    if (currentLineWidth + wordWidth > maxWidth)
                    {
                        // Move to next line
                        totalHeight += currentLineHeight;
                        currentLineWidth = wordWidth;
                        currentLineHeight = lineHeight;
                    }
                    else
                    {
                        currentLineWidth += wordWidth;
                        currentLineHeight = Math.Max(currentLineHeight, lineHeight);
                    }
                }
            }

            // Add the height of the last line
            totalHeight += currentLineHeight;

            return totalHeight;
        }
    }
}

Okay, since I'm trying this for the fiftieth fucking time: PORT THIS CODE FROM SKIASHARP TO NET MAUI 8 GRAPHICS. JUST PORT IT. TRY AND KEEP THE BEHAVIOURS THE SAME. BUT JUST GET IT FUCKING WORKING. DON'T MAKE THINGS AMBIGUOUS. JUST WORK. https://learn.microsoft.com/en-us/dotnet/maui/user-interface/graphics/draw?view=net-maui-8.0 https://learn.microsoft.com/en-us/dotnet/maui/user-interface/graphics/?view=net-maui-8.0