const Errors = require('../misc/errors');

const State = {
  Normal: 1 /* inside  query */,
  String: 2 /* inside string */,
  SlashStarComment: 3 /* inside slash-star comment */,
  Escape: 4 /* found backslash */,
  EOLComment: 5 /* # comment, or // comment, or -- comment */,
  Backtick: 6 /* found backtick */,
  Placeholder: 7 /* found placeholder */
};

/**
 * Split query according to parameters (question mark).
 * Question mark in comment are not taken in account
 *
 * @returns {Array} query separated by parameters
 */
module.exports.splitQuery = function (sql) {
  let partList = [];
  let state = State.Normal;
  let lastChar = '\0';
  let singleQuotes = false;
  let lastParameterPosition = 0;
  let idx = 0;
  let car = sql.charAt(idx++);

  while (car !== '') {
    if (
      state === State.Escape &&
      !((car === "'" && singleQuotes) || (car === '"' && !singleQuotes))
    ) {
      state = State.String;
      car = sql.charAt(idx++);
      continue;
    }

    switch (car) {
      case '*':
        if (state === State.Normal && lastChar == '/') state = State.SlashStarComment;
        break;

      case '/':
        if (state === State.SlashStarComment && lastChar == '*') state = State.Normal;
        break;

      case '#':
        if (state === State.Normal) state = State.EOLComment;
        break;

      case '-':
        if (state === State.Normal && lastChar == '-') {
          state = State.EOLComment;
        }
        break;

      case '\n':
        if (state === State.EOLComment) {
          state = State.Normal;
        }
        break;

      case '"':
        if (state === State.Normal) {
          state = State.String;
          singleQuotes = false;
        } else if (state === State.String && !singleQuotes) {
          state = State.Normal;
        } else if (state === State.Escape && !singleQuotes) {
          state = State.String;
        }
        break;

      case "'":
        if (state === State.Normal) {
          state = State.String;
          singleQuotes = true;
        } else if (state === State.String && singleQuotes) {
          state = State.Normal;
        } else if (state === State.Escape && singleQuotes) {
          state = State.String;
        }
        break;

      case '\\':
        if (state === State.String) state = State.Escape;
        break;

      case '?':
        if (state === State.Normal) {
          partList.push(sql.substring(lastParameterPosition, idx - 1));
          lastParameterPosition = idx;
        }
        break;
      case '`':
        if (state === State.Backtick) {
          state = State.Normal;
        } else if (state === State.Normal) {
          state = State.Backtick;
        }
        break;
    }
    lastChar = car;

    car = sql.charAt(idx++);
  }
  if (lastParameterPosition === 0) {
    partList.push(sql);
  } else {
    partList.push(sql.substring(lastParameterPosition));
  }

  return partList;
};

/**
 * Split query according to parameters using placeholder.
 *
 * @param sql             sql with placeholders
 * @param info            connection information
 * @param initialValues   placeholder object
 * @param displaySql      display sql function
 * @returns {{parts: Array, values: Array}}
 */
