using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;

namespace GitHub.Runner.Worker
{
    public delegate void OnMatcherChanged(object sender, MatcherChangedEventArgs e);

    public sealed class MatcherChangedEventArgs : EventArgs
    {
        public MatcherChangedEventArgs(IssueMatcherConfig config)
        {
            Config = config;
        }

        public IssueMatcherConfig Config { get; }
    }

    public sealed class IssueMatcher
    {
        private string _defaultSeverity;
        private string _defaultFromPath;
        private string _owner;
        private IssuePattern[] _patterns;
        private IssueMatch[] _state;

        public IssueMatcher(IssueMatcherConfig config, TimeSpan timeout)
        {
            _owner = config.Owner;
            _defaultSeverity = config.Severity;
            _defaultFromPath = config.FromPath;
            _patterns = config.Patterns.Select(x => new IssuePattern(x, timeout)).ToArray();
            Reset();
        }

        public string Owner
        {
            get
            {
                if (_owner == null)
                {
                    _owner = string.Empty;
                }

                return _owner;
            }
        }

        public string DefaultSeverity
        {
            get
            {
                if (_defaultSeverity == null)
                {
                    _defaultSeverity = string.Empty;
                }

                return _defaultSeverity;
            }
        }

        public string DefaultFromPath
        {
            get
            {
                if (_defaultFromPath == null)
                {
                    _defaultFromPath = string.Empty;
                }

                return _defaultFromPath;
            }
        }

        public IssueMatch Match(string line)
        {
            // Single pattern
            if (_patterns.Length == 1)
            {
                var pattern = _patterns[0];
                var regexMatch = pattern.Regex.Match(line);

                if (regexMatch.Success)
                {
                    return new IssueMatch(null, pattern, regexMatch.Groups, DefaultSeverity, DefaultFromPath);
                }

                return null;
            }
            // Multiple patterns
            else
            {
                // Each pattern (iterate in reverse)
                for (int i = _patterns.Length - 1; i >= 0; i--)
                {
                    var runningMatch = i > 0 ? _state[i - 1] : null;

                    // First pattern or a running match
                    if (i == 0 || runningMatch != null)
                    {
                        var pattern = _patterns[i];
                        var isLast = i == _patterns.Length - 1;
                        var regexMatch = pattern.Regex.Match(line);

                        // Matched
                        if (regexMatch.Success)
                        {
                            // Last pattern
                            if (isLast)
                            {
                                // Loop
                                if (pattern.Loop)
                                {
                                    // Clear most state, but preserve the running match
                                    Reset();
                                    _state[i - 1] = runningMatch;
                                }
                                // Not loop
                                else
                                {
                                    // Clear the state
                                    Reset();
                                }

                                // Return
                                return new IssueMatch(runningMatch, pattern, regexMatch.Groups, DefaultSeverity, DefaultFromPath);
                            }
                            // Not the last pattern
                            else
                            {
                                // Store the match
                                _state[i] = new IssueMatch(runningMatch, pattern, regexMatch.Groups);
                            }
                        }
                        // Not matched
                        else
                        {
                            // Last pattern
                            if (isLast)
                            {
                                // Break the running match
                                _state[i - 1] = null;
                            }
                            // Not the last pattern
                            else
                            {
                                // Record not matched
                                _state[i] = null;
                            }
                        }
                    }
                }

                return null;
            }
        }

        public void Reset()
        {
            _state = new IssueMatch[_patterns.Length - 1];
        }
    }

    public sealed class IssuePattern
    {
        public IssuePattern(IssuePatternConfig config, TimeSpan timeout)
        {
            File = config.File;
            Line = config.Line;
            Column = config.Column;
            Severity = config.Severity;
            Code = config.Code;
            Message = config.Message;
            FromPath = config.FromPath;
            Loop = config.Loop;
            Regex = new Regex(config.Pattern ?? string.Empty, IssuePatternConfig.RegexOptions, timeout);
        }

        public int? File { get; }

        public int? Line { get; }

        public int? Column { get; }

        public int? Severity { get; }

        public int? Code { get; }

        public int? Message { get; }

