/// <summary>
 /// Constructor that allows the caller to specify file selection criteria.
 /// </summary>
 ///
 /// <remarks>
 /// <para>
 /// This constructor allows the caller to specify a set of criteria for selection of files.
 /// </para>
 ///
 /// <para>
 /// See <see cref="FileSelector.SelectionCriteria"/> for a description of the syntax of
 /// the selectionCriteria string.
 /// </para>
 /// </remarks>
 ///
 /// <param name="selectionCriteria">The criteria for file selection.</param>
 public FileSelector(String selectionCriteria)
 {
     if (!String.IsNullOrEmpty(selectionCriteria))
     {
         _Criterion = _ParseCriterion(selectionCriteria);
     }
 }
        private static SelectionCriterion _ParseCriterion(String s)
        {
            if (s == null)
            {
                return(null);
            }

            // shorthand for filename glob
            if (s.IndexOf(" ") == -1)
            {
                s = "name = " + s;
            }

            // inject spaces after open paren and before close paren
            string[] prPairs = { @"\((\S)", "( $1", @"(\S)\)", "$1 )", };
            for (int i = 0; i + 1 < prPairs.Length; i += 2)
            {
                Regex rgx = new Regex(prPairs[i]);
                s = rgx.Replace(s, prPairs[i + 1]);
            }

            // split the expression into tokens
            string[] tokens = s.Trim().Split(' ', '\t');

            if (tokens.Length < 3)
            {
                throw new ArgumentException(s);
            }

            SelectionCriterion current = null;

            LogicalConjunction pendingConjunction = LogicalConjunction.NONE;

            ParseState state;
            var        stateStack = new System.Collections.Generic.Stack <ParseState>();
            var        critStack  = new System.Collections.Generic.Stack <SelectionCriterion>();

            stateStack.Push(ParseState.Start);

            for (int i = 0; i < tokens.Length; i++)
            {
                switch (tokens[i].ToLower())
                {
                case "and":
                case "xor":
                case "or":
                    state = stateStack.Peek();
                    if (state != ParseState.CriterionDone)
                    {
                        throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));
                    }

                    if (tokens.Length <= i + 3)
                    {
                        throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));
                    }

                    pendingConjunction = (LogicalConjunction)Enum.Parse(typeof(LogicalConjunction), tokens[i].ToUpper());
                    current            = new CompoundCriterion {
                        Left = current, Right = null, Conjunction = pendingConjunction
                    };
                    stateStack.Push(state);
                    stateStack.Push(ParseState.ConjunctionPending);
                    critStack.Push(current);
                    break;

                case "(":
                    state = stateStack.Peek();
                    if (state != ParseState.Start && state != ParseState.ConjunctionPending && state != ParseState.OpenParen)
                    {
                        throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));
                    }

                    if (tokens.Length <= i + 4)
                    {
                        throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));
                    }

                    stateStack.Push(ParseState.OpenParen);
                    break;

                case ")":
                    state = stateStack.Pop();
                    if (stateStack.Peek() != ParseState.OpenParen)
                    {
                        throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));
                    }

                    stateStack.Pop();
                    stateStack.Push(ParseState.CriterionDone);
                    break;

                case "atime":
                case "ctime":
                case "mtime":
                    if (tokens.Length <= i + 2)
                    {
                        throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));
                    }

                    DateTime t;
                    try
                    {
                        t = DateTime.ParseExact(tokens[i + 2], "yyyy-MM-dd-HH:mm:ss", null);
                    }
                    catch (FormatException)
                    {
                        t = DateTime.ParseExact(tokens[i + 2], "yyyy-MM-dd", null);
                    }
                    t       = DateTime.SpecifyKind(t, DateTimeKind.Local).ToUniversalTime();
                    current = new TimeCriterion
                    {
                        Which    = (WhichTime)Enum.Parse(typeof(WhichTime), tokens[i]),
                        Operator = (ComparisonOperator)EnumUtil.Parse(typeof(ComparisonOperator), tokens[i + 1]),
                        Time     = t
                    };
                    i += 2;
                    stateStack.Push(ParseState.CriterionDone);
                    break;


                case "length":
                case "size":
                    if (tokens.Length <= i + 2)
                    {
                        throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));
                    }

                    Int64  sz = 0;
                    string v  = tokens[i + 2];
                    if (v.ToUpper().EndsWith("K"))
                    {
                        sz = Int64.Parse(v.Substring(0, v.Length - 1)) * 1024;
                    }
                    else if (v.ToUpper().EndsWith("KB"))
                    {
                        sz = Int64.Parse(v.Substring(0, v.Length - 2)) * 1024;
                    }
                    else if (v.ToUpper().EndsWith("M"))
                    {
                        sz = Int64.Parse(v.Substring(0, v.Length - 1)) * 1024 * 1024;
                    }
                    else if (v.ToUpper().EndsWith("MB"))
                    {
                        sz = Int64.Parse(v.Substring(0, v.Length - 2)) * 1024 * 1024;
                    }
                    else if (v.ToUpper().EndsWith("G"))
                    {
                        sz = Int64.Parse(v.Substring(0, v.Length - 1)) * 1024 * 1024 * 1024;
                    }
                    else if (v.ToUpper().EndsWith("GB"))
                    {
                        sz = Int64.Parse(v.Substring(0, v.Length - 2)) * 1024 * 1024 * 1024;
                    }
                    else
                    {
                        sz = Int64.Parse(tokens[i + 2]);
                    }

                    current = new SizeCriterion
                    {
                        Size     = sz,
                        Operator = (ComparisonOperator)EnumUtil.Parse(typeof(ComparisonOperator), tokens[i + 1])
                    };
                    i += 2;
                    stateStack.Push(ParseState.CriterionDone);
                    break;

                case "filename":
                case "name":
                {
                    if (tokens.Length <= i + 2)
                    {
                        throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));
                    }

                    ComparisonOperator c =
                        (ComparisonOperator)EnumUtil.Parse(typeof(ComparisonOperator), tokens[i + 1]);

                    if (c != ComparisonOperator.NotEqualTo && c != ComparisonOperator.EqualTo)
                    {
                        throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));
                    }

                    string m = tokens[i + 2];
                    // handle single-quoted filespecs (used to include spaces in filename patterns)
                    if (m.StartsWith("'"))
                    {
                        int ix = i;
                        if (!m.EndsWith("'"))
                        {
                            do
                            {
                                i++;
                                if (tokens.Length <= i + 2)
                                {
                                    throw new ArgumentException(String.Join(" ", tokens, ix, tokens.Length - ix));
                                }
                                m += " " + tokens[i + 2];
                            } while (!tokens[i + 2].EndsWith("'"));
                        }
                        // trim off leading and trailing single quotes
                        m = m.Substring(1, m.Length - 2);
                    }

                    current = new NameCriterion
                    {
                        MatchingFileSpec = m,
                        Operator         = c
                    };
                    i += 2;
                    stateStack.Push(ParseState.CriterionDone);
                }
                break;

                case "attributes":
                {
                    if (tokens.Length <= i + 2)
                    {
                        throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));
                    }

                    ComparisonOperator c =
                        (ComparisonOperator)EnumUtil.Parse(typeof(ComparisonOperator), tokens[i + 1]);

                    if (c != ComparisonOperator.NotEqualTo && c != ComparisonOperator.EqualTo)
                    {
                        throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i));
                    }

                    current = new AttributesCriterion
                    {
                        AttributeString = tokens[i + 2],
                        Operator        = c
                    };
                    i += 2;
                    stateStack.Push(ParseState.CriterionDone);
                }
                break;

                case "":
                    // NOP
                    stateStack.Push(ParseState.Whitespace);
                    break;

                default:
                    throw new ArgumentException("'" + tokens[i] + "'");
                }

                state = stateStack.Peek();
                if (state == ParseState.CriterionDone)
                {
                    stateStack.Pop();
                    if (stateStack.Peek() == ParseState.ConjunctionPending)
                    {
                        while (stateStack.Peek() == ParseState.ConjunctionPending)
                        {
                            var cc = critStack.Pop() as CompoundCriterion;
                            cc.Right = current;
                            current  = cc;    // mark the parent as current (walk up the tree)
                            stateStack.Pop(); // the conjunction is no longer pending

                            state = stateStack.Pop();
                            if (state != ParseState.CriterionDone)
                            {
                                throw new ArgumentException("??");
                            }
                        }
                    }
                    else
                    {
                        stateStack.Push(ParseState.CriterionDone);   // not sure?
                    }
                }

                if (state == ParseState.Whitespace)
                {
                    stateStack.Pop();
                }
            }

            return(current);
        }