#region ================== Namespaces

using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using CodeImp.DoomBuilder.Rendering;
using ScintillaNET;

#endregion

namespace CodeImp.DoomBuilder.Controls
{
	internal partial class ScriptEditorPreviewControl : UserControl
	{
		#region ================== Constants

		private const int HIGHLIGHT_INDICATOR = 8;

		#endregion

		#region ================== Variables

		private Dictionary<int, ScriptStyleType> styletranslation;
		private string highlightedword;
		private Color indicatorcolor;
		private int lastcaretpos;
		private readonly HashSet<char> bracechars; 

		#endregion

		#region ================== Properties

		public string FontName { set { ApplyFont(value); } }
		public int FontSize { set { ApplyFontSize(value); } }
		public bool FontBold { set { ApplyFontBold(value); } }
		public int TabWidth { set { scriptedit.TabWidth = value; } }
		public bool ShowLineNumbers { set { UpdateLineNumbers(value); } }
		public bool ShowFolding { set { UpdateFolding(value); } }

		// Colors
		public Color ScriptBackground 
		{ 
			set
			{ 
				scriptedit.Styles[Style.Default].BackColor = value;
				scriptedit.CaretForeColor = PixelColor.FromColor(value).Inverse().ToColor();
				scriptedit.Styles[Style.LineNumber].BackColor = value;
				scriptedit.SetWhitespaceBackColor(true, value);

				foreach(KeyValuePair<int, ScriptStyleType> group in styletranslation)
					scriptedit.Styles[group.Key].BackColor = value;
			} 
		}

		public Color FoldForeColor { set { for(int i = 25; i < 32; i++) scriptedit.Markers[i].SetBackColor(value); } }
		public Color FoldBackColor
		{
			set
			{
				scriptedit.SetFoldMarginColor(true, value);
				scriptedit.SetFoldMarginHighlightColor(true, value);
				for(int i = 25; i < 32; i++) scriptedit.Markers[i].SetForeColor(value);
			}
		}

		public Color LineNumbers { set { ApplyStyleColor(ScriptStyleType.LineNumber, value); } }
		public Color PlainText { set { ApplyStyleColor(ScriptStyleType.PlainText, value); } }
		public Color Comments { set { ApplyStyleColor(ScriptStyleType.Comment, value); } }
		public Color Keywords { set { ApplyStyleColor(ScriptStyleType.Keyword, value); } }
        public Color Properties { set { ApplyStyleColor(ScriptStyleType.Property, value); } }
        public Color Literals { set { ApplyStyleColor(ScriptStyleType.Literal, value); } }
		public Color Constants { set { ApplyStyleColor(ScriptStyleType.Constant, value); } }
		public Color Strings { set { ApplyStyleColor(ScriptStyleType.String, value); } }
		public Color Includes { set { ApplyStyleColor(ScriptStyleType.Include, value); } }

		public Color SelectionForeColor { set { scriptedit.SetSelectionForeColor(true, value); } }
		public Color SelectionBackColor { set { scriptedit.SetSelectionBackColor(true, value); } }
        public Color WhitespaceColor
        {
            set
            {
                scriptedit.SetWhitespaceForeColor(true, value);
                scriptedit.SetWhitespaceBackColor(false, value); // Otherwise selection back color won't be applied correctly...
            }
        }
        public Color BraceHighlight { set { scriptedit.Styles[Style.BraceLight].BackColor = value; } }
		public Color BadBraceHighlight { set { scriptedit.Styles[Style.BraceBad].BackColor = value; } }
		public Color ScriptIndicator { set { indicatorcolor = value; UpdateWordHighlight(); } }

		#endregion

		#region ================== Constructor / Setup

		public ScriptEditorPreviewControl()
		{
			InitializeComponent();

			if(LicenseManager.UsageMode != LicenseUsageMode.Designtime)
			{
				bracechars = new HashSet<char> { '{', '}', '(', ')', '[', ']' };
				SetupStyles();
			}
		}

