
// RichTextKit
// Copyright © 2019-2020 Topten Software. All Rights Reserved.
// 
// Licensed under the Apache License, Version 2.0 (the "License"); you may 
// not use this product except in compliance with the License. You may obtain 
// a copy of the License at
// 
// http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software 
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 
// License for the specific language governing permissions and limitations 
// under the License.

using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Topten.RichTextKit.Utils;

namespace Topten.RichTextKit
{
	/// <summary>
	/// Represents a block of formatted, laid out and measurable text
	/// </summary>
	public class StyledText
	{
		/// <summary>
		/// Constructor
		/// </summary>
		public StyledText()
		{
		}

		/// <summary>
		/// Constructs a styled text block from unstyled text
		/// </summary>
		/// <param name="codePoints"></param>
		public StyledText( Slice<int> codePoints )
		{
			AddText( codePoints, null );
		}

		/// <summary>
		/// Clear the content of this text block
		/// </summary>
		public virtual void Clear()
		{
			// Reset everything
			_codePoints.Clear();
			StyleRun.Pool.Value.ReturnAndClear( _styleRuns );
			_hasTextDirectionOverrides = false;
			OnChanged();
		}

		/// <summary>
		/// The length of the added text in code points
		/// </summary>
		public int Length => _codePoints.Length;

		/// <summary>
		/// Get the code points of this text block
		/// </summary>
		public Utf32Buffer CodePoints => _codePoints;

		/// <summary>
		/// Get the text runs as added by AddText
		/// </summary>
		public IReadOnlyList<StyleRun> StyleRuns
		{
			get
			{
				return _styleRuns;
			}
		}

		/// <summary>
		/// Converts a code point index to a character index
		/// </summary>
		/// <param name="codePointIndex">The code point index to convert</param>
		/// <returns>The converted index</returns>
		public int CodePointToCharacterIndex( int codePointIndex )
		{
			return _codePoints.Utf32OffsetToUtf16Offset( codePointIndex );
		}

		/// <summary>
		/// Converts a character index to a code point index
		/// </summary>
		/// <param name="characterIndex">The character index to convert</param>
		/// <returns>The converted index</returns>
		public int CharacterToCodePointIndex( int characterIndex )
		{
			return _codePoints.Utf16OffsetToUtf32Offset( characterIndex );
		}

		/// <summary>
		/// Add text to this text block
		/// </summary>
		/// <remarks>
		/// The added text will be internally coverted to UTF32.  
		/// 
		/// Note that all text indicies returned by and accepted by this object will 
		/// be UTF32 "code point indicies".  To convert between UTF16 character indicies 
		/// and UTF32 code point indicies use the <see cref="CodePointToCharacterIndex(int)"/> 
		/// and <see cref="CharacterToCodePointIndex(int)"/> methods
		/// </remarks>
		/// <param name="text">The text to add</param>
		/// <param name="style">The style of the text</param>
		public void AddText( ReadOnlySpan<char> text, IStyle style )
		{
			// Quit if redundant
			if ( text.Length == 0 )
				return;

			// Add to  buffer
			var utf32 = _codePoints.Add( text );

			// Create a run
			var run = StyleRun.Pool.Value.Get();
			run.CodePointBuffer = _codePoints;
			run.Start = utf32.Start;
			run.Length = utf32.Length;
			run.Style = style;
			if ( style != null )
				_hasTextDirectionOverrides |= style.TextDirection != TextDirection.Auto;

			// Add run
			_styleRuns.Add( run );

			// Need new layout
			OnChanged();
		}

		/// <summary>
		/// Add text to this paragraph
		/// </summary>
		/// <param name="text">The text to add</param>
		/// <param name="style">The style of the text</param>
		public void AddText( Slice<int> text, IStyle style )
		{
			if ( text.Length == 0 )
				return;

			// Add to UTF-32 buffer
			var utf32 = _codePoints.Add( text );

			// Create a run
			var run = StyleRun.Pool.Value.Get();
			run.CodePointBuffer = _codePoints;
			run.Start = utf32.Start;
			run.Length = utf32.Length;
			run.Style = style;
			if ( style != null )
				_hasTextDirectionOverrides |= style.TextDirection != TextDirection.Auto;

			// Add run
			_styleRuns.Add( run );

			// Need new layout
			OnChanged();
		}

