// 
// CSharpCompletionEngineBase.cs
//  
// Author:
//       Mike Krüger <mkrueger@xamarin.com>
// 
// Copyright (c) 2011 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 System.Collections.Generic;
using System.Linq;
using System.Text;

using ICSharpCode.NRefactory.CSharp.Resolver;
using ICSharpCode.NRefactory.Editor;
using ICSharpCode.NRefactory.TypeSystem;
using ICSharpCode.NRefactory.Semantics;
using ICSharpCode.NRefactory.TypeSystem.Implementation;
using ICSharpCode.NRefactory.CSharp.TypeSystem;

namespace ICSharpCode.NRefactory.CSharp.Completion
{
	/// <summary>
	/// Acts as a common base between code completion and parameter completion.
	/// </summary>
	public class CSharpCompletionEngineBase
	{
		protected IDocument document;
		protected int offset;
		protected TextLocation location;
		protected IUnresolvedTypeDefinition currentType;
		protected IUnresolvedMember currentMember;
		
		#region Input properties
		public CSharpTypeResolveContext ctx { get; private set; }

		public IProjectContent ProjectContent { get; private set; }
		
		ICompilation compilation;

		protected ICompilation Compilation {
			get {
				if (compilation == null)
					compilation = ProjectContent.Resolve (ctx).Compilation;
				return compilation;
			}
		}

		Version languageVersion = new Version (5, 0);
		public Version LanguageVersion {
			get {
				return languageVersion;
			}
			set {
				languageVersion = value;
			}
		}
		#endregion
		
		protected CSharpCompletionEngineBase(IProjectContent content, ICompletionContextProvider completionContextProvider, CSharpTypeResolveContext ctx)
		{
			if (content == null)
				throw new ArgumentNullException("content");
			if (ctx == null)
				throw new ArgumentNullException("ctx");
			if (completionContextProvider == null)
				throw new ArgumentNullException("completionContextProvider");
			
			this.ProjectContent = content;
			this.CompletionContextProvider = completionContextProvider;
			this.ctx = ctx;
		}
		
		
		public ICompletionContextProvider CompletionContextProvider {
			get;
			private set;
		}
		
		public void SetOffset (int offset)
		{
			Reset ();
			
			this.offset = offset;
			this.location = document.GetLocation (offset);
			CompletionContextProvider.GetCurrentMembers (offset, out currentType, out currentMember);
		}

		public bool GetParameterCompletionCommandOffset (out int cpos)
		{
			// Start calculating the parameter offset from the beginning of the
			// current member, instead of the beginning of the file. 
			cpos = offset - 1;
			var mem = currentMember;
			if (mem == null || (mem is IType) || IsInsideCommentStringOrDirective ()) {
				return false;
			}
			int startPos = document.GetOffset (mem.Region.BeginLine, mem.Region.BeginColumn);
			int parenDepth = 0;
			int chevronDepth = 0;
			Stack<int> indexStack = new Stack<int> ();
			while (cpos > startPos) {
				char c = document.GetCharAt (cpos);
				if (c == ')') {
					parenDepth++;
				}
				if (c == '>') {
					chevronDepth++;
				}
				if (c == '}') {
					if (indexStack.Count > 0) {
						parenDepth = indexStack.Pop ();
					} else {
						parenDepth = 0;
					}
					chevronDepth = 0;
				}
				if (indexStack.Count == 0 && (parenDepth == 0 && c == '(' || chevronDepth == 0 && c == '<')) {
					int p = GetCurrentParameterIndex (startPos, cpos + 1);
					if (p != -1) {
						cpos++;
						return true;
					} else {
						return false;
					}
				}
				if (c == '(') {
					parenDepth--;
				}
				if (c == '<') {
					chevronDepth--;
				}
				if (c == '{') {
					indexStack.Push (parenDepth);
					chevronDepth = 0;
				}
				cpos--;
			}
			return false;
		}
		
		public int GetCurrentParameterIndex(int triggerOffset, int endOffset)
		{
			List<string> list;
			return  GetCurrentParameterIndex (triggerOffset, endOffset, out list);
		}

