using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

namespace SqlDynamite
{
    /// <summary>
    /// A textbox the does syntax highlighting.
    /// </summary>
    public class SyntaxHighlightingTextBox : RichTextBox
    {
        #region Members

        private const int WM_PAINT = 0xF;
        private const int WM_USER = 0x400;
        private const int EM_GETSCROLLPOS = (WM_USER + 221);
        private const int EM_SETSCROLLPOS = (WM_USER + 222);
        
        //Members exposed via properties
        private readonly List<char> _separators = new List<char>();
        private readonly List<HighlightDescriptor> _highlightDescriptors = new List<HighlightDescriptor>();
        private bool _caseSesitive;

        //Internal use members
        private bool _parsing;

        [DllImport("user32")]
        private static extern int SendMessage(IntPtr hwnd, int wMsg, int wParam, IntPtr lParam);

        [DllImport("user32")]
        private static extern int LockWindowUpdate(IntPtr hwnd);

        #endregion

        #region Properties

        /// <summary>
        /// Determines if token recognition is case sensitive.
        /// </summary>
        [Category("Behavior")]
        public bool CaseSensitive
        {
            set => _caseSesitive = value;
        }

        /// <summary>
        /// A collection of characters. a token is every string between two separators.
        /// </summary>
        /// 
        public List<char> Separators => _separators;

        /// <summary>
        /// The collection of highlight descriptors.
        /// </summary>
        /// 
        public List<HighlightDescriptor> HighlightDescriptors => _highlightDescriptors;

        #endregion

        #region Overriden methods

        /// <summary>
        /// The on text changed overrided. Here we parse the text into RTF for the highlighting.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnTextChanged(EventArgs e)
        {
            if (_parsing) return;
            _parsing = true;
            LockWindowUpdate(Handle);
            POINT scrollPos = GetScrollPos();
            base.OnTextChanged(e);

            //Save scroll bar an cursor position, changing the RTF moves the cursor and scrollbars to top position
            int cursorLoc = SelectionStart;

            //Created with an estimate of how big the string builder has to be...
            StringBuilder sb = new
                StringBuilder((int) (Text.Length*1.5 + 150));

            //Adding RTF header
            sb.Append(@"{\rtf1\ansi\deff0{\fonttbl{");

            //Font table creation
            int fontCounter = 0;
            Dictionary<string, int> fonts = new Dictionary<string, int>();
            AddFontToTable(sb, Font, ref fontCounter, fonts);
            foreach (HighlightDescriptor hd in _highlightDescriptors)
            {
                if ((hd.Font != null) && !fonts.ContainsKey(hd.Font.Name))
                {
                    AddFontToTable(sb, hd.Font, ref fontCounter, fonts);
                }
            }
            sb.Append("}}\n");

            //ColorTable

            sb.Append(@"{\colortbl ;");
            Dictionary<Color, int> colors = new Dictionary<Color, int>();
            int colorCounter = 1;
            AddColorToTable(sb, ForeColor, ref colorCounter, colors);
            AddColorToTable(sb, BackColor, ref colorCounter, colors);

            foreach (HighlightDescriptor hd in _highlightDescriptors)
            {
                if (!colors.ContainsKey(hd.Color))
                {
                    AddColorToTable(sb, hd.Color, ref colorCounter, colors);
                }
                if (hd.BackgroundColor.HasValue && !colors.ContainsKey(hd.BackgroundColor.Value))
                {
                    AddColorToTable(sb, hd.BackgroundColor.Value, ref colorCounter, colors);
                }
            }

            //Parsing text

            sb.Append("}\n").Append(@"\viewkind4\uc1\pard\ltrpar");
            SetDefaultSettings(sb, colors, fonts);

            char[] separators = _separators.ToArray();

            //Replacing "\" to "\\" for RTF...
            string[] lines = Text.Replace("\\", "\\\\").Replace("{", "\\{").Replace("}", "\\}").Split('\n');
            for (int lineCounter = 0; lineCounter < lines.Length; lineCounter++)
            {
                if (lineCounter != 0)
                {
                    AddNewLine(sb);
                }
                string line = lines[lineCounter];
                string[] tokens = _caseSesitive ? line.Split(separators) : line.ToUpper().Split(separators);
                if (tokens.Length == 0)
                {
                    sb.Append(line);
                    AddNewLine(sb);
                    continue;
                }

                int tokenCounter = 0;
                for (int i = 0; i < line.Length;)
                {
                    char curChar = line[i];
                    if (_separators.Contains(curChar))
                    {
                        sb.Append(curChar);
                        i++;
                    }
                    else if (tokenCounter < tokens.Length)
                    {

                        string curToken = tokens[tokenCounter++];
                        bool bAddToken = true;
                        foreach (HighlightDescriptor hd in _highlightDescriptors)
                        {
                            string compareStr = _caseSesitive ? hd.Token : hd.Token.ToUpper();
                            bool match = false;

                            //Check if the highlight descriptor matches the current token according to the DescriptorRecognition property.
                            switch (hd.DescriptorRecognition)
                            {
                                case DescriptorRecognition.WholeWord:
                                    if (curToken == compareStr)
                                    {
                                        match = true;
                                    }
                                    break;
                                case DescriptorRecognition.StartsWith:
                                    if (curToken.StartsWith(compareStr))
                                    {
                                        match = true;
                                    }
                                    break;
                                case DescriptorRecognition.Contains:
                                    if (curToken.IndexOf(compareStr, StringComparison.Ordinal) != -1)
                                    {
                                        match = true;
                                    }
                                    break;
                            }
                            if (!match)
                            {
                                //If this token doesn't match check the next one.
                                continue;
                            }

                            //printing this token will be handled by the inner code, don't apply default settings...
                            bAddToken = false;

                            //Set colors to current descriptor settings.
                            SetDescriptorSettings(sb, hd, colors, fonts);

                            //Print text affected by this descriptor.
                            switch (hd.DescriptorType)
                            {
                                case DescriptorType.Word:
                                    sb.Append(line.Substring(i, curToken.Length));
                                    if (hd.Font != null)
                                    {
                                        if (hd.Font.Bold || hd.Font.Italic || hd.Font.Underline || hd.Font.Strikeout)
                                        {
                                            sb.Append(@"}");
                                        }
                                    }
                                    SetDefaultSettings(sb, colors, fonts);
                                    i += curToken.Length;
                                    break;
                                case DescriptorType.ToEOL:
                                    sb.Append(line.Remove(0, i));
                                    i = line.Length;
                                    SetDefaultSettings(sb, colors, fonts);
                                    break;
                                case DescriptorType.ToCloseToken:
                                    while (line.IndexOf(hd.CloseToken, i, StringComparison.Ordinal) == -1 && (lineCounter < lines.Length))
                                    {
                                        sb.Append(line.Remove(0, i));
                                        lineCounter++;
                                        if (lineCounter < lines.Length)
                                        {
                                            AddNewLine(sb);
                                            line = lines[lineCounter];
                                            i = 0;
                                        }
                                        else
                                        {
                                            i = line.Length;
                                        }
                                    }
                                    if (line.IndexOf(hd.CloseToken, i, StringComparison.Ordinal) != -1)
                                    {
                                        sb.Append(line.Substring(i,
                                            line.IndexOf(hd.CloseToken, i, StringComparison.Ordinal) +
                                            hd.CloseToken.Length - i));
                                        line = line.Remove(0, line.IndexOf(hd.CloseToken, i, StringComparison.Ordinal) + hd.CloseToken.Length);
                                        tokenCounter = 0;
                                        tokens = _caseSesitive
                                            ? line.Split(separators)
                                            : line.ToUpper().Split(separators);
                                        SetDefaultSettings(sb, colors, fonts);
                                        i = 0;
                                    }
                                    break;
                            }
                            break;
                        }
                        if (bAddToken)
                        {
                            //Print text with default settings...
                            sb.Append(line.Substring(i, curToken.Length));
                            i += curToken.Length;
                        }
                    }
                    else break;
                }
            }

            Rtf = sb.ToString();

            //Restore cursor and scrollbars location.
            SelectionStart = cursorLoc;

            _parsing = false;

            SetScrollPos(scrollPos);
            LockWindowUpdate((IntPtr)0);
            Invalidate();
        }