		/// <summary>
		/// Add text to this text block
		/// </summary>
		/// <remarks>
		/// The added text will be internally coverted to UTF32.  
		/// 
		/// Note that all text indicies returned by and accepted by this object will 
		/// be UTF32 "code point indicies".  To convert between UTF16 character indicies 
		/// and UTF32 code point indicies use the <see cref="CodePointToCharacterIndex(int)"/> 
		/// and <see cref="CharacterToCodePointIndex(int)"/> methods
		/// </remarks>
		/// <param name="text">The text to add</param>
		/// <param name="style">The style of the text</param>
		public void AddText( string text, IStyle style )
		{
			AddText( text.AsSpan(), style );
		}

		/// <summary>
		/// Add all the text from another text block to this text block
		/// </summary>
		/// <param name="text">Text to add</param>
		public void AddText( StyledText text )
		{
			foreach ( var sr in text.StyleRuns )
			{
				AddText( sr.CodePoints, sr.Style );
			}
		}

		/// <summary>
		/// Add all the text from another text block to this text block
		/// </summary>
		/// <param name="offset">The position at which to insert the text</param>
		/// <param name="text">Text to add</param>
		public void InsertText( int offset, StyledText text )
		{
			foreach ( var sr in text.StyleRuns )
			{
				InsertText( offset, sr.CodePoints, sr.Style );
				offset += sr.CodePoints.Length;
			}
		}

		/// <summary>
		/// Add text to this text block
		/// </summary>
		/// <remarks>
		/// If the style is null, the new text will acquire the style of the character
		/// before the insertion point. If the text block is currently empty the style
		/// must be supplied.  If inserting at the start of a non-empty text block the
		/// style will be that of the first existing style run
		/// </remarks>
		/// <param name="position">The position to insert the text</param>
		/// <param name="text">The text to add</param>
		/// <param name="style">The style of the text (optional)</param>
		public void InsertText( int position, Slice<int> text, IStyle style = null )
		{
			// Redundant?
			if ( text.Length == 0 )
				return;

			if ( style == null && _styleRuns.Count == 0 )
				throw new InvalidOperationException( "Must supply style when inserting into an empty text block" );

			// Add to UTF-32 buffer
			var utf32 = _codePoints.Insert( position, text );

			// Update style runs
			FinishInsert( utf32, style );
		}

		/// <summary>
		/// Add text to this text block
		/// </summary>
		/// <remarks>
		/// If the style is null, the new text will acquire the style of the character
		/// before the insertion point. If the text block is currently empty the style
		/// must be supplied.  If inserting at the start of a non-empty text block the
		/// style will be that of the first existing style run
		/// </remarks>
		/// <param name="position">The position to insert the text</param>
		/// <param name="text">The text to add</param>
		/// <param name="style">The style of the text (optional)</param>
		public void InsertText( int position, ReadOnlySpan<char> text, IStyle style = null )
		{
			// Redundant?
			if ( text.Length == 0 )
				return;

			if ( style == null && _styleRuns.Count == 0 )
				throw new InvalidOperationException( "Must supply style when inserting into an empty text block" );

			// Add to UTF-32 buffer
			var utf32 = _codePoints.Insert( position, text );

			// Update style runs
			FinishInsert( utf32, style );
		}

