using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Reflection;
using System.IO;

namespace ScintillaNet
{
	[TypeConverterAttribute(typeof(System.ComponentModel.ExpandableObjectConverter))]
	public class Lexing : ScintillaHelperBase
	{
		private const string DEFAULT_WORDCHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
		private const string DEFAULT_WHITECHARS = " \t\r\n\0";
		private Dictionary<string, int> _styleNameMap = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
		private Dictionary<string, string> _lexerLanguageMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);


		internal Lexing(Scintilla scintilla)
			: base(scintilla) 
		{
			WhiteSpaceChars = DEFAULT_WHITECHARS;
			WordChars = DEFAULT_WORDCHARS;
			_keywords = new KeywordCollection(scintilla);

			//	Language names are a superset lexer names. For instance the c and cs (c#)
			//	langauges both use the cpp lexer (by default). Languages are kind of a 
			//	SCite concept, while Scintilla only cares about Lexers. However we don't
			//	need to explicetly map a language to a lexer if they are the same name
			//	like cpp.
			_lexerLanguageMap.Add("cs", "cpp");
			_lexerLanguageMap.Add("html", "hypertext");
			_lexerLanguageMap.Add("xml", "hypertext");
		}

		internal bool ShouldSerialize()
		{
			return ShouldSerializeLexerName() ||
				ShouldSerializeLexer() ||
				ShouldSerializeWhiteSpaceChars() ||
				ShouldSerializeWordChars();
		}

		#region Lexer
		public Lexer Lexer
		{
			get
			{
				return (Lexer)NativeScintilla.GetLexer();
			}
			set
			{
				NativeScintilla.SetLexer((int)value);
				_lexerName = value.ToString().ToLower();
				if (_lexerName == "null")
					_lexerName = "";

				loadStyleMap();
			}
		}

		private bool ShouldSerializeLexer()
		{
			return Lexer != Lexer.Container;
		}

		private void ResetLexer()
		{
			Lexer = Lexer.Container;
		}

		#endregion

		#region Language
		private string _lexerName = "container";
		public string LexerName
		{
			get
			{
				return _lexerName;
			}
			set
			{
				if (string.IsNullOrEmpty(value))
					value = "null";
					
				NativeScintilla.SetLexerLanguage(value.ToLower());

				_lexerName = value;

				loadStyleMap();
			}
		}

		private bool ShouldSerializeLexerName()
		{
			return LexerName != "container";
		}

		private void ResetLexerName()
		{
			LexerName = "container";
		} 
		#endregion