        protected override void OnVScroll(EventArgs e)
        {
            if (_parsing) return;
            base.OnVScroll(e);
        }

        /// <summary>
        /// Taking care of Keyboard events
        /// </summary>
        /// <param name="m"></param>
        /// <remarks>
        /// Since even when overriding the OnKeyDown method and not calling the base function 
        /// you don't have full control of the input, I've decided to catch windows messages to handle them.
        /// </remarks>
        protected override void WndProc(ref Message m)
        {
            switch (m.Msg)
            {
                case WM_PAINT:
                {
                    //Don't draw the control while parsing to avoid flicker.
                    if (_parsing)
                    {
                        return;
                    }
                    break;
                }

            }
            base.WndProc(ref m);
        }

        #endregion

        #region Rtf building helper functions

        /// <summary>
        /// Set color and font to default control settings.
        /// </summary>
        /// <param name="sb">the string builder building the RTF</param>
        /// <param name="colors">colors hashtable</param>
        /// <param name="fonts">fonts hashtable</param>
        private void SetDefaultSettings(StringBuilder sb, Dictionary<Color, int> colors, Dictionary<string, int> fonts)
        {
            SetColor(sb, ForeColor, colors);
            SetBackgroundColor(sb, BackColor, colors);
            SetFont(sb, Font, fonts);
            SetFontSize(sb, (int) Font.Size);
            EndTags(sb);
        }

