Editor/GraphicsItems/ScrollingGrid.cs
using Editor;
using System;
using System.Collections.Generic;

namespace AltCurves.GraphicsItems;

/// <summary>
/// A 2D grid that can display any arbitrary coordinate range.
/// +X is right, +Y is up on the rendered grid (opposite Y inversion from normal widget space)
/// </summary>
public class ScrollingGrid : GraphicsItem
{
	/// <summary>
	/// The coordinate range this grid should display
	/// </summary>
	public CoordinateRange2D Range
	{
		get => _rangeInternal;
		set
		{
			_rangeInternal = value;
			Update();
		}
	}

	public int MajorXLines { get; set; } = 8;
	public int MajorYLines { get; set; } = 8;
	public int MinorXLines { get; set; } = 4;
	public int MinorYLines { get; set; } = 4;

	/// <summary>
	/// The (string, pixel position) tuple of current major gridlines on the X axis
	/// </summary>
	public List<(string, float)> MajorLinesX { get; private set; } = new();

	/// <summary>
	/// The pixel position list of current major gridlines on the X axis
	/// </summary>
	public List<float> MinorLinesX { get; private set; } = new();

	/// <summary>
	/// The (string, pixel position) tuple list of current major gridlines on the Y axis
	/// </summary>
	public List<(string, float)> MajorLinesY { get; private set; } = new();

	/// <summary>
	/// The pixel position list of current major gridlines on the Y axis
	/// </summary>
	public List<float> MinorLinesY { get; private set; } = new();

	public double GridBaseX { get; private set; } = 0.0;
	public double GridBaseY { get; private set; } = 0.0;
	public double GridStepX { get; private set; } = 1.0;
	public double GridStepY { get; private set; } = 1.0;

	private CoordinateRange2D _rangeInternal; // Don't modify directly

	public ScrollingGrid()
	{
		Clip = true;
		_rangeInternal = new()
		{
			MinX = -1.0f,
			MaxX = 1.0f,
			MinY = -1.0f,
			MaxY = 1.0f
		};
	}

	protected override void OnPaint()
	{
		base.OnPaint();

#if CURVE_DEBUG 
		Paint.DrawText( new( 30.0f, 30.0f ), $"Bounds:{Range}" );
#endif

		(MajorLinesX, MinorLinesX, GridBaseX, GridStepX) = CalculateGridlines( Range.MinX, Range.MaxX, Width, MajorXLines, MinorXLines );
		(MajorLinesY, MinorLinesY, GridBaseY, GridStepY) = CalculateGridlines( Range.MinY, Range.MaxY, Height, MajorYLines, MinorYLines, invert: true );

		// JMCB TODO: Benchmark, potential optimization around line batching

		// Draw major lines
		Paint.SetPen( Color.White.WithAlpha( 0.3f ), 1.0f, PenStyle.Solid );
		foreach ( var line in MajorLinesX )
		{
			Paint.DrawLine( new( line.Item2, 0.0f ), new( line.Item2, Height ) );
		}
		foreach ( var line in MajorLinesY )
		{
			Paint.DrawLine( new( 0.0f, line.Item2 ), new( Width, line.Item2 ) );
		}

		// Minor lines
		Paint.SetPen( Color.White.WithAlpha( 0.2f ), 0.25f, PenStyle.Dot );
		foreach ( var line in MinorLinesX )
		{
			Paint.DrawLine( new( line, 0.0f ), new( line, Height ) );
		}
		foreach ( var line in MinorLinesY )
		{
			Paint.DrawLine( new( 0.0f, line ), new( Width, line ) );
		}

		// Text
		Paint.SetPen( Color.White.WithAlpha( 1.0f ), style: PenStyle.Solid );
		Paint.SetDefaultFont( size: 10 );
		foreach ( var line in MajorLinesX )
		{
			var textSize = Paint.MeasureText( line.Item1 );
			Paint.DrawText( new( line.Item2 - textSize.x / 2.0f, 0.0f ), line.Item1 );
		}
		foreach ( var line in MajorLinesY )
		{
			var textSize = Paint.MeasureText( line.Item1 );
			Paint.DrawText( new( 0.0f, line.Item2 - textSize.y / 2.0f ), line.Item1 );
		}
	}

	/// <summary>
	/// Given the min/max coordinates on an axis and a given widget pixel size, output the pixel offset
	/// for a series of major and minor gridlines, preferring to round to nice numbers.
	/// </summary>
	private static (List<(string, float)> majorLines, List<float> minorLines, double stepPos, double stepSize)
		CalculateGridlines( double rangeMin, double rangeMax, float widgetDimension, int majorSteps = 8, int minorSteps = 4, bool invert = false )
	{
		var gridLinesMajor = new List<(string, float)>();
		var gridLinesMinor = new List<float>();

		var curveWidth = rangeMax - rangeMin;
		var stepSize = curveWidth / majorSteps;

		// Find the nearest order of magnitude below the step size, then round the step size to a nice number
		var magnitude = Math.Pow( 10, Math.Floor( Math.Log10( stepSize ) ) );
		var scaledStep = Math.Round( stepSize / magnitude );
		if ( scaledStep < 2f )
			scaledStep = 1f;
		else if ( scaledStep < 5f )
			scaledStep = 2f;
		else
			scaledStep = 5f;

		var finalStep = scaledStep * magnitude;
		var start = Math.Floor( rangeMin / finalStep ) * finalStep;
		var end = Math.Ceiling( rangeMax / finalStep ) * finalStep;

		for ( var pos = start; pos <= end; pos += finalStep )
		{
			var majorWidgetSpace = (pos - rangeMin) / (rangeMax - rangeMin) * widgetDimension;
			if ( invert ) majorWidgetSpace = widgetDimension - majorWidgetSpace;

			gridLinesMajor.Add( (pos.ToString( "0.#####" ), (float)majorWidgetSpace) );

			for ( var minorLine = 1; minorLine < minorSteps; minorLine++ )
			{
				var minorPosCurve = pos + minorLine * finalStep / minorSteps;
				var minorPosWidget = (minorPosCurve - rangeMin) / (rangeMax - rangeMin) * widgetDimension;
				if ( invert ) minorPosWidget = widgetDimension - minorPosWidget;

				gridLinesMinor.Add( (float)minorPosWidget );
			}
		}

		return (gridLinesMajor, gridLinesMinor, start, finalStep / minorSteps);
	}
}