module.exports.splitQueryPlaceholder = function (sql, info, initialValues, displaySql) {
  let partList = [];
  let values = [];
  let state = State.Normal;
  let lastChar = '\0';

  let singleQuotes = false;
  let lastParameterPosition = 0;

  let idx = 0;
  let car = sql.charAt(idx++);
  let placeholderName;

  while (car !== '') {
    if (
      state === State.Escape &&
      !((car === "'" && singleQuotes) || (car === '"' && !singleQuotes))
    ) {
      state = State.String;
      car = sql.charAt(idx++);
      continue;
    }

    switch (car) {
      case '*':
        if (state === State.Normal && lastChar == '/') state = State.SlashStarComment;
        break;

      case '/':
        if (state === State.SlashStarComment && lastChar == '*') state = State.Normal;
        break;

      case '#':
        if (state === State.Normal) state = State.EOLComment;
        break;

      case '-':
        if (state === State.Normal && lastChar == '-') {
          state = State.EOLComment;
        }
        break;

      case '\n':
        if (state === State.EOLComment) {
          state = State.Normal;
        }
        break;

      case '"':
        if (state === State.Normal) {
          state = State.String;
          singleQuotes = false;
        } else if (state === State.String && !singleQuotes) {
          state = State.Normal;
        } else if (state === State.Escape && !singleQuotes) {
          state = State.String;
        }
        break;

      case "'":
        if (state === State.Normal) {
          state = State.String;
          singleQuotes = true;
        } else if (state === State.String && singleQuotes) {
          state = State.Normal;
          singleQuotes = false;
        } else if (state === State.Escape && singleQuotes) {
          state = State.String;
        }
        break;

      case '\\':
        if (state === State.String) state = State.Escape;
        break;

      case ':':
        if (state === State.Normal) {
          partList.push(sql.substring(lastParameterPosition, idx - 1));
          placeholderName = '';
          while (
            ((car = sql.charAt(idx++)) !== '' && car >= '0' && car <= '9') ||
            (car >= 'A' && car <= 'Z') ||
            (car >= 'a' && car <= 'z') ||
            car === '-' ||
            car === '_'
          ) {
            placeholderName += car;
          }
          idx--;
          const val = initialValues[placeholderName];
          if (val === undefined) {
            throw Errors.createError(
              "Placeholder '" + placeholderName + "' is not defined",
              displaySql.call(),
              false,
              info,
              'HY000',
              Errors.ER_PLACEHOLDER_UNDEFINED
            );
          }
          values.push(val);
          lastParameterPosition = idx;
        }
        break;
      case '`':
        if (state === State.Backtick) {
          state = State.Normal;
        } else if (state === State.Normal) {
          state = State.Backtick;
        }
    }
    lastChar = car;

    car = sql.charAt(idx++);
  }
  if (lastParameterPosition === 0) {
    partList.push(sql);
  } else {
    partList.push(sql.substring(lastParameterPosition));
  }

  return { parts: partList, values: values };
};

/**
 * Split query according to parameters (question mark).
 *
 * The only rewritten queries follow these notation: INSERT [LOW_PRIORITY | DELAYED |
 * HIGH_PRIORITY] [IGNORE] [INTO] tbl_name [PARTITION (partition_list)] [(col,...)] {VALUES |
 * VALUE} (...) [ ON DUPLICATE KEY UPDATE col=expr [, col=expr] ... ] With expr without
 * parameter.
 *
 * Query with INSERT ... SELECT / containing LAST_INSERT_ID() will not be rewritten
 *
 * query parts will be split this way :
 * - pre-value part
 * - after value part
 * [- after parameter part] (after each parameter)
 * - ending part
 *
 * example : INSERT INTO MyTABLE VALUES (9, ?, 5, ?, 8) ON DUPLICATE KEY UPDATE col2=col2+10
 * will result in :
 * - pre-value       : "INSERT INTO MyTABLE VALUES"
 * - after value     : " (9, "
 * - after parameter : ", 5, "
 * - after parameter : ", 8)"
 * - ending          : " ON DUPLICATE KEY UPDATE col2=col2+10"
 *
 *
 * @returns {JSON} query separated by parameters
 */