		// This sets up the script editor with default settings
		private void SetupStyles()
		{
			// Symbol margin
			scriptedit.Margins[0].Type = MarginType.Symbol;
			scriptedit.Margins[0].Width = 20;
			scriptedit.Margins[0].Mask = 0; // No markers here
			scriptedit.Margins[0].Cursor = MarginCursor.Arrow;
			scriptedit.Margins[0].Sensitive = true;

			// Line numbers margin
			if(General.Settings.ScriptShowLineNumbers)
			{
				scriptedit.Margins[1].Type = MarginType.Number;
				scriptedit.Margins[1].Width = 16;
			}
			scriptedit.Margins[1].Mask = 0; // No markers here

			// Spacing margin
			scriptedit.Margins[2].Type = MarginType.Symbol;
			scriptedit.Margins[2].Width = 5;
			scriptedit.Margins[2].Cursor = MarginCursor.Arrow;
			scriptedit.Margins[2].Mask = 0; // No markers here

			// Set lexer
			scriptedit.Lexer = Lexer.CppNoCase;

			// Set script editor preview text
			scriptedit.Text = "#include \"zcommon.acs\"" + "\n" +
							  "script 667 ENTER //A simple script example" + "\n" +
							  "{" + "\n" +
								"    int red = CR_RED;" + "\n" +
								"\tPrint(s:\"Welcome!\");" + "\n" +
								"\tThing_ChangeTID(0, 667);" + "\n" +
			                  "} } // <- A spare brace!";

			// No text editing beyond this point!
			scriptedit.ReadOnly = true;

            // Set keywords (0)
            const string keywords = "Print HudMessageBold Thing_ChangeTID";
            scriptedit.SetKeywords(0, keywords.ToLowerInvariant());

            // Set constants (1)
            const string constants = "CR_RED";
            scriptedit.SetKeywords(1, constants.ToLowerInvariant());

            // Set properties (3)
            const string properties = "script enter int";
            scriptedit.SetKeywords(3, properties.ToLowerInvariant());

            // Reset document slyle
            scriptedit.ClearDocumentStyle();
			scriptedit.StyleResetDefault();

			// Create style translation dictionary
			styletranslation = new Dictionary<int, ScriptStyleType>
			{
				{ 0, ScriptStyleType.PlainText }, // End of line
				{ 10, ScriptStyleType.PlainText }, // Operator
				{ 11, ScriptStyleType.PlainText }, // Identifier
				{ 33, ScriptStyleType.LineNumber },
				{ 1, ScriptStyleType.Comment },
				{ 2, ScriptStyleType.Comment },
				{ 5, ScriptStyleType.Keyword },
				{ 4, ScriptStyleType.Literal },
				{ 7, ScriptStyleType.Literal },
				{ 16, ScriptStyleType.Constant },
				{ 37, ScriptStyleType.LineNumber },
				{ 6, ScriptStyleType.String },
				{ 9, ScriptStyleType.Include },
                { 19, ScriptStyleType.Property },
            };

			// Set the default style and settings
			scriptedit.Styles[Style.Default].Font = General.Settings.ScriptFontName;
			scriptedit.Styles[Style.Default].Size = General.Settings.ScriptFontSize;
			scriptedit.Styles[Style.Default].Bold = General.Settings.ScriptFontBold;
			scriptedit.Styles[Style.Default].Italic = false;
			scriptedit.Styles[Style.Default].Underline = false;
			scriptedit.Styles[Style.Default].Case = StyleCase.Mixed;
			scriptedit.Styles[Style.Default].ForeColor = General.Colors.PlainText.ToColor();
			scriptedit.Styles[Style.Default].BackColor = General.Colors.ScriptBackground.ToColor();
			scriptedit.CaretPeriod = SystemInformation.CaretBlinkTime;
			scriptedit.CaretForeColor = General.Colors.ScriptBackground.Inverse().ToColor();

			// Set tabulation settings
			scriptedit.UseTabs = General.Settings.ScriptUseTabs;
			scriptedit.TabWidth = General.Settings.ScriptTabWidth;

			// This applies the default style to all styles
			scriptedit.StyleClearAll();

			// Set style for linenumbers and margins
			scriptedit.Styles[Style.LineNumber].BackColor = General.Colors.ScriptBackground.ToColor();
			scriptedit.SetFoldMarginColor(true, General.Colors.ScriptFoldBackColor.ToColor());
			scriptedit.SetFoldMarginHighlightColor(true, General.Colors.ScriptFoldBackColor.ToColor());
			for(int i = 25; i < 32; i++)
			{
				scriptedit.Markers[i].SetForeColor(General.Colors.ScriptFoldBackColor.ToColor());
				scriptedit.Markers[i].SetBackColor(General.Colors.ScriptFoldForeColor.ToColor());
			}

			// Set style for (mis)matching braces
			scriptedit.Styles[Style.BraceLight].BackColor = General.Colors.ScriptBraceHighlight.ToColor();
			scriptedit.Styles[Style.BraceBad].BackColor = General.Colors.ScriptBadBraceHighlight.ToColor();

			// Set whitespace color
			scriptedit.SetWhitespaceForeColor(true, General.Colors.ScriptWhitespace.ToColor());

			// Set selection colors
			scriptedit.SetSelectionForeColor(true, General.Colors.ScriptSelectionForeColor.ToColor());
			scriptedit.SetSelectionBackColor(true, General.Colors.ScriptSelectionBackColor.ToColor());
			indicatorcolor = General.Colors.ScriptIndicator.ToColor();

			// Set words colors
			foreach(KeyValuePair<int, ScriptStyleType> group in styletranslation)
			{
				// Apply color to style
				int colorindex;
				switch(group.Value)
				{
					case ScriptStyleType.PlainText: colorindex = ColorCollection.PLAINTEXT; break;
					case ScriptStyleType.Comment: colorindex = ColorCollection.COMMENTS; break;
					case ScriptStyleType.Constant: colorindex = ColorCollection.CONSTANTS; break;
					case ScriptStyleType.Keyword: colorindex = ColorCollection.KEYWORDS; break;
					case ScriptStyleType.LineNumber: colorindex = ColorCollection.LINENUMBERS; break;
					case ScriptStyleType.Literal: colorindex = ColorCollection.LITERALS; break;
					case ScriptStyleType.String: colorindex = ColorCollection.STRINGS; break;
					case ScriptStyleType.Include: colorindex = ColorCollection.INCLUDES; break;
                    case ScriptStyleType.Property: colorindex = ColorCollection.PROPERTIES; break;
                    default: colorindex = ColorCollection.PLAINTEXT; break;
				}

				scriptedit.Styles[group.Key].ForeColor = General.Colors.Colors[colorindex].ToColor();
				scriptedit.Styles[group.Key].BackColor = General.Colors.ScriptBackground.ToColor();
			}

			// Setup folding
			UpdateFolding(General.Settings.ScriptShowFolding);

			// Rearrange the layout
			this.PerformLayout();
		}

