Editor/DiffViewWindow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
using Editor;

/// <summary>
/// Side-by-side diff view window. Shows local (left) vs remote (right) text
/// with changed/added/removed lines highlighted. Scrollable via keyboard and drag.
/// Input is rendered upstream as sorted YAML by <see cref="SyncToolYamlRenderer"/>
/// so this window only needs to do a line-by-line diff.
/// </summary>
public class DiffViewWindow : DockWindow
{
	private readonly string _name;
	private readonly string[] _localLines;
	private readonly string[] _remoteLines;
	private readonly DiffLine[] _diffLines;
	private float _scroll;
	private Vector2 _mousePos;
	private bool _dragging;
	private float _dragStartY;
	private float _dragStartScroll;

	private enum LineKind { Same, Changed, Added, Removed }
	private enum DiffOpKind { Same, Added, Removed }

	private struct DiffLine
	{
		public int? LocalNum;
		public int? RemoteNum;
		public string LocalText;
		public string RemoteText;
		public LineKind Kind;
	}

	private struct DiffOp
	{
		public DiffOpKind Kind;
		public int? LocalIndex;
		public int? RemoteIndex;
	}

	private const float LineH = 16f;
	private const float HeaderH = 90f;

	public DiffViewWindow( string name, string localText, string remoteText )
	{
		_name = name;
		Title = $"Diff \u2014 {name}";
		Size = new Vector2( 820, 600 );
		MinimumSize = new Vector2( 600, 300 );

		// Input is already normalized (sorted YAML) by SyncToolYamlRenderer,
		// so we just split on newlines without further parsing.
		_localLines = SplitLines( localText );
		_remoteLines = SplitLines( remoteText );
		_diffLines = BuildDiff( _localLines, _remoteLines );
	}

	private static string[] SplitLines( string text )
	{
		if ( string.IsNullOrEmpty( text ) ) return Array.Empty<string>();
		return text.Replace( "\r\n", "\n" ).Replace( '\r', '\n' ).Split( '\n' );
	}

	private float MaxScroll => Math.Max( 0, _diffLines.Length * LineH - ( Height - HeaderH - 20 ) );

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

		var y = 38f;
		var pad = 12f;
		var w = Width - pad * 2;
		var halfW = ( w - 8 ) / 2;
		var gutterW = 32f;

		// Header
		Paint.SetDefaultFont( size: 12, weight: 700 );
		Paint.SetPen( Color.White );
		Paint.DrawText( new Rect( pad, y, w, 20 ), $"Diff — {_name}", TextFlag.LeftCenter );
		y += 28;

		// Column headers
		Paint.SetDefaultFont( size: 9, weight: 600 );
		Paint.SetPen( Color.Cyan.WithAlpha( 0.7f ) );
		Paint.DrawText( new Rect( pad, y, halfW, 16 ), "LOCAL (your files)", TextFlag.LeftCenter );
		Paint.SetPen( Color.Orange.WithAlpha( 0.7f ) );
		Paint.DrawText( new Rect( pad + halfW + 8, y, halfW, 16 ), "REMOTE (project dashboard)", TextFlag.LeftCenter );
		y += 20;

		// Stats bar
		var added = _diffLines.Count( d => d.Kind == LineKind.Added );
		var removed = _diffLines.Count( d => d.Kind == LineKind.Removed );
		var changed = _diffLines.Count( d => d.Kind == LineKind.Changed );

		Paint.SetDefaultFont( size: 8 );
		var statsX = pad;
		if ( changed > 0 ) { Paint.SetPen( Color.Yellow.WithAlpha( 0.7f ) ); Paint.DrawText( new Rect( statsX, y, 80, 14 ), $"~{changed} changed", TextFlag.LeftCenter ); statsX += 75; }
		if ( added > 0 ) { Paint.SetPen( Color.Green.WithAlpha( 0.7f ) ); Paint.DrawText( new Rect( statsX, y, 70, 14 ), $"+{added} added", TextFlag.LeftCenter ); statsX += 65; }
		if ( removed > 0 ) { Paint.SetPen( Color.Red.WithAlpha( 0.7f ) ); Paint.DrawText( new Rect( statsX, y, 80, 14 ), $"-{removed} removed", TextFlag.LeftCenter ); }
		if ( added == 0 && removed == 0 && changed == 0 ) { Paint.SetPen( Color.Green.WithAlpha( 0.7f ) ); Paint.DrawText( new Rect( statsX, y, 200, 14 ), "Files are identical", TextFlag.LeftCenter ); }

