//
// CSharpIndentEngine.cs
//
// Author:
//       Mike Krüger <mkrueger@xamarin.com>
//
// Copyright (c) 2012 Xamarin Inc. (http://xamarin.com)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
using System;
using ICSharpCode.NRefactory.Editor;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;

namespace ICSharpCode.NRefactory.CSharp
{
	public class CSharpIndentEngine
	{
		readonly IDocument document;
		readonly CSharpFormattingOptions options;
		readonly TextEditorOptions textEditorOptions;
		readonly StringBuilder wordBuf = new StringBuilder();
		readonly StringBuilder currentIndent = new StringBuilder();
		Indent thisLineindent;
		Indent indent;
		Indent indentDelta;

		public IList<string> ConditionalSymbols {
			get;
			set;
		}

		public TextLocation Location {
			get {
				return new TextLocation(line, col);
			}
		}

		public int Offset {
			get {
				return offset;
			}
		}

		public string ThisLineIndent {
			get {
				return thisLineindent.IndentString;
			}
		}

		public string NewLineIndent {
			get {
				return indent.IndentString + indentDelta.IndentString;
			}
		}

		public bool NeedsReindent {
			get {
				return ThisLineIndent != currentIndent.ToString();
			}
		}

		public CSharpIndentEngine(IDocument document, TextEditorOptions textEditorOptions, CSharpFormattingOptions formattingOptions)
		{
			this.document = document;
			this.options = formattingOptions;
			this.textEditorOptions = textEditorOptions;
			this.indent = new Indent(textEditorOptions);
			this.indentDelta = new Indent(textEditorOptions);
			this.thisLineindent = new Indent(textEditorOptions);
		}

		CSharpIndentEngine(CSharpIndentEngine prototype)
		{
			this.document = prototype.document;
			this.options = prototype.options;
			this.textEditorOptions = prototype.textEditorOptions;
			this.indent = prototype.indent.Clone();
			this.indentDelta = prototype.indentDelta.Clone();
			this.thisLineindent = prototype.thisLineindent.Clone();
			this.offset = prototype.offset;
			this.inside = prototype.inside;
			this.IsLineStart = prototype.IsLineStart;
			this.pc = prototype.pc;
			this.parenStack = new Stack<TextLocation>(prototype.parenStack.Reverse());
			this.currentBody = prototype.currentBody;
			this.nextBody = prototype.nextBody;
			this.addContinuation = prototype.addContinuation;
			this.line = prototype.line;
			this.col = prototype.col;
			this.popNextParenBlock = prototype.popNextParenBlock;
		}

		public CSharpIndentEngine Clone()
		{
			return new CSharpIndentEngine(this);
		}

		int offset;
		Inside inside = Inside.Empty;
		bool IsLineStart = true;
		char pc;
		Stack<TextLocation> parenStack = new Stack<TextLocation>();
		Body currentBody;
		Body nextBody;
		bool addContinuation;
		int line, col;
		bool popNextParenBlock;
		bool readPreprocessorExpression;

		public void Reset()
		{
			offset = 0;
			thisLineindent.Reset();
			indent.Reset();
			pc = '\0';
			IsLineStart = true;
			addContinuation = false;
			popNextParenBlock = false;
			parenStack.Clear();
			inside = Inside.Empty;
			nextBody = currentBody = Body.None;
			line = col = 1;
		}

		public void UpdateToOffset(int toOffset)
		{
			if (toOffset < offset)
				Reset();
			for (int i = offset; i < toOffset; i++)
				Push(document.GetCharAt(i));
		}

		public bool IsInStringOrChar {
			get {
				return inside.HasFlag(Inside.StringOrChar);
			}
		}

		public bool IsInComment {
			get {
				return inside.HasFlag(Inside.Comment);
			}
		}

		public bool IsInPreProcessorComment {
			get {
				return inside.HasFlag(Inside.PreProcessorComment);
			}
		}

		public bool IsInPreProcessorDirective {
			get {
				return inside.HasFlag(Inside.PreProcessor);
			}
		}

		public bool IsInVerbatimString {
			get {
				return inside.HasFlag(Inside.VerbatimString);
			}
		}

		public bool IsInsideDocLineComment {
			get {
				return inside.HasFlag(Inside.DocComment);
			}
		}

		public bool IsInsideMultiLineComment {
			get {
				return inside.HasFlag(Inside.MultiLineComment);
			}
		}

