/*
 * Copyright (C) 2016-2022 ActionTech.
 * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher.
 */

package com.actiontech.dble.backend.mysql.nio.handler.builder.sqlvisitor;

import com.actiontech.dble.plan.common.item.Item;
import com.actiontech.dble.plan.common.item.Item.ItemType;
import com.actiontech.dble.plan.common.item.function.operator.ItemBoolFunc2;
import com.actiontech.dble.plan.common.item.function.operator.cmpfunc.ItemFuncIn;
import com.actiontech.dble.plan.common.item.function.operator.logic.ItemCondAnd;
import com.actiontech.dble.plan.common.item.function.operator.logic.ItemCondOr;
import com.actiontech.dble.plan.common.item.function.operator.logic.ItemFuncNot;
import com.actiontech.dble.plan.common.ptr.StringPtr;
import com.actiontech.dble.plan.node.JoinNode;
import com.actiontech.dble.plan.node.PlanNode;
import com.actiontech.dble.plan.node.PlanNode.PlanNodeType;
import com.actiontech.dble.plan.node.TableNode;
import com.actiontech.dble.plan.util.PlanUtil;
import com.alibaba.druid.sql.ast.SQLHint;
import com.alibaba.druid.sql.dialect.mysql.visitor.MySqlOutputVisitor;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * execute the node which can push down,may be all global tables,and maybe contain global tables
 *
 * @author ActionTech
 * @CreateTime 2014/12/10
 */
public abstract class MysqlVisitor {
    // the max column size of mysql
    static final int MAX_COL_LENGTH = 255;
    // map :sel name->push name
    Map<String, String> pushNameMap = new HashMap<>();
    boolean isTopQuery = false;
    protected PlanNode query;
    private long randomIndex = 0L;

    /* is all function can be push down?if not,it need to calc by middle-ware */
    boolean existUnPushDownGroup = false;
    boolean visited = false;
    // -- start replaceable string builder
    ReplaceableStringBuilder replaceableSqlBuilder = new ReplaceableStringBuilder();
    // tmp sql
    StringBuilder sqlBuilder;
    StringPtr replaceableWhere = new StringPtr("");
    Item whereFilter = null;

    Map<String, String> mapTableToSimple = new HashMap<>();

    MysqlVisitor(PlanNode query, boolean isTopQuery) {
        this.query = query;
        this.isTopQuery = isTopQuery;
    }

    public ReplaceableStringBuilder getSql() {
        return replaceableSqlBuilder;
    }

    public Map<String, String> getMapTableToSimple() {
        return mapTableToSimple;
    }

    public abstract void visit();


    void buildTableName(TableNode tableNode, StringBuilder sb) {
        String tableName = "`" + tableNode.getPureName() + "`";
        String fullName = "`" + tableNode.getPureSchema() + "`." + tableName;
        mapTableToSimple.put(tableNode.getPureSchema() + "." + tableNode.getPureName(), tableNode.getPureName());
        mapTableToSimple.put(fullName, tableName);
        sb.append(" ").append(fullName);
        String alias = tableNode.getAlias();
        if (alias != null) {
            sb.append(" `").append(alias).append("`");
        }
        List<SQLHint> hintList = tableNode.getHintList();
        if (hintList != null && !hintList.isEmpty()) {
            sb.append(' ');
            boolean isFirst = true;
            for (SQLHint hint : hintList) {
                if (isFirst)
                    isFirst = false;
                else
                    sb.append(" ");
                MySqlOutputVisitor ov = new MySqlOutputVisitor(sb);
                ov.setShardingSupport(false);
                hint.accept(ov);
            }
        }
    }

    /* change where to replaceable */
    void buildWhere(PlanNode planNode) {
        if (!visited)
            replaceableSqlBuilder.getCurrentElement().setRepString(replaceableWhere);
        StringBuilder whereBuilder = new StringBuilder();
        Item filter = planNode.getWhereFilter();
        if (filter != null) {
            String pdName = visitUnSelPushDownName(filter, false);
            whereBuilder.append(" where ").append(pdName);
        }

        replaceableWhere.set(whereBuilder.toString());
        // refresh sqlbuilder
        sqlBuilder = replaceableSqlBuilder.getCurrentElement().getSb();
    }

