package org.seasar.doma.jdbc.dialect;

import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.seasar.doma.DomaNullPointerException;
import org.seasar.doma.expr.ExpressionFunctions;
import org.seasar.doma.internal.jdbc.dialect.StandardCountGettingTransformer;
import org.seasar.doma.internal.jdbc.dialect.StandardForUpdateTransformer;
import org.seasar.doma.internal.jdbc.dialect.StandardPagingTransformer;
import org.seasar.doma.internal.jdbc.sql.PreparedSqlBuilder;
import org.seasar.doma.internal.util.AssertionUtil;
import org.seasar.doma.internal.util.CharSequenceUtil;
import org.seasar.doma.jdbc.JdbcException;
import org.seasar.doma.jdbc.JdbcMappingFunction;
import org.seasar.doma.jdbc.JdbcMappingHint;
import org.seasar.doma.jdbc.JdbcMappingVisitor;
import org.seasar.doma.jdbc.JdbcUnsupportedOperationException;
import org.seasar.doma.jdbc.PreparedSql;
import org.seasar.doma.jdbc.ScriptBlockContext;
import org.seasar.doma.jdbc.SelectForUpdateType;
import org.seasar.doma.jdbc.SelectOptions;
import org.seasar.doma.jdbc.SelectOptionsAccessor;
import org.seasar.doma.jdbc.Sql;
import org.seasar.doma.jdbc.SqlLogFormatter;
import org.seasar.doma.jdbc.SqlLogFormattingFunction;
import org.seasar.doma.jdbc.SqlLogFormattingVisitor;
import org.seasar.doma.jdbc.SqlNode;
import org.seasar.doma.jdbc.criteria.metamodel.PropertyMetamodel;
import org.seasar.doma.jdbc.criteria.option.ForUpdateOption;
import org.seasar.doma.jdbc.criteria.query.AliasManager;
import org.seasar.doma.jdbc.criteria.query.CriteriaBuilder;
import org.seasar.doma.jdbc.id.AutoGeneratedKeysType;
import org.seasar.doma.jdbc.type.EnumType;
import org.seasar.doma.jdbc.type.JdbcType;
import org.seasar.doma.jdbc.type.JdbcTypes;
import org.seasar.doma.message.Message;
import org.seasar.doma.wrapper.ArrayWrapper;
import org.seasar.doma.wrapper.BigDecimalWrapper;
import org.seasar.doma.wrapper.BigIntegerWrapper;
import org.seasar.doma.wrapper.BlobWrapper;
import org.seasar.doma.wrapper.BooleanWrapper;
import org.seasar.doma.wrapper.ByteWrapper;
import org.seasar.doma.wrapper.BytesWrapper;
import org.seasar.doma.wrapper.ClobWrapper;
import org.seasar.doma.wrapper.DateWrapper;
import org.seasar.doma.wrapper.DoubleWrapper;
import org.seasar.doma.wrapper.EnumWrapper;
import org.seasar.doma.wrapper.FloatWrapper;
import org.seasar.doma.wrapper.IntegerWrapper;
import org.seasar.doma.wrapper.LocalDateTimeWrapper;
import org.seasar.doma.wrapper.LocalDateWrapper;
import org.seasar.doma.wrapper.LocalTimeWrapper;
import org.seasar.doma.wrapper.LongWrapper;
import org.seasar.doma.wrapper.NClobWrapper;
import org.seasar.doma.wrapper.ObjectWrapper;
import org.seasar.doma.wrapper.SQLXMLWrapper;
import org.seasar.doma.wrapper.ShortWrapper;
import org.seasar.doma.wrapper.StringWrapper;
import org.seasar.doma.wrapper.TimeWrapper;
import org.seasar.doma.wrapper.TimestampWrapper;
import org.seasar.doma.wrapper.UtilDateWrapper;
import org.seasar.doma.wrapper.Wrapper;

/** A standard implementation of {@link Dialect}. */
public class StandardDialect implements Dialect {

  /** the quotation mark of the start */
  protected static final char OPEN_QUOTE = '"';

  /** the quotation mark of the end */
  protected static final char CLOSE_QUOTE = '"';

  /** the set of {@literal SQLState} code that represents unique violation */
  protected static final Set<String> UNIQUE_CONSTRAINT_VIOLATION_STATE_CODES =
      new HashSet<String>(Arrays.asList("23", "27", "44"));