        /// <summary>
        /// Set Color and font to a highlight descriptor settings.
        /// </summary>
        /// <param name="sb">the string builder building the RTF</param>
        /// <param name="hd">the HighlightDescriptor with the font and color settings to apply.</param>
        /// <param name="colors">colors hashtable</param>
        /// <param name="fonts">fonts hashtable</param>
        private static void SetDescriptorSettings(StringBuilder sb, HighlightDescriptor hd, Dictionary<Color, int> colors,
            Dictionary<string, int> fonts)
        {
            SetColor(sb, hd.Color, colors);
            if (hd.BackgroundColor.HasValue)
            {
                SetBackgroundColor(sb, hd.BackgroundColor.Value, colors);
            }
            if (hd.Font != null)
            {
                SetFont(sb, hd.Font, fonts);
                SetFontSize(sb, (int) hd.Font.Size);
                SetFontStyle(sb, hd.Font.Style);
            }
            EndTags(sb);

        }

        /// <summary>
        /// Sets the color to the specified color
        /// </summary>
        private static void SetColor(StringBuilder sb, Color color, Dictionary<Color, int> colors)
        {
            sb.Append(@"\cf").Append(colors[color]);
        }

        /// <summary>
        /// Sets the background color to the specified color
        /// </summary>
        private static void SetBackgroundColor(StringBuilder sb, Color color, Dictionary<Color, int> colors)
        {
            sb.Append(@"\highlight").Append(colors[color]);
        }

        /// <summary>
        /// Sets the font style
        /// </summary>
        private static void SetFontStyle(StringBuilder sb, FontStyle fontStyle)
        {
            if (fontStyle == FontStyle.Bold)
            {
                sb.Append(@"{\b");
            }
            else if (fontStyle == FontStyle.Italic)
            {
                sb.Append(@"{\i");
            }
            else if (fontStyle == FontStyle.Underline)
            {
                sb.Append(@"{\ul");
            }
            else if (fontStyle == FontStyle.Strikeout)
            {
                sb.Append(@"{\strike");
            }
        }

        /// <summary>
        /// Sets the font to the specified font.
        /// </summary>
        private static void SetFont(StringBuilder sb, Font font, Dictionary<string, int> fonts)
        {
            if (font == null) return;
            sb.Append(@"\f").Append(fonts[font.Name]);
        }

        /// <summary>
        /// Sets the font size to the specified font size.
        /// </summary>
        private static void SetFontSize(StringBuilder sb, int size)
        {
            sb.Append(@"\fs").Append(size*2);
        }

        /// <summary>
        /// Adds a newLine mark to the RTF.
        /// </summary>
        private static void AddNewLine(StringBuilder sb)
        {
            sb.Append("\\par\n");
        }

        /// <summary>
        /// Ends a RTF tags section.
        /// </summary>
        private static void EndTags(StringBuilder sb)
        {
            sb.Append(' ');
        }

        /// <summary>
        /// Adds a font to the RTF's font table and to the fonts hashtable.
        /// </summary>
        /// <param name="sb">The RTF's string builder</param>
        /// <param name="font">the Font to add</param>
        /// <param name="counter">a counter, containing the amount of fonts in the table</param>
        /// <param name="fonts">an hashtable. the key is the font's name. the value is it's index in the table</param>
        private static void AddFontToTable(StringBuilder sb, Font font, ref int counter, Dictionary<string, int> fonts)
        {

            sb.Append(@"{\f").Append(counter).Append(@"\fnil\fcharset1").Append(font.Name).Append(";}");
            fonts.Add(font.Name, counter++);
        }

        /// <summary>
        /// Adds a color to the RTF's color table and to the colors hashtable.
        /// </summary>
        /// <param name="sb">The RTF's string builder</param>
        /// <param name="color">the color to add</param>
        /// <param name="counter">a counter, containing the amount of colors in the table</param>
        /// <param name="colors">an hashtable. the key is the color. the value is it's index in the table</param>
        private static void AddColorToTable(StringBuilder sb, Color color, ref int counter, Dictionary<Color, int> colors)
        {

            sb.Append(@"\red").Append(color.R).Append(@"\green").Append(color.G).Append(@"\blue")
                .Append(color.B).Append(";");
            colors.Add(color, counter++);
        }

        #endregion

        #region Win32

        /// <summary>
        /// Sends a win32 message to get the scrollbars' position.
        /// </summary>
        /// <returns>a POINT structure containing horizontal and vertical scrollbar position.</returns>
        private unsafe POINT GetScrollPos()
        {
            POINT res = new POINT();
            IntPtr ptr = new IntPtr(&res);
            SendMessage(Handle, EM_GETSCROLLPOS, 0, ptr);
            return res;

        }

        /// <summary>
        /// Sends a win32 message to set scrollbars position.
        /// </summary>
        /// <param name="point">a POINT containing H/Vscrollbar scrollpos.</param>
        private unsafe void SetScrollPos(POINT point)
        {
            IntPtr ptr = new IntPtr(&point);
            SendMessage(Handle, EM_SETSCROLLPOS, 0, ptr);

        }

        [StructLayout(LayoutKind.Sequential)]
        private readonly struct POINT
        {
            private readonly int x;
            private readonly int y;
        }

        #endregion

    }

}