    void buildWhere(JoinNode planNode, MysqlVisitor leftVisitor, MysqlVisitor rightVisitor) {
        if (!visited)
            replaceableSqlBuilder.getCurrentElement().setRepString(replaceableWhere);
        StringBuilder whereBuilder = new StringBuilder();
        Item filter = planNode.getWhereFilter();
        if (filter != null) {
            String pdName = visitUnSelPushDownName(filter, false);
            whereBuilder.append(" where ").append(pdName);
        } else {
            whereBuilder.append(" where 1=1 ");
        }
        // is not left join
        if (leftVisitor.getWhereFilter() != null && !planNode.getLeftOuter()) {
            String pdName = visitUnSelPushDownName(leftVisitor.getWhereFilter(), false);
            whereBuilder.append(" and (");
            whereBuilder.append(pdName);
            whereBuilder.append(")");
        }
        // is not right join
        if (rightVisitor.getWhereFilter() != null && !planNode.getRightOuter()) {
            String pdName = visitUnSelPushDownName(rightVisitor.getWhereFilter(), false);
            whereBuilder.append(" and (");
            whereBuilder.append(pdName);
            whereBuilder.append(")");
        }
        // left join
        if (leftVisitor.getWhereFilter() != null && !planNode.getRightOuter() && planNode.getLeftOuter()) {
            String pdName = visitUnSelPushDownName(leftVisitor.getWhereFilter(), false);
            whereBuilder.append(" and (");
            whereBuilder.append(pdName);
            whereBuilder.append(")");
        }
        //right join
        if (rightVisitor.getWhereFilter() != null && !planNode.getLeftOuter() && planNode.getRightOuter()) {
            String pdName = visitUnSelPushDownName(rightVisitor.getWhereFilter(), false);
            whereBuilder.append(" and (");
            whereBuilder.append(pdName);
            whereBuilder.append(")");
        }
        replaceableWhere.set(whereBuilder.toString());
        // refresh sqlbuilder
        sqlBuilder = replaceableSqlBuilder.getCurrentElement().getSb();
    }

    // generate an alias for aggregate function
    public static String getMadeAggAlias(String aggFuncName) {
        return "_$" + aggFuncName + "$_";
    }

    String getRandomAliasName() {
        return "rpda_" + randomIndex++;
    }

    /**
     * generate push down
     */
    protected abstract String visitPushDownNameSel(Item o);

    // pushDown's name of not in select list
    protected String visitUnSelPushDownName(Item item, boolean canUseAlias) {
        if (item.isWithSubQuery()) {
            item = PlanUtil.rebuildSubQueryItem(item);
        }
        String selName = getItemName(item);
        String nameInMap = pushNameMap.get(selName);
        if (nameInMap != null) {
            item.setPushDownName(nameInMap);
            if (canUseAlias && !(query.type() == PlanNodeType.JOIN && item.type().equals(ItemType.FIELD_ITEM))) {
                // join: select t1.id,t2.id from t1,t2 order by t1.id
                // try to use the origin group by,order by
                selName = nameInMap;
            }
        }
        return selName;
    }

    // try to trim sharding from field_item
    protected String getItemName(Item item) {
        if (item instanceof ItemCondOr) {
            StringBuilder sb = new StringBuilder();
            sb.append(" ( ");
            for (int index = 0; index < item.getArgCount(); index++) {
                if (index > 0) {
                    sb.append(" OR ");
                }
                sb.append(getItemName(item.arguments().get(index)));
            }
            sb.append(")");
            return sb.toString();
        } else if (item instanceof ItemCondAnd) {
            StringBuilder sb = new StringBuilder();
            sb.append(" ( ");
            for (int index = 0; index < item.getArgCount(); index++) {
                if (index > 0) {
                    sb.append(" AND ");
                }
                sb.append(getItemName(item.arguments().get(index)));
            }
            sb.append(")");
            return sb.toString();
        } else if (item instanceof ItemFuncNot) {
            return " ( NOT " + getItemName(item.arguments().get(0)) + ")";
        } else if (item instanceof ItemBoolFunc2) {
            Item a = item.arguments().get(0);
            Item b = item.arguments().get(1);
            return getItemName(a) + " " + ((ItemBoolFunc2) item).funcName() + " " + getItemName(b);
        } else if (item.type().equals(ItemType.FIELD_ITEM)) {
            String tableName = "`" + item.getTableName() + "`.`" + item.getItemName() + "`";
            if (item.getDbName() == null) {
                return tableName;
            }
            if (item.getReferTables().size() == 0) {
                return tableName;
            }
            PlanNode tbNode = item.getReferTables().iterator().next();
            if (!(tbNode instanceof TableNode)) {
                return tableName;
            }
            if (!((TableNode) tbNode).getTableName().equals(item.getTableName())) {
                return tableName;
            }
            return "`" + item.getDbName() + "`." + tableName;
        } else if (item instanceof ItemFuncIn) {
            Item a = item.arguments().get(0);
            StringBuilder sb = new StringBuilder();
            sb.append(getItemName(a));
            if (((ItemFuncIn) item).isNegate()) {
                sb.append(" not ");
            }
            sb.append(" in (");
            for (int index = 1; index < item.arguments().size(); index++) {
                if (index > 1) {
                    sb.append(",");
                }
                sb.append(getItemName(item.arguments().get(index)));
            }
            sb.append(")");
            return sb.toString();
        } else {
            return item.getItemName();
        }
    }

    public Item getWhereFilter() {
        return whereFilter;
    }

    public boolean isTopQuery() {
        return isTopQuery;
    }

    public boolean isExistUnPushDownGroup() {
        return existUnPushDownGroup;
    }

    public StringBuilder getSqlBuilder() {
        return sqlBuilder;
    }
}