		/// <summary>
		/// Add text to this text block
		/// </summary>
		/// <remarks>
		/// If the style is null, the new text will acquire the style of the character
		/// before the insertion point. If the text block is currently empty the style
		/// must be supplied.  If inserting at the start of a non-empty text block the
		/// style will be that of the first existing style run
		/// </remarks>
		/// <param name="position">The position to insert the text</param>
		/// <param name="text">The text to add</param>
		/// <param name="style">The style of the text (optional)</param>
		public void InsertText( int position, string text, IStyle style = null )
		{
			// Redundant?
			if ( text.Length == 0 )
				return;

			if ( style == null && _styleRuns.Count == 0 )
				throw new InvalidOperationException( "Must supply style when inserting into an empty text block" );

			// Add to UTF-32 buffer
			var utf32 = _codePoints.Insert( position, text );

			// Update style runs
			FinishInsert( utf32, style );
		}

		/// <summary>
		/// Deletes text from this text block
		/// </summary>
		/// <param name="position">The code point index to delete from</param>
		/// <param name="length">The number of code points to delete</param>
		public void DeleteText( int position, int length )
		{
			if ( length == 0 )
				return;

			// Delete text from the code point buffer
			_codePoints.Delete( position, length );

			// Fix up style runs
			for ( int i = 0; i < _styleRuns.Count; i++ )
			{
				// Get the run
				var sr = _styleRuns[i];

				// Ignore runs before the deleted range
				if ( sr.End <= position )
					continue;

				// Runs that start before the deleted range
				if ( sr.Start < position )
				{
					if ( sr.End <= position + length )
					{
						// Truncate runs the overlap with the start of the delete range
						sr.Length = position - sr.Start;
						continue;
					}
					else
					{
						// Shorten runs that completely cover the deleted range
						sr.Length -= length;
						continue;
					}
				}

				// Runs that start within the deleted range
				if ( sr.Start < position + length )
				{
					if ( sr.End <= position + length )
					{
						// Delete runs that are completely within the deleted range
						_styleRuns.RemoveAt( i );
						StyleRun.Pool.Value.Return( sr );
						i--;
						continue;
					}
					else
					{
						// Runs that overlap the end of the deleted range, just
						// keep the part past the deleted range
						sr.Length = sr.End - (position + length);
						sr.Start = position;
						continue;
					}
				}

				// Run is after the deleted range, shuffle it back
				sr.Start -= length;
			}

			// coalesc runs
			CoalescStyleRuns();

			// Need new layout
			OnChanged();
		}

		/// <summary>
		/// Overwrites the styles of existing text in the text block
		/// </summary>
		/// <param name="position">The code point index of the start of the text</param>
		/// <param name="length">The length of the text</param>
		/// <param name="style">The new style to be applied</param>
		public void ApplyStyle( int position, int length, IStyle style )
		{
			// Check args
			if ( position < 0 || position + length > this.Length )
				throw new ArgumentException( "Invalid range" );
			if ( style == null )
				throw new ArgumentNullException( nameof( style ) );

			// Redundant?
			if ( length == 0 )
				return;

			// Easy case when applying same style to entire text block
			if ( position == 0 && length == this.Length )
			{
				// Remove excess runs
				while ( _styleRuns.Count > 1 )
				{
					StyleRun.Pool.Value.Return( _styleRuns[1] );
					_styleRuns.RemoveAt( 1 );
				}

				// Reconfigure the first
				_styleRuns[0].Start = 0;
				_styleRuns[0].Length = length;
				_styleRuns[0].Style = style;

				// Reset text direction overrides flag
				_hasTextDirectionOverrides = style.TextDirection != TextDirection.Auto;

				// Invalidate and done
				OnChanged();
				return;
			}

			// Get all intersecting runs
			int newRunPos = -1;
			foreach ( var subRun in _styleRuns.GetIntersectingRunsReverse( position, length ) )
			{
				if ( subRun.Partial )
				{
					var run = _styleRuns[subRun.Index];


					if ( subRun.Offset == 0 )
					{
						// Overlaps start of existing run, keep end
						run.Start += subRun.Length;
						run.Length -= subRun.Length;
						newRunPos = subRun.Index;
					}
					else if ( subRun.Offset + subRun.Length == run.Length )
					{
						// Overlaps end of existing run, keep start
						run.Length = subRun.Offset;
						newRunPos = subRun.Index + 1;
					}
					else
					{
						// Internal to existing run, keep start and end

						// Create new run for end
						var endRun = StyleRun.Pool.Value.Get();
						endRun.CodePointBuffer = _codePoints;
						endRun.Start = run.Start + subRun.Offset + subRun.Length;
						endRun.Length = run.End - endRun.Start;
						endRun.Style = run.Style;
						_styleRuns.Insert( subRun.Index + 1, endRun );

						// Shorten the existing run to keep start
						run.Length = subRun.Offset;

						newRunPos = subRun.Index + 1;
					}
				}
				else
				{
					// Remove completely covered style runs
					StyleRun.Pool.Value.Return( _styleRuns[subRun.Index] );
					_styleRuns.RemoveAt( subRun.Index );
					newRunPos = subRun.Index;
				}
			}

			// Create style run for the new style
			var newRun = StyleRun.Pool.Value.Get();
			newRun.CodePointBuffer = _codePoints;
			newRun.Start = position;
			newRun.Length = length;
			newRun.Style = style;

			_hasTextDirectionOverrides |= style.TextDirection != TextDirection.Auto;

			// Insert it
			_styleRuns.Insert( newRunPos, newRun );

			// Coalesc
			CoalescStyleRuns();

			// Need to redo layout
			OnChanged();
		}