		public bool IsInsideStringLiteral {
			get {
				return inside.HasFlag(Inside.StringLiteral);
			}
		}

		[Flags]
		public enum Inside
		{
			Empty              = 0,

			PreProcessor       = (1 << 0),
			PreProcessorComment = (1 << 12),

			MultiLineComment   = (1 << 1),
			LineComment        = (1 << 2),
			DocComment         = (1 << 11),
			Comment            = (MultiLineComment | LineComment | DocComment),

			VerbatimString     = (1 << 3),
			StringLiteral      = (1 << 4),
			CharLiteral        = (1 << 5),
			String             = (VerbatimString | StringLiteral),
			StringOrChar       = (String | CharLiteral),

			Attribute          = (1 << 6),
			ParenList          = (1 << 7),

			FoldedStatement    = (1 << 8),
			Block              = (1 << 9),
			Case               = (1 << 10),

			FoldedOrBlock      = (FoldedStatement | Block),
			FoldedBlockOrCase  = (FoldedStatement | Block | Case)
		}

																#region Pre processor evaluation (from cs-tokenizer.cs)
		static bool is_identifier_start_character(int c)
		{
			return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || Char.IsLetter((char)c);
		}

		static bool is_identifier_part_character(char c)
		{
			if (c >= 'a' && c <= 'z')
				return true;

			if (c >= 'A' && c <= 'Z')
				return true;

			if (c == '_' || (c >= '0' && c <= '9'))
				return true;

			if (c < 0x80)
				return false;

			return Char.IsLetter(c) || Char.GetUnicodeCategory(c) == UnicodeCategory.ConnectorPunctuation;
		}

		bool eval_val(string s)
		{
			if (s == "true")
				return true;
			if (s == "false")
				return false;

			return ConditionalSymbols != null && ConditionalSymbols.Contains(s);
		}

		bool pp_primary(ref string s)
		{
			s = s.Trim();
			int len = s.Length;

			if (len > 0) {
				char c = s [0];

				if (c == '(') {
					s = s.Substring(1);
					bool val = pp_expr(ref s, false);
					if (s.Length > 0 && s [0] == ')') {
						s = s.Substring(1);
						return val;
					}
					return false;
				}

				if (is_identifier_start_character(c)) {
					int j = 1;

					while (j < len) {
						c = s [j];

						if (is_identifier_part_character(c)) {
							j++;
							continue;
						}
						bool v = eval_val(s.Substring(0, j));
						s = s.Substring(j);
						return v;
					}
					bool vv = eval_val(s);
					s = "";
					return vv;
				}
			}
			return false;
		}

		bool pp_unary(ref string s)
		{
			s = s.Trim();
			int len = s.Length;

			if (len > 0) {
				if (s [0] == '!') {
					if (len > 1 && s [1] == '=') {
						return false;
					}
					s = s.Substring(1);
					return ! pp_primary(ref s);
				} else
					return pp_primary(ref s);
			} else {
				return false;
			}
		}

		bool pp_eq(ref string s)
		{
			bool va = pp_unary(ref s);

			s = s.Trim();
			int len = s.Length;
			if (len > 0) {
				if (s [0] == '=') {
					if (len > 2 && s [1] == '=') {
						s = s.Substring(2);
						return va == pp_unary(ref s);
					} else {
						return false;
					}
				} else if (s [0] == '!' && len > 1 && s [1] == '=') {
					s = s.Substring(2);

					return va != pp_unary(ref s);

				} 
			}

			return va;

		}

		bool pp_and(ref string s)
		{
			bool va = pp_eq(ref s);

			s = s.Trim();
			int len = s.Length;
			if (len > 0) {
				if (s [0] == '&') {
					if (len > 2 && s [1] == '&') {
						s = s.Substring(2);
						return (va & pp_and(ref s));
					} else {
						return false;
					}
				} 
			}
			return va;
		}

		//
		// Evaluates an expression for `#if' or `#elif'
		//
		bool pp_expr(ref string s, bool isTerm)
		{
			bool va = pp_and(ref s);
			s = s.Trim();
			int len = s.Length;
			if (len > 0) {
				char c = s [0];

				if (c == '|') {
					if (len > 2 && s [1] == '|') {
						s = s.Substring(2);
						return va | pp_expr(ref s, isTerm);
					} else {

						return false;
					}
				}
				if (isTerm) {
					return false;
				}
			}

			return va;
		}

