// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax;

namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
	// Tokenizer _loosely_ based on http://dev.w3.org/html5/spec/Overview.html#tokenization
	internal class HtmlTokenizer : Tokenizer
	{
		public HtmlTokenizer( ITextDocument source )
			: base( source )
		{
			base.CurrentState = StartState;
		}

		protected override int StartState => (int)HtmlTokenizerState.Data;

		private new HtmlTokenizerState? CurrentState => (HtmlTokenizerState?)base.CurrentState;

		public override SyntaxKind RazorCommentKind
		{
			get { return SyntaxKind.RazorCommentLiteral; }
		}

		public override SyntaxKind RazorCommentTransitionKind
		{
			get { return SyntaxKind.RazorCommentTransition; }
		}

		public override SyntaxKind RazorCommentStarKind
		{
			get { return SyntaxKind.RazorCommentStar; }
		}

		protected override SyntaxToken CreateToken( string content, SyntaxKind type, RazorDiagnostic[] errors )
		{
			return SyntaxFactory.Token( type, content, errors );
		}

		protected override StateResult Dispatch()
		{
			switch ( CurrentState )
			{
				case HtmlTokenizerState.Data:
					return Data();
				case HtmlTokenizerState.Text:
					return Text();
				case HtmlTokenizerState.AfterRazorCommentTransition:
					return AfterRazorCommentTransition();
				case HtmlTokenizerState.EscapedRazorCommentTransition:
					return EscapedRazorCommentTransition();
				case HtmlTokenizerState.RazorCommentBody:
					return RazorCommentBody();
				case HtmlTokenizerState.StarAfterRazorCommentBody:
					return StarAfterRazorCommentBody();
				case HtmlTokenizerState.AtTokenAfterRazorCommentBody:
					return AtTokenAfterRazorCommentBody();
				default:
					Debug.Fail( "Invalid TokenizerState" );
					return default( StateResult );
			}
		}

		// Optimize memory allocation by returning constants for the most frequent cases
		protected override string GetTokenContent( SyntaxKind type )
		{
			var tokenLength = Buffer.Length;

			if ( tokenLength == 1 )
			{
				switch ( type )
				{
					case SyntaxKind.OpenAngle:
						return "<";
					case SyntaxKind.Bang:
						return "!";
					case SyntaxKind.ForwardSlash:
						return "/";
					case SyntaxKind.QuestionMark:
						return "?";
					case SyntaxKind.LeftBracket:
						return "[";
					case SyntaxKind.CloseAngle:
						return ">";
					case SyntaxKind.RightBracket:
						return "]";
					case SyntaxKind.Equals:
						return "=";
					case SyntaxKind.DoubleQuote:
						return "\"";
					case SyntaxKind.SingleQuote:
						return "'";
					case SyntaxKind.Whitespace:
						if ( Buffer[0] == ' ' )
						{
							return " ";
						}
						if ( Buffer[0] == '\t' )
						{
							return "\t";
						}
						break;
					case SyntaxKind.NewLine:
						if ( Buffer[0] == '\n' )
						{
							return "\n";
						}
						break;
				}
			}

			if ( tokenLength == 2 && type == SyntaxKind.NewLine )
			{
				return "\r\n";
			}

			return base.GetTokenContent( type );
		}

		// http://dev.w3.org/html5/spec/Overview.html#data-state
		private StateResult Data()
		{
			if ( ParserHelpers.IsWhitespace( CurrentCharacter ) )
			{
				return Stay( Whitespace() );
			}
			else if ( ParserHelpers.IsNewLine( CurrentCharacter ) )
			{
				return Stay( Newline() );
			}
			else if ( CurrentCharacter == '@' )
			{
				TakeCurrent();
				if ( CurrentCharacter == '*' )
				{
					return Transition(
						HtmlTokenizerState.AfterRazorCommentTransition,
						EndToken( SyntaxKind.RazorCommentTransition ) );
				}
				else if ( CurrentCharacter == '@' )
				{
					// Could be escaped comment transition
					return Transition(
						HtmlTokenizerState.EscapedRazorCommentTransition,
						EndToken( SyntaxKind.Transition ) );
				}

				return Stay( EndToken( SyntaxKind.Transition ) );
			}
			else if ( AtToken() )
			{
				return Stay( Token() );
			}
			else
			{
				return Transition( HtmlTokenizerState.Text );
			}
		}

		private StateResult EscapedRazorCommentTransition()
		{
			TakeCurrent();
			return Transition( HtmlTokenizerState.Data, EndToken( SyntaxKind.Transition ) );
		}

		private StateResult Text()
		{
			var prev = '\0';
			while ( !EndOfFile &&
				!(ParserHelpers.IsWhitespace( CurrentCharacter ) || ParserHelpers.IsNewLine( CurrentCharacter )) &&
				!AtToken() )
			{
				prev = CurrentCharacter;
				TakeCurrent();
			}

			if ( CurrentCharacter == '@' )
			{
				var next = Peek();
				if ( (ParserHelpers.IsLetter( prev ) || ParserHelpers.IsDecimalDigit( prev )) &&
					(ParserHelpers.IsLetter( next ) || ParserHelpers.IsDecimalDigit( next )) )
				{
					TakeCurrent(); // Take the "@"
					return Stay(); // Stay in the Text state
				}
			}

			// Output the Text token and return to the Data state to tokenize the next character (if there is one)
			return Transition( HtmlTokenizerState.Data, EndToken( SyntaxKind.Text ) );
		}

		private SyntaxToken Token()
		{
			Debug.Assert( AtToken() );
			var sym = CurrentCharacter;
			TakeCurrent();
			switch ( sym )
			{
				case '<':
					return EndToken( SyntaxKind.OpenAngle );
				case '!':
					return EndToken( SyntaxKind.Bang );
				case '/':
					return EndToken( SyntaxKind.ForwardSlash );
				case '?':
					return EndToken( SyntaxKind.QuestionMark );
				case '[':
					return EndToken( SyntaxKind.LeftBracket );
				case '>':
					return EndToken( SyntaxKind.CloseAngle );
				case ']':
					return EndToken( SyntaxKind.RightBracket );
				case '=':
					return EndToken( SyntaxKind.Equals );
				case '"':
					return EndToken( SyntaxKind.DoubleQuote );
				case '\'':
					return EndToken( SyntaxKind.SingleQuote );
				case '-':
					Debug.Assert( CurrentCharacter == '-' );
					TakeCurrent();
					return EndToken( SyntaxKind.DoubleHyphen );
				default:
					Debug.Fail( "Unexpected token!" );
					return EndToken( SyntaxKind.Marker );
			}
		}

		private SyntaxToken Whitespace()
		{
			while ( ParserHelpers.IsWhitespace( CurrentCharacter ) )
			{
				TakeCurrent();
			}
			return EndToken( SyntaxKind.Whitespace );
		}

		private SyntaxToken Newline()
		{
			Debug.Assert( ParserHelpers.IsNewLine( CurrentCharacter ) );
			// CSharp Spec §2.3.1
			var checkTwoCharNewline = CurrentCharacter == '\r';
			TakeCurrent();
			if ( checkTwoCharNewline && CurrentCharacter == '\n' )
			{
				TakeCurrent();
			}
			return EndToken( SyntaxKind.NewLine );
		}

		private bool AtToken()
		{
			switch ( CurrentCharacter )
			{
				case '<':
				case '!':
				case '/':
				case '?':
				case '[':
				case '>':
				case ']':
				case '=':
				case '"':
				case '\'':
				case '@':
					return true;
				case '-':
					return Peek() == '-';
			}

			return false;
		}

		private StateResult Transition( HtmlTokenizerState state )
		{
			return Transition( (int)state, result: null );
		}

		private StateResult Transition( HtmlTokenizerState state, SyntaxToken result )
		{
			return Transition( (int)state, result );
		}

		private enum HtmlTokenizerState
		{
			Data,
			Text,

			// Razor Comments - need to be the same for HTML and CSharp
			AfterRazorCommentTransition = RazorCommentTokenizerState.AfterRazorCommentTransition,
			EscapedRazorCommentTransition = RazorCommentTokenizerState.EscapedRazorCommentTransition,
			RazorCommentBody = RazorCommentTokenizerState.RazorCommentBody,
			StarAfterRazorCommentBody = RazorCommentTokenizerState.StarAfterRazorCommentBody,
			AtTokenAfterRazorCommentBody = RazorCommentTokenizerState.AtTokenAfterRazorCommentBody,
		}
	}
}