  private static final CriteriaBuilder CRITERIA_SQL_BUILDER = new StandardCriteriaBuilder();

  /** the visitor that maps {@link Wrapper} to {@link JdbcType} */
  protected final JdbcMappingVisitor jdbcMappingVisitor;

  /** the visitor that maps {@link Wrapper} to {@link SqlLogFormatter} */
  protected final SqlLogFormattingVisitor sqlLogFormattingVisitor;

  /** the aggregation of expression functions */
  protected final ExpressionFunctions expressionFunctions;

  public StandardDialect() {
    this(
        new StandardJdbcMappingVisitor(),
        new StandardSqlLogFormattingVisitor(),
        new StandardExpressionFunctions());
  }

  public StandardDialect(JdbcMappingVisitor jdbcMappingVisitor) {
    this(
        jdbcMappingVisitor,
        new StandardSqlLogFormattingVisitor(),
        new StandardExpressionFunctions());
  }

  public StandardDialect(SqlLogFormattingVisitor sqlLogFormattingVisitor) {
    this(
        new StandardJdbcMappingVisitor(),
        sqlLogFormattingVisitor,
        new StandardExpressionFunctions());
  }

  public StandardDialect(ExpressionFunctions expressionFunctions) {
    this(
        new StandardJdbcMappingVisitor(),
        new StandardSqlLogFormattingVisitor(),
        expressionFunctions);
  }

  public StandardDialect(
      JdbcMappingVisitor jdbcMappingVisitor, SqlLogFormattingVisitor sqlLogFormattingVisitor) {
    this(jdbcMappingVisitor, sqlLogFormattingVisitor, new StandardExpressionFunctions());
  }

  public StandardDialect(
      JdbcMappingVisitor jdbcMappingVisitor,
      SqlLogFormattingVisitor sqlLogFormattingVisitor,
      ExpressionFunctions expressionFunctions) {
    if (jdbcMappingVisitor == null) {
      throw new DomaNullPointerException("jdbcMappingVisitor");
    }
    if (sqlLogFormattingVisitor == null) {
      throw new DomaNullPointerException("sqlLogFormattingVisitor");
    }
    if (expressionFunctions == null) {
      throw new DomaNullPointerException("expressionFunctions");
    }
    this.jdbcMappingVisitor = jdbcMappingVisitor;
    this.sqlLogFormattingVisitor = sqlLogFormattingVisitor;
    this.expressionFunctions = expressionFunctions;
  }

  @Override
  public String getName() {
    return "standard";
  }

  @Override
  public SqlNode transformSelectSqlNode(SqlNode sqlNode, SelectOptions options) {
    if (sqlNode == null) {
      throw new DomaNullPointerException("sqlNode");
    }
    if (options == null) {
      throw new DomaNullPointerException("options");
    }
    SqlNode transformed = sqlNode;
    if (SelectOptionsAccessor.isCount(options)) {
      transformed = toCountCalculatingSqlNode(sqlNode);
    }
    long offset = SelectOptionsAccessor.getOffset(options);
    long limit = SelectOptionsAccessor.getLimit(options);
    if (offset >= 0 || limit >= 0) {
      transformed = toPagingSqlNode(transformed, offset, limit);
    }
    SelectForUpdateType forUpdateType = SelectOptionsAccessor.getForUpdateType(options);
    if (forUpdateType != null) {
      String[] aliases = SelectOptionsAccessor.getAliases(options);
      if (!supportsSelectForUpdate(forUpdateType, false)) {
        switch (forUpdateType) {
          case NORMAL:
            throw new JdbcException(Message.DOMA2023, getName());
          case WAIT:
            throw new JdbcException(Message.DOMA2079, getName());
          case NOWAIT:
            throw new JdbcException(Message.DOMA2080, getName());
          default:
            AssertionUtil.assertUnreachable();
        }
      }
      if (aliases.length > 0) {
        if (!supportsSelectForUpdate(forUpdateType, true)) {
          switch (forUpdateType) {
            case NORMAL:
              throw new JdbcException(Message.DOMA2024, getName());
            case WAIT:
              throw new JdbcException(Message.DOMA2081, getName());
            case NOWAIT:
              throw new JdbcException(Message.DOMA2082, getName());
            default:
              AssertionUtil.assertUnreachable();
          }
        }
      }
      int waitSeconds = SelectOptionsAccessor.getWaitSeconds(options);
      transformed = toForUpdateSqlNode(transformed, forUpdateType, waitSeconds, aliases);
    }
    return transformed;
  }

