/*
 * 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.lucene.queryparser.flexible.standard.parser;

import java.util.Locale;
import org.apache.lucene.queryparser.flexible.core.messages.QueryParserMessages;
import org.apache.lucene.queryparser.flexible.core.parser.EscapeQuerySyntax;
import org.apache.lucene.queryparser.flexible.core.util.UnescapedCharSequence;
import org.apache.lucene.queryparser.flexible.messages.MessageImpl;

/** Implementation of {@link EscapeQuerySyntax} for the standard lucene syntax. */
public class EscapeQuerySyntaxImpl implements EscapeQuerySyntax {

  private static final char[] wildcardChars = {'*', '?'};

  private static final String[] escapableTermExtraFirstChars = {"+", "-", "@"};

  private static final String[] escapableTermChars = {
    "\"", "<", ">", "=", "!", "(", ")", "^", "[", "{", ":", "]", "}", "~", "/"
  };

  // TODO: check what to do with these "*", "?", "\\"
  private static final String[] escapableQuotedChars = {"\""};
  private static final String[] escapableWhiteChars = {" ", "\t", "\n", "\r", "\f", "\b", "\u3000"};
  private static final String[] escapableWordTokens = {
    "AND", "OR", "NOT", "TO", "WITHIN", "SENTENCE", "PARAGRAPH", "INORDER"
  };

  private static final CharSequence escapeChar(CharSequence str, Locale locale) {
    if (str == null || str.length() == 0) return str;

    CharSequence buffer = str;

    // regular escapable Char for terms
    for (int i = 0; i < escapableTermChars.length; i++) {
      buffer = replaceIgnoreCase(buffer, escapableTermChars[i].toLowerCase(locale), "\\", locale);
    }

    // First Character of a term as more escaping chars
    for (int i = 0; i < escapableTermExtraFirstChars.length; i++) {
      if (buffer.charAt(0) == escapableTermExtraFirstChars[i].charAt(0)) {
        buffer = "\\" + buffer.charAt(0) + buffer.subSequence(1, buffer.length());
        break;
      }
    }

    return buffer;
  }

  private final CharSequence escapeQuoted(CharSequence str, Locale locale) {
    if (str == null || str.length() == 0) return str;

    CharSequence buffer = str;

    for (int i = 0; i < escapableQuotedChars.length; i++) {
      buffer = replaceIgnoreCase(buffer, escapableTermChars[i].toLowerCase(locale), "\\", locale);
    }
    return buffer;
  }

  private static final CharSequence escapeTerm(CharSequence term, Locale locale) {
    if (term == null) return term;

    // Escape single Chars
    term = escapeChar(term, locale);
    term = escapeWhiteChar(term, locale);

    // Escape Parser Words
    for (int i = 0; i < escapableWordTokens.length; i++) {
      if (escapableWordTokens[i].equalsIgnoreCase(term.toString())) return "\\" + term;
    }
    return term;
  }

  /**
   * replace with ignore case
   *
   * @param string string to get replaced
   * @param sequence1 the old character sequence in lowercase
   * @param escapeChar the new character to prefix sequence1 in return string.
   * @return the new String
   */
  private static CharSequence replaceIgnoreCase(
      CharSequence string, CharSequence sequence1, CharSequence escapeChar, Locale locale) {
    if (escapeChar == null || sequence1 == null || string == null) throw new NullPointerException();

    // empty string case
    int count = string.length();
    int sequence1Length = sequence1.length();
    if (sequence1Length == 0) {
      StringBuilder result = new StringBuilder((count + 1) * escapeChar.length());
      result.append(escapeChar);
      for (int i = 0; i < count; i++) {
        result.append(string.charAt(i));
        result.append(escapeChar);
      }
      return result.toString();
    }

    // normal case
    StringBuilder result = new StringBuilder();
    char first = sequence1.charAt(0);
    int start = 0, copyStart = 0, firstIndex;
    while (start < count) {
      if ((firstIndex = string.toString().toLowerCase(locale).indexOf(first, start)) == -1) break;
      boolean found = true;
      if (sequence1.length() > 1) {
        if (firstIndex + sequence1Length > count) break;
        for (int i = 1; i < sequence1Length; i++) {
          if (string.toString().toLowerCase(locale).charAt(firstIndex + i) != sequence1.charAt(i)) {
            found = false;
            break;
          }
        }
      }
      if (found) {
        result.append(string.toString().substring(copyStart, firstIndex));
        result.append(escapeChar);
        result.append(string.toString().substring(firstIndex, firstIndex + sequence1Length));
        copyStart = start = firstIndex + sequence1Length;
      } else {
        start = firstIndex + 1;
      }
    }
    if (result.length() == 0 && copyStart == 0) return string;
    result.append(string.toString().substring(copyStart));
    return result.toString();
  }