module.exports.splitRewritableQuery = function (sql) {
  let reWritablePrepare = true;
  let multipleQueriesPrepare = true;
  let partList = [];
  let lastChar = '\0';

  let lastParameterPosition = 0;

  let preValuePart1 = null;
  let preValuePart2 = null;
  let postValuePart = null;

  let singleQuotes = false;

  let isInParenthesis = 0;
  let isFirstChar = true;
  let isInsert = false;
  let semicolon = false;
  let hasParam = false;
  let state = State.Normal;

  let idx = 0;
  let car = sql.charAt(idx++);
  while (car !== '') {
    if (
      state === State.Escape &&
      !((car === "'" && singleQuotes) || (car === '"' && !singleQuotes))
    ) {
      state = State.String;
      car = sql.charAt(idx++);
      continue;
    }

    switch (car) {
      case '*':
        if (state === State.Normal && lastChar == '/') {
          state = State.SlashStarComment;
        }
        break;

      case '/':
        if (state === State.SlashStarComment && lastChar == '*') {
          state = State.Normal;
        }
        break;

      case '#':
        if (state === State.Normal) {
          state = State.EOLComment;
        }
        break;

      case '-':
        if (state === State.Normal && lastChar == '-') {
          state = State.EOLComment;
        }
        break;

      case '\n':
        if (state === State.EOLComment) {
          state = State.Normal;
        }
        break;

      case '"':
        if (state === State.Normal) {
          state = State.String;
          singleQuotes = false;
        } else if (state === State.String && !singleQuotes) {
          state = State.Normal;
        } else if (state === State.Escape && !singleQuotes) {
          state = State.String;
        }
        break;
      case ';':
        if (state === State.Normal) {
          semicolon = true;
          multipleQueriesPrepare = false;
        }
        break;
      case "'":
        if (state === State.Normal) {
          state = State.String;
          singleQuotes = true;
        } else if (state === State.String && singleQuotes) {
          state = State.Normal;
        } else if (state === State.Escape && singleQuotes) {
          state = State.String;
        }
        break;

      case '\\':
        if (state === State.String) {
          state = State.Escape;
        }
        break;

      case '?':
        if (state === State.Normal) {
          hasParam = true;
          let part = sql.substring(lastParameterPosition, idx - 1);
          lastParameterPosition = idx;

          if (preValuePart1 === null) {
            preValuePart1 = part;
            preValuePart2 = '';
          } else if (preValuePart2 === null) {
            preValuePart2 = part;
          } else {
            if (postValuePart) {
              //having parameters after the last ")" of value is not rewritable
              reWritablePrepare = false;
              partList.push(postValuePart + part);
              postValuePart = null;
            } else partList.push(part);
          }
        }
        break;
      case '`':
        if (state === State.Backtick) {
          state = State.Normal;
        } else if (state === State.Normal) {
          state = State.Backtick;
        }
        break;

      case 's':
      case 'S':
        if (
          state === State.Normal &&
          postValuePart === null &&
          sql.length > idx + 5 &&
          (sql.charAt(idx) === 'e' || sql.charAt(idx) === 'E') &&
          (sql.charAt(idx + 1) === 'l' || sql.charAt(idx + 1) === 'L') &&
          (sql.charAt(idx + 2) === 'e' || sql.charAt(idx + 2) === 'E') &&
          (sql.charAt(idx + 3) === 'c' || sql.charAt(idx + 3) === 'C') &&
          (sql.charAt(idx + 4) === 't' || sql.charAt(idx + 4) === 'T')
        ) {
          //field/table name might contain 'select'
          if (
            idx > 1 &&
            sql.charAt(idx - 2) > ' ' &&
            '();><=-+,'.indexOf(sql.charAt(idx - 2)) === -1
          ) {
            break;
          }
          if (sql.charAt(idx + 5) > ' ' && '();><=-+,'.indexOf(sql.charAt(idx + 5)) === -1) {
            break;
          }

          //SELECT queries, INSERT FROM SELECT not rewritable
          reWritablePrepare = false;
        }
        break;
      case 'v':
      case 'V':
        if (
          state === State.Normal &&
          !preValuePart1 &&
          (lastChar == ')' || lastChar <= ' ') &&
          sql.length > idx + 6 &&
          (sql.charAt(idx) === 'a' || sql.charAt(idx) === 'A') &&
          (sql.charAt(idx + 1) === 'l' || sql.charAt(idx + 1) === 'L') &&
          (sql.charAt(idx + 2) === 'u' || sql.charAt(idx + 2) === 'U') &&
          (sql.charAt(idx + 3) === 'e' || sql.charAt(idx + 3) === 'E') &&
          (sql.charAt(idx + 4) === 's' || sql.charAt(idx + 4) === 'S') &&
          (sql.charAt(idx + 5) === '(' || sql.charAt(idx + 5) <= ' ')
        ) {
          idx += 5;
          preValuePart1 = sql.substring(lastParameterPosition, idx);
          lastParameterPosition = idx;
        }
        break;
      case 'l':
      case 'L':
        if (
          state === State.Normal &&
          sql.length > idx + 13 &&
          (sql.charAt(idx) === 'a' || sql.charAt(idx) === 'A') &&
          (sql.charAt(idx + 1) === 's' || sql.charAt(idx + 1) === 'S') &&
          (sql.charAt(idx + 2) === 't' || sql.charAt(idx + 2) === 'T') &&
          sql.charAt(idx + 3) === '_' &&
          (sql.charAt(idx + 4) === 'i' || sql.charAt(idx + 4) === 'I') &&
          (sql.charAt(idx + 5) === 'n' || sql.charAt(idx + 5) === 'N') &&
          (sql.charAt(idx + 6) === 's' || sql.charAt(idx + 6) === 'S') &&
          (sql.charAt(idx + 7) === 'e' || sql.charAt(idx + 7) === 'E') &&
          (sql.charAt(idx + 8) === 'r' || sql.charAt(idx + 8) === 'R') &&
          (sql.charAt(idx + 9) === 't' || sql.charAt(idx + 9) === 'T') &&
          sql.charAt(idx + 10) === '_' &&
          (sql.charAt(idx + 11) === 'i' || sql.charAt(idx + 11) === 'I') &&
          (sql.charAt(idx + 12) === 'd' || sql.charAt(idx + 12) === 'D') &&
          sql.charAt(idx + 13) === '('
        ) {
          reWritablePrepare = false;
          idx += 13;
        }
        break;
      case '(':
        if (state === State.Normal) {
          isInParenthesis++;
        }
        break;
      case ')':
        if (state === State.Normal) {
          isInParenthesis--;
          if (isInParenthesis === 0 && preValuePart2 !== null && postValuePart === null) {
            postValuePart = sql.substring(lastParameterPosition, idx);
            lastParameterPosition = idx;
          }
        }
        break;
      default:
        if (state === State.Normal && isFirstChar && car > ' ') {
          if (
            (car === 'I' || car === 'i') &&
            sql.length > idx + 6 &&
            (sql.charAt(idx) === 'n' || sql.charAt(idx) === 'N') &&
            (sql.charAt(idx + 1) === 's' || sql.charAt(idx + 1) === 'S') &&
            (sql.charAt(idx + 2) === 'e' || sql.charAt(idx + 2) === 'E') &&
            (sql.charAt(idx + 3) === 'r' || sql.charAt(idx + 3) === 'R') &&
            (sql.charAt(idx + 4) === 't' || sql.charAt(idx + 4) === 'T') &&
            (sql.charAt(idx + 5) === '(' || sql.charAt(idx + 5) <= ' ')
          ) {
            isInsert = true;
          }
          isFirstChar = false;
        }
        //multiple queries
        if (state === State.Normal && semicolon && car >= ' ') {
          reWritablePrepare = false;
          multipleQueriesPrepare = true;
        }
        break;
    }

    lastChar = car;
    car = sql.charAt(idx++);
  }

  if (state === State.EOLComment) multipleQueriesPrepare = false;

  if (!hasParam) {
    //permit to have rewrite without parameter
    if (preValuePart1 === null) {
      partList.unshift('');
      partList.unshift(sql);
    } else {
      partList.unshift(sql.substring(lastParameterPosition, idx));
      partList.unshift(preValuePart1);
    }
    lastParameterPosition = idx;
  } else {
    partList.unshift(preValuePart2 !== null ? preValuePart2 : '');
    partList.unshift(preValuePart1 !== null ? preValuePart1 : '');
  }

  if (!isInsert) {
    reWritablePrepare = false;
  }

  //postValuePart is the value after the last parameter and parenthesis
  //if no param, don't add to the list.
  if (hasParam) {
    partList.push(postValuePart !== null ? postValuePart : '');
  }
  partList.push(sql.substring(lastParameterPosition, idx));

  return {
    partList: partList,
    reWritable: reWritablePrepare,
    multipleQueries: multipleQueriesPrepare
  };
};

