using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using System.Text.RegularExpressions;
using System.Drawing;
using NotepadEnhanced.AppState;

namespace NotepadEnhanced.GUI.Controls
{
    public class PlainTextBox : RichTextBox
    {
        [Description("Occurs when the length of the selection changes")]
        public event EventHandler SelectionLengthChanged;
        private bool _smartTabbing = UserSettings.Instance.SmartTabbing;
        private int _lastSelectionLength;
        private string _pendingWhiteSpace = string.Empty;

        public PlainTextBox()
        {
            Document = new Document(this);
            base.Font = UserSettings.Instance.Font;
            base.ForeColor = UserSettings.Instance.FontColor;
            base.BackColor = UserSettings.Instance.BackColor;
            this.WordWrap = UserSettings.Instance.WordWrap;
            this.ShowSelectionMargin = UserSettings.Instance.ShowSelectionMargin;
            base.Dock = DockStyle.Fill;
            this.AcceptsTab = base.AllowDrop = true;
            this.DetectUrls = false;
            this.HideSelection = false;
        }

        protected override void OnFontChanged(EventArgs e)
        {
            base.OnFontChanged(e);

            using (Graphics graphics = this.CreateGraphics())
            {
                LineHeight = graphics.MeasureString("A8", this.Font).Height;
            }
        }

        protected override void OnHandleCreated(EventArgs e)
        {
            base.OnHandleCreated(e);
            this.AutoWordSelection = !UserSettings.Instance.AutoWordSelection;
            this.AutoWordSelection = UserSettings.Instance.AutoWordSelection;
        }

        private string GetWhiteSpaceFromCurrentLine()
        {
            StringBuilder SB = new StringBuilder();
            int lineIndex = CurrentLine - 1;

            if (this.Lines.Length > 0)
            {
                for (int i = 0; i < this.Lines[lineIndex].Length; i++)
                {
                    // If iterated char is equal to 
                    if (this.Lines[lineIndex][i].Equals('\t'))
                    {
                        SB.Append('\t');
                    }
                    else if (this.Lines[lineIndex][i].Equals(' '))
                    {
                        SB.Append(' ');
                    }
                    else
                    {
                        break;
                    }
                }
            }

            return SB.ToString();
        }

        private void InsertWhiteSpaceWithoutAutoscroll(string text)
        {
            bool focused = this.Focused;
            int lastSelection = this.SelectionStart;
            int lastLength = this.SelectionLength;
            //allow auto scroll if selection is at end of line
            int line = this.GetLineFromCharIndex(lastSelection);
            bool autoScroll = (line == this.Lines.Length);

            if (!autoScroll)
            {
                //shift focus from RichTextBox to some other control
                if (focused) this.Parent.Focus();
                //hide selection
                SendMessage(this.Handle, EM_HIDESELECTION, 1, 0);
            }

            this.Text = Text.Insert(lastSelection, text);

            if (!autoScroll)
            {
                //restore initial selection
                this.SelectionStart = lastSelection;
                this.SelectionLength = lastLength;
                //unhide selection
                SendMessage(this.Handle, EM_HIDESELECTION, 0, 0);
                //restore focus
                if (focused) this.Focus();
            }
        }

        private void Find(string pattern, bool matchCase)
        {
            int pos;
            var options = (matchCase) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; 
            bool samePattern = (LastPattern == pattern);
            bool hasPattern = pattern.Length > 0;
            bool hasContent = this.Text.Length > 0;

            if (LastPos > -1 && samePattern && hasPattern && hasContent)
            {
                if (LastPos + 1 > this.Text.Length)
                {
                    pos = this.Text.IndexOf(pattern, 0, options);
                }
                else
                {
                    pos = this.Text.IndexOf(pattern, LastPos + 1, options);
                }
            }
            else
            {
                pos = this.Text.IndexOf(pattern, 0, options);
            }

            if (pos < 0 || pos <= LastPos)
            {
                this.Select(LastPos, 0);
                LastPos = 0;
                LastPattern = string.Empty;
            }
            else
            {
                LastPos = pos;
                LastPattern = pattern;
                this.Select(LastPos, LastPattern.Length);
                this.Focus();
            }
        }

        /// <summary>
        /// Interfaces with the Active PlainTextBoxe's iterative search
        /// </summary>
        /// <returns>Returns true if nothing is found on current iteration</returns>
        public bool FindNext(string pattern, bool matchCase)
        {
            if (this.LastPos >= 0) this.Find(pattern, matchCase);
            return string.IsNullOrEmpty(LastPattern);
        }

        /// <summary>
        /// Adjusts the selection start to go to the line.
        /// </summary>
        /// <returns>returns true if can navigate to line successfully</returns>
        public bool GoTo(int line)
        {
            if (line == -1) return false;
            this.SelectionLength = 0;

            if (this.Lines.Length > line && this.Text.Length > 0)
            {
                int firstChar = this.GetFirstCharIndexFromLine(line);
                this.SelectionStart = firstChar;
                return true;
            }

                this.SelectionStart = this.Text.Length;
                return false;
        }