  /**
   * escape all tokens that are part of the parser syntax on a given string
   *
   * @param str string to get replaced
   * @param locale locale to be used when performing string compares
   * @return the new String
   */
  private static final CharSequence escapeWhiteChar(CharSequence str, Locale locale) {
    if (str == null || str.length() == 0) return str;

    CharSequence buffer = str;

    for (int i = 0; i < escapableWhiteChars.length; i++) {
      buffer = replaceIgnoreCase(buffer, escapableWhiteChars[i].toLowerCase(locale), "\\", locale);
    }
    return buffer;
  }

  @Override
  public CharSequence escape(CharSequence text, Locale locale, Type type) {
    if (text == null || text.length() == 0) return text;

    // escape wildcards and the escape char (this has to be perform before
    // anything else)
    // since we need to preserve the UnescapedCharSequence and escape the
    // original escape chars
    if (text instanceof UnescapedCharSequence) {
      text = ((UnescapedCharSequence) text).toStringEscaped(wildcardChars);
    } else {
      text = new UnescapedCharSequence(text).toStringEscaped(wildcardChars);
    }

    if (type == Type.STRING) {
      return escapeQuoted(text, locale);
    } else {
      return escapeTerm(text, locale);
    }
  }

  /**
   * Returns a String where the escape char has been removed, or kept only once if there was a
   * double escape.
   *
   * <p>Supports escaped unicode characters, e. g. translates <code>A</code> to <code>A</code>.
   */
  public static UnescapedCharSequence discardEscapeChar(CharSequence input) throws ParseException {
    // Create char array to hold unescaped char sequence
    char[] output = new char[input.length()];
    boolean[] wasEscaped = new boolean[input.length()];

    // The length of the output can be less than the input
    // due to discarded escape chars. This variable holds
    // the actual length of the output
    int length = 0;

    // We remember whether the last processed character was
    // an escape character
    boolean lastCharWasEscapeChar = false;

    // The multiplier the current unicode digit must be multiplied with.
    // E. g. the first digit must be multiplied with 16^3, the second with
    // 16^2...
    int codePointMultiplier = 0;

    // Used to calculate the codepoint of the escaped unicode character
    int codePoint = 0;

    for (int i = 0; i < input.length(); i++) {
      char curChar = input.charAt(i);
      if (codePointMultiplier > 0) {
        codePoint += hexToInt(curChar) * codePointMultiplier;
        codePointMultiplier >>>= 4;
        if (codePointMultiplier == 0) {
          output[length++] = (char) codePoint;
          codePoint = 0;
        }
      } else if (lastCharWasEscapeChar) {
        if (curChar == 'u') {
          // found an escaped unicode character
          codePointMultiplier = 16 * 16 * 16;
        } else {
          // this character was escaped
          output[length] = curChar;
          wasEscaped[length] = true;
          length++;
        }
        lastCharWasEscapeChar = false;
      } else {
        if (curChar == '\\') {
          lastCharWasEscapeChar = true;
        } else {
          output[length] = curChar;
          length++;
        }
      }
    }

    if (codePointMultiplier > 0) {
      throw new ParseException(
          new MessageImpl(QueryParserMessages.INVALID_SYNTAX_ESCAPE_UNICODE_TRUNCATION));
    }

    if (lastCharWasEscapeChar) {
      throw new ParseException(
          new MessageImpl(QueryParserMessages.INVALID_SYNTAX_ESCAPE_CHARACTER));
    }

    return new UnescapedCharSequence(output, wasEscaped, 0, length);
  }

  /** Returns the numeric value of the hexadecimal character */
  private static final int hexToInt(char c) throws ParseException {
    if ('0' <= c && c <= '9') {
      return c - '0';
    } else if ('a' <= c && c <= 'f') {
      return c - 'a' + 10;
    } else if ('A' <= c && c <= 'F') {
      return c - 'A' + 10;
    } else {
      throw new ParseException(
          new MessageImpl(QueryParserMessages.INVALID_SYNTAX_ESCAPE_NONE_HEX_UNICODE, c));
    }
  }
}