		private KeywordCollection _keywords;
		[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public KeywordCollection Keywords
		{
			get
			{
				return _keywords;
			}
		}


		public void LoadLexerLibrary(string path)
		{
			NativeScintilla.LoadLexerLibrary(path);
		}

		public void Colorize(int startPos, int endPos)
		{
			NativeScintilla.Colourise(startPos, endPos);
		}

		public void Colorize()
		{
			Colorize(0, -1);
		}

		public string GetProperty(string name)
		{
			string s;
			NativeScintilla.GetProperty(name, out s);
			return s;
		}

		public void SetProperty(string name, string value)
		{
			NativeScintilla.SetProperty(name, value);
		}

		public string GetPropertyExpanded(string name)
		{
			string s;
			NativeScintilla.GetPropertyExpanded(name, out s);
			return s;
		}

		public int GetPropertyInt(string name)
		{
			return GetPropertyInt(name, 0);
		}

		public int GetPropertyInt(string name, int defaultValue)
		{
			return NativeScintilla.GetPropertyInt(name, defaultValue);
		}

		public void SetKeywords(int keywordSet, string list)
		{
			NativeScintilla.SetKeywords(keywordSet, list);
		}


		#region WordChars
		private string _wordChars;
		internal char[] WordCharsArr = null;
		public string WordChars
		{
			get
			{				
				return _wordChars;
			}
			set
			{
				_wordChars = value;
				WordCharsArr = _wordChars.ToCharArray();
				NativeScintilla.SetWordChars(value);
			}
		}

		private bool ShouldSerializeWordChars()
		{
			return _wordChars != DEFAULT_WORDCHARS;
		}

		private void ResetWordChars()
		{
			WordChars = DEFAULT_WORDCHARS;
		}

		#endregion

		#region WhiteSpaceChars
		internal char[] WhiteSpaceCharsArr;
		private string _whiteSpaceChars;
		[TypeConverter(typeof(ScintillaNet.WhiteSpaceStringConverter))]
		public string WhiteSpaceChars
		{
			get
			{
				return _whiteSpaceChars;
			}
			set
			{
				_whiteSpaceChars = value;
				WhiteSpaceCharsArr = _whiteSpaceChars.ToCharArray();
				NativeScintilla.SetWhitespaceChars(value);
			}
		}

		private bool ShouldSerializeWhiteSpaceChars()
		{
			return _whiteSpaceChars != DEFAULT_WHITECHARS;
		}

		private void ResetWhiteSpaceChars()
		{
			_whiteSpaceChars = DEFAULT_WHITECHARS;
		} 
		#endregion


		private void loadStyleMap()
		{
			if (Scintilla.IsDesignMode)
				return;

			_styleNameMap.Clear();

			//	These are global constants that always apply
			_styleNameMap.Add("BRACEBAD",Constants.STYLE_BRACEBAD);
			_styleNameMap.Add("BRACELIGHT",Constants.STYLE_BRACELIGHT);
			_styleNameMap.Add("CALLTIP",Constants.STYLE_CALLTIP);
			_styleNameMap.Add("CONTROLCHAR",Constants.STYLE_CONTROLCHAR);
			_styleNameMap.Add("DEFAULT",Constants.STYLE_DEFAULT);
			_styleNameMap.Add("LINENUMBER",Constants.STYLE_LINENUMBER);

			string lexname = this.Lexer.ToString().ToLower();

			using (Stream s = GetType().Assembly.GetManifestResourceStream("ScintillaNet.Configuration.Builtin.LexerStyleNames." + lexname + ".txt"))
			{
				if (s == null)
					return;

				using (StreamReader sr = new StreamReader(s))
				{
					while (!sr.EndOfStream)
					{
						string[] arr = sr.ReadLine().Split('=');
						if (arr.Length != 2)
							continue;

						string key = arr[0].Trim();
						int value = int.Parse(arr[1].Trim());
						
						_styleNameMap.Remove(key);
						_styleNameMap.Add(key, value);
					}
				}
			}

		}

		[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public Dictionary<string, int> StyleNameMap
		{
			get
			{
				return _styleNameMap;
			}
		}

		[Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public Dictionary<string, string> LexerLanguageMap
		{
			get
			{
				return _lexerLanguageMap;
			}
		}


		private string _lineCommentPrefix = string.Empty;
		public string LineCommentPrefix
		{
			get
			{
				return _lineCommentPrefix;
			}
			set
			{
				if (value == null)
					value = string.Empty;

				_lineCommentPrefix = value;
			}
		}

		private string _streamCommentPrefix = string.Empty;
		public string StreamCommentPrefix
		{
			get
			{
				return _streamCommentPrefix;
			}
			set
			{
				if (value == null)
					value = string.Empty;

				_streamCommentPrefix = value;
			}
		}

		private string _streamCommentSufix = string.Empty;
		public string StreamCommentSufix
		{
			get
			{
				return _streamCommentSufix;
			}
			set
			{
				if (value == null)
					value = string.Empty;

				_streamCommentSufix = value;
			}
		}

		public void LineComment()
		{
			if (string.IsNullOrEmpty(_lineCommentPrefix))
				return;

			//	So the theory behind line commenting is that for every selected line
			//	we look for the first non-whitespace character and insert the line
			//	comment prefix. Lines without non-whitespace are skipped.
			NativeScintilla.BeginUndoAction();

			Range selRange = Scintilla.Selection.Range;
			int start = selRange.StartingLine.Number;
			int end = selRange.EndingLine.Number;

			//	We're tracking the new end of the selection range including
			//	the amount it expands because we're inserting new text.
			int offset = _lineCommentPrefix.Length;

			for (int i = start; i <= end; i++)
			{
				Line l = Scintilla.Lines[i];
				int firstWordChar = findFirstNonWhitespaceChar(l.Text);
				if (firstWordChar >= 0)
				{
					Scintilla.InsertText(l.StartPosition + firstWordChar, _lineCommentPrefix);
					selRange.End += offset;
				}
			}

			NativeScintilla.EndUndoAction();

			//	An odd side-effect of InsertText is that we lose the current
			//	selection. This is undesirable. This is why we were tracking
			//	the end position offset.
			selRange.Select();
		}

		public void LineUncomment()
		{
			if (string.IsNullOrEmpty(_lineCommentPrefix))
				return;

			NativeScintilla.BeginUndoAction();

			//	Uncommenting is a lot like line commenting. However in addition
			//	to looking for a non-whitespace character, the string that follows
			//	it MUST be our line Comment Prefix. If this is the case the prefex
			//	is removed from the line at its position.
			Range selRange = Scintilla.Selection.Range;
			int start = selRange.StartingLine.Number;
			int end = selRange.EndingLine.Number;

			int offset = _lineCommentPrefix.Length;

			for (int i = start; i <= end; i++)
			{
				Line l = Scintilla.Lines[i];
				int firstWordChar = findFirstNonWhitespaceChar(l.Text);
				if (firstWordChar >= 0)
				{
					int startPos = l.StartPosition + firstWordChar;
					Range commentRange = Scintilla.GetRange(startPos, startPos + offset);
					if (commentRange.Text == _lineCommentPrefix)
						commentRange.Text = string.Empty;
				}
			}

			NativeScintilla.EndUndoAction();
		}

		public void ToggleLineComment()
		{
			if (string.IsNullOrEmpty(_lineCommentPrefix))
				return;

			NativeScintilla.BeginUndoAction();

			Range selRange = Scintilla.Selection.Range;
			int start = selRange.StartingLine.Number;
			int end = selRange.EndingLine.Number;

			int offset = _lineCommentPrefix.Length;

			for (int i = start; i <= end; i++)
			{
				Line l = Scintilla.Lines[i];
				int firstWordChar = findFirstNonWhitespaceChar(l.Text);
				if (firstWordChar >= 0)
				{
					int startPos = l.StartPosition + firstWordChar;
					Range commentRange = Scintilla.GetRange(startPos, startPos + offset);
					if (commentRange.Text == _lineCommentPrefix)
					{
						commentRange.Text = string.Empty;
						selRange.End -= offset;
					}
					else
					{
						Scintilla.InsertText(l.StartPosition + firstWordChar, _lineCommentPrefix);
						selRange.End += offset;
					}
				}
			}

			NativeScintilla.EndUndoAction();
			selRange.Select();
		}



		private int findFirstNonWhitespaceChar(string s)
		{
			for (int i = 0; i < s.Length; i++)
			{
				if (s[i].ToString().IndexOfAny(WhiteSpaceCharsArr) == -1)
					return i;
			}

			return -1;
		}

		public void StreamComment()
		{
			if (string.IsNullOrEmpty(_streamCommentPrefix) || string.IsNullOrEmpty(_streamCommentSufix))
				return;

			NativeScintilla.BeginUndoAction();
			
			Range selRange = Scintilla.Selection.Range;
			Scintilla.InsertText(selRange.Start, _streamCommentPrefix);
			Scintilla.InsertText(selRange.End+ _streamCommentPrefix.Length, _streamCommentSufix);
			selRange.End += _streamCommentPrefix.Length + _streamCommentSufix.Length;
			selRange.Select();

			NativeScintilla.EndUndoAction();
		}
	}


	public class KeywordCollection : ScintillaHelperBase
	{
		private Dictionary<string, string[]> _lexerKeywordListMap;
		private Dictionary<string, Dictionary<string, int>> _lexerStyleMap;
		private Dictionary<string, Lexer> _lexerAliasMap;

		internal KeywordCollection(Scintilla scintilla)
			: base(scintilla) 
		{
			//	Auugh, this plagued me for a while. Each of the lexers cna define their own "Name"
			//	and also asign themsleves to a Scintilla Lexer Constant. Most of the time these
			//	match the defined constant, but sometimes they don't. We'll always use the constant
			//	name since it's easier to use, consistent and will always have valid characters.
			//	However its still valid to access the lexers by this name (as SetLexerLanguage
			//	uses this value) so we'll create a lookup.
			_lexerAliasMap = new Dictionary<string, Lexer>(StringComparer.OrdinalIgnoreCase);
			
			//	I have no idea how Progress fits into this. It's defined with the PS lexer const
			//	and a name of "progress"

			_lexerAliasMap.Add("PL/M", Lexer.Plm);
			_lexerAliasMap.Add("props", Lexer.Properties);
			_lexerAliasMap.Add("inno", Lexer.InnoSetup);
			_lexerAliasMap.Add("clarion", Lexer.Clw);
			_lexerAliasMap.Add("clarionnocase", Lexer.ClwNoCase	);


			//_lexerKeywordListMap = new Dictionary<string,string[]>(StringComparer.OrdinalIgnoreCase);


			//_lexerKeywordListMap.Add("xml", new string[] { "HTML elements and attributes", "JavaScript keywords", "VBScript keywords", "Python keywords", "PHP keywords", "SGML and DTD keywords" });
			//_lexerKeywordListMap.Add("yaml", new string[] { "Keywords" });
			//	baan, kix, ave, scriptol, diff, props, makefile, errorlist, latex, null, lot, haskell
			//	lexers don't have keyword list names

			
		}

	
		private string[] _keywords = new string[] { "", "", "", "", "", "", "", "", "" };
		
		public string this[int keywordSet]
		{
			get
			{
				return _keywords[keywordSet];
			}
			set
            {
				_keywords[keywordSet] = value;
				NativeScintilla.SetKeywords(keywordSet, value);
            }
		}

		public string this[string keywordSetName]
		{
			get
			{
				return this[getKeyowrdSetIndex(keywordSetName)];
			}
			set
			{
				this[getKeyowrdSetIndex(keywordSetName)] = value;
			}
		}

		private int getKeyowrdSetIndex(string name)
		{
			string lexerName = Scintilla.Lexing.Lexer.ToString().ToLower();
			if (_lexerKeywordListMap.ContainsKey(lexerName))
				throw new ApplicationException("lexer " + lexerName + " does not support named keyword lists");

			int index = ((IList)_lexerKeywordListMap[lexerName]).IndexOf(name);

			if (index < 0)
				throw new ArgumentException("Keyword Set name does not exist for lexer " + lexerName, "keywordSetName");

			return index;
		}
	}
}


