using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
// ReSharper disable All

namespace Minimatch
{
    ///<summary>Contains options that control how Minimatch matches strings.</summary>
    public class Options
    {
        ///<summary>Suppresses the behavior of treating # at the start of a pattern as a comment.</summary>
        public bool NoComment { get; set; }
        ///<summary>Suppresses the behavior of treating a leading ! character as negation.</summary>
        public bool NoNegate { get; set; }
        ///<summary>Do not expand {a,b} and {1.3} brace sets.</summary>
        public bool NoBrace { get; set; }
        ///<summary>Disable ** matching against multiple folder names.</summary>
        public bool NoGlobStar { get; set; }
        ///<summary>Ignores case differences when matching.</summary>
        public bool NoCase { get; set; }
        ///<summary>Disable "extglob" style patterns like <c>+(a|b)</c>.</summary>
        public bool NoExt { get; set; }
        ///<summary>Allow patterns to match filenames starting with a period, even if the pattern does not explicitly have a period in that spot.
        ///Note that by default, <c>a/**/b</c>  will not match <c>a/.d/b</c>, unless dot is set.</summary>
        public bool Dot { get; set; }
        ///<summary>When a match is not found by Match(), return a list containing the pattern itself. If not set, an empty list is returned if there are no matches.</summary>
        public bool NoNull { get; set; }
        ///<summary>Returns from negate expressions the same as if they were not negated. (ie, true on a hit, false on a miss).</summary>
        public bool FlipNegate { get; set; }

        ///<summary>If set, then patterns without slashes will be matched against the basename of the path if it contains slashes. For example, <c>a?b</c> would match the path <c>/xyz/123/acb</c>, but not <c>/xyz/acb/123</c>.</summary>
        public bool MatchBase { get; set; }

        internal RegexOptions RegexOptions { get { return NoCase ? RegexOptions.IgnoreCase : RegexOptions.None; } }

        ///<summary>If true, backslahes in patterns and paths will be treated as forward slashes.  This disables escape characters.</summary>
        public bool AllowWindowsPaths { get; set; }

        // Aliases:
        ///<summary>Ignores case differences when matching.  This is the same as NoCase.</summary>
        public bool IgnoreCase
        {
            get { return NoCase; }
            set { NoCase = value; }
        }
    }


    ///<summary>Parses a single glob pattern and tests strings against it.</summary>
    public class Minimatcher
    {
        ///<summary>Creates a filter function that tests input against a pattern.</summary>
        public static Func<string, bool> CreateFilter(string pattern, Options options = null)
        {
            if (pattern == null) throw new ArgumentNullException("pattern");
            // "" only matches ""
            if (String.IsNullOrWhiteSpace(pattern)) return String.IsNullOrEmpty;

            var m = new Minimatcher(pattern, options);
            return m.IsMatch;
        }
        ///<summary>Tests a single input against a pattern.</summary>
        ///<remarks>This function reparses this input on each invocation.  For performance, avoid this function and reuse a Minimatcher instance instead.</remarks>
        public static bool Check(string input, string pattern, Options options = null)
        {
            if (input == null) throw new ArgumentNullException("input");
            if (pattern == null) throw new ArgumentNullException("pattern");

            // shortcut: comments match nothing.
            if (options != null && !options.NoComment && pattern[0] == '#')
            {
                return false;
            }

            // "" only matches ""
            if (String.IsNullOrWhiteSpace(pattern)) return input == "";

            return new Minimatcher(pattern, options).IsMatch(input);
        }

        ///<summary>Filters a list of inputs against a single pattern.</summary>
        ///<remarks>This function reparses this input on each invocation.  For performance, avoid this function and reuse a Minimatcher instance instead.</remarks>
        public static IEnumerable<string> Filter(IEnumerable<string> list, string pattern, Options options = null)
        {
            var mm = new Minimatcher(pattern, options);
            list = list.Where(mm.IsMatch);
            if (options != null && options.NoNull)
                list = list.DefaultIfEmpty(pattern);
            return list;
        }