		#endregion

		#region ================== Methods

		private void ApplyStyleColor(ScriptStyleType type, Color color)
		{
			foreach(KeyValuePair<int, ScriptStyleType> group in styletranslation)
				if(group.Value == type) scriptedit.Styles[group.Key].ForeColor = color;
		}

		private void ApplyFont(string font)
		{
			foreach(KeyValuePair<int, ScriptStyleType> group in styletranslation)
				scriptedit.Styles[group.Key].Font = font;
		}

		private void ApplyFontBold(bool bold)
		{
			foreach(KeyValuePair<int, ScriptStyleType> group in styletranslation)
				scriptedit.Styles[group.Key].Bold = bold;
		}

		private void ApplyFontSize(int size)
		{
			foreach(KeyValuePair<int, ScriptStyleType> group in styletranslation)
				scriptedit.Styles[group.Key].Size = size;
		}

		private void UpdateLineNumbers(bool show)
		{
			if(show)
			{
				scriptedit.Margins[1].Type = MarginType.Number;
				scriptedit.Margins[1].Width = 16;
			}
			else
			{
				scriptedit.Margins[1].Type = MarginType.Symbol;
				scriptedit.Margins[1].Width = 0;
			}
		}

		private void UpdateFolding(bool show)
		{
			if(show)
			{
				// Instruct the lexer to calculate folding
				scriptedit.SetProperty("fold", "1");
				scriptedit.SetProperty("fold.compact", "0"); // 1 = folds blank lines
				scriptedit.SetProperty("fold.comment", "1"); // Enable block comment folding
				scriptedit.SetProperty("fold.preprocessor", "1"); // Enable #region folding
				scriptedit.SetFoldFlags(FoldFlags.LineAfterContracted); // Draw line below if not expanded

				// Configure a margin to display folding symbols
				scriptedit.Margins[2].Type = MarginType.Symbol;
				scriptedit.Margins[2].Mask = Marker.MaskFolders;
				scriptedit.Margins[2].Sensitive = true;
				scriptedit.Margins[2].Width = 12;

				// Configure folding markers with respective symbols
				scriptedit.Markers[Marker.Folder].Symbol = MarkerSymbol.BoxPlus;
				scriptedit.Markers[Marker.FolderOpen].Symbol = MarkerSymbol.BoxMinus;
				scriptedit.Markers[Marker.FolderEnd].Symbol = MarkerSymbol.BoxPlusConnected;
				scriptedit.Markers[Marker.FolderMidTail].Symbol = MarkerSymbol.TCorner;
				scriptedit.Markers[Marker.FolderOpenMid].Symbol = MarkerSymbol.BoxMinusConnected;
				scriptedit.Markers[Marker.FolderSub].Symbol = MarkerSymbol.VLine;
				scriptedit.Markers[Marker.FolderTail].Symbol = MarkerSymbol.LCorner;

				// Enable automatic folding
				scriptedit.AutomaticFold = (AutomaticFold.Show | AutomaticFold.Click | AutomaticFold.Change);
			}
			else
			{
				// Disable folding
				scriptedit.SetProperty("fold", "0");
				scriptedit.SetProperty("fold.compact", "0");

				scriptedit.Margins[2].Type = MarginType.Symbol;
				scriptedit.Margins[2].Mask = 0; // No markers here
				scriptedit.Margins[2].Sensitive = false;
				scriptedit.Margins[2].Width = 5;

				scriptedit.AutomaticFold = AutomaticFold.None;
			}
		}

