// 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.Runtime.CompilerServices;
using System.Text;

namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
{
	public sealed class CodeWriter
	{
		private static readonly char[] NewLineCharacters = { '\r', '\n' };

		private readonly StringBuilder _builder;

		private string _newLine;

		private int _absoluteIndex;
		private int _currentLineIndex;
		private int _currentLineCharacterIndex;

		public CodeWriter() : this( Environment.NewLine, RazorCodeGenerationOptions.CreateDefault() )
		{
		}

		public CodeWriter( string newLine, RazorCodeGenerationOptions options )
		{
			NewLine = newLine;
			IndentWithTabs = options.IndentWithTabs;
			TabSize = options.IndentSize;
			_builder = new StringBuilder();
		}

		public int CurrentIndent { get; set; }

		public int Length => _builder.Length;

		public string NewLine
		{
			get => _newLine;
			set
			{
				if ( value == null )
				{
					throw new ArgumentNullException( nameof( value ) );
				}

				if ( value != "\r\n" && value != "\n" )
				{
					throw new ArgumentException();
				}

				_newLine = value;
			}
		}

		public bool IndentWithTabs { get; }

		public int TabSize { get; }

		public SourceLocation Location => new SourceLocation( _absoluteIndex, _currentLineIndex, _currentLineCharacterIndex );

		public char this[int index]
		{
			get
			{
				if ( index < 0 || index >= _builder.Length )
				{
					throw new IndexOutOfRangeException( nameof( index ) );
				}

				return _builder[index];
			}
		}

		public CodeWriter Indent( int size )
		{
			if ( size == 0 || (Length != 0 && this[Length - 1] != '\n') )
			{
				return this;
			}

			var actualSize = 0;
			if ( IndentWithTabs )
			{
				// Avoid writing directly to the StringBuilder here, that will throw off the manual indexing
				// done by the base class.
				var tabs = size / TabSize;
				actualSize += tabs;
				_builder.Append( '\t', tabs );

				var spaces = size % TabSize;
				actualSize += spaces;
				_builder.Append( ' ', spaces );
			}
			else
			{
				actualSize = size;
				_builder.Append( ' ', size );
			}

			_currentLineCharacterIndex += actualSize;
			_absoluteIndex += actualSize;

			return this;
		}

		public CodeWriter Write( string value )
		{
			if ( value == null )
			{
				throw new ArgumentNullException( nameof( value ) );
			}

			return Write( value, 0, value.Length );
		}

		internal CodeWriter Write( StringSegment value )
		{
			return WriteCore( value.Buffer, value.Offset, value.Length );
		}

		public CodeWriter Write( string value, int startIndex, int count )
		{
			if ( value == null )
			{
				throw new ArgumentNullException( nameof( value ) );
			}

			if ( startIndex < 0 )
			{
				throw new ArgumentOutOfRangeException( nameof( startIndex ) );
			}

			if ( count < 0 )
			{
				throw new ArgumentOutOfRangeException( nameof( count ) );
			}

			if ( startIndex > value.Length - count )
			{
				throw new ArgumentOutOfRangeException( nameof( startIndex ) );
			}

			return WriteCore( value, startIndex, count );
		}

		[MethodImpl( MethodImplOptions.AggressiveInlining )]
		internal CodeWriter WriteCore( string value, int startIndex, int count )
		{
			if ( count == 0 )
			{
				return this;
			}

			Indent( CurrentIndent );

			_builder.Append( value, startIndex, count );

			_absoluteIndex += count;

			// The data string might contain a partial newline where the previously
			// written string has part of the newline.
			var i = startIndex;
			int? trailingPartStart = null;

			if (
				// Check the last character of the previous write operation.
				_builder.Length - count - 1 >= 0 &&
				_builder[_builder.Length - count - 1] == '\r' &&

				// Check the first character of the current write operation.
				_builder[_builder.Length - count] == '\n' )
			{
				// This is newline that's spread across two writes. Skip the first character of the
				// current write operation.
				//
				// We don't need to increment our newline counter because we already did that when we
				// saw the \r.
				i += 1;
				trailingPartStart = 1;
			}

			// Iterate the string, stopping at each occurrence of a newline character. This lets us count the
			// newline occurrences and keep the index of the last one.
			while ( (i = value.IndexOfAny( NewLineCharacters, i )) >= 0 )
			{
				// Newline found.
				_currentLineIndex++;
				_currentLineCharacterIndex = 0;

				i++;

				// We might have stopped at a \r, so check if it's followed by \n and then advance the index to
				// start the next search after it.
				if ( count > i &&
					value[i - 1] == '\r' &&
					value[i] == '\n' )
				{
					i++;
				}

				// The 'suffix' of the current line starts after this newline token.
				trailingPartStart = i;
			}

			if ( trailingPartStart == null )
			{
				// No newlines, just add the length of the data buffer
				_currentLineCharacterIndex += count;
			}
			else
			{
				// Newlines found, add the trailing part of 'data'
				_currentLineCharacterIndex += (count - trailingPartStart.Value);
			}

			return this;
		}

		public CodeWriter WriteLine()
		{
			_builder.Append( NewLine );

			_currentLineIndex++;
			_currentLineCharacterIndex = 0;
			_absoluteIndex += NewLine.Length;

			return this;
		}

		public CodeWriter WriteLine( string value )
		{
			if ( value == null )
			{
				throw new ArgumentNullException( nameof( value ) );
			}

			return Write( value ).WriteLine();
		}

		public string GenerateCode()
		{
			return _builder.ToString();
		}
	}
}
