/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.hadoop.hbase.filter;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EmptyStackException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.regex.Pattern;
import org.apache.hadoop.hbase.CompareOperator;
import org.apache.hadoop.hbase.filter.CompareFilter.CompareOp;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.yetus.audience.InterfaceAudience;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class allows a user to specify a filter via a string The string is parsed using the methods
 * of this class and a filter object is constructed. This filter object is then wrapped in a scanner
 * object which is then returned
 * <p>
 * This class addresses the HBASE-4176 JIRA. More documentation on this Filter Language can be found
 * at: https://issues.apache.org/jira/browse/HBASE-4176
 */
@InterfaceAudience.Public
public class ParseFilter {
  private static final Logger LOG = LoggerFactory.getLogger(ParseFilter.class);

  private static HashMap<ByteBuffer, Integer> operatorPrecedenceHashMap;
  private static HashMap<String, String> filterHashMap;

  static {
    // Registers all the filter supported by the Filter Language
    filterHashMap = new HashMap<>();
    filterHashMap.put("KeyOnlyFilter", ParseConstants.FILTER_PACKAGE + "." + "KeyOnlyFilter");
    filterHashMap.put("FirstKeyOnlyFilter",
      ParseConstants.FILTER_PACKAGE + "." + "FirstKeyOnlyFilter");
    filterHashMap.put("PrefixFilter", ParseConstants.FILTER_PACKAGE + "." + "PrefixFilter");
    filterHashMap.put("ColumnPrefixFilter",
      ParseConstants.FILTER_PACKAGE + "." + "ColumnPrefixFilter");
    filterHashMap.put("MultipleColumnPrefixFilter",
      ParseConstants.FILTER_PACKAGE + "." + "MultipleColumnPrefixFilter");
    filterHashMap.put("ColumnCountGetFilter",
      ParseConstants.FILTER_PACKAGE + "." + "ColumnCountGetFilter");
    filterHashMap.put("PageFilter", ParseConstants.FILTER_PACKAGE + "." + "PageFilter");
    filterHashMap.put("ColumnPaginationFilter",
      ParseConstants.FILTER_PACKAGE + "." + "ColumnPaginationFilter");
    filterHashMap.put("InclusiveStopFilter",
      ParseConstants.FILTER_PACKAGE + "." + "InclusiveStopFilter");
    filterHashMap.put("TimestampsFilter", ParseConstants.FILTER_PACKAGE + "." + "TimestampsFilter");
    filterHashMap.put("RowFilter", ParseConstants.FILTER_PACKAGE + "." + "RowFilter");
    filterHashMap.put("FamilyFilter", ParseConstants.FILTER_PACKAGE + "." + "FamilyFilter");
    filterHashMap.put("QualifierFilter", ParseConstants.FILTER_PACKAGE + "." + "QualifierFilter");
    filterHashMap.put("ValueFilter", ParseConstants.FILTER_PACKAGE + "." + "ValueFilter");
    filterHashMap.put("ColumnRangeFilter",
      ParseConstants.FILTER_PACKAGE + "." + "ColumnRangeFilter");
    filterHashMap.put("SingleColumnValueFilter",
      ParseConstants.FILTER_PACKAGE + "." + "SingleColumnValueFilter");
    filterHashMap.put("SingleColumnValueExcludeFilter",
      ParseConstants.FILTER_PACKAGE + "." + "SingleColumnValueExcludeFilter");
    filterHashMap.put("DependentColumnFilter",
      ParseConstants.FILTER_PACKAGE + "." + "DependentColumnFilter");
    filterHashMap.put("ColumnValueFilter",
      ParseConstants.FILTER_PACKAGE + "." + "ColumnValueFilter");

    // Creates the operatorPrecedenceHashMap
    operatorPrecedenceHashMap = new HashMap<>();
    operatorPrecedenceHashMap.put(ParseConstants.SKIP_BUFFER, 1);
    operatorPrecedenceHashMap.put(ParseConstants.WHILE_BUFFER, 1);
    operatorPrecedenceHashMap.put(ParseConstants.AND_BUFFER, 2);
    operatorPrecedenceHashMap.put(ParseConstants.OR_BUFFER, 3);
  }

  /**
   * Parses the filterString and constructs a filter using it
   * <p>
   * @param filterString filter string given by the user
   * @return filter object we constructed
   */
  public Filter parseFilterString(String filterString) throws CharacterCodingException {
    return parseFilterString(Bytes.toBytes(filterString));
  }