        ///<summary>Compiles a pattern into a single regular expression.</summary>
        public static Regex CreateRegex(string pattern, Options options = null)
        {
            return new Minimatcher(pattern, options).MakeRegex();
        }


        readonly Options options;

        string pattern;
        bool negate = false;
        bool comment = false;
        bool empty = false;

        ///<summary>Creates a new Minimatcher instance, parsing the pattern into a regex.</summary>
        public Minimatcher(string pattern, Options options = null)
        {
            if (pattern == null) throw new ArgumentNullException("pattern");
            this.options = options ?? new Options();
            this.pattern = pattern.Trim();
            if (this.options.AllowWindowsPaths)
                this.pattern = this.pattern.Replace('\\', '/');

            this.Make();
        }

        ///<summary>Checks whether a given string matches this pattern.</summary>
        public bool IsMatch(string input) { return Match(input, false); }

        ///<summary>Filters a list of inputs against this pattern.</summary>
        public IEnumerable<string> Filter(IEnumerable<string> list)
        {
            list = list.Where(IsMatch);
            if (options.NoNull)
                list = list.DefaultIfEmpty(pattern);
            return list;
        }


        Regex regexp;
        bool isError;

        IEnumerable<string> globSet;
        IEnumerable<IEnumerable<ParseItem>> set;
        IEnumerable<IEnumerable<string>> globParts;


        // any single thing other than /
        // don't need to escape / when using new RegExp()
        const string qmark = "[^/]"

          // * => any number of characters
          , star = qmark + "*?"

          // ** when dots are allowed.  Anything goes, except .. and .
            // not (^ or / followed by one or two dots followed by $ or /),
            // followed by anything, any number of times.
          , twoStarDot = "(?:(?!(?:\\/|^)(?:\\.{1,2})($|\\/)).)*?"

          // not a ^ or / followed by a dot,
            // followed by anything, any number of times.
          , twoStarNoDot = "(?:(?!(?:\\/|^)\\.).)*?";

        // characters that need to be escaped in RegExp.
        static readonly HashSet<Char> reSpecials = new HashSet<char>("().*{}+?[]^$\\!".ToCharArray());
        static readonly Regex slashSplit = new Regex("/+");

        void Make()
        {
            // empty patterns and comments match nothing.
            if (!options.NoComment && !string.IsNullOrEmpty(pattern) && pattern[0] == '#')
            {
                this.comment = true;
                return;
            }
            if (String.IsNullOrEmpty(pattern))
            {
                this.empty = true;
                return;
            }

            // step 1: figure out negation, etc.
            this.ParseNegate();

            // step 2: expand braces
            this.globSet = BraceExpand(pattern, options);

            // step 3: now we have a set, so turn each one into a series of path-portion
            // matching patterns.
            // These will be regexps, except in the case of "**", which is
            // set to the GLOBSTAR object for globstar behavior,
            // and will not contain any / characters
            this.globParts = globSet.Select(s => slashSplit.Split(s)).ToList();

            // glob --> regexps
            this.set = globParts.Select(g => g.Select(t => this.Parse(t, false)))
                                .Where(g => !g.Contains(null))
                                .Select(g => g.Select(t => t.Item1))
                                .ToList();
        }

        void ParseNegate()
        {
            var negateOffset = 0;

            if (options.NoNegate) return;

            for (var i = 0; i < pattern.Length && pattern[i] == '!'; i++)
            {
                negate = !negate;
                negateOffset++;
            }

            if (negateOffset > 0) this.pattern = pattern.Substring(negateOffset);
        }