        public int? FromPath { get; }

        public bool Loop { get; }

        public Regex Regex { get; }
    }

    public sealed class IssueMatch
    {
        public IssueMatch(IssueMatch runningMatch, IssuePattern pattern, GroupCollection groups, string defaultSeverity = null, string defaultFromPath = null)
        {
            File = runningMatch?.File ?? GetValue(groups, pattern.File);
            Line = runningMatch?.Line ?? GetValue(groups, pattern.Line);
            Column = runningMatch?.Column ?? GetValue(groups, pattern.Column);
            Severity = runningMatch?.Severity ?? GetValue(groups, pattern.Severity);
            Code = runningMatch?.Code ?? GetValue(groups, pattern.Code);
            Message = runningMatch?.Message ?? GetValue(groups, pattern.Message);
            FromPath = runningMatch?.FromPath ?? GetValue(groups, pattern.FromPath);

            if (string.IsNullOrEmpty(Severity) && !string.IsNullOrEmpty(defaultSeverity))
            {
                Severity = defaultSeverity;
            }

            if (string.IsNullOrEmpty(FromPath) && !string.IsNullOrEmpty(defaultFromPath))
            {
                FromPath = defaultFromPath;
            }
        }

        public string File { get; }

        public string Line { get; }

        public string Column { get; }

        public string Severity { get; }

        public string Code { get; }

        public string Message { get; }

        public string FromPath { get; }

        private string GetValue(GroupCollection groups, int? index)
        {
            if (index.HasValue && index.Value < groups.Count)
            {
                var group = groups[index.Value];
                return group.Value;
            }

            return null;
        }
    }

    [DataContract]
    public sealed class IssueMatchersConfig
    {
        [DataMember(Name = "problemMatcher")]
        private List<IssueMatcherConfig> _matchers;

        public List<IssueMatcherConfig> Matchers
        {
            get
            {
                if (_matchers == null)
                {
                    _matchers = new List<IssueMatcherConfig>();
                }

                return _matchers;
            }

            set
            {
                _matchers = value;
            }
        }

        public void Validate()
        {
            var distinctOwners = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            if (_matchers?.Count > 0)
            {
                foreach (var matcher in _matchers)
                {
                    matcher.Validate();

                    if (!distinctOwners.Add(matcher.Owner))
                    {
                        // Not localized since this is a programming contract
                        throw new ArgumentException($"Duplicate owner name '{matcher.Owner}'");
                    }
                }
            }
        }
    }

    [DataContract]
    public sealed class IssueMatcherConfig
    {
        [DataMember(Name = "owner")]
        private string _owner;

        [DataMember(Name = "severity")]
        private string _severity;

        [DataMember(Name = "pattern")]
        private IssuePatternConfig[] _patterns;

        [DataMember(Name = "fromPath")]
        private string _fromPath;

        public string Owner
        {
            get
            {
                if (_owner == null)
                {
                    _owner = string.Empty;
                }

                return _owner;
            }

            set
            {
                _owner = value;
            }
        }

        public string Severity
        {
            get
            {
                if (_severity == null)
                {
                    _severity = string.Empty;
                }

                return _severity;
            }

            set
            {
                _severity = value;
            }
        }

        public string FromPath
        {
            get
            {
                if (_fromPath == null)
                {
                    _fromPath = string.Empty;
                }

                return _fromPath;
            }

            set
            {
                _fromPath = value;
            }
        }

        public IssuePatternConfig[] Patterns
        {
            get
            {
                if (_patterns == null)
                {
                    _patterns = new IssuePatternConfig[0];
                }

                return _patterns;
            }

            set
            {
                _patterns = value;
            }
        }