  protected SqlNode toCountCalculatingSqlNode(SqlNode sqlNode) {
    return sqlNode;
  }

  protected SqlNode toPagingSqlNode(SqlNode sqlNode, long offset, long limit) {
    StandardPagingTransformer transformer = new StandardPagingTransformer(offset, limit);
    return transformer.transform(sqlNode);
  }

  protected SqlNode toForUpdateSqlNode(
      SqlNode sqlNode, SelectForUpdateType forUpdateType, int waitSeconds, String... aliases) {
    StandardForUpdateTransformer transformer =
        new StandardForUpdateTransformer(forUpdateType, waitSeconds, aliases);
    return transformer.transform(sqlNode);
  }

  @Override
  public SqlNode transformSelectSqlNodeForGettingCount(SqlNode sqlNode) {
    if (sqlNode == null) {
      throw new DomaNullPointerException("sqlNode");
    }
    return toCountGettingSqlNode(sqlNode);
  }

  protected SqlNode toCountGettingSqlNode(SqlNode sqlNode) {
    StandardCountGettingTransformer transformer = new StandardCountGettingTransformer();
    return transformer.transform(sqlNode);
  }

  @Override
  public boolean isUniqueConstraintViolated(SQLException sqlException) {
    if (sqlException == null) {
      throw new DomaNullPointerException("sqlException");
    }
    String state = getSQLState(sqlException);
    if (state != null && state.length() >= 2) {
      String code = state.substring(0, 2);
      return UNIQUE_CONSTRAINT_VIOLATION_STATE_CODES.contains(code);
    }
    return false;
  }

  protected String getSQLState(SQLException sqlException) {
    SQLException cause = getCauseSQLException(sqlException);
    return cause.getSQLState();
  }

  protected int getErrorCode(SQLException sqlException) {
    SQLException cause = getCauseSQLException(sqlException);
    return cause.getErrorCode();
  }

  protected SQLException getCauseSQLException(SQLException sqlException) {
    SQLException cause = sqlException;
    for (Throwable t : sqlException) {
      if (t instanceof SQLException) {
        cause = (SQLException) t;
      }
    }
    return cause;
  }

  @Override
  public Throwable getRootCause(SQLException sqlException) {
    if (sqlException == null) {
      throw new DomaNullPointerException("sqlException");
    }
    Throwable cause = sqlException;
    for (Throwable t : sqlException) {
      cause = t;
    }
    return cause;
  }

  @Override
  public boolean supportsAutoGeneratedKeys() {
    return false;
  }

  @Override
  public boolean supportsBatchUpdateResults() {
    return true;
  }

  @Override
  public boolean supportsIdentity() {
    return false;
  }

  @Override
  public boolean supportsSequence() {
    return false;
  }

  @Override
  public boolean supportsIdentityReservation() {
    return false;
  }

  @Override
  public boolean includesIdentityColumn() {
    return false;
  }

  @Override
  public boolean supportsSelectForUpdate(SelectForUpdateType type, boolean withTargets) {
    return false;
  }

  @Override
  public boolean supportsResultSetReturningAsOutParameter() {
    return false;
  }

  @Override
  public JdbcType<ResultSet> getResultSetType() {
    throw new JdbcUnsupportedOperationException(getClass().getName(), "getResultSetType");
  }

  @Override
  public Sql<?> getIdentitySelectSql(
      String catalogName,
      String schemaName,
      String tableName,
      String columnName,
      boolean isQuoteRequired,
      boolean isIdColumnQuoteRequired) {
    throw new JdbcUnsupportedOperationException(getClass().getName(), "getIdentitySelectSql");
  }

  @Override
  public Sql<?> getIdentityReservationSql(
      String catalogName,
      String schemaName,
      String tableName,
      String columnName,
      boolean isQuoteRequired,
      boolean isIdColumnQuoteRequired,
      int reservationSize) {
    throw new JdbcUnsupportedOperationException(getClass().getName(), "getIdentityReservationSql");
  }

  @Override
  public PreparedSql getSequenceNextValSql(String qualifiedSequenceName, long allocationSize) {
    throw new JdbcUnsupportedOperationException(getClass().getName(), "getSequenceNextValString");
  }