        static readonly Regex hasBraces = new Regex(@"\{.*\}");
        static readonly Regex numericSet = new Regex(@"^\{(-?[0-9]+)\.\.(-?[0-9]+)\}");
        // Brace expansion:
        // a{b,c}d -> abd acd
        // a{b,}c -> abc ac
        // a{0..3}d -> a0d a1d a2d a3d
        // a{b,c{d,e}f}g -> abg acdfg acefg
        // a{b,c}d{e,f}g -> abdeg acdeg abdeg abdfg
        //
        // Invalid sets are not expanded.
        // a{2..}b -> a{2..}b
        // a{b}c -> a{b}c
        ///<summary>Expands all brace ranges in a pattern, returning a sequence containing every possible combination.</summary>
        public static IEnumerable<string> BraceExpand(string pattern, Options options)
        {
            if (options.NoBrace || !hasBraces.IsMatch(pattern))
            {
                // shortcut. no need to expand.
                return new[] { pattern };
            }
            bool escaping = false;
            int i;
            // examples and comments refer to this crazy pattern:
            // a{b,c{d,e},{f,g}h}x{y,z}
            // expected:
            // abxy
            // abxz
            // acdxy
            // acdxz
            // acexy
            // acexz
            // afhxy
            // afhxz
            // aghxy
            // aghxz

            // everything before the first \{ is just a prefix.
            // So, we pluck that off, and work with the rest,
            // and then prepend it to everything we find.
            if (pattern[0] != '{')
            {
                // console.error(pattern)
                string prefix = null;
                for (i = 0; i < pattern.Length; i++)
                {
                    var c = pattern[i];
                    // console.error(i, c)
                    if (c == '\\')
                    {
                        escaping = !escaping;
                    }
                    else if (c == '{' && !escaping)
                    {
                        prefix = pattern.Substring(0, i);
                        break;
                    }
                }

                // actually no sets, all { were escaped.
                if (prefix == null)
                {
                    // console.error("no sets")
                    return new[] { pattern };
                }

                return BraceExpand(pattern.Substring(i), options).Select(t => prefix + t);
            }

            // now we have something like:
            // {b,c{d,e},{f,g}h}x{y,z}
            // walk through the set, expanding each part, until
            // the set ends.  then, we'll expand the suffix.
            // If the set only has a single member, then'll put the {} back

            // first, handle numeric sets, since they're easier
            var numset = numericSet.Match(pattern);
            if (numset.Success)
            {
                // console.error("numset", numset[1], numset[2])
                var suf = BraceExpand(pattern.Substring(numset.Length), options).ToList();
                int start = int.Parse(numset.Groups[1].Value),
                end = int.Parse(numset.Groups[2].Value),
                inc = start > end ? -1 : 1;
                var retVal = new List<string>();
                for (var w = start; w != (end + inc); w += inc)
                {
                    // append all the suffixes
                    for (var ii = 0; ii < suf.Count; ii++)
                    {
                        retVal.Add(w.ToString() + suf[ii]);
                    }
                }
                return retVal;
            }

            // ok, walk through the set
            // We hope, somewhat optimistically, that there
            // will be a } at the end.
            // If the closing brace isn't found, then the pattern is
            // interpreted as braceExpand("\\" + pattern) so that
            // the leading \{ will be interpreted literally.
            i = 1; // skip the \{
            int depth = 1;
            var set = new List<string>();
            string member = "";

            for (i = 1; i < pattern.Length && depth > 0; i++)
            {
                var c = pattern[i];
                // console.error("", i, c)

                if (escaping)
                {
                    escaping = false;
                    member += "\\" + c;
                }
                else
                {
                    switch (c)
                    {
                        case '\\':
                            escaping = true;
                            continue;

                        case '{':
                            depth++;
                            member += "{";
                            continue;

                        case '}':
                            depth--;
                            // if this closes the actual set, then we're done
                            if (depth == 0)
                            {
                                set.Add(member);
                                member = "";
                                // pluck off the close-brace
                                break;
                            }
                            else
                            {
                                member += c;
                                continue;
                            }

                        case ',':
                            if (depth == 1)
                            {
                                set.Add(member);
                                member = "";
                            }
                            else
                            {
                                member += c;
                            }
                            continue;

                        default:
                            member += c;
                            continue;
                    } // switch
                } // else
            } // for

            // now we've either finished the set, and the suffix is
            // pattern.substr(i), or we have *not* closed the set,
            // and need to escape the leading brace
            if (depth != 0)
            {
                // console.error("didn't close", pattern)
                return BraceExpand("\\" + pattern, options);
            }

            // ["b", "c{d,e}","{f,g}h"] ->
            //   ["b", "cd", "ce", "fh", "gh"]
            var addBraces = set.Count == 1;

            set = set.SelectMany(p => BraceExpand(p, options)).ToList();

            if (addBraces)
                set = set.Select(s => "{" + s + "}").ToList();
            // now attach the suffixes.
            // x{y,z} -> ["xy", "xz"]
            // console.error("set", set)
            // console.error("suffix", pattern.substr(i))
            return BraceExpand(pattern.Substring(i), options).SelectMany(s1 => set.Select(s2 => s2 + s1));
        }

