using System;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using C1.Silverlight;
using C1.Silverlight.RichTextBox;
using C1.Silverlight.RichTextBox.Documents;

namespace ZipBrowser
{
    /// <summary>
    /// Types of syntax supported by the <see cref="ColorizedTextBox"/> class.
    /// </summary>
    public enum SyntaxType
    {
        PlainText,
        Xml,
        CS,
        VB
    }

    /// <summary>
    /// C1RichTextBox with syntax coloring.
    /// Set the <see cref="SyntaxType"/> property to select the type of syntax to apply to the text.
    /// </summary>
    public class ColorizedTextBox : C1RichTextBox
    {
        // ** fields
        C1ContextMenu _ctxMenu;
        SyntaxType _syntax = SyntaxType.PlainText;
        C1RangeStyleCollection _rangeStyles;
        IEnumerable<SyntaxRule> _rules;
        List<Match> _matches = new List<Match>();
        Storyboard _sb;

        // ** ctor
        public ColorizedTextBox()
        {
            // set default properties
            ReturnMode = ReturnMode.SoftLineBreak;
            TextWrapping = TextWrapping.NoWrap;
            ClipboardMode = ClipboardMode.PlainText;

            // create collection of range styles to apply colorization
            _rangeStyles = new C1RangeStyleCollection();

            // setup storyboard to avoid re-colorizing too often while the user types
            _sb = new Storyboard();
            _sb.Duration = new Duration(TimeSpan.FromMilliseconds(250));
            _sb.Completed += (ss, ee) =>
            {
                UpdateSyntaxColoring();
            };

            // reset colorize storyboard when the text changes
            TextChanged += (ss, ee) =>
            {
                _sb.Stop();
                _sb.Seek(TimeSpan.Zero);
                _sb.Begin();
            };

            // update colors when the control loads and when the document changes
            Loaded += (ss, ee) => { UpdateSyntaxColoring(); };
            DocumentChanged += (ss, ee) => { UpdateSyntaxColoring(); };

            // set up context menu
            var miSelectAll = new C1MenuItem();
            miSelectAll.Header = "Select All (Ctrl + A)";
            miSelectAll.Click += (s, e) => { this.SelectAll(); };
            var miCopy = new C1MenuItem();
            miCopy.Header = "Copy (Ctrl + C)";
            miCopy.Click += (s, e) => { this.ClipboardCopy(); };
            MouseRightButtonUp += (s, e) =>
            {
                e.Handled = true;
            };
            MouseRightButtonDown += (s, e) =>
            {
                e.Handled = true;
                foreach (C1MenuItem item in _ctxMenu.Items)
                {
                    item.FontFamily = this.FontFamily;
                    item.FontSize = this.FontSize;
                }
                _ctxMenu.Show(this, e.GetPosition(this));
            };
            _ctxMenu = new C1.Silverlight.C1ContextMenu();
            _ctxMenu.Items.Add(miSelectAll);
            _ctxMenu.Items.Add(miCopy);
        }

        // ** object model
        public SyntaxType SyntaxType
        {
            get { return _syntax; }
            set
            {
                if (value != _syntax)
                {
                    _syntax = value;
                    _rules = SyntaxRule.GetRules(value);
                    UpdateSyntaxColoring();
                }
            }
        }