module.exports.searchPlaceholder = function (sql, info, initialValues, displaySql) {
  let sqlPlaceHolder = '';
  const rowNumber = initialValues.length;
  let values = new Array(rowNumber);
  for (let i = 0; i < rowNumber; i++) values[i] = [];
  let state = State.Normal;
  let lastChar = '\0';

  let singleQuotes = false;
  let lastParameterPosition = 0;

  let idx = 0;
  let car = sql.charAt(idx++);
  let placeholderName;

  while (car !== '') {
    if (
      state === State.Escape &&
      !((car === "'" && singleQuotes) || (car === '"' && !singleQuotes))
    ) {
      state = State.String;
      lastChar = car;
      car = sql.charAt(idx++);
      continue;
    }

    switch (car) {
      case '*':
        if (state === State.Normal && lastChar == '/') state = State.SlashStarComment;
        break;

      case '/':
        if (state === State.SlashStarComment && lastChar == '*') state = State.Normal;
        break;

      case '#':
        if (state === State.Normal) state = State.EOLComment;
        break;

      case '-':
        if (state === State.Normal && lastChar == '-') {
          state = State.EOLComment;
        }
        break;

      case '\n':
        if (state === State.EOLComment) {
          state = State.Normal;
        }
        break;

      case '"':
        if (state === State.Normal) {
          state = State.String;
          singleQuotes = false;
        } else if (state === State.String && !singleQuotes) {
          state = State.Normal;
        } else if (state === State.Escape && !singleQuotes) {
          state = State.String;
        }
        break;

      case "'":
        if (state === State.Normal) {
          state = State.String;
          singleQuotes = true;
        } else if (state === State.String && singleQuotes) {
          state = State.Normal;
          singleQuotes = false;
        } else if (state === State.Escape && singleQuotes) {
          state = State.String;
        }
        break;

      case '\\':
        if (state === State.String) state = State.Escape;
        break;

      case ':':
        if (state === State.Normal) {
          sqlPlaceHolder += sql.substring(lastParameterPosition, idx - 1) + '?';
          placeholderName = '';
          while (
            ((car = sql.charAt(idx++)) !== '' && car >= '0' && car <= '9') ||
            (car >= 'A' && car <= 'Z') ||
            (car >= 'a' && car <= 'z') ||
            car === '-' ||
            car === '_'
          ) {
            placeholderName += car;
          }
          idx--;
          for (let i = 0; i < rowNumber; i++) {
            const val = initialValues[i][placeholderName];
            if (val !== undefined) {
              values[i].push(val);
            } else {
              values[i].push(null);
            }
          }
          lastParameterPosition = idx;
        }
        break;
      case '`':
        if (state === State.Backtick) {
          state = State.Normal;
        } else if (state === State.Normal) {
          state = State.Backtick;
        }
    }
    lastChar = car;

    car = sql.charAt(idx++);
  }
  if (lastParameterPosition === 0) {
    sqlPlaceHolder = sql;
  } else {
    sqlPlaceHolder += sql.substring(lastParameterPosition);
  }

  return { sql: sqlPlaceHolder, values: values };
};