		public int GetCurrentParameterIndex (int triggerOffset, int endOffset, out List<string> usedNamedParameters)
		{
			usedNamedParameters =new List<string> ();
			var parameter = new Stack<int> ();
			var bracketStack = new Stack<Stack<int>> ();
			bool inSingleComment = false, inString = false, inVerbatimString = false, inChar = false, inMultiLineComment = false;
			var word = new StringBuilder ();
			bool foundCharAfterOpenBracket = false;
			for (int i = triggerOffset; i < endOffset; i++) {
				char ch = document.GetCharAt (i);
				char nextCh = i + 1 < document.TextLength ? document.GetCharAt (i + 1) : '\0';
				if (ch == ':') {
					usedNamedParameters.Add (word.ToString ());
					word.Length = 0;
				} else if (char.IsLetterOrDigit (ch) || ch =='_') {
					word.Append (ch);
				} else if (char.IsWhiteSpace (ch)) {

				} else {
					word.Length = 0;
				}
				if (!char.IsWhiteSpace(ch) && parameter.Count > 0)
					foundCharAfterOpenBracket = true;

				switch (ch) {
					case '{':
						if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment) {
							break;
						}
						bracketStack.Push (parameter);
						parameter = new Stack<int> ();
						break;
					case '[':
					case '(':
						if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment) {
							break;
						}
						parameter.Push (0);
						break;
					case '}':
						if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment) {
							break;
						}
						if (bracketStack.Count > 0) {
							parameter = bracketStack.Pop ();
						} else {
							return -1;
						}
						break;
					case ']':
					case ')':
						if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment) {
							break;
						}
						if (parameter.Count > 0) {
							parameter.Pop ();
						} else {
							return -1;
						}
						break;
					case '<':
						if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment) {
							break;
						}
						parameter.Push (0);
						break;
					case '=':
						if (nextCh == '>') {
							i++;
							continue;
						}
						break;
					case '>':
						if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment) {
							break;
						}
						if (parameter.Count > 0) {
							parameter.Pop ();
						}
						break;
					case ',':
						if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment) {
							break;
						}
						if (parameter.Count > 0) {
							parameter.Push (parameter.Pop () + 1);
						}
						break;
					case '/':
						if (inString || inChar || inVerbatimString) {
							break;
						}
						if (nextCh == '/') {
							i++;
							inSingleComment = true;
						}
						if (nextCh == '*') {
							inMultiLineComment = true;
						}
						break;
					case '*':
						if (inString || inChar || inVerbatimString || inSingleComment) {
							break;
						}
						if (nextCh == '/') {
							i++;
							inMultiLineComment = false;
						}
						break;
					case '@':
						if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment) {
							break;
						}
						if (nextCh == '"') {
							i++;
							inVerbatimString = true;
						}
						break;
					case '\\':
						if (inString || inChar) {
							i++;
						}
						break;
					case '"':
						if (inSingleComment || inMultiLineComment || inChar) {
							break;
						}
						if (inVerbatimString) {
							if (nextCh == '"') {
								i++;
								break;
							}
							inVerbatimString = false;
							break;
						}
						inString = !inString;
						break;
					case '\'':
						if (inSingleComment || inMultiLineComment || inString || inVerbatimString) {
							break;
						}
						inChar = !inChar;
						break;
					default:
						if (NewLine.IsNewLine(ch)) {
							inSingleComment = false;
							inString = false;
							inChar = false;
						}
						break;
				}
			}
			if (parameter.Count != 1 || bracketStack.Count > 0) {
				return -1;
			}
			if (!foundCharAfterOpenBracket)
				return 0;
			return parameter.Pop() + 1;
		}

		#region Context helper methods
		public class MiniLexer
		{
			readonly string text;

			public bool IsFistNonWs               = true;
			public bool IsInSingleComment         = false;
			public bool IsInString                = false;
			public bool IsInVerbatimString        = false;
			public bool IsInChar                  = false;
			public bool IsInMultiLineComment      = false;
			public bool IsInPreprocessorDirective = false;

			public MiniLexer(string text)
			{
				this.text = text;
			}

			public void Parse(Action<char> act = null)
			{
				Parse(0, text.Length, act);
			}

			public void Parse(int start, int length, Action<char> act = null)
			{
				for (int i = start; i < length; i++) {
					char ch = text [i];
					char nextCh = i + 1 < text.Length ? text [i + 1] : '\0';
					switch (ch) {
						case '#':
							if (IsFistNonWs)
								IsInPreprocessorDirective = true;
							break; 
						case '/':
							if (IsInString || IsInChar || IsInVerbatimString || IsInSingleComment || IsInMultiLineComment)
								break;
							if (nextCh == '/') {
								i++;
								IsInSingleComment = true;
								IsInPreprocessorDirective = false;
							}
							if (nextCh == '*' && !IsInPreprocessorDirective) {
								IsInMultiLineComment = true;
								i++;
							}
							break;
						case '*':
							if (IsInString || IsInChar || IsInVerbatimString || IsInSingleComment)
								break;
							if (nextCh == '/') {
								i++;
								IsInMultiLineComment = false;
							}
							break;
						case '@':
							if (IsInString || IsInChar || IsInVerbatimString || IsInSingleComment || IsInMultiLineComment)
								break;
							if (nextCh == '"') {
								i++;
								IsInVerbatimString = true;
							}
							break;
						case '\n':
						case '\r':
							IsInSingleComment = false;
							IsInString = false;
							IsInChar = false;
							IsFistNonWs = true;
							IsInPreprocessorDirective = false;
							break;
						case '\\':
							if (IsInString || IsInChar)
								i++;
							break;
						case '"':
							if (IsInSingleComment || IsInMultiLineComment || IsInChar)
								break;
							if (IsInVerbatimString) {
								if (nextCh == '"') {
									i++;
									break;
								}
								IsInVerbatimString = false;
								break;
							}
							IsInString = !IsInString;
							break;
						case '\'':
							if (IsInSingleComment || IsInMultiLineComment || IsInString || IsInVerbatimString)
								break;
							IsInChar = !IsInChar;
							break;
					}
					if (act != null)
						act(ch);
					IsFistNonWs &= ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r';
				}
			}
		}

		
		protected bool IsInsideCommentStringOrDirective(int offset)
		{
			var lexer = new MiniLexer(document.Text);
			lexer.Parse(0, offset);
			return
				lexer.IsInSingleComment || 
				lexer.IsInString ||
				lexer.IsInVerbatimString ||
				lexer.IsInChar ||
				lexer.IsInMultiLineComment || 
				lexer.IsInPreprocessorDirective;
		}


		protected bool IsInsideCommentStringOrDirective()
		{
			var text = GetMemberTextToCaret();
			var lexer = new MiniLexer(text.Item1);
			lexer.Parse();
			return
				lexer.IsInSingleComment || 
					lexer.IsInString ||
					lexer.IsInVerbatimString ||
					lexer.IsInChar ||
					lexer.IsInMultiLineComment || 
					lexer.IsInPreprocessorDirective;
		}

		protected bool IsInsideDocComment ()
		{
			var text = GetMemberTextToCaret ();
			bool inSingleComment = false, inString = false, inVerbatimString = false, inChar = false, inMultiLineComment = false;
			bool singleLineIsDoc = false;
			
			for (int i = 0; i < text.Item1.Length - 1; i++) {
				char ch = text.Item1 [i];
				char nextCh = text.Item1 [i + 1];
				
				switch (ch) {
				case '/':
					if (inString || inChar || inVerbatimString)
						break;
					if (nextCh == '/') {
						i++;
						inSingleComment = true;
						singleLineIsDoc = i + 1 < text.Item1.Length && text.Item1 [i + 1] == '/';
						if (singleLineIsDoc) {
							i++;
						}
					}
					if (nextCh == '*')
						inMultiLineComment = true;
					break;
				case '*':
					if (inString || inChar || inVerbatimString || inSingleComment)
						break;
					if (nextCh == '/') {
						i++;
						inMultiLineComment = false;
					}
					break;
				case '@':
					if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment)
						break;
					if (nextCh == '"') {
						i++;
						inVerbatimString = true;
					}
					break;
				case '\n':
				case '\r':
					inSingleComment = false;
					inString = false;
					inChar = false;
					break;
				case '\\':
					if (inString || inChar)
						i++;
					break;
				case '"':
					if (inSingleComment || inMultiLineComment || inChar)
						break;
					if (inVerbatimString) {
						if (nextCh == '"') {
							i++;
							break;
						}
						inVerbatimString = false;
						break;
					}
					inString = !inString;
					break;
				case '\'':
					if (inSingleComment || inMultiLineComment || inString || inVerbatimString)
						break;
					inChar = !inChar;
					break;
				}
			}
			
			return inSingleComment && singleLineIsDoc;
		}

		protected CSharpResolver GetState ()
		{
			return new CSharpResolver (ctx);
			/*var state = new CSharpResolver (ctx);
			
			state.CurrentMember = currentMember;
			state.CurrentTypeDefinition = currentType;
			state.CurrentUsingScope = CSharpUnresolvedFile.GetUsingScope (location);
			if (state.CurrentMember != null) {
				var node = Unit.GetNodeAt (location);
				if (node == null)
					return state;
				var navigator = new NodeListResolveVisitorNavigator (new[] { node });
				var visitor = new ResolveVisitor (state, CSharpUnresolvedFile, navigator);
				Unit.AcceptVisitor (visitor, null);
				try {
					var newState = visitor.GetResolverStateBefore (node);
					if (newState != null)
						state = newState;
				} catch (Exception) {
				}
			}
			
			return state;*/
		}
				#endregion
		
		#region Basic parsing/resolving functions
		static Stack<Tuple<char, int>> GetBracketStack (string memberText)
		{
			var bracketStack = new Stack<Tuple<char, int>> ();
			
			bool inSingleComment = false, inString = false, inVerbatimString = false, inChar = false, inMultiLineComment = false;
			
			for (int i = 0; i < memberText.Length; i++) {
				char ch = memberText [i];
				char nextCh = i + 1 < memberText.Length ? memberText [i + 1] : '\0';
				switch (ch) {
				case '(':
				case '[':
				case '{':
					if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment)
						break;
					bracketStack.Push (Tuple.Create (ch, i));
					break;
				case ')':
				case ']':
				case '}':
					if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment)
						break;
					if (bracketStack.Count > 0)
						bracketStack.Pop ();
					break;
				case '/':
					if (inString || inChar || inVerbatimString)
						break;
					if (nextCh == '/') {
						i++;
						inSingleComment = true;
					}
					if (nextCh == '*')
						inMultiLineComment = true;
					break;
				case '*':
					if (inString || inChar || inVerbatimString || inSingleComment)
						break;
					if (nextCh == '/') {
						i++;
						inMultiLineComment = false;
					}
					break;
				case '@':
					if (inString || inChar || inVerbatimString || inSingleComment || inMultiLineComment)
						break;
					if (nextCh == '"') {
						i++;
						inVerbatimString = true;
					}
					break;
				case '\\':
					if (inString || inChar)
						i++;
					break;
				case '"':
					if (inSingleComment || inMultiLineComment || inChar)
						break;
					if (inVerbatimString) {
						if (nextCh == '"') {
							i++;
							break;
						}
						inVerbatimString = false;
						break;
					}
					inString = !inString;
					break;
				case '\'':
					if (inSingleComment || inMultiLineComment || inString || inVerbatimString)
						break;
					inChar = !inChar;
					break;
				default :
					if (NewLine.IsNewLine(ch)) {
						inSingleComment = false;
						inString = false;
						inChar = false;
					}
					break;
				}
			}
			return bracketStack;
		}
		
		public static void AppendMissingClosingBrackets (StringBuilder wrapper, string memberText, bool appendSemicolon)
		{
			var bracketStack = GetBracketStack (memberText);
			bool didAppendSemicolon = !appendSemicolon;
			//char lastBracket = '\0';
			while (bracketStack.Count > 0) {
				var t = bracketStack.Pop ();
				switch (t.Item1) {
				case '(':
					wrapper.Append (')');
					if (appendSemicolon)
						didAppendSemicolon = false;
					//lastBracket = ')';
					break;
				case '[':
					wrapper.Append (']');
					if (appendSemicolon)
						didAppendSemicolon = false;
					//lastBracket = ']';
					break;
				case '<':
					wrapper.Append ('>');
					if (appendSemicolon)
						didAppendSemicolon = false;
					//lastBracket = '>';
					break;
				case '{':
					int o = t.Item2 - 1;
					if (!didAppendSemicolon) {
						didAppendSemicolon = true;
						wrapper.Append (';');
					}
						
					bool didAppendCatch = false;
					while (o >= "try".Length) {
						char ch = memberText [o];
						if (!char.IsWhiteSpace (ch)) {
							if (ch == 'y' && memberText [o - 1] == 'r' && memberText [o - 2] == 't') {
								wrapper.Append ("} catch {}");
								didAppendCatch = true;
							}
							break;
						}
						o--;
					}
					if (!didAppendCatch)
						wrapper.Append ('}');
					break;
				}
			}
			if (!didAppendSemicolon)
				wrapper.Append (';');
		}

		protected SyntaxTree ParseStub(string continuation, bool appendSemicolon = true, string afterContinuation = null)
		{
			var mt = GetMemberTextToCaret();
			if (mt == null) {
				return null;
			}

			string memberText = mt.Item1;
			var memberLocation = mt.Item2;
			int closingBrackets = 1;
			int generatedLines = 0;
			var wrapper = new StringBuilder();
			bool wrapInClass = memberLocation != new TextLocation(1, 1);
			if (wrapInClass) {
				wrapper.Append("class Stub {");
				wrapper.AppendLine();
				closingBrackets++;
				generatedLines++;
			}
			wrapper.Append(memberText);
			wrapper.Append(continuation);
			AppendMissingClosingBrackets(wrapper, memberText, appendSemicolon);
			wrapper.Append(afterContinuation);
			if (closingBrackets > 0) { 
				wrapper.Append(new string('}', closingBrackets));
			}
			var parser = new CSharpParser ();
			foreach (var sym in CompletionContextProvider.ConditionalSymbols)
				parser.CompilerSettings.ConditionalSymbols.Add (sym);
			parser.InitialLocation = new TextLocation(memberLocation.Line - generatedLines, 1);
			var result = parser.Parse(wrapper.ToString ());
			return result;
		}
		
		protected virtual void Reset ()
		{
			memberText = null;
		}

		Tuple<string, TextLocation> memberText;
		protected Tuple<string, TextLocation> GetMemberTextToCaret()
		{
			if (memberText == null)
				memberText = CompletionContextProvider.GetMemberTextToCaret(offset, currentType, currentMember);
			return memberText;
		}
		
		protected ExpressionResult GetInvocationBeforeCursor(bool afterBracket)
		{
			SyntaxTree baseUnit;
			baseUnit = ParseStub("a", false);

			var section = baseUnit.GetNodeAt<AttributeSection>(location.Line, location.Column - 2);
			var attr = section != null ? section.Attributes.LastOrDefault() : null;
			if (attr != null) {
				return new ExpressionResult((AstNode)attr, baseUnit);
			}

			//var memberLocation = currentMember != null ? currentMember.Region.Begin : currentType.Region.Begin;
			var mref = baseUnit.GetNodeAt(location.Line, location.Column - 1, n => n is InvocationExpression || n is ObjectCreateExpression); 
			AstNode expr = null;
			if (mref is InvocationExpression) {
				expr = ((InvocationExpression)mref).Target;
			} else if (mref is ObjectCreateExpression) {
				expr = mref;
			} else {
				baseUnit = ParseStub(")};", false);
				mref = baseUnit.GetNodeAt(location.Line, location.Column - 1, n => n is InvocationExpression || n is ObjectCreateExpression); 
				if (mref is InvocationExpression) {
					expr = ((InvocationExpression)mref).Target;
				} else if (mref is ObjectCreateExpression) {
					expr = mref;
				}
			}

			if (expr == null) {
				// work around for missing ';' bug in mcs:
				baseUnit = ParseStub("a", true);
			
				section = baseUnit.GetNodeAt<AttributeSection>(location.Line, location.Column - 2);
				attr = section != null ? section.Attributes.LastOrDefault() : null;
				if (attr != null) {
					return new ExpressionResult((AstNode)attr, baseUnit);
				}
	
				//var memberLocation = currentMember != null ? currentMember.Region.Begin : currentType.Region.Begin;
				mref = baseUnit.GetNodeAt(location.Line, location.Column - 1, n => n is InvocationExpression || n is ObjectCreateExpression); 
				expr = null;
				if (mref is InvocationExpression) {
					expr = ((InvocationExpression)mref).Target;
				} else if (mref is ObjectCreateExpression) {
					expr = mref;
				}
			}

			if (expr == null) {
				return null;
			}
			return new ExpressionResult ((AstNode)expr, baseUnit);
		}
		
		public class ExpressionResult
		{
			public AstNode Node { get; private set; }
			public SyntaxTree Unit  { get; private set; }
			
			
			public ExpressionResult (AstNode item2, SyntaxTree item3)
			{
				this.Node = item2;
				this.Unit = item3;
			}
			
			public override string ToString ()
			{
				return string.Format ("[ExpressionResult: Node={0}, Unit={1}]", Node, Unit);
			}
		}
		
		protected Tuple<ResolveResult, CSharpResolver> ResolveExpression (ExpressionResult tuple)
		{
			return ResolveExpression (tuple.Node);
		}

		protected Tuple<ResolveResult, CSharpResolver> ResolveExpression(AstNode expr)
		{
			if (expr == null) {
				return null;
			}
			AstNode resolveNode;
			if (expr is Expression || expr is AstType) {
				resolveNode = expr;
			} else if (expr is VariableDeclarationStatement) {
				resolveNode = ((VariableDeclarationStatement)expr).Type;
			} else {
				resolveNode = expr;
			}
			try {
				var root = expr.AncestorsAndSelf.FirstOrDefault(n => n is EntityDeclaration || n is SyntaxTree);
				if (root == null) {
					return null;
				}
				if (root is Accessor)
					root = root.Parent;
				var csResolver = CompletionContextProvider.GetResolver (GetState(), root);
				var result = csResolver.Resolve(resolveNode);
				var state = csResolver.GetResolverStateBefore(resolveNode);
				return Tuple.Create(result, state);
			} catch (Exception e) {
				Console.WriteLine(e);
				return null;
			}
		}
		
		#endregion
	}
}