  /**
   * Parses the filterString and constructs a filter using it
   * <p>
   * @param filterStringAsByteArray filter string given by the user
   * @return filter object we constructed
   */
  public Filter parseFilterString(byte[] filterStringAsByteArray) throws CharacterCodingException {
    // stack for the operators and parenthesis
    Stack<ByteBuffer> operatorStack = new Stack<>();
    // stack for the filter objects
    Stack<Filter> filterStack = new Stack<>();

    Filter filter = null;
    for (int i = 0; i < filterStringAsByteArray.length; i++) {
      if (filterStringAsByteArray[i] == ParseConstants.LPAREN) {
        // LPAREN found
        operatorStack.push(ParseConstants.LPAREN_BUFFER);
      } else if (
        filterStringAsByteArray[i] == ParseConstants.WHITESPACE
          || filterStringAsByteArray[i] == ParseConstants.TAB
      ) {
        // WHITESPACE or TAB found
        continue;
      } else if (checkForOr(filterStringAsByteArray, i)) {
        // OR found
        i += ParseConstants.OR_ARRAY.length - 1;
        reduce(operatorStack, filterStack, ParseConstants.OR_BUFFER);
        operatorStack.push(ParseConstants.OR_BUFFER);
      } else if (checkForAnd(filterStringAsByteArray, i)) {
        // AND found
        i += ParseConstants.AND_ARRAY.length - 1;
        reduce(operatorStack, filterStack, ParseConstants.AND_BUFFER);
        operatorStack.push(ParseConstants.AND_BUFFER);
      } else if (checkForSkip(filterStringAsByteArray, i)) {
        // SKIP found
        i += ParseConstants.SKIP_ARRAY.length - 1;
        reduce(operatorStack, filterStack, ParseConstants.SKIP_BUFFER);
        operatorStack.push(ParseConstants.SKIP_BUFFER);
      } else if (checkForWhile(filterStringAsByteArray, i)) {
        // WHILE found
        i += ParseConstants.WHILE_ARRAY.length - 1;
        reduce(operatorStack, filterStack, ParseConstants.WHILE_BUFFER);
        operatorStack.push(ParseConstants.WHILE_BUFFER);
      } else if (filterStringAsByteArray[i] == ParseConstants.RPAREN) {
        // RPAREN found
        if (operatorStack.empty()) {
          throw new IllegalArgumentException("Mismatched parenthesis");
        }
        ByteBuffer argumentOnTopOfStack = operatorStack.peek();
        if (argumentOnTopOfStack.equals(ParseConstants.LPAREN_BUFFER)) {
          operatorStack.pop();
          continue;
        }
        while (!argumentOnTopOfStack.equals(ParseConstants.LPAREN_BUFFER)) {
          filterStack.push(popArguments(operatorStack, filterStack));
          if (operatorStack.empty()) {
            throw new IllegalArgumentException("Mismatched parenthesis");
          }
          argumentOnTopOfStack = operatorStack.pop();
        }
      } else {
        // SimpleFilterExpression found
        byte[] filterSimpleExpression = extractFilterSimpleExpression(filterStringAsByteArray, i);
        i += (filterSimpleExpression.length - 1);
        filter = parseSimpleFilterExpression(filterSimpleExpression);
        filterStack.push(filter);
      }
    }

    // Finished parsing filterString
    while (!operatorStack.empty()) {
      filterStack.push(popArguments(operatorStack, filterStack));
    }
    if (filterStack.empty()) {
      throw new IllegalArgumentException("Incorrect Filter String");
    }
    filter = filterStack.pop();
    if (!filterStack.empty()) {
      throw new IllegalArgumentException("Incorrect Filter String");
    }
    return filter;
  }

  /**
   * Extracts a simple filter expression from the filter string given by the user
   * <p>
   * A simpleFilterExpression is of the form: FilterName('arg', 'arg', 'arg') The user given filter
   * string can have many simpleFilterExpressions combined using operators.
   * <p>
   * This function extracts a simpleFilterExpression from the larger filterString given the start
   * offset of the simpler expression
   * <p>
   * @param filterStringAsByteArray     filter string given by the user
   * @param filterExpressionStartOffset start index of the simple filter expression
   * @return byte array containing the simple filter expression
   */
  public byte[] extractFilterSimpleExpression(byte[] filterStringAsByteArray,
    int filterExpressionStartOffset) throws CharacterCodingException {
    int quoteCount = 0;
    for (int i = filterExpressionStartOffset; i < filterStringAsByteArray.length; i++) {
      if (filterStringAsByteArray[i] == ParseConstants.SINGLE_QUOTE) {
        if (isQuoteUnescaped(filterStringAsByteArray, i)) {
          quoteCount++;
        } else {
          // To skip the next quote that has been escaped
          i++;
        }
      }
      if (filterStringAsByteArray[i] == ParseConstants.RPAREN && (quoteCount % 2) == 0) {
        byte[] filterSimpleExpression = new byte[i - filterExpressionStartOffset + 1];
        Bytes.putBytes(filterSimpleExpression, 0, filterStringAsByteArray,
          filterExpressionStartOffset, i - filterExpressionStartOffset + 1);
        return filterSimpleExpression;
      }
    }
    throw new IllegalArgumentException("Incorrect Filter String");
  }