        private class PatternListEntry
        {
            public char Type { get; set; }
            public int Start { get; set; }
            public int ReStart { get; set; }
        }

        abstract class ParseItem
        {
            public string Source { get; protected set; }

            public static readonly ParseItem Empty = new LiteralItem("");
            public static ParseItem Literal(string source) { return new LiteralItem(source); }
            public abstract string RegexSource(Options options);

            public abstract bool Match(string input, Options options);
        }
        class LiteralItem : ParseItem
        {
            public LiteralItem(string source) { Source = source; }
            public override string RegexSource(Options options) { return Regex.Escape(Source); }
            public override bool Match(string input, Options options)
            {
                return input.Equals(Source, options.NoCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
            }
        }
        class MagicItem : ParseItem
        {
            public MagicItem(string source, Options options)
            {
                Source = source;
                regex = new Lazy<Regex>(() => new Regex("^" + source + "$", options.RegexOptions));
            }
            readonly Lazy<Regex> regex;

            public override string RegexSource(Options options) { return Source; }
            public override bool Match(string input, Options options)
            {
                return regex.Value.IsMatch(input);
            }
        }
        class GlobStar : ParseItem
        {
            private GlobStar() { }
            public static readonly ParseItem Instance = new GlobStar();

            public override string RegexSource(Options options)
            {
                return options.NoGlobStar ? star
                : options.Dot ? twoStarDot
                : twoStarNoDot;
            }

            public override bool Match(string input, Options options) { throw new NotSupportedException(); }
        }