		private void UpdateWordHighlight()
		{
			// If a word is selected, highlight the same words
			if(scriptedit.SelectedText != highlightedword)
			{
				// Highlight only when whole word is selected
				if(!string.IsNullOrEmpty(scriptedit.SelectedText) && scriptedit.GetWordFromPosition(scriptedit.SelectionStart) == scriptedit.SelectedText) {
					HighlightWord(scriptedit.SelectedText);
				}
				else
				{
					// Clear highlight
					scriptedit.IndicatorCurrent = HIGHLIGHT_INDICATOR;
					scriptedit.IndicatorClearRange(0, scriptedit.TextLength);
				}

				highlightedword = scriptedit.SelectedText;
			}
		}

		private void HighlightWord(string text)
		{
			// Remove all uses of our indicator
			scriptedit.IndicatorCurrent = HIGHLIGHT_INDICATOR;
			scriptedit.IndicatorClearRange(0, scriptedit.TextLength);

			// Update indicator appearance
			scriptedit.Indicators[HIGHLIGHT_INDICATOR].Style = IndicatorStyle.RoundBox;
			scriptedit.Indicators[HIGHLIGHT_INDICATOR].Under = true;
			scriptedit.Indicators[HIGHLIGHT_INDICATOR].ForeColor = indicatorcolor;
			scriptedit.Indicators[HIGHLIGHT_INDICATOR].OutlineAlpha = 50;
			scriptedit.Indicators[HIGHLIGHT_INDICATOR].Alpha = 30;

			// Search the document
			scriptedit.TargetStart = 0;
			scriptedit.TargetEnd = scriptedit.TextLength;
			scriptedit.SearchFlags = SearchFlags.WholeWord;

			while(scriptedit.SearchInTarget(text) != -1)
			{
				//mxd. Don't mark currently selected word
				if(scriptedit.SelectionStart != scriptedit.TargetStart && scriptedit.SelectionEnd != scriptedit.TargetEnd)
				{
					// Mark the search results with the current indicator
					scriptedit.IndicatorFillRange(scriptedit.TargetStart, scriptedit.TargetEnd - scriptedit.TargetStart);
				}

				// Search the remainder of the document
				scriptedit.TargetStart = scriptedit.TargetEnd;
				scriptedit.TargetEnd = scriptedit.TextLength;
			}
		}

		#endregion

		#region ================== Events

		private void scriptedit_UpdateUI(object sender, UpdateUIEventArgs e)
		{
			UpdateWordHighlight();

			// Has the caret changed position?
			int caretpos = scriptedit.CurrentPosition;
			if(lastcaretpos != caretpos)
			{
				lastcaretpos = caretpos;
				int bracepos1 = -1;

				// Is there a brace to the left or right?
				if(caretpos > 0 && bracechars.Contains((char)scriptedit.GetCharAt(caretpos - 1)))
					bracepos1 = (caretpos - 1);
				else if(bracechars.Contains((char)(scriptedit.GetCharAt(caretpos))))
					bracepos1 = caretpos;

				if(bracepos1 > -1)
				{
					// Find the matching brace
					int bracepos2 = scriptedit.BraceMatch(bracepos1);
					if(bracepos2 == Scintilla.InvalidPosition)
						scriptedit.BraceBadLight(bracepos1);
					else
						scriptedit.BraceHighlight(bracepos1, bracepos2);
				}
				else
				{
					// Turn off brace matching
					scriptedit.BraceHighlight(Scintilla.InvalidPosition, Scintilla.InvalidPosition);
				}
			}
		}

		#endregion
	}
}