  @Override
  public String applyQuote(String name) {
    return OPEN_QUOTE + name + CLOSE_QUOTE;
  }

  @Override
  public String removeQuote(String name) {
    if (name == null || name.length() <= 2) {
      return name;
    }
    char[] chars = name.toCharArray();
    if (chars[0] == OPEN_QUOTE && chars[chars.length - 1] == CLOSE_QUOTE) {
      return new String(chars, 1, chars.length - 2);
    }
    return name;
  }

  @Override
  public JdbcMappingVisitor getJdbcMappingVisitor() {
    return jdbcMappingVisitor;
  }

  @Override
  public SqlLogFormattingVisitor getSqlLogFormattingVisitor() {
    return sqlLogFormattingVisitor;
  }

  @Override
  public ExpressionFunctions getExpressionFunctions() {
    return expressionFunctions;
  }

  @Override
  public ScriptBlockContext createScriptBlockContext() {
    return new StandardScriptBlockContext();
  }

  @Override
  public String getScriptBlockDelimiter() {
    return null;
  }

  @Override
  public AutoGeneratedKeysType getAutoGeneratedKeysType() {
    return AutoGeneratedKeysType.DEFAULT;
  }

  @Override
  public CriteriaBuilder getCriteriaBuilder() {
    return CRITERIA_SQL_BUILDER;
  }

  public static class StandardJdbcMappingVisitor implements JdbcMappingVisitor {

    @Override
    public Void visitArrayWrapper(ArrayWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.ARRAY);
    }

    @Override
    public Void visitBigDecimalWrapper(
        BigDecimalWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q) throws SQLException {
      return p.apply(wrapper, JdbcTypes.BIG_DECIMAL);
    }

    @Override
    public Void visitBigIntegerWrapper(
        BigIntegerWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q) throws SQLException {
      return p.apply(wrapper, JdbcTypes.BIG_INTEGER);
    }