        static readonly Regex escapeCheck = new Regex(@"((?:\\{2})*)(\\?)\|");
        // parse a component of the expanded set.
        // At this point, no pattern may contain "/" in it
        // so we're going to return a 2d array, where each entry is the full
        // pattern, split on '/', and then turned into a regular expression.
        // A regexp is made at the end which joins each array with an
        // escaped /, and another full one which joins each regexp with |.
        //
        // Following the lead of Bash 4.1, note that "**" only has special meaning
        // when it is the *only* thing in a path portion.  Otherwise, any series
        // of * is equivalent to a single *.  Globstar behavior is enabled by
        // default, and can be disabled by setting options.noglobstar.
        private Tuple<ParseItem, bool> Parse(string pattern, bool isSub)
        {
            // shortcuts
            if (!options.NoGlobStar && pattern == "**") return Tuple.Create(GlobStar.Instance, false);
            if (pattern == "") return Tuple.Create(ParseItem.Empty, false);

            string re = "";
            bool hasMagic = options.NoCase, escaping = false, inClass = false;
            // ? => one single character
            var patternListStack = new Stack<PatternListEntry>();
            char plType;
            char? stateChar = null;

            int reClassStart = -1, classStart = -1;
            // . and .. never match anything that doesn't start with .,
            // even when options.dot is set.
            string patternStart = pattern[0] == '.' ? "" // anything
                // not (start or / followed by . or .. followed by / or end)
              : options.Dot ? "(?!(?:^|\\/)\\.{1,2}(?:$|\\/))"
              : "(?!\\.)";

            void clearStateChar()
            {
                if (stateChar != null)
                {
                    // we had some state-tracking character
                    // that wasn't consumed by this pass.
                    switch (stateChar)
                    {
                        case '*':
                            re += star;
                            hasMagic = true;
                            break;
                        case '?':
                            re += qmark;
                            hasMagic = true;
                            break;
                        default:
                            re += "\\" + stateChar;
                            break;
                    }
                    stateChar = null;
                }
            }

            for (var i = 0; i < pattern.Length; i++)
            {
                var c = pattern[i];
                //if (options.debug) {
                //  console.error("%s\t%s %s %j", pattern, i, re, c)
                //}

                // skip over any that are escaped.
                if (escaping && reSpecials.Contains(c))
                {
                    re += "\\" + c;
                    escaping = false;
                    continue;
                }

                switch (c)
                {
                    case '/':
                        // completely not allowed, even escaped.
                        // Should already be path-split by now.
                        return null;

                    case '\\':
                        clearStateChar();
                        escaping = true;
                        continue;

                    // the various stateChar values
                    // for the 'extglob' stuff.
                    case '?':
                    case '*':
                    case '+':
                    case '@':
                    case '!':
                        //if (options.debug) {
                        //  console.error("%s\t%s %s %j <-- stateChar", pattern, i, re, c)
                        //}

                        // all of those are literals inside a class, except that
                        // the glob [!a] means [^a] in regexp
                        if (inClass)
                        {
                            if (c == '!' && i == classStart + 1) c = '^';
                            re += c;
                            continue;
                        }

                        // if we already have a stateChar, then it means
                        // that there was something like ** or +? in there.
                        // Handle the stateChar, then proceed with this one.
                        clearStateChar();
                        stateChar = c;
                        // if extglob is disabled, then +(asdf|foo) isn't a thing.
                        // just clear the statechar *now*, rather than even diving into
                        // the patternList stuff.
                        if (options.NoExt) clearStateChar();
                        continue;

                    case '(':
                        if (inClass)
                        {
                            re += "(";
                            continue;
                        }

                        if (stateChar == null)
                        {
                            re += "\\(";
                            continue;
                        }

                        plType = stateChar.Value;
                        patternListStack.Push(new PatternListEntry { Type = plType, Start = i - 1, ReStart = re.Length });
                        // negation is (?:(?!js)[^/]*)
                        re += stateChar == '!' ? "(?:(?!" : "(?:";
                        stateChar = null;
                        continue;

                    case ')':
                        if (inClass || !patternListStack.Any())
                        {
                            re += "\\)";
                            continue;
                        }

                        hasMagic = true;
                        re += ')';
                        plType = patternListStack.Pop().Type;
                        // negation is (?:(?!js)[^/]*)
                        // The others are (?:<pattern>)<type>
                        switch (plType)
                        {
                            case '!':
                                re += "[^/]*?)";
                                break;
                            case '?':
                            case '+':
                            case '*': re += plType; break;
                            case '@': break; // the default anyway
                        }
                        continue;

                    case '|':
                        if (inClass || !patternListStack.Any() || escaping)
                        {
                            re += "\\|";
                            escaping = false;
                            continue;
                        }

                        re += "|";
                        continue;

                    // these are mostly the same in regexp and glob
                    case '[':
                        // swallow any state-tracking char before the [
                        clearStateChar();

                        if (inClass)
                        {
                            re += "\\" + c;
                            continue;
                        }

                        inClass = true;
                        classStart = i;
                        reClassStart = re.Length;
                        re += c;
                        continue;

                    case ']':
                        //  a right bracket shall lose its special
                        //  meaning and represent itself in
                        //  a bracket expression if it occurs
                        //  first in the list.  -- POSIX.2 2.8.3.2
                        if (i == classStart + 1 || !inClass)
                        {
                            re += "\\" + c;
                            escaping = false;
                            continue;
                        }

                        // finish up the class.
                        hasMagic = true;
                        inClass = false;
                        re += c;
                        continue;

                    default:
                        // swallow any state char that wasn't consumed
                        clearStateChar();

                        if (escaping)
                        {
                            // no need
                            escaping = false;
                        }
                        else if (reSpecials.Contains(c) && !(c == '^' && inClass))
                        {
                            re += "\\";
                        }

                        re += c;
                        break;
                } // switch
            } // for


            // handle the case where we left a class open.
            // "[abc" is valid, equivalent to "\[abc"
            if (inClass)
            {
                // split where the last [ was, and escape it
                // this is a huge pita.  We now have to re-walk
                // the contents of the would-be class to re-translate
                // any characters that were passed through as-is
                string cs = pattern.Substring(classStart + 1);
                var sp = this.Parse(cs, true);
                re = re.Substring(0, reClassStart) + "\\[" + sp.Item1.Source;
                hasMagic = hasMagic || sp.Item2;
            }

            // handle the case where we had a +( thing at the *end*
            // of the pattern.
            // each pattern list stack adds 3 chars, and we need to go through
            // and escape any | chars that were passed through as-is for the regexp.
            // Go through and escape them, taking care not to double-escape any
            // | chars that were already escaped.
            while (patternListStack.Any())
            {
                var pl = patternListStack.Pop();
                var tail = re.Substring(pl.ReStart + 3);
                // maybe some even number of \, then maybe 1 \, followed by a |
                tail = escapeCheck.Replace(tail, m =>
                {
                    string escape = m.Groups[2].Value;
                    // the | isn't already escaped, so escape it.
                    if (String.IsNullOrEmpty(escape)) escape = "\\";

                    // need to escape all those slashes *again*, without escaping the
                    // one that we need for escaping the | character.  As it works out,
                    // escaping an even number of slashes can be done by simply repeating
                    // it exactly after itself.  That's why this trick works.
                    //
                    // I am sorry that you have to see this.
                    return m.Groups[1].Value + m.Groups[1].Value + escape + "|";
                });

                // console.error("tail=%j\n   %s", tail, tail)
                var t = pl.Type == '*' ? star
                      : pl.Type == '?' ? qmark
                      : "\\" + pl.Type;

                hasMagic = true;
                re = re.Remove(pl.ReStart)
                   + t + "\\("
                   + tail;
            }

            // handle trailing things that only matter at the very end.
            clearStateChar();
            if (escaping)
            {
                // trailing \\
                re += "\\\\";
            }

            // only need to apply the nodot start if the re starts with
            // something that could conceivably capture a dot
            var addPatternStart = false;
            switch (re[0])
            {
                case '.':
                case '[':
                case '(': addPatternStart = true; break;
            }

            // if the re is not "" at this point, then we need to make sure
            // it doesn't match against an empty path part.
            // Otherwise a/* will match a/, which it should not.
            if (re != "" && hasMagic) re = "(?=.)" + re;

            if (addPatternStart) re = patternStart + re;

            // parsing just a piece of a larger pattern.
            if (isSub)
            {
                return Tuple.Create(ParseItem.Literal(re), hasMagic);
            }

            // skip the regexp for non-magical patterns
            // unescape anything in it, though, so that it'll be
            // an exact match against a file etc.
            if (!hasMagic)
            {
                return Tuple.Create(ParseItem.Literal(GlobUnescape(pattern)), false);
            }
            return new Tuple<ParseItem, bool>(new MagicItem(re, options), false);
        }