		bool eval(string s)
		{
			bool v = pp_expr(ref s, true);
			s = s.Trim();
			if (s.Length != 0) {
				return false;
			}

			return v;
		}
		#endregion
		
		public void Push(char ch)
		{
			if (readPreprocessorExpression) {
				wordBuf.Append(ch);
			}

			if (inside.HasFlag(Inside.VerbatimString) && pc == '"' && ch != '"') {
				inside &= ~Inside.StringLiteral;
			}
			switch (ch) {
				case '#':
					if (IsLineStart)
						inside = Inside.PreProcessor;
					break;
				case '/':
					if (IsInStringOrChar || IsInPreProcessorComment)
						break;
					if (pc == '/') {
						if (inside.HasFlag(Inside.Comment)) {
							inside |= Inside.DocComment;
						} else {
							inside |= Inside.Comment;
						}
					}
					break;
				case '*':
					if (IsInStringOrChar || IsInComment || IsInPreProcessorComment)
						break;
					if (pc == '/')
						inside |= Inside.MultiLineComment;
					break;
				case ' ':
					currentIndent.Append(' ');
					break;
				case '\t':
					var nextTabStop = (col - 1 + textEditorOptions.IndentSize) / textEditorOptions.IndentSize;
					col = 1 + nextTabStop * textEditorOptions.IndentSize;
					currentIndent.Append('\t');
					offset++;
					return;

				case '"':
					if (IsInComment || IsInPreProcessorComment)
						break;
					if (inside.HasFlag(Inside.StringLiteral)) {
						if (pc != '\\')
							inside &= ~Inside.StringLiteral;
						break;
					}

					if (pc == '@') {
						inside |= Inside.VerbatimString;
					} else {
						inside |= Inside.StringLiteral;
					}
					break;
				case '<':
				case '[':
				case '(':
					if (IsInComment || IsInStringOrChar || IsInPreProcessorComment)
						break;
					parenStack.Push(new TextLocation(line, col));
					popNextParenBlock = true;
					indent.Push(IndentType.Block);
					break;
				case '>':
				case ']':
				case ')':
					if (IsInComment || IsInStringOrChar || IsInPreProcessorComment)
						break;
					if (popNextParenBlock && parenStack.Count > 0)
						parenStack.Pop();
					if (indent.Count > 0)
						indent.Pop();
					indent.ExtraSpaces = 0;
					break;
				case ',':
					if (IsInComment || IsInStringOrChar || IsInPreProcessorComment)
						break;
					if (parenStack.Count > 0 && parenStack.Peek().Line == line) {
						if (indent.Count > 0)
							indent.Pop();
						popNextParenBlock = false;
						indent.ExtraSpaces = parenStack.Peek().Column - 1 - thisLineindent.CurIndent;
					}
					break;
				case '{':
					if (IsInComment || IsInStringOrChar || IsInPreProcessorComment)
						break;
					currentBody = nextBody;
					if (indent.Count > 0 && indent.Peek() == IndentType.Continuation)
						indent.Pop();
					addContinuation = false;
					AddIndentation(currentBody);
					break;
				case '}':
					if (IsInComment || IsInStringOrChar || IsInPreProcessorComment)
						break;
					if (indentDelta.CurIndent > 0) {
						indentDelta.Pop();
						if (indentDelta.Count > 0 && indentDelta.Peek() == IndentType.Continuation)
							indentDelta.Pop();
					} else {
						if (thisLineindent.Count > 0)
							thisLineindent.Pop();
						if (indent.Count > 0)
							indent.Pop();
					}
					break;
				case ';':
					if (IsInComment || IsInStringOrChar || IsInPreProcessorComment)
						break;
					if (indent.Count > 0 && indent.Peek() == IndentType.Continuation)
						indent.Pop();
					break;
				case '\'':
					if (IsInComment || inside.HasFlag(Inside.StringLiteral) || IsInPreProcessorComment)
						break;
					if (inside.HasFlag(Inside.CharLiteral)) {
						if (pc != '\\')
							inside &= ~Inside.CharLiteral;
					} else {
						inside &= Inside.CharLiteral;
					}
					break;
				default:
					var nl = NewLine.GetDelimiterLength(ch, pc);
					if (nl == 2)
						break;
					if (nl == 1) {
						if (readPreprocessorExpression) {
							if (!eval(wordBuf.ToString()))
								inside |= Inside.PreProcessorComment;
						}

						inside &= ~(Inside.Comment | Inside.String | Inside.CharLiteral | Inside.PreProcessor);
						CheckKeyword(wordBuf.ToString());
						wordBuf.Length = 0;
						indent.Push(indentDelta);
						indentDelta = new Indent(textEditorOptions);


						if (addContinuation) {
							indent.Push(IndentType.Continuation);
						}
						thisLineindent = indent.Clone();
						addContinuation = false;
						IsLineStart = true;
						readPreprocessorExpression = false;
						col = 1;
						line++;
						currentIndent.Length = 0;
					}
					break;

			}

			if (!IsInComment && !IsInStringOrChar && !readPreprocessorExpression) {
				if ((wordBuf.Length == 0 ? char.IsLetter(ch) : char.IsLetterOrDigit(ch)) || ch == '_') {
					wordBuf.Append(ch);
				} else {
					if (inside.HasFlag(Inside.PreProcessor)) {
						if (wordBuf.ToString() == "endif") {
							inside &= ~Inside.PreProcessorComment;
						} else if (wordBuf.ToString() == "if") {
							readPreprocessorExpression = true;
						} else if (wordBuf.ToString() == "elif") {
							inside &= ~Inside.PreProcessorComment;
							readPreprocessorExpression = true;
						}
					} else {
						CheckKeyword(wordBuf.ToString());
					}
					wordBuf.Length = 0;
				}
			}
			if (addContinuation) {
				indent.Push(IndentType.Continuation);
				addContinuation = false;
			}
			IsLineStart &= ch == ' ' || ch == '\t' || NewLine.IsNewLine (ch);
			pc = ch;
			if (!NewLine.IsNewLine (ch))
				col++;
			offset++;
		}

