package org.seasar.doma.jdbc.dialect;

import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import org.seasar.doma.DomaNullPointerException;
import org.seasar.doma.expr.ExpressionFunctions;
import org.seasar.doma.internal.jdbc.dialect.MysqlCountCalculatingTransformer;
import org.seasar.doma.internal.jdbc.dialect.MysqlCountGettingTransformer;
import org.seasar.doma.internal.jdbc.dialect.MysqlForUpdateTransformer;
import org.seasar.doma.internal.jdbc.dialect.MysqlPagingTransformer;
import org.seasar.doma.internal.jdbc.sql.PreparedSqlBuilder;
import org.seasar.doma.jdbc.JdbcMappingVisitor;
import org.seasar.doma.jdbc.ScriptBlockContext;
import org.seasar.doma.jdbc.SelectForUpdateType;
import org.seasar.doma.jdbc.SqlLogFormattingVisitor;
import org.seasar.doma.jdbc.SqlNode;
import org.seasar.doma.jdbc.criteria.query.CriteriaBuilder;

/** A dialect for MySQL. */
public class MysqlDialect extends StandardDialect {

  /** the set of {@literal SQLState} code that represents unique violation */
  protected static final Set<Integer> UNIQUE_CONSTRAINT_VIOLATION_ERROR_CODES =
      new HashSet<>(Arrays.asList(1022, 1062));

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

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

  public MysqlDialect() {
    this(
        new MysqlJdbcMappingVisitor(),
        new MysqlSqlLogFormattingVisitor(),
        new MysqlExpressionFunctions());
  }

  public MysqlDialect(JdbcMappingVisitor jdbcMappingVisitor) {
    this(jdbcMappingVisitor, new MysqlSqlLogFormattingVisitor(), new MysqlExpressionFunctions());
  }

  public MysqlDialect(SqlLogFormattingVisitor sqlLogFormattingVisitor) {
    this(new MysqlJdbcMappingVisitor(), sqlLogFormattingVisitor, new MysqlExpressionFunctions());
  }

  public MysqlDialect(ExpressionFunctions expressionFunctions) {
    this(new MysqlJdbcMappingVisitor(), new MysqlSqlLogFormattingVisitor(), expressionFunctions);
  }

  public MysqlDialect(
      JdbcMappingVisitor jdbcMappingVisitor, SqlLogFormattingVisitor sqlLogFormattingVisitor) {
    this(jdbcMappingVisitor, sqlLogFormattingVisitor, new MysqlExpressionFunctions());
  }

  public MysqlDialect(
      JdbcMappingVisitor jdbcMappingVisitor,
      SqlLogFormattingVisitor sqlLogFormattingVisitor,
      ExpressionFunctions expressionFunctions) {
    super(jdbcMappingVisitor, sqlLogFormattingVisitor, expressionFunctions);
  }

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

  @Override
  public boolean isUniqueConstraintViolated(SQLException sqlException) {
    if (sqlException == null) {
      throw new DomaNullPointerException("sqlException");
    }
    int code = getErrorCode(sqlException);
    return UNIQUE_CONSTRAINT_VIOLATION_ERROR_CODES.contains(code);
  }

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

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

  @Override
  public boolean supportsSelectForUpdate(SelectForUpdateType type, boolean withTargets) {
    return type == SelectForUpdateType.NORMAL && !withTargets;
  }

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

  @Override
  protected SqlNode toCountCalculatingSqlNode(SqlNode sqlNode) {
    MysqlCountCalculatingTransformer transformer = new MysqlCountCalculatingTransformer();
    return transformer.transform(sqlNode);
  }

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

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

  @Override
  protected SqlNode toCountGettingSqlNode(SqlNode sqlNode) {
    MysqlCountGettingTransformer transformer = new MysqlCountGettingTransformer();
    return transformer.transform(sqlNode);
  }

  @Override
  public String getScriptBlockDelimiter() {
    return "/";
  }

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

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

  @Override
  public CriteriaBuilder getCriteriaBuilder() {
    return new MysqlCriteriaBuilder();
  }

  public static class MysqlJdbcMappingVisitor extends StandardJdbcMappingVisitor {}

  public static class MysqlSqlLogFormattingVisitor extends StandardSqlLogFormattingVisitor {}

  public static class MysqlExpressionFunctions extends StandardExpressionFunctions {

    public MysqlExpressionFunctions() {
      super();
    }

    public MysqlExpressionFunctions(char[] wildcards) {
      super(wildcards);
    }

    protected MysqlExpressionFunctions(char escapeChar, char[] wildcards) {
      super(escapeChar, wildcards);
    }
  }

  public static class MysqlScriptBlockContext extends StandardScriptBlockContext {

    protected MysqlScriptBlockContext() {
      sqlBlockStartKeywordsList.add(Arrays.asList("create", "procedure"));
      sqlBlockStartKeywordsList.add(Arrays.asList("create", "function"));
      sqlBlockStartKeywordsList.add(Arrays.asList("create", "trigger"));
      sqlBlockStartKeywordsList.add(Arrays.asList("alter", "procedure"));
      sqlBlockStartKeywordsList.add(Arrays.asList("alter", "function"));
      sqlBlockStartKeywordsList.add(Arrays.asList("alter", "trigger"));
      sqlBlockStartKeywordsList.add(Collections.singletonList("declare"));
      sqlBlockStartKeywordsList.add(Collections.singletonList("begin"));
    }
  }

  public static class MysqlCriteriaBuilder extends StandardCriteriaBuilder {
    @Override
    public void offsetAndFetch(PreparedSqlBuilder buf, int offset, int limit) {
      buf.appendSql(" limit ");
      if (limit > 0) {
        buf.appendSql(Integer.toString(limit));
      } else {
        buf.appendSql(MysqlPagingTransformer.MAXIMUM_LIMIT);
      }
      buf.appendSql(" offset ");
      buf.appendSql(Integer.toString(offset));
    }
  }
}