        // ** implementation
        void UpdateSyntaxColoring()
        {
            // remove old coloring
            StyleOverrides.Clear();
            _rangeStyles.Clear();
            _matches.Clear();
            StyleOverrides.Add(_rangeStyles);

            // apply new coloring
            if (_rules != null)
            {
                var range = Document.ContentRange;
                var input = range.Text;
                foreach (var rule in _rules)
                {
                    ApplyRule(rule, range, input);
                }
            }
        }
        void ApplyRule(SyntaxRule rule, C1TextRange range, string input)
        {
            if (rule.Regex != null)
            {
                foreach (Match m in rule.Regex.Matches(input))
                {
                    // get matching range
                    var matchRange = GetRangeAtTextOffset(range.Start, m);

                    // skip overlapping ranges (not pretty, but works...)
                    bool overlap = false;
                    foreach (var match in _matches)
                    {
                        if (match.Index + match.Length > m.Index &&
                            match.Index < m.Index + m.Length)
                        {
                            overlap = true;
                            break;
                        }
                    }

                    // no overlap? apply styles
                    if (!overlap)
                    {
                        _matches.Add(m);

                        // apply all rules
                        foreach (var pair in rule.StyleDictionary)
                        {
                            // apply this style
                            if (pair.Key == string.Empty)
                            {
                                _rangeStyles.Add(new C1RangeStyle(matchRange, pair.Value));
                            }
                            else
                            {
                                var g = m.Groups[pair.Key];
                                if (g != null)
                                {
                                    var c = g.Captures;
                                    for (int i = 0; i < c.Count; i++)
                                    {
                                        var subRange = GetRangeAtTextOffset(range.Start, c[i]);
                                        _rangeStyles.Add(new C1RangeStyle(subRange, pair.Value));
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        C1TextRange GetRangeAtTextOffset(C1TextPointer pos, Capture capture)
        {
            var start = pos.GetPositionAtOffset(capture.Index, C1TextRange.TextTagFilter);
            var end = start.GetPositionAtOffset(capture.Length, C1TextRange.TextTagFilter);
            return new C1TextRange(start, end);
        }
    }
    /// <summary>
    /// Syntax rule combining a Regex and a foreground color to apply to all matches.
    /// </summary>
    class SyntaxRule
    {
        // ** fields
        Regex _regex;

        // static groups of rules to apply for each supported syntax type
        static IEnumerable<SyntaxRule> _rulesPlainText;
        static IEnumerable<SyntaxRule> _rulesXml;
        static IEnumerable<SyntaxRule> _rulesCS;
        static IEnumerable<SyntaxRule> _rulesVB;

        // colors assigned to common syntax elements
        static Color _clrKeyword = Color.FromArgb(0xff, 0x00, 0x00, 0xc0);
        static Color _clrComment = Color.FromArgb(0xff, 0x00, 0x80, 0x00);
        static Color _clrString = Color.FromArgb(0xff, 0xa3, 0x15, 0x15);


        // ** object model
        public Regex Regex
        {
            get { return _regex; }
            set { _regex = value; }
        }
        public Dictionary<string, C1TextElementStyle> StyleDictionary = new Dictionary<string, C1TextElementStyle>();
        public static IEnumerable<SyntaxRule> GetRules(SyntaxType syntax)
        {
            switch (syntax)
            {
                case SyntaxType.PlainText:
                    if (_rulesPlainText == null)
                    {
                        _rulesPlainText = new List<SyntaxRule>();
                    }
                    return _rulesPlainText;
                case SyntaxType.Xml:
                    if (_rulesXml == null)
                    {
                        _rulesXml = GetSyntaxRulesXml();
                    }
                    return _rulesXml;
                case SyntaxType.CS:
                    if (_rulesCS == null)
                    {
                        _rulesCS = GetSyntaxRulesCS();
                    }
                    return _rulesCS;
                case SyntaxType.VB:
                    if (_rulesVB == null)
                    {
                        _rulesVB = GetSyntaxRulesVB();
                    }
                    return _rulesVB;
            }
            throw new Exception("Unknown syntax.");
        }

        // build static syntax rule tables
        static IEnumerable<SyntaxRule> GetSyntaxRulesXml()
        {
            // RegEx patterns
            const string commentPattern = @"<!--.*-->";
            const string metaPattern = @"<%@.*%>";
            const string tagPattern =
                @"</?(?<tagName>[a-zA-Z0-9_:\-]+)" +
                @"(\s+(?<attName>[a-zA-Z0-9_:\-]+)(?<attValue>(=""[^""]+"")?))*\s*/?>";

            // rules
            return new SyntaxRule[]
            {
                SyntaxRule.Create(commentPattern, true, _clrComment),
                SyntaxRule.Create(tagPattern, true, _clrKeyword, "tagName", Colors.Purple, "attName", _clrString),
                SyntaxRule.Create(metaPattern, false, _clrComment),
            };
        }
        static IEnumerable<SyntaxRule> GetSyntaxRulesCS()
        {
            // RegEx patterns
            const string mlCommentPattern = @"/\*.*\*/";
            const string slCommentPattern = @"//.*";
            const string pragmaPattern = @"^[ \t]*#.*";
            const string stringPattern = @"""[^""]*""";
            const string keywordPattern = @"\b(" +
                @"abstract|as|base|bool|break|byte|case|catch|char|checked|class|const|continue|" +
                @"decimal|default|delegate|do|double|else|enum|event|explicit|extern|false|finally|" +
                @"fixed|float|for|foreach|goto|if|implicit|in|int|interface|internal|is|lock|long|" +
                @"namespace|new|null|object|operator|out|override|params|partial|private|protected|" +
                @"public|readonly|ref|return|sbyte|sealed|short|sizeof|stackalloc|static|string|struct|" +
                @"switch|this|throw|true|try|typeof|uint|ulong|unchecked|unsafe|ushort|using|var|" +
                @"virtual|void|volatile|while|yield" +
                @")\b";

            // rules
            return new SyntaxRule[]
            {
                SyntaxRule.Create(mlCommentPattern, true, _clrComment),
                SyntaxRule.Create(pragmaPattern, false, _clrKeyword),
                SyntaxRule.Create(slCommentPattern, false, _clrComment),
                SyntaxRule.Create(stringPattern, false, _clrString),
                SyntaxRule.Create(keywordPattern, false, _clrKeyword),
            };
        }
        static IEnumerable<SyntaxRule> GetSyntaxRulesVB()
        {
            // RegEx patterns
            const string commentPattern = @"'.*";
            const string stringPattern = @"""[^""]*""";
            const string keywordPattern = @"\b(" +
                @"AddHandler|AddressOf|Alias|And|AndAlso|Ansi|As|Assembly|Auto|Boolean|ByRef|Byte|" +
                @"ByVal|Call|Case|Catch|CBool|CByte|CChar|CDate|CDbl|CDec|Char|CInt|Class|CLng|CObj|" +
                @"Const|CShort|CSng|CStr|CType|Date|Decimal|Declare|Default|Delegate|Dim|DirectCast|" +
                @"Do|Double|Each|Else|ElseIf|End|Enum|Erase|Error|Event|Exit|False|Finally|For|Friend|" +
                @"Function|Get|GetType|GoSub|GoTo|Handles|If|Implements|Imports|In|Inherits|Integer|" +
                @"Interface|Is|Let|Lib|Like|Long|Loop|Me|Mod|Module|MustInherit|MustOverride|MyBase|" +
                @"MyClass|Namespace|New|Next|Not|Nothing|NotInheritable|NotOverridable|Object|On|" +
                @"Option|Optional|Or|OrElse|Overloads|Overridable|Overrides|ParamArray|Preserve|Private|" +
                @"Property|Protected|Public|RaiseEvent|ReadOnly|ReDim|REM|RemoveHandler|Resume|Return|" +
                @"Select|Set|Shadows|Shared|Short|Single|Static|Step|Stop|String|Structure|Sub|SyncLock|" +
                @"Then|Throw|To|True|Try|TypeOf|Unicode|Until|Variant|When|While|With|WithEvents|WriteOnly|" +
                @"Xor" +
                @")\b";

            // rules
            return new SyntaxRule[]
            {
                SyntaxRule.Create(commentPattern, false, _clrComment),
                SyntaxRule.Create(stringPattern, false, _clrString),
                SyntaxRule.Create(keywordPattern, false, _clrKeyword),
            };
        }
        static SyntaxRule Create(string pattern, bool multiLine, Color color, params object[] nameColorPairs)
        {
            var rule = new SyntaxRule();

            // save rule pattern
            rule.Regex = new Regex(pattern, multiLine ? RegexOptions.Singleline : RegexOptions.Multiline);

            // default style for this pattern
            var style = new C1TextElementStyle();
            style[C1TextElement.ForegroundProperty] = new SolidColorBrush(color);
            rule.StyleDictionary[string.Empty] = style;

            // styles for tags within the pattern (optional)
            for (int i = 0; i < nameColorPairs.Length; i += 2)
            {
                var name = (string)nameColorPairs[i];
                style = new C1TextElementStyle();
                style[C1TextElement.ForegroundProperty] = new SolidColorBrush((Color)nameColorPairs[i + 1]);
                rule.StyleDictionary[name] = style;
            }

            // done
            return rule;
        }
    }
}