        public void Validate()
        {
            // Validate owner
            if (string.IsNullOrEmpty(_owner))
            {
                throw new ArgumentException("Owner must not be empty");
            }

            // Validate severity
            switch ((_severity ?? string.Empty).ToUpperInvariant())
            {
                case "":
                case "ERROR":
                case "WARNING":
                case "NOTICE":
                    break;
                default:
                    throw new ArgumentException($"Matcher '{_owner}' contains unexpected default severity '{_severity}'");
            }

            // Validate at least one pattern
            if (_patterns == null || _patterns.Length == 0)
            {
                throw new ArgumentException($"Matcher '{_owner}' does not contain any patterns");
            }

            int? file = null;
            int? line = null;
            int? column = null;
            int? severity = null;
            int? code = null;
            int? message = null;
            int? fromPath = null;

            // Validate each pattern config
            for (var i = 0; i < _patterns.Length; i++)
            {
                var isFirst = i == 0;
                var isLast = i == _patterns.Length - 1;
                var pattern = _patterns[i];
                pattern.Validate(isFirst,
                    isLast,
                    ref file,
                    ref line,
                    ref column,
                    ref severity,
                    ref code,
                    ref message,
                    ref fromPath);
            }

            if (message == null)
            {
                throw new ArgumentException($"At least one pattern must set 'message'");
            }
        }
    }

    [DataContract]
    public sealed class IssuePatternConfig
    {
        private const string _filePropertyName = "file";
        private const string _linePropertyName = "line";
        private const string _columnPropertyName = "column";
        private const string _severityPropertyName = "severity";
        private const string _codePropertyName = "code";
        private const string _messagePropertyName = "message";
        private const string _fromPathPropertyName = "fromPath";
        private const string _loopPropertyName = "loop";
        private const string _regexpPropertyName = "regexp";
        internal static readonly RegexOptions RegexOptions = RegexOptions.CultureInvariant | RegexOptions.ECMAScript;

        [DataMember(Name = _filePropertyName)]
        public int? File { get; set; }

        [DataMember(Name = _linePropertyName)]
        public int? Line { get; set; }

        [DataMember(Name = _columnPropertyName)]
        public int? Column { get; set; }

        [DataMember(Name = _severityPropertyName)]
        public int? Severity { get; set; }

        [DataMember(Name = _codePropertyName)]
        public int? Code { get; set; }

        [DataMember(Name = _messagePropertyName)]
        public int? Message { get; set; }

        [DataMember(Name = _fromPathPropertyName)]
        public int? FromPath { get; set; }

        [DataMember(Name = _loopPropertyName)]
        public bool Loop { get; set; }

        [DataMember(Name = _regexpPropertyName)]
        public string Pattern { get; set; }

        public void Validate(
            bool isFirst,
            bool isLast,
            ref int? file,
            ref int? line,
            ref int? column,
            ref int? severity,
            ref int? code,
            ref int? message,
            ref int? fromPath)
        {
            // Only the last pattern in a multiline matcher may set 'loop'
            if (Loop && (isFirst || !isLast))
            {
                throw new ArgumentException($"Only the last pattern in a multiline matcher may set '{_loopPropertyName}'");
            }

            if (Loop && Message == null)
            {
                throw new ArgumentException($"The {_loopPropertyName} pattern must set '{_messagePropertyName}'");
            }

            var regex = new Regex(Pattern ?? string.Empty, RegexOptions);
            var groupCount = regex.GetGroupNumbers().Length;

            Validate(_filePropertyName, groupCount, File, ref file);
            Validate(_linePropertyName, groupCount, Line, ref line);
            Validate(_columnPropertyName, groupCount, Column, ref column);
            Validate(_severityPropertyName, groupCount, Severity, ref severity);
            Validate(_codePropertyName, groupCount, Code, ref code);
            Validate(_messagePropertyName, groupCount, Message, ref message);
            Validate(_fromPathPropertyName, groupCount, FromPath, ref fromPath);
        }

        private void Validate(string propertyName, int groupCount, int? newValue, ref int? trackedValue)
        {
            if (newValue == null)
            {
                return;
            }

            // The property '___' is set twice
            if (trackedValue != null)
            {
                throw new ArgumentException($"The property '{propertyName}' is set twice");
            }

            // Out of range
            if (newValue.Value < 0 || newValue >= groupCount)
            {
                throw new ArgumentException($"The property '{propertyName}' is set to {newValue} which is out of range");
            }

            // Record the value
            if (newValue != null)
            {
                trackedValue = newValue;
            }
        }
    }
}