		void AddIndentation(BraceStyle braceStyle)
		{
			switch (braceStyle) {
				case BraceStyle.DoNotChange:
				case BraceStyle.EndOfLine:
				case BraceStyle.EndOfLineWithoutSpace:
				case BraceStyle.NextLine:
				case BraceStyle.NextLineShifted:
				case BraceStyle.BannerStyle:
					indentDelta.Push(IndentType.Block);
					break;

				case BraceStyle.NextLineShifted2:
					indentDelta.Push(IndentType.DoubleBlock);
					break;
			}
		}

		void AddIndentation(Body body)
		{
			switch (body) {
				case Body.None:
					indentDelta.Push(IndentType.Block);
					break;
				case Body.Namespace:
					AddIndentation(options.NamespaceBraceStyle);
					break;
				case Body.Class:
					AddIndentation(options.ClassBraceStyle);
					break;
				case Body.Struct:
					AddIndentation(options.StructBraceStyle);
					break;
				case Body.Interface:
					AddIndentation(options.InterfaceBraceStyle);
					break;
				case Body.Enum:
					AddIndentation(options.EnumBraceStyle);
					break;
				case Body.Switch:
					if (options.IndentSwitchBody)
						indentDelta.Push(IndentType.Empty);
					break;
				default:
					throw new ArgumentOutOfRangeException();
			}
		}

		enum Body
		{
			None,
			Namespace,
			Class,
			Struct,
			Interface,
			Enum,
			Switch
		}

		void CheckKeyword(string keyword)
		{
			switch (currentBody) {
				case Body.None:
					if (keyword == "namespace") {
						nextBody = Body.Namespace;
						return;
					}
					goto case Body.Namespace;
				case Body.Namespace:
					if (keyword == "class") {
						nextBody = Body.Class;
						return;
					}
					if (keyword == "enum") {
						nextBody = Body.Enum;
						return;
					}
					if (keyword == "struct") {
						nextBody = Body.Struct;
						return;
					}
					if (keyword == "interface") {
						nextBody = Body.Interface;
						return;
					}
					break;
				case Body.Class:
				case Body.Enum:
				case Body.Struct:
				case Body.Interface:
					if (keyword == "switch")
						nextBody = Body.Switch;
					if (keyword == "do" || keyword == "if" || keyword == "for" || keyword == "foreach" || keyword == "while") {
						addContinuation = true;
					}
					break;
			}
		}
	}
}