		// Scroll info
		var totalLines = _diffLines.Length;
		var visibleStart = (int)( _scroll / LineH ) + 1;
		var visibleLines = (int)( ( Height - HeaderH - 20 ) / LineH );
		var visibleEnd = Math.Min( visibleStart + visibleLines, totalLines );
		Paint.SetPen( Color.White.WithAlpha( 0.3f ) );
		Paint.DrawText( new Rect( pad + w - 120, y, 120, 14 ), $"Lines {visibleStart}-{visibleEnd} of {totalLines}", TextFlag.RightCenter );

		y += 18;

		// Separator
		Paint.SetPen( Color.White.WithAlpha( 0.1f ) );
		Paint.DrawLine( new Vector2( pad, y ), new Vector2( pad + w, y ) );
		y += 4;

		// Clip area for diff lines
		var clipTop = y;
		var clipH = Height - clipTop - 10;
		var startLine = Math.Max( 0, (int)( _scroll / LineH ) );
		var maxVisibleLines = (int)( clipH / LineH );

		Paint.SetDefaultFont( size: 9 );

		for ( int i = startLine; i < _diffLines.Length && i < startLine + maxVisibleLines; i++ )
		{
			var line = _diffLines[i];
			var ly = clipTop + ( i - startLine ) * LineH;

			// Background highlight
			var bgColor = line.Kind switch
			{
				LineKind.Changed => Color.Yellow.WithAlpha( 0.06f ),
				LineKind.Added => Color.Green.WithAlpha( 0.06f ),
				LineKind.Removed => Color.Red.WithAlpha( 0.06f ),
				_ => Color.Transparent
			};

			if ( bgColor.a > 0 )
			{
				Paint.SetBrush( bgColor );
				Paint.SetPen( Color.Transparent );
				Paint.DrawRect( new Rect( pad, ly, w, LineH ) );
			}

			// Center divider
			Paint.SetPen( Color.White.WithAlpha( 0.06f ) );
			var divX = pad + halfW + 4;
			Paint.DrawLine( new Vector2( divX, ly ), new Vector2( divX, ly + LineH ) );

			// Line numbers
			Paint.SetDefaultFont( size: 8 );
			Paint.SetPen( Color.White.WithAlpha( 0.2f ) );
			if ( line.LocalNum.HasValue )
				Paint.DrawText( new Rect( pad, ly, gutterW - 4, LineH ), line.LocalNum.Value.ToString(), TextFlag.RightCenter );
			if ( line.RemoteNum.HasValue )
				Paint.DrawText( new Rect( pad + halfW + 8, ly, gutterW - 4, LineH ), line.RemoteNum.Value.ToString(), TextFlag.RightCenter );

			// Content
			Paint.SetDefaultFont( size: 9 );

			var localColor = line.Kind switch
			{
				LineKind.Removed => Color.Red.WithAlpha( 0.8f ),
				LineKind.Changed => Color.Yellow.WithAlpha( 0.7f ),
				_ => Color.White.WithAlpha( 0.6f )
			};
			Paint.SetPen( localColor );
			var localPrefix = line.Kind == LineKind.Removed ? "- " : line.Kind == LineKind.Changed ? "~ " : "  ";
			Paint.DrawText( new Rect( pad + gutterW, ly, halfW - gutterW, LineH ),
				localPrefix + ( line.LocalText ?? "" ), TextFlag.LeftCenter );

			var remoteColor = line.Kind switch
			{
				LineKind.Added => Color.Green.WithAlpha( 0.8f ),
				LineKind.Changed => Color.Yellow.WithAlpha( 0.7f ),
				_ => Color.White.WithAlpha( 0.6f )
			};
			Paint.SetPen( remoteColor );
			var remotePrefix = line.Kind == LineKind.Added ? "+ " : line.Kind == LineKind.Changed ? "~ " : "  ";
			Paint.DrawText( new Rect( pad + halfW + 8 + gutterW, ly, halfW - gutterW, LineH ),
				remotePrefix + ( line.RemoteText ?? "" ), TextFlag.LeftCenter );
		}