        #region Eventing
        protected override void OnKeyDown(KeyEventArgs e)
        {
            base.OnKeyDown(e);

            if (_smartTabbing && e.KeyCode.Equals(Keys.Enter))
            {
                _pendingWhiteSpace = GetWhiteSpaceFromCurrentLine();
            }
            else if (e.Control && e.KeyCode.Equals(Keys.Oemcomma)) // Decapitalize
            {
                int lastSelectionStart = this.SelectionStart;
                int lastSelectionLength = this.SelectionLength;
                this.SelectedText = this.SelectedText.ToLower();
                this.SelectionStart = lastSelectionStart;
                this.SelectionLength = lastSelectionLength;
            }
            else if (e.Control && e.KeyCode.Equals(Keys.OemPeriod)) // Capitalize
            {
                int lastSelectionStart = this.SelectionStart;
                int lastSelectionLength = this.SelectionLength;
                this.SelectedText = this.SelectedText.ToUpper();
                this.SelectionStart = lastSelectionStart;
                this.SelectionLength = lastSelectionLength;
            }
            else if (e.Control && e.KeyCode.Equals(Keys.U))
            {
                this.SelectedText = this.SelectedText.ToUpper();
            }
        }

        protected override void OnSelectionChanged(EventArgs e)
        {
            base.OnSelectionChanged(e);

            // If last selection length does not equal current
            if (_lastSelectionLength != SelectionLength && SelectionLengthChanged != null)
            {
                    SelectionLengthChanged(this, e);
            }

            _lastSelectionLength = this.SelectionLength;
        }

        protected override void OnTextChanged(EventArgs e)
        {
            base.OnTextChanged(e);

            if (_smartTabbing && _pendingWhiteSpace.Length > 0)
            {
                int lastSelectionStart = this.SelectionStart;
                InsertWhiteSpaceWithoutAutoscroll(_pendingWhiteSpace);
                this.SelectionStart = lastSelectionStart + _pendingWhiteSpace.Length;
                _pendingWhiteSpace = string.Empty;
            }
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets the last pattern to be searched for
        /// </summary>
        public string LastPattern { get; private set; }

        /// <summary>
        /// Gets the last position after finding a word or yielding no results
        /// </summary>
        private int LastPos { get; set; }

        /// <summary>
        /// Gets the current culumn that the selection start is located at
        /// </summary>
        public int CurrentColumn
        {
            get
            {
                int currentColumn = this.SelectionStart;
                currentColumn -= this.GetFirstCharIndexOfCurrentLine();
                return currentColumn + 1;
            }
        }

        /// <summary>
        /// Gets the current line that the selection start is located at
        /// </summary>
        public int CurrentLine
        {
            get {return this.GetLineFromCharIndex(this.SelectionStart) + 1; }
        }

        /// <summary>
        /// Gets how many words are in the text
        /// </summary>
        public int WordCount
        {
            get { return Regex.Matches(this.Text, @"[\S]+").Count; }
        }

        /// <summary>
        /// Gets how many characters are in the text
        /// </summary>
        public int CharacterCount
        {
            get
            {
                string temp = this.Text.Replace(" ", string.Empty);
                return temp.Replace("\n", string.Empty).Length;
            }
        }

        /// <summary>
        /// Gets how many sentences are in the text
        /// </summary>
        public int SentenceCount
        {
            get {  return Regex.Matches(this.Text, @"(\S.+?[.!?])(?=\s+|$)").Count;  }
        }

        public bool SmartTabbing
        {
            get { return _smartTabbing; }
            set { _smartTabbing = value; }
        }

        public Document Document { get; private set; }

        /// <summary>
        /// Gets the height of a single line in the plaintextbox
        /// </summary>
        private float LineHeight { get; set; }

        /// <summary>
        /// Gets the width of the text selected in pixels with a slight adjustment
        /// </summary>
        public int SelectionWidth
        {
            get 
            {
                int start = this.GetPositionFromCharIndex(this.SelectionStart).X;
                int end = this.GetPositionFromCharIndex(this.SelectionStart + this.SelectionLength).X;
                return end - start;
            }
        }

        /// <summary>
        /// Gets the height of the selection in pixels
        /// </summary>
        public int SelectionHeight
        {
            get
            {
                int start = this.GetPositionFromCharIndex(this.SelectionStart).Y;
                int selectionEnd = this.SelectionStart + this.SelectionLength;

                if (this.Text.Length > selectionEnd && this.Text[selectionEnd] == '\n')
                {
                    selectionEnd = (selectionEnd <= 1) ? 0 : selectionEnd - 1;
                }

                int end = this.GetPositionFromCharIndex(selectionEnd).Y;

                // This accounts for a bug in the GetPositionFromCharIndex method
                if (selectionEnd.Equals(this.TextLength))
                {
                    end += (int)(LineHeight + 0.5);
                }

                int result = end - start;
                return (result < LineHeight) ? (int)(LineHeight + 0.5) : result;
            }
        }
        #endregion

        // For anti auto scroll logic
        [DllImport("user32.dll")]
        private static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, Int32 wParam, Int32 lParam);
        private const int WM_USER = 0x400;
        private const int EM_HIDESELECTION = WM_USER + 63;
    }
}