        Regex MakeRegex()
        {
            if (this.regexp != null || isError) return this.regexp;

            // at this point, this.set is a 2d array of partial
            // pattern strings, or "**".
            //
            // It's better to use .match().  This function shouldn't
            // be used, really, but it's pretty convenient sometimes,
            // when you just want to work with a regex.
            if (comment || empty || !set.Any())
            {
                this.isError = true;
                return null;
            }
            var re = String.Join("|", set.Select(pattern =>
                  String.Join("\\/", pattern.Select(p => p.RegexSource(options))
              )));

            // must match entire pattern
            // ending in a * or ** will make it less strict.
            re = "^(?:" + re + ")$";

            // can match anything, as long as it's not this.
            if (this.negate) re = "^(?!" + re + ").*$";

            try
            {
                return this.regexp = new Regex(re, options.RegexOptions);
            }
            catch
            {
                this.isError = true;
                return null;
            }
        }


        private bool Match(string input, bool partial)
        {
            // console.error("match", f, this.pattern)
            // short-circuit in the case of busted things.
            // comments, etc.
            if (this.comment) return false;
            if (this.empty) return input == "";

            if (input == "/" && partial) return true;

            // windows: need to use /, not \
            // On other platforms, \ is a valid (albeit bad) filename char.

            if (options.AllowWindowsPaths)
                input = input.Replace("\\", "/");

            // treat the test path as a set of pathparts.
            var f = slashSplit.Split(input);
            //if (options.debug) {
            //  console.error(this.pattern, "split", f)
            //}

            // just ONE of the pattern sets in this.set needs to match
            // in order for it to be valid.  If negating, then just one
            // match means that we have failed.
            // Either way, return on the first hit.

            foreach (var pattern in set)
            {
                var hit = this.MatchOne(f, pattern.ToList(), partial);
                if (hit)
                {
                    if (options.FlipNegate) return true;
                    return !this.negate;
                }

            }

            // didn't get any hits.  this is success if it's a negative
            // pattern, failure otherwise.
            if (options.FlipNegate) return false;
            return this.negate;
        }

