package cn.axzo.msg.center.util;

import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.SQLExpr;
import com.alibaba.druid.sql.ast.SQLStatement;
import com.alibaba.druid.sql.ast.expr.SQLBinaryOpExpr;
import com.alibaba.druid.sql.ast.expr.SQLBinaryOperator;
import com.alibaba.druid.sql.ast.expr.SQLIdentifierExpr;
import com.alibaba.druid.sql.ast.expr.SQLIntegerExpr;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlSelectQueryBlock;
import com.alibaba.druid.sql.dialect.mysql.ast.statement.MySqlUpdateStatement;
import com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser;
import com.alibaba.druid.sql.visitor.SQLASTVisitor;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * 把select和update语句中mybatis-plus自动添加到where条件里面的is_delete条件去掉(替换成常量表达式)
 *
 * @author yanglin
 */
@Intercepts(value = {
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "update",
                args = {MappedStatement.class, Object.class})
})
public class DeleteAwareInterceptor implements Interceptor {

    private static final ThreadLocal<Boolean> ENABLE = new ThreadLocal<>();

    /**
     * 不建议此种使用方式，容易忘记把 {@link DeleteAwareInterceptor#disableDeleteAware} 写到finally代码块中
     * 建议使用 {@link DeleteAwareInterceptor#execute}
     */
    @Deprecated
    public static void enableDeleteAware() {
        ENABLE.set(Boolean.TRUE);
    }

    /**
     * 不建议此种使用方式，容易忘记把 {@link DeleteAwareInterceptor#disableDeleteAware} 写到finally代码块中
     * 建议使用 {@link DeleteAwareInterceptor#execute}
     */
    @Deprecated
    public static void disableDeleteAware() {
        ENABLE.remove();
    }

    public static <T> T execute(Supplier<T> supplier) {
        try {
            enableDeleteAware();
            return supplier.get();
        } finally {
            disableDeleteAware();
        }
    }

    public static void execute(Runnable runnable) {
        try {
            enableDeleteAware();
            runnable.run();
        } finally {
            disableDeleteAware();
        }
    }

    public static boolean isDeleteAware() {
        Boolean enabled = ENABLE.get();
        return enabled != null && enabled;
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (!isDeleteAware()) {
            return invocation.proceed();
        }

        final Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        SqlCommandType commandType = ms.getSqlCommandType();
        BoundSql boundSql = ms.getBoundSql(args[1]);
        SQLStatement stmt;
        if (commandType == SqlCommandType.UPDATE) {
            stmt = new MySqlStatementParser(boundSql.getSql()).parseUpdateStatement();
        } else if (commandType == SqlCommandType.SELECT) {
            stmt = new MySqlStatementParser(boundSql.getSql()).parseSelect();
        } else {
            return invocation.proceed();
        }
        stmt.accept(new ReplaceIsDeleteExprVisitor());
        String sqlWithoutIsDelete = stmt.toString();
        args[0] = copyMappedStatement(ms, parameterObject -> new CustomBoundSql(
                ms.getConfiguration(), sqlWithoutIsDelete, boundSql.getParameterMappings(), parameterObject, boundSql));
        return invocation.proceed();
    }

    private static class ReplaceIsDeleteExprVisitor implements SQLASTVisitor {

        @Override
        public void endVisit(SQLBinaryOpExpr x) {
            // 不是 is_delete = 0/1 的形式
            if (!(x.getLeft() instanceof SQLIdentifierExpr)
                    || !(x.getRight() instanceof SQLIntegerExpr)) {
                return;
            }
            // 字段名不叫 is_delete
            if (!((SQLIdentifierExpr) x.getLeft()).getName().equalsIgnoreCase("is_delete")) {
                return;
            }
            if (x.getParent() instanceof MySqlSelectQueryBlock) {
                ((MySqlSelectQueryBlock) x.getParent()).removeCondition(x);
            } else if (x.getParent() instanceof MySqlUpdateStatement) {
                ((MySqlUpdateStatement) x.getParent()).removeCondition(x);
            } else if (x.getParent() instanceof SQLBinaryOpExpr) {
                SQLBinaryOpExpr parent = (SQLBinaryOpExpr) x.getParent();
                Consumer<SQLExpr> replaceFun = newExpr -> {
                    if (parent.getLeft() == x) {
                        parent.setLeft(newExpr);
                    } else if (parent.getRight() == x) {
                        parent.setRight(newExpr);
                    }
                };
                // 下面就取巧设置了, 可以少写很多代码
                if (parent.getOperator() == SQLBinaryOperator.BooleanAnd) {
                    replaceFun.accept(SQLUtils.toMySqlExpr("1 = 1"));
                } else if (parent.getOperator() == SQLBinaryOperator.BooleanOr) {
                    replaceFun.accept(SQLUtils.toMySqlExpr("0 = 1"));
                }
            }
        }

    }

    private static class CustomBoundSql extends BoundSql {

        final BoundSql delegate;

        CustomBoundSql(Configuration configuration, String sql, List<ParameterMapping> parameterMappings,
                       Object parameterObject, BoundSql delegate) {
            super(configuration, sql, parameterMappings, parameterObject);
            this.delegate = delegate;
        }

        @Override
        public boolean hasAdditionalParameter(String name) {
            if (super.hasAdditionalParameter(name))
                return true;
            return delegate.hasAdditionalParameter(name);
        }

        @Override
        public Object getAdditionalParameter(String name) {
            Object parameter = super.getAdditionalParameter(name);
            if (parameter != null)
                return parameter;
            return delegate.getAdditionalParameter(name);
        }

    }

    private MappedStatement copyMappedStatement(MappedStatement ms, SqlSource sqlSource) {
        MappedStatement.Builder builder =
                new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), sqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length != 0) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

}