		// Scrollbar track
		if ( MaxScroll > 0 )
		{
			var trackX = pad + w - 4;
			var trackH = clipH;
			var thumbH = Math.Max( 20, trackH * ( clipH / ( _diffLines.Length * LineH ) ) );
			var thumbY = clipTop + ( _scroll / MaxScroll ) * ( trackH - thumbH );

			Paint.SetBrush( Color.White.WithAlpha( 0.05f ) );
			Paint.SetPen( Color.Transparent );
			Paint.DrawRect( new Rect( trackX, clipTop, 4, trackH ) );

			Paint.SetBrush( Color.White.WithAlpha( 0.2f ) );
			Paint.DrawRect( new Rect( trackX, thumbY, 4, thumbH ), 2 );
		}
	}

	// ── Scrolling via keyboard, mouse wheel, and drag ──

	protected override void OnMouseWheel( WheelEvent e )
	{
		var direction = e.Delta > 0 ? -1 : 1;
		_scroll = Math.Clamp( _scroll + direction * LineH * 3, 0, MaxScroll );
		_dragging = false;
		Update();
		e.Accept();
	}

	protected override void OnKeyPress( KeyEvent e )
	{
		_dragging = false;
		var handled = true;
		switch ( e.Key )
		{
			case KeyCode.Up: _scroll = Math.Max( 0, _scroll - LineH ); break;
			case KeyCode.Down: _scroll = Math.Min( MaxScroll, _scroll + LineH ); break;
			case KeyCode.PageUp: _scroll = Math.Max( 0, _scroll - LineH * 20 ); break;
			case KeyCode.PageDown: _scroll = Math.Min( MaxScroll, _scroll + LineH * 20 ); break;
			case KeyCode.Home: _scroll = 0; break;
			case KeyCode.End: _scroll = MaxScroll; break;
			default: handled = false; break;
		}

		if ( handled ) Update();
		else base.OnKeyPress( e );
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );
		if ( _dragging )
		{
			_dragging = false;
		}
		else
		{
			_dragging = true;
			_dragStartY = e.LocalPosition.y;
			_dragStartScroll = _scroll;
		}
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		base.OnMouseMove( e );
		if ( _dragging )
		{
			var delta = _dragStartY - e.LocalPosition.y;
			_scroll = Math.Clamp( _dragStartScroll + delta, 0, MaxScroll );
		}
		_mousePos = e.LocalPosition;
		Update();
	}

	// No OnMouseRelease on DockWindow — stop drag on next click or key
	private void StopDrag() => _dragging = false;

	[Button( "Page Up", Icon = "arrow_upward" )]
	public void PageUp()
	{
		_scroll = Math.Max( 0, _scroll - LineH * 20 );
		Update();
	}

	[Button( "Page Down", Icon = "arrow_downward" )]
	public void PageDown()
	{
		_scroll = Math.Min( MaxScroll, _scroll + LineH * 20 );
		Update();
	}

	// ── Diff algorithm ──

	private static DiffLine[] BuildDiff( string[] localLines, string[] remoteLines )
	{
		var ops = BuildOperations( localLines, remoteLines );
		var result = new List<DiffLine>();
		var index = 0;

		while ( index < ops.Count )
		{
			if ( ops[index].Kind == DiffOpKind.Same )
			{
				var op = ops[index++];
				result.Add( new DiffLine
				{
					LocalNum = op.LocalIndex + 1,
					RemoteNum = op.RemoteIndex + 1,
					LocalText = GetLineText( localLines, op.LocalIndex ),
					RemoteText = GetLineText( remoteLines, op.RemoteIndex ),
					Kind = LineKind.Same
				} );
				continue;
			}

			var removed = new List<DiffOp>();
			var added = new List<DiffOp>();
			while ( index < ops.Count && ops[index].Kind != DiffOpKind.Same )
			{
				if ( ops[index].Kind == DiffOpKind.Removed ) removed.Add( ops[index] );
				else added.Add( ops[index] );
				index++;
			}

			var paired = Math.Min( removed.Count, added.Count );
			for ( int i = 0; i < paired; i++ )
			{
				result.Add( new DiffLine
				{
					LocalNum = removed[i].LocalIndex + 1,
					RemoteNum = added[i].RemoteIndex + 1,
					LocalText = GetLineText( localLines, removed[i].LocalIndex ),
					RemoteText = GetLineText( remoteLines, added[i].RemoteIndex ),
					Kind = LineKind.Changed
				} );
			}

			for ( int i = paired; i < removed.Count; i++ )
			{
				result.Add( new DiffLine
				{
					LocalNum = removed[i].LocalIndex + 1,
					RemoteNum = null,
					LocalText = GetLineText( localLines, removed[i].LocalIndex ),
					RemoteText = null,
					Kind = LineKind.Removed
				} );
			}

			for ( int i = paired; i < added.Count; i++ )
			{
				result.Add( new DiffLine
				{
					LocalNum = null,
					RemoteNum = added[i].RemoteIndex + 1,
					LocalText = null,
					RemoteText = GetLineText( remoteLines, added[i].RemoteIndex ),
					Kind = LineKind.Added
				} );
			}
		}

		return result.ToArray();
	}

	private static List<DiffOp> BuildOperations( string[] localLines, string[] remoteLines )
	{
		var localCount = localLines.Length;
		var remoteCount = remoteLines.Length;
		var lcs = new int[localCount + 1, remoteCount + 1];

		for ( int local = localCount - 1; local >= 0; local-- )
		{
			var localText = GetLineText( localLines, local );
			for ( int remote = remoteCount - 1; remote >= 0; remote-- )
			{
				if ( localText == GetLineText( remoteLines, remote ) )
				{
					lcs[local, remote] = lcs[local + 1, remote + 1] + 1;
				}
				else
				{
					lcs[local, remote] = Math.Max( lcs[local + 1, remote], lcs[local, remote + 1] );
				}
			}
		}

		var ops = new List<DiffOp>();
		var localIndex = 0;
		var remoteIndex = 0;

		while ( localIndex < localCount && remoteIndex < remoteCount )
		{
			var localText = GetLineText( localLines, localIndex );
			var remoteText = GetLineText( remoteLines, remoteIndex );

			if ( localText == remoteText )
			{
				ops.Add( new DiffOp
				{
					Kind = DiffOpKind.Same,
					LocalIndex = localIndex,
					RemoteIndex = remoteIndex
				} );
				localIndex++;
				remoteIndex++;
			}
			else if ( lcs[localIndex + 1, remoteIndex] >= lcs[localIndex, remoteIndex + 1] )
			{
				ops.Add( new DiffOp
				{
					Kind = DiffOpKind.Removed,
					LocalIndex = localIndex
				} );
				localIndex++;
			}
			else
			{
				ops.Add( new DiffOp
				{
					Kind = DiffOpKind.Added,
					RemoteIndex = remoteIndex
				} );
				remoteIndex++;
			}
		}

		while ( localIndex < localCount )
		{
			ops.Add( new DiffOp
			{
				Kind = DiffOpKind.Removed,
				LocalIndex = localIndex
			} );
			localIndex++;
		}

		while ( remoteIndex < remoteCount )
		{
			ops.Add( new DiffOp
			{
				Kind = DiffOpKind.Added,
				RemoteIndex = remoteIndex
			} );
			remoteIndex++;
		}

		return ops;
	}

	private static string GetLineText( string[] lines, int? index )
	{
		if ( !index.HasValue ) return null;
		return lines[index.Value].TrimEnd( '\r' );
	}
}