/**
 * Split query according to named parameters.
 *
 * The only rewritten queries follow these notation: INSERT [LOW_PRIORITY | DELAYED |
 * HIGH_PRIORITY] [IGNORE] [INTO] tbl_name [PARTITION (partition_list)] [(col,...)] {VALUES |
 * VALUE} (...) [ ON DUPLICATE KEY UPDATE col=expr [, col=expr] ... ] With expr without
 * parameter.
 *
 * Query with INSERT ... SELECT / containing LAST_INSERT_ID() will not be rewritten
 *
 * query parts will be split this way :
 * - pre-value part
 * - after value part
 * [- after parameter part] (after each parameter)
 * - ending part
 *
 * example : INSERT INTO MyTABLE VALUES (9, :param1, 5, :param2, 8) ON DUPLICATE KEY UPDATE col2=col2+10
 * will result in :
 * - pre-value       : "INSERT INTO MyTABLE VALUES"
 * - after value     : " (9, "
 * - after parameter : ", 5, "
 * - after parameter : ", 8)"
 * - ending          : " ON DUPLICATE KEY UPDATE col2=col2+10"
 *
 *
 * @returns {JSON} query separated by parameters
 */
module.exports.splitRewritableNamedParameterQuery = function (sql, initialValues) {
  let reWritablePrepare = true;
  let multipleQueriesPrepare = true;
  let partList = [];
  let values = new Array(initialValues.length);
  for (let i = 0; i < values.length; i++) values[i] = [];
  let lastChar = '\0';

  let lastParameterPosition = 0;

  let preValuePart1 = null;
  let preValuePart2 = null;
  let postValuePart = null;

  let singleQuotes = false;

  let isInParenthesis = 0;
  let isFirstChar = true;
  let isInsert = false;
  let semicolon = false;
  let hasParam = false;
  let placeholderName;
  let state = State.Normal;

  let idx = 0;
  let car = sql.charAt(idx++);
  while (car !== '') {
    if (
      state === State.Escape &&
      !((car === "'" && singleQuotes) || (car === '"' && !singleQuotes))
    ) {
      state = State.String;
      car = sql.charAt(idx++);
      continue;
    }

    switch (car) {
      case '*':
        if (state === State.Normal && lastChar == '/') {
          state = State.SlashStarComment;
        }
        break;

      case '/':
        if (state === State.SlashStarComment && lastChar == '*') {
          state = State.Normal;
        }
        break;

      case '#':
        if (state === State.Normal) {
          state = State.EOLComment;
        }
        break;

      case '-':
        if (state === State.Normal && lastChar == '-') {
          state = State.EOLComment;
        }
        break;

      case '\n':
        if (state === State.EOLComment) {
          state = State.Normal;
        }
        break;

      case '"':
        if (state === State.Normal) {
          state = State.String;
          singleQuotes = false;
        } else if (state === State.String && !singleQuotes) {
          state = State.Normal;
        } else if (state === State.Escape && !singleQuotes) {
          state = State.String;
        }
        break;
      case ';':
        if (state === State.Normal) {
          semicolon = true;
          multipleQueriesPrepare = false;
        }
        break;
      case "'":
        if (state === State.Normal) {
          state = State.String;
          singleQuotes = true;
        } else if (state === State.String && singleQuotes) {
          state = State.Normal;
        } else if (state === State.Escape && singleQuotes) {
          state = State.String;
        }
        break;

      case '\\':
        if (state === State.String) {
          state = State.Escape;
        }
        break;

      case ':':
        if (state === State.Normal) {
          let part = sql.substring(lastParameterPosition, idx - 1);
          placeholderName = '';
          while (
            ((car = sql.charAt(idx++)) !== '' && car >= '0' && car <= '9') ||
            (car >= 'A' && car <= 'Z') ||
            (car >= 'a' && car <= 'z') ||
            car === '-' ||
            car === '_'
          ) {
            placeholderName += car;
          }
          idx--;
          hasParam = true;
          initialValues.forEach((row, idx) => {
            if (row[placeholderName] !== undefined) {
              values[idx].push(row[placeholderName]);
            } else {
              values[idx].push(null);
            }
          });

          lastParameterPosition = idx;

          if (preValuePart1 === null) {
            preValuePart1 = part;
            preValuePart2 = '';
          } else if (preValuePart2 === null) {
            preValuePart2 = part;
          } else {
            if (postValuePart) {
              //having parameters after the last ")" of value is not rewritable
              reWritablePrepare = false;
              partList.push(postValuePart + part);
              postValuePart = null;
            } else partList.push(part);
          }
        }
        break;

      case '`':
        if (state === State.Backtick) {
          state = State.Normal;
        } else if (state === State.Normal) {
          state = State.Backtick;
        }
        break;

      case 's':
      case 'S':
        if (
          state === State.Normal &&
          postValuePart === null &&
          sql.length > idx + 5 &&
          (sql.charAt(idx) === 'e' || sql.charAt(idx) === 'E') &&
          (sql.charAt(idx + 1) === 'l' || sql.charAt(idx + 1) === 'L') &&
          (sql.charAt(idx + 2) === 'e' || sql.charAt(idx + 2) === 'E') &&
          (sql.charAt(idx + 3) === 'c' || sql.charAt(idx + 3) === 'C') &&
          (sql.charAt(idx + 4) === 't' || sql.charAt(idx + 4) === 'T')
        ) {
          //field/table name might contain 'select'
          if (
            idx > 1 &&
            sql.charAt(idx - 2) > ' ' &&
            '();><=-+,'.indexOf(sql.charAt(idx - 2)) === -1
          ) {
            break;
          }
          if (sql.charAt(idx + 5) > ' ' && '();><=-+,'.indexOf(sql.charAt(idx + 5)) === -1) {
            break;
          }

          //SELECT queries, INSERT FROM SELECT not rewritable
          reWritablePrepare = false;
        }
        break;
      case 'v':
      case 'V':
        if (
          state === State.Normal &&
          !preValuePart1 &&
          (lastChar == ')' || lastChar <= ' ') &&
          sql.length > idx + 6 &&
          (sql.charAt(idx) === 'a' || sql.charAt(idx) === 'A') &&
          (sql.charAt(idx + 1) === 'l' || sql.charAt(idx + 1) === 'L') &&
          (sql.charAt(idx + 2) === 'u' || sql.charAt(idx + 2) === 'U') &&
          (sql.charAt(idx + 3) === 'e' || sql.charAt(idx + 3) === 'E') &&
          (sql.charAt(idx + 4) === 's' || sql.charAt(idx + 4) === 'S') &&
          (sql.charAt(idx + 5) === '(' || sql.charAt(idx + 5) <= ' ')
        ) {
          idx += 5;
          preValuePart1 = sql.substring(lastParameterPosition, idx);
          lastParameterPosition = idx;
        }
        break;
      case 'l':
      case 'L':
        if (
          state === State.Normal &&
          sql.length > idx + 13 &&
          (sql.charAt(idx) === 'a' || sql.charAt(idx) === 'A') &&
          (sql.charAt(idx + 1) === 's' || sql.charAt(idx + 1) === 'S') &&
          (sql.charAt(idx + 2) === 't' || sql.charAt(idx + 2) === 'T') &&
          sql.charAt(idx + 3) === '_' &&
          (sql.charAt(idx + 4) === 'i' || sql.charAt(idx + 4) === 'I') &&
          (sql.charAt(idx + 5) === 'n' || sql.charAt(idx + 5) === 'N') &&
          (sql.charAt(idx + 6) === 's' || sql.charAt(idx + 6) === 'S') &&
          (sql.charAt(idx + 7) === 'e' || sql.charAt(idx + 7) === 'E') &&
          (sql.charAt(idx + 8) === 'r' || sql.charAt(idx + 8) === 'R') &&
          (sql.charAt(idx + 9) === 't' || sql.charAt(idx + 9) === 'T') &&
          sql.charAt(idx + 10) === '_' &&
          (sql.charAt(idx + 11) === 'i' || sql.charAt(idx + 11) === 'I') &&
          (sql.charAt(idx + 12) === 'd' || sql.charAt(idx + 12) === 'D') &&
          sql.charAt(idx + 13) === '('
        ) {
          reWritablePrepare = false;
          idx += 13;
        }
        break;
      case '(':
        if (state === State.Normal) {
          isInParenthesis++;
        }
        break;
      case ')':
        if (state === State.Normal) {
          isInParenthesis--;
          if (isInParenthesis === 0 && preValuePart2 !== null && postValuePart === null) {
            postValuePart = sql.substring(lastParameterPosition, idx);
            lastParameterPosition = idx;
          }
        }
        break;
      default:
        if (state === State.Normal && isFirstChar && car > ' ') {
          if (
            (car === 'I' || car === 'i') &&
            sql.length > idx + 6 &&
            (sql.charAt(idx) === 'n' || sql.charAt(idx) === 'N') &&
            (sql.charAt(idx + 1) === 's' || sql.charAt(idx + 1) === 'S') &&
            (sql.charAt(idx + 2) === 'e' || sql.charAt(idx + 2) === 'E') &&
            (sql.charAt(idx + 3) === 'r' || sql.charAt(idx + 3) === 'R') &&
            (sql.charAt(idx + 4) === 't' || sql.charAt(idx + 4) === 'T') &&
            (sql.charAt(idx + 5) === '(' || sql.charAt(idx + 5) <= ' ')
          ) {
            isInsert = true;
          }
          isFirstChar = false;
        }
        //multiple queries
        if (state === State.Normal && semicolon && car >= ' ') {
          reWritablePrepare = false;
          multipleQueriesPrepare = true;
        }
        break;
    }

    lastChar = car;
    car = sql.charAt(idx++);
  }

  if (state === State.EOLComment) multipleQueriesPrepare = false;

  if (!hasParam) {
    //permit to have rewrite without parameter
    if (preValuePart1 === null) {
      partList.unshift('');
      partList.unshift(sql);
    } else {
      partList.unshift(sql.substring(lastParameterPosition, idx));
      partList.unshift(preValuePart1);
    }
    lastParameterPosition = idx;
  } else {
    partList.unshift(preValuePart2 !== null ? preValuePart2 : '');
    partList.unshift(preValuePart1 !== null ? preValuePart1 : '');
  }

  if (!isInsert) {
    reWritablePrepare = false;
  }

  //postValuePart is the value after the last parameter and parenthesis
  //if no param, don't add to the list.
  if (hasParam) {
    partList.push(postValuePart !== null ? postValuePart : '');
  }
  partList.push(sql.substring(lastParameterPosition, idx));

  return {
    partList: partList,
    reWritable: reWritablePrepare,
    multipleQueries: multipleQueriesPrepare,
    values: values
  };
};

/**
 * Ensure that filename requested by server corresponds to query
 * protocol : https://mariadb.com/kb/en/library/local_infile-packet/
 *
 * @param sql         query
 * @param parameters  parameters if any
 * @param fileName    server requested file
 * @returns {boolean} is filename corresponding to query
 */
module.exports.validateFileName = function (sql, parameters, fileName) {
  let queryValidator = new RegExp(
    "^(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*\\s*LOAD\\s+DATA\\s+((LOW_PRIORITY|CONCURRENT)\\s+)?LOCAL\\s+INFILE\\s+'" +
      fileName +
      "'",
    'i'
  );
  if (queryValidator.test(sql)) return true;

  if (parameters != null) {
    queryValidator = new RegExp(
      '^(\\s*\\/\\*([^\\*]|\\*[^\\/])*\\*\\/)*\\s*LOAD\\s+DATA\\s+((LOW_PRIORITY|CONCURRENT)\\s+)?LOCAL\\s+INFILE\\s+\\?',
      'i'
    );
    if (queryValidator.test(sql) && parameters.length > 0) {
      return parameters[0].toLowerCase() === fileName.toLowerCase();
    }
  }
  return false;
};