		/// <summary>
		/// Extract text from this styled text block
		/// </summary>
		/// <param name="from">The code point offset to extract from</param>
		/// <param name="length">The number of code points to extract</param>
		/// <returns>A new text block with the RHS split part of the text</returns>
		public StyledText Extract( int from, int length )
		{
			// Create a new text block with the same attributes as this one
			var other = new StyledText();

			// Copy text to the new paragraph
			foreach ( var subRun in _styleRuns.GetInterectingRuns( from, length ) )
			{
				var sr = _styleRuns[subRun.Index];
				other.AddText( sr.CodePoints.SubSlice( subRun.Offset, subRun.Length ), sr.Style );
			}

			return other;
		}

		/// <summary>
		/// Gets the style of the text at a specified offset
		/// </summary>
		/// <remarks>
		/// When on a style run boundary, returns the style of the preceeding run
		/// </remarks>
		/// <param name="offset">The code point offset in the text</param>
		/// <returns>An IStyle</returns>
		public IStyle GetStyleAtOffset( int offset )
		{
			if ( Length == 0 || _styleRuns.Count == 0 )
				return null;

			if ( offset == 0 )
				return _styleRuns[0].Style;

			int runIndex = _styleRuns.BinarySearch( offset, ( sr, a ) =>
			{
				if ( a <= sr.Start )
					return 1;
				if ( a > sr.End )
					return -1;
				return 0;
			} );

			if ( runIndex < 0 )
				runIndex = ~runIndex;

			if ( runIndex >= _styleRuns.Count )
				runIndex = _styleRuns.Count - 1;

			return _styleRuns[runIndex].Style;
		}