    @Override
    public Void visitBlobWrapper(BlobWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.BLOB);
    }

    @Override
    public Void visitBooleanWrapper(
        BooleanWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q) throws SQLException {
      return p.apply(wrapper, JdbcTypes.BOOLEAN);
    }

    @Override
    public Void visitByteWrapper(ByteWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.BYTE);
    }

    @Override
    public Void visitBytesWrapper(BytesWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.BYTES);
    }

    @Override
    public Void visitClobWrapper(ClobWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.CLOB);
    }

    @Override
    public Void visitDateWrapper(DateWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.DATE);
    }

    @Override
    public Void visitDoubleWrapper(DoubleWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.DOUBLE);
    }

    @Override
    public Void visitFloatWrapper(FloatWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.FLOAT);
    }

    @Override
    public Void visitIntegerWrapper(
        IntegerWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q) throws SQLException {
      return p.apply(wrapper, JdbcTypes.INTEGER);
    }

    @Override
    public Void visitLocalDateWrapper(
        LocalDateWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q) throws SQLException {
      return p.apply(wrapper, JdbcTypes.LOCAL_DATE);
    }

    @Override
    public Void visitLocalDateTimeWrapper(
        LocalDateTimeWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.LOCAL_DATE_TIME);
    }

    @Override
    public Void visitLocalTimeWrapper(
        LocalTimeWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q) throws SQLException {
      return p.apply(wrapper, JdbcTypes.LOCAL_TIME);
    }

    @Override
    public Void visitLongWrapper(LongWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.LONG);
    }

    @Override
    public Void visitNClobWrapper(NClobWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.NCLOB);
    }

    @Override
    public Void visitShortWrapper(ShortWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.SHORT);
    }

    @Override
    public Void visitStringWrapper(StringWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.STRING);
    }

    @Override
    public Void visitSQLXMLWrapper(SQLXMLWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.SQLXML);
    }

    @Override
    public Void visitTimeWrapper(TimeWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.TIME);
    }

    @Override
    public Void visitTimestampWrapper(
        TimestampWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q) throws SQLException {
      return p.apply(wrapper, JdbcTypes.TIMESTAMP);
    }

    @Override
    public <E extends Enum<E>> Void visitEnumWrapper(
        EnumWrapper<E> wrapper, JdbcMappingFunction p, JdbcMappingHint q) throws SQLException {
      return p.apply(wrapper, new EnumType<E>(wrapper.getBasicClass()));
    }

    @Override
    public Void visitUtilDateWrapper(
        UtilDateWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q) throws SQLException {
      return p.apply(wrapper, JdbcTypes.UTIL_DATE);
    }

    @Override
    public Void visitObjectWrapper(ObjectWrapper wrapper, JdbcMappingFunction p, JdbcMappingHint q)
        throws SQLException {
      return p.apply(wrapper, JdbcTypes.OBJECT);
    }
  }

  public static class StandardSqlLogFormattingVisitor implements SqlLogFormattingVisitor {

    @Override
    public String visitArrayWrapper(ArrayWrapper wrapper, SqlLogFormattingFunction p, Void q)
        throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.ARRAY);
    }

    @Override
    public String visitBigDecimalWrapper(
        BigDecimalWrapper wrapper, SqlLogFormattingFunction p, Void q) {
      return p.apply(wrapper, JdbcTypes.BIG_DECIMAL);
    }

    @Override
    public String visitBigIntegerWrapper(
        BigIntegerWrapper wrapper, SqlLogFormattingFunction p, Void q) throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.BIG_INTEGER);
    }

    @Override
    public String visitBlobWrapper(BlobWrapper wrapper, SqlLogFormattingFunction p, Void q)
        throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.BLOB);
    }

    @Override
    public String visitBooleanWrapper(BooleanWrapper wrapper, SqlLogFormattingFunction p, Void q)
        throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.BOOLEAN);
    }

    @Override
    public String visitByteWrapper(ByteWrapper wrapper, SqlLogFormattingFunction p, Void q)
        throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.BYTE);
    }

    @Override
    public String visitBytesWrapper(BytesWrapper wrapper, SqlLogFormattingFunction p, Void q)
        throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.BYTES);
    }

    @Override
    public String visitClobWrapper(ClobWrapper wrapper, SqlLogFormattingFunction p, Void q)
        throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.CLOB);
    }

    @Override
    public String visitDateWrapper(DateWrapper wrapper, SqlLogFormattingFunction p, Void q) {
      return p.apply(wrapper, JdbcTypes.DATE);
    }

    @Override
    public String visitDoubleWrapper(DoubleWrapper wrapper, SqlLogFormattingFunction p, Void q)
        throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.DOUBLE);
    }

    @Override
    public String visitFloatWrapper(FloatWrapper wrapper, SqlLogFormattingFunction p, Void q)
        throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.FLOAT);
    }

    @Override
    public String visitIntegerWrapper(IntegerWrapper wrapper, SqlLogFormattingFunction p, Void q) {
      return p.apply(wrapper, JdbcTypes.INTEGER);
    }

    @Override
    public String visitLocalDateWrapper(
        LocalDateWrapper wrapper, SqlLogFormattingFunction p, Void q) throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.LOCAL_DATE);
    }

    @Override
    public String visitLocalDateTimeWrapper(
        LocalDateTimeWrapper wrapper, SqlLogFormattingFunction p, Void q) throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.LOCAL_DATE_TIME);
    }

    @Override
    public String visitLocalTimeWrapper(
        LocalTimeWrapper wrapper, SqlLogFormattingFunction p, Void q) throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.LOCAL_TIME);
    }

    @Override
    public String visitLongWrapper(LongWrapper wrapper, SqlLogFormattingFunction p, Void q) {
      return p.apply(wrapper, JdbcTypes.LONG);
    }

    @Override
    public String visitNClobWrapper(NClobWrapper wrapper, SqlLogFormattingFunction p, Void q)
        throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.NCLOB);
    }

    @Override
    public String visitShortWrapper(ShortWrapper wrapper, SqlLogFormattingFunction p, Void q)
        throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.SHORT);
    }

    @Override
    public String visitStringWrapper(StringWrapper wrapper, SqlLogFormattingFunction p, Void q) {
      return p.apply(wrapper, JdbcTypes.STRING);
    }

    @Override
    public String visitSQLXMLWrapper(SQLXMLWrapper wrapper, SqlLogFormattingFunction p, Void q)
        throws RuntimeException {
      return p.apply(wrapper, JdbcTypes.SQLXML);
    }

    @Override
    public String visitTimeWrapper(TimeWrapper wrapper, SqlLogFormattingFunction p, Void q) {
      return p.apply(wrapper, JdbcTypes.TIME);
    }

    @Override
    public String visitTimestampWrapper(
        TimestampWrapper wrapper, SqlLogFormattingFunction p, Void q) {
      return p.apply(wrapper, JdbcTypes.TIMESTAMP);
    }

    @Override
    public <E extends Enum<E>> String visitEnumWrapper(
        EnumWrapper<E> wrapper, SqlLogFormattingFunction p, Void q) throws RuntimeException {
      return p.apply(wrapper, new EnumType<E>(wrapper.getBasicClass()));
    }

    @Override
    public String visitUtilDateWrapper(
        UtilDateWrapper wrapper, SqlLogFormattingFunction p, Void q) {
      return p.apply(wrapper, JdbcTypes.UTIL_DATE);
    }

    @Override
    public String visitObjectWrapper(ObjectWrapper wrapper, SqlLogFormattingFunction p, Void q) {
      return p.apply(wrapper, JdbcTypes.OBJECT);
    }
  }

  /** A standard implementation of {@link ExpressionFunctions}. */
  public static class StandardExpressionFunctions implements ExpressionFunctions {

    /** the default escape character */
    private static char DEFAULT_ESCAPE_CHAR = '$';

    /** the default wild card characters */
    private static final char[] DEFAULT_WILDCARDS = {'%', '_'};

    /** the escape character for the SQL LIKE operator */
    protected final char escapeChar;

    /** the wild card characters for the SQL LIKE operator */
    protected final char[] wildcards;

    /** the default pattern for escape wild card characters */
    protected final Pattern defaultWildcardReplacementPattern;

    /** the default replacement string for wild card characters */
    protected final String defaultReplacement;

    protected StandardExpressionFunctions() {
      this(DEFAULT_WILDCARDS);
    }

    /** @param wildcards the wild card characters for the SQL LIKE operator */
    protected StandardExpressionFunctions(char[] wildcards) {
      this(DEFAULT_ESCAPE_CHAR, wildcards);
    }

    /**
     * @param escapeChar the escape character for the SQL LIKE operator
     * @param wildcards the wild card characters for the SQL LIKE operator
     */
    protected StandardExpressionFunctions(char escapeChar, char[] wildcards) {
      this.escapeChar = escapeChar;
      this.wildcards = wildcards != null ? wildcards : DEFAULT_WILDCARDS;
      this.defaultWildcardReplacementPattern =
          createWildcardReplacementPattern(escapeChar, this.wildcards);
      this.defaultReplacement = createWildcardReplacement(escapeChar);
    }

    /**
     * @param escapeChar the escape character for the SQL LIKE operator
     * @param wildcards the wild card characters for the SQL LIKE operator
     * @param defaultWildcardReplacementPattern the default pattern for escape wild card characters
     * @param defaultReplacement the default replacement string for wild card characters
     */
    protected StandardExpressionFunctions(
        char escapeChar,
        char[] wildcards,
        Pattern defaultWildcardReplacementPattern,
        String defaultReplacement) {
      this.escapeChar = escapeChar;
      this.wildcards = wildcards != null ? wildcards : DEFAULT_WILDCARDS;
      this.defaultWildcardReplacementPattern = defaultWildcardReplacementPattern;
      this.defaultReplacement = defaultReplacement;
    }

    @Override
    public String escape(CharSequence text, char escapeChar) {
      if (text == null) {
        return null;
      }
      return escapeWildcard(text, escapeChar);
    }

    @Override
    public String escape(CharSequence text) {
      if (text == null) {
        return null;
      }
      return escapeWildcard(defaultWildcardReplacementPattern, text, defaultReplacement);
    }

    @Override
    public String prefix(CharSequence text) {
      if (text == null) {
        return null;
      }
      String escaped = escapeWildcard(defaultWildcardReplacementPattern, text, defaultReplacement);
      return escaped + "%";
    }

    @Override
    public String prefix(CharSequence text, char escapeChar) {
      if (text == null) {
        return null;
      }
      return escapeWildcard(text, escapeChar) + "%";
    }

    @Override
    public String suffix(CharSequence text) {
      if (text == null) {
        return null;
      }
      String escaped = escapeWildcard(defaultWildcardReplacementPattern, text, defaultReplacement);
      return "%" + escaped;
    }

    @Override
    public String suffix(CharSequence text, char escapeChar) {
      if (text == null) {
        return null;
      }
      return "%" + escapeWildcard(text, escapeChar);
    }

    @Override
    public String infix(CharSequence text) {
      if (text == null) {
        return null;
      }
      if (text.length() == 0) {
        return "%";
      }
      String escaped = escapeWildcard(defaultWildcardReplacementPattern, text, defaultReplacement);
      return "%" + escaped + "%";
    }

    @Override
    public String infix(CharSequence text, char escapeChar) {
      if (text == null) {
        return null;
      }
      if (text.length() == 0) {
        return "%";
      }
      return "%" + escapeWildcard(text, escapeChar) + "%";
    }

    /**
     * Escapes wild card characters.
     *
     * @param input the target string for escaping
     * @param escapeChar the escape character
     * @return the escaped string
     */
    protected String escapeWildcard(CharSequence input, char escapeChar) {
      Pattern pattern = createWildcardReplacementPattern(escapeChar, wildcards);
      String replacement = createWildcardReplacement(escapeChar);
      return escapeWildcard(pattern, input, replacement);
    }

    /**
     * Escapes wild card characters with the regular expression.
     *
     * @param pattern the regular expression pattern
     * @param input the target string for escaping
     * @param replacement the replacement string
     * @return the escaped string
     */
    protected String escapeWildcard(Pattern pattern, CharSequence input, String replacement) {
      Matcher matcher = pattern.matcher(input);
      return matcher.replaceAll(replacement);
    }

    @Override
    public java.util.Date roundDownTimePart(java.util.Date date) {
      if (date == null) {
        return null;
      }
      Calendar calendar = makeRoundedDownClandar(date);
      return new java.util.Date(calendar.getTimeInMillis());
    }

    @Override
    public Date roundDownTimePart(Date date) {
      if (date == null) {
        return null;
      }
      Calendar calendar = makeRoundedDownClandar(date);
      return new Date(calendar.getTimeInMillis());
    }

    @Override
    public Timestamp roundDownTimePart(Timestamp timestamp) {
      if (timestamp == null) {
        return null;
      }
      Calendar calendar = makeRoundedDownClandar(timestamp);
      return new Timestamp(calendar.getTimeInMillis());
    }

    @Override
    public LocalDateTime roundDownTimePart(LocalDateTime localDateTime) {
      if (localDateTime == null) {
        return null;
      }
      return localDateTime.truncatedTo(ChronoUnit.DAYS);
    }

    protected Calendar makeRoundedDownClandar(java.util.Date date) {
      Calendar calendar = Calendar.getInstance();
      calendar.setTime(date);
      calendar.set(Calendar.HOUR_OF_DAY, 0);
      calendar.set(Calendar.MINUTE, 0);
      calendar.set(Calendar.SECOND, 0);
      calendar.set(Calendar.MILLISECOND, 0);
      return calendar;
    }

    @Override
    public java.util.Date roundUpTimePart(java.util.Date date) {
      if (date == null) {
        return null;
      }
      Calendar calendar = makeRoundedUpClandar(date);
      return new java.util.Date(calendar.getTimeInMillis());
    }

    @Override
    public Date roundUpTimePart(Date date) {
      if (date == null) {
        return null;
      }
      Calendar calendar = makeRoundedUpClandar(date);
      return new Date(calendar.getTimeInMillis());
    }

    @Override
    public Timestamp roundUpTimePart(Timestamp timestamp) {
      if (timestamp == null) {
        return null;
      }
      Calendar calendar = makeRoundedUpClandar(timestamp);
      return new Timestamp(calendar.getTimeInMillis());
    }

    @Override
    public LocalDate roundUpTimePart(LocalDate localDate) {
      if (localDate == null) {
        return null;
      }
      return localDate.plusDays(1);
    }

    @Override
    public LocalDateTime roundUpTimePart(LocalDateTime localDateTime) {
      if (localDateTime == null) {
        return null;
      }
      return localDateTime.plusDays(1).truncatedTo(ChronoUnit.DAYS);
    }

    protected Calendar makeRoundedUpClandar(java.util.Date date) {
      Calendar calendar = Calendar.getInstance();
      calendar.setTime(date);
      calendar.add(Calendar.DATE, 1);
      calendar.set(Calendar.HOUR_OF_DAY, 0);
      calendar.set(Calendar.MINUTE, 0);
      calendar.set(Calendar.SECOND, 0);
      calendar.set(Calendar.MILLISECOND, 0);
      return calendar;
    }

    /**
     * Creates the regular expression pattern that is represents the wild card characters .
     *
     * @param escapeChar the escape character
     * @param wildcards the wild card character
     * @return the pattern
     */
    protected Pattern createWildcardReplacementPattern(char escapeChar, char[] wildcards) {
      StringBuilder buf = new StringBuilder();
      buf.append("[");
      for (char wildcard : wildcards) {
        if (escapeChar == '[' || escapeChar == ']') {
          buf.append("\\");
        }
        buf.append(Matcher.quoteReplacement(String.valueOf(escapeChar)));
        if (wildcard == '[' || wildcard == ']') {
          buf.append("\\");
        }
        buf.append(wildcard);
      }
      buf.append("]");
      return Pattern.compile(buf.toString());
    }

    /**
     * Create the replacement string that replaces wild card characters.
     *
     * @param escapeChar the escape character
     * @return the replacement string
     */
    protected String createWildcardReplacement(char escapeChar) {
      return Matcher.quoteReplacement(String.valueOf(escapeChar)) + "$0";
    }

    @Override
    public boolean isEmpty(CharSequence charSequence) {
      return CharSequenceUtil.isEmpty(charSequence);
    }

    @Override
    public boolean isNotEmpty(CharSequence charSequence) {
      return CharSequenceUtil.isNotEmpty(charSequence);
    }

    @Override
    public boolean isBlank(CharSequence charSequence) {
      return CharSequenceUtil.isBlank(charSequence);
    }

    @Override
    public boolean isNotBlank(CharSequence charSequence) {
      return CharSequenceUtil.isNotBlank(charSequence);
    }
  }

  /** A standard implementation of {@link ScriptBlockContext}. */
  public static class StandardScriptBlockContext implements ScriptBlockContext {

    /** the key wards that represents the start of a block */
    protected List<List<String>> sqlBlockStartKeywordsList = new ArrayList<List<String>>();

    /** the key words */
    protected List<String> keywords = new ArrayList<String>();

    /** {@code true} if this context is inside of a block */
    protected boolean inBlock;

    @Override
    public void addKeyword(String keyword) {
      if (!inBlock) {
        keywords.add(keyword);
        check();
      }
    }

    /** Whether this context is inside of a block. */
    protected void check() {
      for (List<String> startKeywords : sqlBlockStartKeywordsList) {
        if (startKeywords.size() > keywords.size()) {
          continue;
        }
        Iterator<String> startKeywordsIt = startKeywords.iterator();
        Iterator<String> keywordsIt = keywords.iterator();
        for (; startKeywordsIt.hasNext(); ) {
          String word1 = startKeywordsIt.next();
          String word2 = keywordsIt.next();
          inBlock = word1.equalsIgnoreCase(word2);
          if (!inBlock) {
            break;
          }
        }
        if (inBlock) {
          break;
        }
      }
    }

    @Override
    public boolean isInBlock() {
      return inBlock;
    }
  }

  public static class StandardCriteriaBuilder implements CriteriaBuilder {

    @Override
    public void concat(PreparedSqlBuilder buf, Runnable leftOperand, Runnable rightOperand) {
      buf.appendSql("concat(");
      leftOperand.run();
      buf.appendSql(", ");
      rightOperand.run();
      buf.appendSql(")");
    }

    @Override
    public void offsetAndFetch(PreparedSqlBuilder buf, int offset, int limit) {
      buf.appendSql(" offset ");
      buf.appendSql(Integer.toString(offset));
      buf.appendSql(" rows");
      if (limit > 0) {
        buf.appendSql(" fetch first ");
        buf.appendSql(Integer.toString(limit));
        buf.appendSql(" rows only");
      }
    }

    @Override
    public void lockWithTableHint(
        PreparedSqlBuilder buf, ForUpdateOption option, Consumer<PropertyMetamodel<?>> column) {}

    @Override
    public void forUpdate(
        PreparedSqlBuilder buf,
        ForUpdateOption option,
        Consumer<PropertyMetamodel<?>> column,
        AliasManager aliasManager) {
      option.accept(
          new ForUpdateOption.Visitor() {
            @Override
            public void visit(ForUpdateOption.Basic basic) {
              appendSql();
            }

            @Override
            public void visit(ForUpdateOption.NoWait noWait) {
              appendSql();
            }

            @Override
            public void visit(ForUpdateOption.Wait wait) {
              appendSql();
            }

            private void appendSql() {
              buf.appendSql(" for update");
            }
          });
    }
  }
}