  /**
   * Constructs a filter object given a simple filter expression
   * <p>
   * @param filterStringAsByteArray filter string given by the user
   * @return filter object we constructed
   */
  public Filter parseSimpleFilterExpression(byte[] filterStringAsByteArray)
    throws CharacterCodingException {

    String filterName = Bytes.toString(getFilterName(filterStringAsByteArray));
    ArrayList<byte[]> filterArguments = getFilterArguments(filterStringAsByteArray);
    if (!filterHashMap.containsKey(filterName)) {
      throw new IllegalArgumentException("Filter Name " + filterName + " not supported");
    }
    try {
      filterName = filterHashMap.get(filterName);
      Class<?> c = Class.forName(filterName);
      Class<?>[] argTypes = new Class[] { ArrayList.class };
      Method m = c.getDeclaredMethod("createFilterFromArguments", argTypes);
      return (Filter) m.invoke(null, filterArguments);
    } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException
      | InvocationTargetException e) {
      LOG.warn("Exception while invoking createFilterFromArguments", e);
      throw new IllegalArgumentException(
        "Incorrect filter string " + new String(filterStringAsByteArray, StandardCharsets.UTF_8));
    }
  }

  /**
   * Returns the filter name given a simple filter expression
   * <p>
   * @param filterStringAsByteArray a simple filter expression
   * @return name of filter in the simple filter expression
   */
  public static byte[] getFilterName(byte[] filterStringAsByteArray) {
    int filterNameStartIndex = 0;
    int filterNameEndIndex = 0;

    for (int i = filterNameStartIndex; i < filterStringAsByteArray.length; i++) {
      if (
        filterStringAsByteArray[i] == ParseConstants.LPAREN
          || filterStringAsByteArray[i] == ParseConstants.WHITESPACE
      ) {
        filterNameEndIndex = i;
        break;
      }
    }

    if (filterNameEndIndex == 0) {
      throw new IllegalArgumentException("Incorrect Filter Name");
    }

    byte[] filterName = new byte[filterNameEndIndex - filterNameStartIndex];
    Bytes.putBytes(filterName, 0, filterStringAsByteArray, 0,
      filterNameEndIndex - filterNameStartIndex);
    return filterName;
  }

  /**
   * Returns the arguments of the filter from the filter string
   * <p>
   * @param filterStringAsByteArray filter string given by the user
   * @return an ArrayList containing the arguments of the filter in the filter string
   */
  public static ArrayList<byte[]> getFilterArguments(byte[] filterStringAsByteArray) {
    int argumentListStartIndex = Bytes.searchDelimiterIndex(filterStringAsByteArray, 0,
      filterStringAsByteArray.length, ParseConstants.LPAREN);
    if (argumentListStartIndex == -1) {
      throw new IllegalArgumentException("Incorrect argument list");
    }

    int argumentStartIndex = 0;
    int argumentEndIndex = 0;
    ArrayList<byte[]> filterArguments = new ArrayList<>();

    for (int i = argumentListStartIndex + 1; i < filterStringAsByteArray.length; i++) {

      if (
        filterStringAsByteArray[i] == ParseConstants.WHITESPACE
          || filterStringAsByteArray[i] == ParseConstants.COMMA
          || filterStringAsByteArray[i] == ParseConstants.RPAREN
      ) {
        continue;
      }

      // The argument is in single quotes - for example 'prefix'
      if (filterStringAsByteArray[i] == ParseConstants.SINGLE_QUOTE) {
        argumentStartIndex = i;
        for (int j = argumentStartIndex + 1; j < filterStringAsByteArray.length; j++) {
          if (filterStringAsByteArray[j] == ParseConstants.SINGLE_QUOTE) {
            if (isQuoteUnescaped(filterStringAsByteArray, j)) {
              argumentEndIndex = j;
              i = j + 1;
              byte[] filterArgument = createUnescapdArgument(filterStringAsByteArray,
                argumentStartIndex, argumentEndIndex);
              filterArguments.add(filterArgument);
              break;
            } else {
              // To jump over the second escaped quote
              j++;
            }
          } else if (j == filterStringAsByteArray.length - 1) {
            throw new IllegalArgumentException("Incorrect argument list");
          }
        }
      } else {
        // The argument is an integer, boolean, comparison operator like <, >, != etc
        argumentStartIndex = i;
        for (int j = argumentStartIndex; j < filterStringAsByteArray.length; j++) {
          if (
            filterStringAsByteArray[j] == ParseConstants.WHITESPACE
              || filterStringAsByteArray[j] == ParseConstants.COMMA
              || filterStringAsByteArray[j] == ParseConstants.RPAREN
          ) {
            argumentEndIndex = j - 1;
            i = j;
            byte[] filterArgument = new byte[argumentEndIndex - argumentStartIndex + 1];
            Bytes.putBytes(filterArgument, 0, filterStringAsByteArray, argumentStartIndex,
              argumentEndIndex - argumentStartIndex + 1);
            filterArguments.add(filterArgument);
            break;
          } else if (j == filterStringAsByteArray.length - 1) {
            throw new IllegalArgumentException("Incorrect argument list");
          }
        }
      }
    }
    return filterArguments;
  }

  /**
   * This function is called while parsing the filterString and an operator is parsed
   * <p>
   * @param operatorStack the stack containing the operators and parenthesis
   * @param filterStack   the stack containing the filters
   * @param operator      the operator found while parsing the filterString
   */
  public void reduce(Stack<ByteBuffer> operatorStack, Stack<Filter> filterStack,
    ByteBuffer operator) {
    while (
      !operatorStack.empty() && !ParseConstants.LPAREN_BUFFER.equals(operatorStack.peek())
        && hasHigherPriority(operatorStack.peek(), operator)
    ) {
      filterStack.push(popArguments(operatorStack, filterStack));
    }
  }

  /**
   * Pops an argument from the operator stack and the number of arguments required by the operator
   * from the filterStack and evaluates them
   * <p>
   * @param operatorStack the stack containing the operators
   * @param filterStack   the stack containing the filters
   * @return the evaluated filter
   */
  public static Filter popArguments(Stack<ByteBuffer> operatorStack, Stack<Filter> filterStack) {
    ByteBuffer argumentOnTopOfStack = operatorStack.peek();

    if (argumentOnTopOfStack.equals(ParseConstants.OR_BUFFER)) {
      // The top of the stack is an OR
      try {
        ArrayList<Filter> listOfFilters = new ArrayList<>();
        while (!operatorStack.empty() && operatorStack.peek().equals(ParseConstants.OR_BUFFER)) {
          Filter filter = filterStack.pop();
          listOfFilters.add(0, filter);
          operatorStack.pop();
        }
        Filter filter = filterStack.pop();
        listOfFilters.add(0, filter);
        Filter orFilter = new FilterList(FilterList.Operator.MUST_PASS_ONE, listOfFilters);
        return orFilter;
      } catch (EmptyStackException e) {
        throw new IllegalArgumentException("Incorrect input string - an OR needs two filters");
      }

    } else if (argumentOnTopOfStack.equals(ParseConstants.AND_BUFFER)) {
      // The top of the stack is an AND
      try {
        ArrayList<Filter> listOfFilters = new ArrayList<>();
        while (!operatorStack.empty() && operatorStack.peek().equals(ParseConstants.AND_BUFFER)) {
          Filter filter = filterStack.pop();
          listOfFilters.add(0, filter);
          operatorStack.pop();
        }
        Filter filter = filterStack.pop();
        listOfFilters.add(0, filter);
        Filter andFilter = new FilterList(FilterList.Operator.MUST_PASS_ALL, listOfFilters);
        return andFilter;
      } catch (EmptyStackException e) {
        throw new IllegalArgumentException("Incorrect input string - an AND needs two filters");
      }

    } else if (argumentOnTopOfStack.equals(ParseConstants.SKIP_BUFFER)) {
      // The top of the stack is a SKIP
      try {
        Filter wrappedFilter = filterStack.pop();
        Filter skipFilter = new SkipFilter(wrappedFilter);
        operatorStack.pop();
        return skipFilter;
      } catch (EmptyStackException e) {
        throw new IllegalArgumentException("Incorrect input string - a SKIP wraps a filter");
      }

    } else if (argumentOnTopOfStack.equals(ParseConstants.WHILE_BUFFER)) {
      // The top of the stack is a WHILE
      try {
        Filter wrappedFilter = filterStack.pop();
        Filter whileMatchFilter = new WhileMatchFilter(wrappedFilter);
        operatorStack.pop();
        return whileMatchFilter;
      } catch (EmptyStackException e) {
        throw new IllegalArgumentException("Incorrect input string - a WHILE wraps a filter");
      }

    } else if (argumentOnTopOfStack.equals(ParseConstants.LPAREN_BUFFER)) {
      // The top of the stack is a LPAREN
      try {
        Filter filter = filterStack.pop();
        operatorStack.pop();
        return filter;
      } catch (EmptyStackException e) {
        throw new IllegalArgumentException("Incorrect Filter String");
      }

    } else {
      throw new IllegalArgumentException("Incorrect arguments on operatorStack");
    }
  }

  /**
   * Returns which operator has higher precedence
   * <p>
   * If a has higher precedence than b, it returns true If they have the same precedence, it returns
   * false
   */
  public boolean hasHigherPriority(ByteBuffer a, ByteBuffer b) {
    if ((operatorPrecedenceHashMap.get(a) - operatorPrecedenceHashMap.get(b)) < 0) {
      return true;
    }
    return false;
  }

  /**
   * Removes the single quote escaping a single quote - thus it returns an unescaped argument
   * <p>
   * @param filterStringAsByteArray filter string given by user
   * @param argumentStartIndex      start index of the argument
   * @param argumentEndIndex        end index of the argument
   * @return returns an unescaped argument
   */
  public static byte[] createUnescapdArgument(byte[] filterStringAsByteArray,
    int argumentStartIndex, int argumentEndIndex) {
    int unescapedArgumentLength = 2;
    for (int i = argumentStartIndex + 1; i <= argumentEndIndex - 1; i++) {
      unescapedArgumentLength++;
      if (
        filterStringAsByteArray[i] == ParseConstants.SINGLE_QUOTE && i != (argumentEndIndex - 1)
          && filterStringAsByteArray[i + 1] == ParseConstants.SINGLE_QUOTE
      ) {
        i++;
        continue;
      }
    }

    byte[] unescapedArgument = new byte[unescapedArgumentLength];
    int count = 1;
    unescapedArgument[0] = '\'';
    for (int i = argumentStartIndex + 1; i <= argumentEndIndex - 1; i++) {
      if (
        filterStringAsByteArray[i] == ParseConstants.SINGLE_QUOTE && i != (argumentEndIndex - 1)
          && filterStringAsByteArray[i + 1] == ParseConstants.SINGLE_QUOTE
      ) {
        unescapedArgument[count++] = filterStringAsByteArray[i + 1];
        i++;
      } else {
        unescapedArgument[count++] = filterStringAsByteArray[i];
      }
    }
    unescapedArgument[unescapedArgumentLength - 1] = '\'';
    return unescapedArgument;
  }

  /**
   * Checks if the current index of filter string we are on is the beginning of the keyword 'OR'
   * <p>
   * @param filterStringAsByteArray filter string given by the user
   * @param indexOfOr               index at which an 'O' was read
   * @return true if the keyword 'OR' is at the current index
   */
  public static boolean checkForOr(byte[] filterStringAsByteArray, int indexOfOr)
    throws CharacterCodingException, ArrayIndexOutOfBoundsException {

    try {
      if (
        filterStringAsByteArray[indexOfOr] == ParseConstants.O
          && filterStringAsByteArray[indexOfOr + 1] == ParseConstants.R
          && (filterStringAsByteArray[indexOfOr - 1] == ParseConstants.WHITESPACE
            || filterStringAsByteArray[indexOfOr - 1] == ParseConstants.RPAREN)
          && (filterStringAsByteArray[indexOfOr + 2] == ParseConstants.WHITESPACE
            || filterStringAsByteArray[indexOfOr + 2] == ParseConstants.LPAREN)
      ) {
        return true;
      } else {
        return false;
      }
    } catch (ArrayIndexOutOfBoundsException e) {
      return false;
    }
  }

  /**
   * Checks if the current index of filter string we are on is the beginning of the keyword 'AND'
   * <p>
   * @param filterStringAsByteArray filter string given by the user
   * @param indexOfAnd              index at which an 'A' was read
   * @return true if the keyword 'AND' is at the current index
   */
  public static boolean checkForAnd(byte[] filterStringAsByteArray, int indexOfAnd)
    throws CharacterCodingException {

    try {
      if (
        filterStringAsByteArray[indexOfAnd] == ParseConstants.A
          && filterStringAsByteArray[indexOfAnd + 1] == ParseConstants.N
          && filterStringAsByteArray[indexOfAnd + 2] == ParseConstants.D
          && (filterStringAsByteArray[indexOfAnd - 1] == ParseConstants.WHITESPACE
            || filterStringAsByteArray[indexOfAnd - 1] == ParseConstants.RPAREN)
          && (filterStringAsByteArray[indexOfAnd + 3] == ParseConstants.WHITESPACE
            || filterStringAsByteArray[indexOfAnd + 3] == ParseConstants.LPAREN)
      ) {
        return true;
      } else {
        return false;
      }
    } catch (ArrayIndexOutOfBoundsException e) {
      return false;
    }
  }

  /**
   * Checks if the current index of filter string we are on is the beginning of the keyword 'SKIP'
   * <p>
   * @param filterStringAsByteArray filter string given by the user
   * @param indexOfSkip             index at which an 'S' was read
   * @return true if the keyword 'SKIP' is at the current index
   */
  public static boolean checkForSkip(byte[] filterStringAsByteArray, int indexOfSkip)
    throws CharacterCodingException {

    try {
      if (
        filterStringAsByteArray[indexOfSkip] == ParseConstants.S
          && filterStringAsByteArray[indexOfSkip + 1] == ParseConstants.K
          && filterStringAsByteArray[indexOfSkip + 2] == ParseConstants.I
          && filterStringAsByteArray[indexOfSkip + 3] == ParseConstants.P
          && (indexOfSkip == 0
            || filterStringAsByteArray[indexOfSkip - 1] == ParseConstants.WHITESPACE
            || filterStringAsByteArray[indexOfSkip - 1] == ParseConstants.RPAREN
            || filterStringAsByteArray[indexOfSkip - 1] == ParseConstants.LPAREN)
          && (filterStringAsByteArray[indexOfSkip + 4] == ParseConstants.WHITESPACE
            || filterStringAsByteArray[indexOfSkip + 4] == ParseConstants.LPAREN)
      ) {
        return true;
      } else {
        return false;
      }
    } catch (ArrayIndexOutOfBoundsException e) {
      return false;
    }
  }

  /**
   * Checks if the current index of filter string we are on is the beginning of the keyword 'WHILE'
   * <p>
   * @param filterStringAsByteArray filter string given by the user
   * @param indexOfWhile            index at which an 'W' was read
   * @return true if the keyword 'WHILE' is at the current index
   */
  public static boolean checkForWhile(byte[] filterStringAsByteArray, int indexOfWhile)
    throws CharacterCodingException {

    try {
      if (
        filterStringAsByteArray[indexOfWhile] == ParseConstants.W
          && filterStringAsByteArray[indexOfWhile + 1] == ParseConstants.H
          && filterStringAsByteArray[indexOfWhile + 2] == ParseConstants.I
          && filterStringAsByteArray[indexOfWhile + 3] == ParseConstants.L
          && filterStringAsByteArray[indexOfWhile + 4] == ParseConstants.E
          && (indexOfWhile == 0
            || filterStringAsByteArray[indexOfWhile - 1] == ParseConstants.WHITESPACE
            || filterStringAsByteArray[indexOfWhile - 1] == ParseConstants.RPAREN
            || filterStringAsByteArray[indexOfWhile - 1] == ParseConstants.LPAREN)
          && (filterStringAsByteArray[indexOfWhile + 5] == ParseConstants.WHITESPACE
            || filterStringAsByteArray[indexOfWhile + 5] == ParseConstants.LPAREN)
      ) {
        return true;
      } else {
        return false;
      }
    } catch (ArrayIndexOutOfBoundsException e) {
      return false;
    }
  }

  /**
   * Returns a boolean indicating whether the quote was escaped or not
   * <p>
   * @param array      byte array in which the quote was found
   * @param quoteIndex index of the single quote
   * @return returns true if the quote was unescaped
   */
  public static boolean isQuoteUnescaped(byte[] array, int quoteIndex) {
    if (array == null) {
      throw new IllegalArgumentException("isQuoteUnescaped called with a null array");
    }

    if (quoteIndex == array.length - 1 || array[quoteIndex + 1] != ParseConstants.SINGLE_QUOTE) {
      return true;
    } else {
      return false;
    }
  }

  /**
   * Takes a quoted byte array and converts it into an unquoted byte array For example: given a byte
   * array representing 'abc', it returns a byte array representing abc
   * <p>
   * @param quotedByteArray the quoted byte array
   * @return Unquoted byte array
   */
  public static byte[] removeQuotesFromByteArray(byte[] quotedByteArray) {
    if (
      quotedByteArray == null || quotedByteArray.length < 2
        || quotedByteArray[0] != ParseConstants.SINGLE_QUOTE
        || quotedByteArray[quotedByteArray.length - 1] != ParseConstants.SINGLE_QUOTE
    ) {
      throw new IllegalArgumentException("removeQuotesFromByteArray needs a quoted byte array");
    } else {
      byte[] targetString = new byte[quotedByteArray.length - 2];
      Bytes.putBytes(targetString, 0, quotedByteArray, 1, quotedByteArray.length - 2);
      return targetString;
    }
  }

  /**
   * Converts an int expressed in a byte array to an actual int
   * <p>
   * This doesn't use Bytes.toInt because that assumes that there will be {@link Bytes#SIZEOF_INT}
   * bytes available.
   * <p>
   * @param numberAsByteArray the int value expressed as a byte array
   * @return the int value
   */
  public static int convertByteArrayToInt(byte[] numberAsByteArray) {

    long tempResult = ParseFilter.convertByteArrayToLong(numberAsByteArray);

    if (tempResult > Integer.MAX_VALUE) {
      throw new IllegalArgumentException("Integer Argument too large");
    } else if (tempResult < Integer.MIN_VALUE) {
      throw new IllegalArgumentException("Integer Argument too small");
    }

    int result = (int) tempResult;
    return result;
  }

  /**
   * Converts a long expressed in a byte array to an actual long
   * <p>
   * This doesn't use Bytes.toLong because that assumes that there will be {@link Bytes#SIZEOF_INT}
   * bytes available.
   * <p>
   * @param numberAsByteArray the long value expressed as a byte array
   * @return the long value
   */
  public static long convertByteArrayToLong(byte[] numberAsByteArray) {
    if (numberAsByteArray == null) {
      throw new IllegalArgumentException("convertByteArrayToLong called with a null array");
    }

    int i = 0;
    long result = 0;
    boolean isNegative = false;

    if (numberAsByteArray[i] == ParseConstants.MINUS_SIGN) {
      i++;
      isNegative = true;
    }

    while (i != numberAsByteArray.length) {
      if (
        numberAsByteArray[i] < ParseConstants.ZERO || numberAsByteArray[i] > ParseConstants.NINE
      ) {
        throw new IllegalArgumentException("Byte Array should only contain digits");
      }
      result = result * 10 + (numberAsByteArray[i] - ParseConstants.ZERO);
      if (result < 0) {
        throw new IllegalArgumentException("Long Argument too large");
      }
      i++;
    }

    if (isNegative) {
      return -result;
    } else {
      return result;
    }
  }

  /**
   * Converts a boolean expressed in a byte array to an actual boolean
   * <p>
   * This doesn't used Bytes.toBoolean because Bytes.toBoolean(byte []) assumes that 1 stands for
   * true and 0 for false. Here, the byte array representing "true" and "false" is parsed
   * <p>
   * @param booleanAsByteArray the boolean value expressed as a byte array
   * @return the boolean value
   */
  public static boolean convertByteArrayToBoolean(byte[] booleanAsByteArray) {
    if (booleanAsByteArray == null) {
      throw new IllegalArgumentException("convertByteArrayToBoolean called with a null array");
    }

    if (
      booleanAsByteArray.length == 4
        && (booleanAsByteArray[0] == 't' || booleanAsByteArray[0] == 'T')
        && (booleanAsByteArray[1] == 'r' || booleanAsByteArray[1] == 'R')
        && (booleanAsByteArray[2] == 'u' || booleanAsByteArray[2] == 'U')
        && (booleanAsByteArray[3] == 'e' || booleanAsByteArray[3] == 'E')
    ) {
      return true;
    } else if (
      booleanAsByteArray.length == 5
        && (booleanAsByteArray[0] == 'f' || booleanAsByteArray[0] == 'F')
        && (booleanAsByteArray[1] == 'a' || booleanAsByteArray[1] == 'A')
        && (booleanAsByteArray[2] == 'l' || booleanAsByteArray[2] == 'L')
        && (booleanAsByteArray[3] == 's' || booleanAsByteArray[3] == 'S')
        && (booleanAsByteArray[4] == 'e' || booleanAsByteArray[4] == 'E')
    ) {
      return false;
    } else {
      throw new IllegalArgumentException("Incorrect Boolean Expression");
    }
  }

  /**
   * Takes a compareOperator symbol as a byte array and returns the corresponding CompareOperator
   * @param compareOpAsByteArray the comparatorOperator symbol as a byte array
   * @return the Compare Operator
   */
  public static CompareOperator createCompareOperator(byte[] compareOpAsByteArray) {
    ByteBuffer compareOp = ByteBuffer.wrap(compareOpAsByteArray);
    if (compareOp.equals(ParseConstants.LESS_THAN_BUFFER)) return CompareOperator.LESS;
    else if (compareOp.equals(ParseConstants.LESS_THAN_OR_EQUAL_TO_BUFFER))
      return CompareOperator.LESS_OR_EQUAL;
    else if (compareOp.equals(ParseConstants.GREATER_THAN_BUFFER)) return CompareOperator.GREATER;
    else if (compareOp.equals(ParseConstants.GREATER_THAN_OR_EQUAL_TO_BUFFER))
      return CompareOperator.GREATER_OR_EQUAL;
    else if (compareOp.equals(ParseConstants.NOT_EQUAL_TO_BUFFER)) return CompareOperator.NOT_EQUAL;
    else if (compareOp.equals(ParseConstants.EQUAL_TO_BUFFER)) return CompareOperator.EQUAL;
    else throw new IllegalArgumentException("Invalid compare operator");
  }

  /**
   * Takes a compareOperator symbol as a byte array and returns the corresponding CompareOperator
   * @deprecated Since 2.0
   *             <p>
   * @param compareOpAsByteArray the comparatorOperator symbol as a byte array
   * @return the Compare Operator
   * @deprecated Since 2.0.0. Will be removed in 3.0.0. Use {@link #createCompareOperator(byte [])}
   */
  @Deprecated
  public static CompareFilter.CompareOp createCompareOp(byte[] compareOpAsByteArray) {
    ByteBuffer compareOp = ByteBuffer.wrap(compareOpAsByteArray);
    if (compareOp.equals(ParseConstants.LESS_THAN_BUFFER)) return CompareOp.LESS;
    else if (compareOp.equals(ParseConstants.LESS_THAN_OR_EQUAL_TO_BUFFER))
      return CompareOp.LESS_OR_EQUAL;
    else if (compareOp.equals(ParseConstants.GREATER_THAN_BUFFER)) return CompareOp.GREATER;
    else if (compareOp.equals(ParseConstants.GREATER_THAN_OR_EQUAL_TO_BUFFER))
      return CompareOp.GREATER_OR_EQUAL;
    else if (compareOp.equals(ParseConstants.NOT_EQUAL_TO_BUFFER)) return CompareOp.NOT_EQUAL;
    else if (compareOp.equals(ParseConstants.EQUAL_TO_BUFFER)) return CompareOp.EQUAL;
    else throw new IllegalArgumentException("Invalid compare operator");
  }

  /**
   * Parses a comparator of the form comparatorType:comparatorValue form and returns a comparator
   * <p>
   * @param comparator the comparator in the form comparatorType:comparatorValue
   * @return the parsed comparator
   */
  public static ByteArrayComparable createComparator(byte[] comparator) {
    if (comparator == null) throw new IllegalArgumentException("Incorrect Comparator");
    byte[][] parsedComparator = ParseFilter.parseComparator(comparator);
    byte[] comparatorType = parsedComparator[0];
    byte[] comparatorValue = parsedComparator[1];

    if (Bytes.equals(comparatorType, ParseConstants.binaryType))
      return new BinaryComparator(comparatorValue);
    else if (Bytes.equals(comparatorType, ParseConstants.binaryPrefixType))
      return new BinaryPrefixComparator(comparatorValue);
    else if (Bytes.equals(comparatorType, ParseConstants.regexStringType))
      return new RegexStringComparator(new String(comparatorValue, StandardCharsets.UTF_8));
    else if (Bytes.equals(comparatorType, ParseConstants.regexStringNoCaseType))
      return new RegexStringComparator(new String(comparatorValue, StandardCharsets.UTF_8),
        Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
    else if (Bytes.equals(comparatorType, ParseConstants.substringType))
      return new SubstringComparator(new String(comparatorValue, StandardCharsets.UTF_8));
    else throw new IllegalArgumentException("Incorrect comparatorType");
  }

  /**
   * Splits a column in comparatorType:comparatorValue form into separate byte arrays
   * <p>
   * @param comparator the comparator
   * @return the parsed arguments of the comparator as a 2D byte array
   */
  public static byte[][] parseComparator(byte[] comparator) {
    final int index =
      Bytes.searchDelimiterIndex(comparator, 0, comparator.length, ParseConstants.COLON);
    if (index == -1) {
      throw new IllegalArgumentException("Incorrect comparator");
    }

    byte[][] result = new byte[2][0];
    result[0] = new byte[index];
    System.arraycopy(comparator, 0, result[0], 0, index);

    final int len = comparator.length - (index + 1);
    result[1] = new byte[len];
    System.arraycopy(comparator, index + 1, result[1], 0, len);

    return result;
  }

  /**
   * Return a Set of filters supported by the Filter Language
   */
  public Set<String> getSupportedFilters() {
    return filterHashMap.keySet();
  }

  /**
   * Returns all known filters
   * @return an unmodifiable map of filters
   */
  public static Map<String, String> getAllFilters() {
    return Collections.unmodifiableMap(filterHashMap);
  }

  /**
   * Register a new filter with the parser. If the filter is already registered, an
   * IllegalArgumentException will be thrown.
   * @param name        a name for the filter
   * @param filterClass fully qualified class name
   */
  public static void registerFilter(String name, String filterClass) {
    if (LOG.isInfoEnabled()) LOG.info("Registering new filter " + name);

    filterHashMap.put(name, filterClass);
  }
}