		/// <summary>
		/// Completes the insertion of text by inserting it's style run
		/// and updating the offsets of existing style runs.
		/// </summary>
		/// <param name="utf32">The utf32 slice that was inserted</param>
		/// <param name="style">The style of the inserted text</param>
		void FinishInsert( Slice<int> utf32, IStyle style )
		{
			// Update style runs
			int newRunIndex = 0;
			for ( int i = 0; i < _styleRuns.Count; i++ )
			{
				// Get the style run
				var sr = _styleRuns[i];

				// Before inserted text?
				if ( sr.End < utf32.Start )
					continue;

				// Special case for inserting at very start of text block
				// with no supplied style.
				if ( sr.Start == 0 && utf32.Start == 0 && style == null )
				{
					sr.Length += utf32.Length;
					continue;
				}

				// After inserted text?
				if ( sr.Start >= utf32.Start )
				{
					sr.Start += utf32.Length;
					continue;
				}

				// Inserting exactly at the end of a style run?
				if ( sr.End == utf32.Start )
				{
					if ( style == null || style == sr.Style )
					{
						// Extend the existing run
						sr.Length += utf32.Length;

						// Force style to null to suppress later creation
						// of a style run for it.
						style = null;
					}
					else
					{
						// Remember this is where to insert the new
						// style run
						newRunIndex = i + 1;
					}
					continue;
				}

				Debug.Assert( sr.End > utf32.Start );
				Debug.Assert( sr.Start < utf32.Start );

				// Inserting inside an existing run
				if ( style == null || style == sr.Style )
				{
					// Extend the existing style run to cover
					// the newly inserted text with the same style
					sr.Length += utf32.Length;

					// Force style to null to suppress later creation
					// of a style run for it.
					style = null;
				}
				else
				{
					// Split this run and insert the new style run between

					// Create the second part
					var split = StyleRun.Pool.Value.Get();
					split.CodePointBuffer = _codePoints;
					split.Start = utf32.Start + utf32.Length;
					split.Length = sr.End - utf32.Start;
					split.Style = sr.Style;
					_styleRuns.Insert( i + 1, split );

					// Shorten this part
					sr.Length = utf32.Start - sr.Start;

					// Insert the newly styled run after this one
					newRunIndex = i + 1;

					// Skip the second part of the split in this for loop
					// as we've already calculated it
					i++;
				}
			}

			// Create a new style run
			if ( style != null )
			{
				var run = StyleRun.Pool.Value.Get();
				run.CodePointBuffer = _codePoints;
				run.Start = utf32.Start;
				run.Length = utf32.Length;
				run.Style = style;
				_hasTextDirectionOverrides |= style.TextDirection != TextDirection.Auto;
				_styleRuns.Insert( newRunIndex, run );
			}

			// Coalesc if necessary
			if ( (newRunIndex > 0 && _styleRuns[newRunIndex - 1].Style == style) ||
				(newRunIndex + 1 < _styleRuns.Count && _styleRuns[newRunIndex + 1].Style == style) )
			{
				CoalescStyleRuns();
			}


			// Need new layout
			OnChanged();
		}

		/// <summary>
		/// Combines any consecutive style runs with the same style
		/// into a single run
		/// </summary>
		void CoalescStyleRuns()
		{
			// Nothing to do if no style runs
			if ( _styleRuns.Count == 0 )
			{
				_hasTextDirectionOverrides = false;
				return;
			}

			// Since we're iterating the entire set of style runs, might as 
			// we recalculate this flag while we're at it
			_hasTextDirectionOverrides = _styleRuns[0].Style.TextDirection != TextDirection.Auto;

			// No need to coalesc a single run
			if ( _styleRuns.Count == 1 )
				return;

			// Coalesc...
			var prev = _styleRuns[0];
			for ( int i = 1; i < _styleRuns.Count; i++ )
			{
				// Get the run
				var run = _styleRuns[i];

				// Update flag
				_hasTextDirectionOverrides |= run.Style.TextDirection != TextDirection.Auto;

				// Can run be coalesced?
				if ( run.Style == prev.Style )
				{
					// Yes
					prev.Length += run.Length;
					StyleRun.Pool.Value.Return( run );
					_styleRuns.RemoveAt( i );
					i--;
				}
				else
				{
					// No, move on..
					prev = run;
				}
			}
		}


		/// <summary>
		/// Called whenever the content of this styled text block changes
		/// </summary>
		protected virtual void OnChanged()
		{
		}

		/// <summary>
		/// All code points as supplied by user, accumulated into a single buffer
		/// </summary>
		protected Utf32Buffer _codePoints = new Utf32Buffer();

		/// <summary>
		/// A list of style runs, as supplied by user
		/// </summary>
		protected List<StyleRun> _styleRuns = new List<StyleRun>();

		/// <summary>
		/// Set to true if any style runs have a directionality override.
		/// </summary>
		protected bool _hasTextDirectionOverrides = false;

		/// <inheritdoc />
		public override string ToString()
		{
			return Utf32Utils.FromUtf32( CodePoints.AsSlice() );
		}
	}
}
