// 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.Diagnostics;

namespace Microsoft.AspNetCore.Razor.Language
{
	internal class DefaultRazorSourceLineCollection : RazorSourceLineCollection
	{
		private readonly RazorSourceDocument _document;
		private readonly int[] _lineStarts;

		public DefaultRazorSourceLineCollection( RazorSourceDocument document )
		{
			_document = document;
			_lineStarts = GetLineStarts();
		}

		public override int Count => _lineStarts.Length;

		public override int GetLineLength( int index )
		{
			if ( index < 0 || index >= _lineStarts.Length )
			{
				throw new IndexOutOfRangeException( nameof( index ) );
			}

			if ( index == _lineStarts.Length - 1 )
			{
				// Last line is special.
				return _document.Length - _lineStarts[index];
			}

			return _lineStarts[index + 1] - _lineStarts[index];
		}

		internal override SourceLocation GetLocation( int position )
		{
			if ( position < 0 || position > _document.Length )
			{
				throw new IndexOutOfRangeException( nameof( position ) );
			}

			var index = Array.BinarySearch<int>( _lineStarts, position );
			if ( index >= 0 )
			{
				// We have an exact match for the start of a line.
				Debug.Assert( _lineStarts[index] == position );

				return new SourceLocation( _document.GetFilePathForDisplay(), position, index, characterIndex: 0 );
			}


			// Index is the complement of the line *after* the one we want, because BinarySearch tells
			// us where we'd put position *if* it were the start of a line.
			index = (~index) - 1;
			if ( index == -1 )
			{
				// There's no preceding line, so it's based on the start of the string
				return new SourceLocation( _document.GetFilePathForDisplay(), position, 0, position );
			}
			else
			{
				var characterIndex = position - _lineStarts[index];
				return new SourceLocation( _document.GetFilePathForDisplay(), position, index, characterIndex );
			}
		}

		private int[] GetLineStarts()
		{
			var starts = new List<int>();

			// We always consider a document to have at least a 0th line, even if it's empty.
			starts.Add( 0 );

			var length = _document.Length;
			for ( var i = 0; i < length; i++ )
			{
				var c = _document[i];

				switch ( c )
				{
					case '\r':
						if ( i + 1 < length && _document[i + 1] == '\n' )
						{
							i++;
						}

						starts.Add( i + 1 );
						break;

					case '\n':
						starts.Add( i + 1 );
						break;

					case '\u0085':
					case '\u2028':
					case '\u2029':
						starts.Add( i + 1 );
						break;
				}
			}

			return starts.ToArray();
		}
	}
}