        // set partial to true to test if, for example,
        // "/a/b" matches the start of "/*/b/*/d"
        // Partial means, if you run out of file before you run
        // out of pattern, then that's fine, as long as all
        // the parts match.
        bool MatchOne(IList<string> file, IList<ParseItem> pattern, bool partial)
        {

            //if (options.debug) {
            //  console.error("matchOne",
            //                { "this": this
            //                , file: file
            //                , pattern: pattern })
            //}

            if (options.MatchBase && pattern.Count == 1)
            {
                file = new[] { file.Last(s => !String.IsNullOrEmpty(s)) };
            }

            //if (options.debug) {
            //  console.error("matchOne", file.length, pattern.length)
            //}
            int fi = 0, pi = 0;
            for (; (fi < file.Count) && (pi < pattern.Count); fi++, pi++)
            {

                //if (options.debug) {
                //  console.error("matchOne loop")
                //}
                ParseItem p = pattern[pi];
                string f = file[fi];

                //if (options.debug) {
                //  console.error(pattern, p, f)
                //}

                // should be impossible.
                // some invalid regexp stuff in the set.
                if (p == null) return false;

                if (p is GlobStar)
                {
                    //if (options.debug)
                    //  console.error('GLOBSTAR', [pattern, p, f])

                    // "**"
                    // a/**/b/**/c would match the following:
                    // a/b/x/y/z/c
                    // a/x/y/z/b/c
                    // a/b/x/b/x/c
                    // a/b/c
                    // To do this, take the rest of the pattern after
                    // the **, and see if it would match the file remainder.
                    // If so, return success.
                    // If not, the ** "swallows" a segment, and try again.
                    // This is recursively awful.
                    //
                    // a/**/b/**/c matching a/b/x/y/z/c
                    // - a matches a
                    // - doublestar
                    //   - matchOne(b/x/y/z/c, b/**/c)
                    //     - b matches b
                    //     - doublestar
                    //       - matchOne(x/y/z/c, c) -> no
                    //       - matchOne(y/z/c, c) -> no
                    //       - matchOne(z/c, c) -> no
                    //       - matchOne(c, c) yes, hit
                    int fr = fi, pr = pi + 1;
                    if (pr == pattern.Count)
                    {
                        //if (options.debug)
                        //  console.error('** at the end')
                        // a ** at the end will just swallow the rest.
                        // We have found a match.
                        // however, it will not swallow /.x, unless
                        // options.dot is set.
                        // . and .. are *never* matched by **, for explosively
                        // exponential reasons.
                        for (; fi < file.Count; fi++)
                        {
                            if (file[fi] == "." || file[fi] == ".." ||
                                (!options.Dot && !string.IsNullOrEmpty(file[fi]) && file[fi][0] == '.')) return false;
                        }
                        return true;
                    }

                    // ok, let's see if we can swallow whatever we can.
                    while (fr < file.Count)
                    {
                        var swallowee = file[fr];

                        //if (options.debug) {
                        //  console.error('\nglobstar while',
                        //                file, fr, pattern, pr, swallowee)
                        //}

                        // XXX remove this slice.  Just pass the start index.
                        if (this.MatchOne(file.Skip(fr).ToList(), pattern.Skip(pr).ToList(), partial))
                        {
                            //if (options.debug)
                            //  console.error('globstar found match!', fr, file.Count, swallowee)
                            // found a match.
                            return true;
                        }
                        else
                        {
                            // can't swallow "." or ".." ever.
                            // can only swallow ".foo" when explicitly asked.
                            if (swallowee == "." || swallowee == ".." ||
                                (!options.Dot && swallowee[0] == '.'))
                            {
                                //if (options.debug)
                                //  console.error("dot detected!", file, fr, pattern, pr)
                                break;
                            }

                            // ** swallows a segment, and continue.
                            //if (options.debug)
                            //  console.error('globstar swallow a segment, and continue')
                            fr++;
                        }
                    }
                    // no match was found.
                    // However, in partial mode, we can't say this is necessarily over.
                    // If there's more *pattern* left, then 
                    if (partial)
                    {
                        // ran out of file
                        // console.error("\n>>> no match, partial?", file, fr, pattern, pr)
                        if (fr == file.Count) return true;
                    }
                    return false;
                }

                // something other than **
                // non-magic patterns just have to match exactly
                // patterns with magic have been turned into regexps.
                if (!p.Match(f, options))
                    return false;
            }

            // Note: ending in / means that we'll get a final ""
            // at the end of the pattern.  This can only match a
            // corresponding "" at the end of the file.
            // If the file ends in /, then it can only match a
            // a pattern that ends in /, unless the pattern just
            // doesn't have any more for it. But, a/b/ should *not*
            // match "a/b/*", even though "" matches against the
            // [^/]*? pattern, except in partial mode, where it might
            // simply not be reached yet.
            // However, a/b/ should still satisfy a/*

            // now either we fell off the end of the pattern, or we're done.
            if (fi == file.Count && pi == pattern.Count)
            {
                // ran out of pattern and filename at the same time.
                // an exact hit!
                return true;
            }
            else if (fi == file.Count)
            {
                // ran out of file, but still had pattern left.
                // this is ok if we're doing the match as part of
                // a glob fs traversal.
                return partial;
            }
            else if (pi == pattern.Count)
            {
                // ran out of pattern, still have file left.
                // this is only acceptable if we're on the very last
                // empty segment of a file with a trailing slash.
                // a/* should match a/b/
                var emptyFileEnd = (fi == file.Count - 1) && (file[fi] == "");
                return emptyFileEnd;
            }

            // should be unreachable.
            throw new InvalidOperationException("wtf?");
        }


        // replace stuff like \* with *
        static readonly Regex globUnescaper = new Regex(@"\\(.)");
        static string GlobUnescape(string s)
        {
            return globUnescaper.Replace(s, "$1");
        }
    }
}
