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

package com.actiontech.dble.plan.optimizer;

import com.actiontech.dble.DbleServer;
import com.actiontech.dble.config.model.SystemConfig;
import com.actiontech.dble.config.model.sharding.SchemaConfig;
import com.actiontech.dble.plan.common.exception.MySQLOutPutException;
import com.actiontech.dble.plan.common.item.subquery.ItemSubQuery;
import com.actiontech.dble.plan.node.JoinInnerNode;
import com.actiontech.dble.plan.node.NoNameNode;
import com.actiontech.dble.plan.node.PlanNode;
import com.actiontech.dble.plan.node.TableNode;
import com.actiontech.dble.plan.util.PlanUtil;
import com.actiontech.dble.route.util.RouterUtil;
import com.actiontech.dble.server.util.SchemaUtil;
import com.actiontech.dble.singleton.TraceManager;
import com.google.common.collect.ImmutableMap;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public final class MyOptimizer {
    private MyOptimizer() {
    }

    public static PlanNode optimize(PlanNode node, @Nonnull HintPlanInfo hintPlanInfo) {

        TraceManager.TraceObject traceObject = TraceManager.threadTrace("optimize-for-sql");
        TraceManager.log(ImmutableMap.of("plan-node", node), traceObject);
        try {

            if (!hintPlanInfo.isIn2join()) {
                if (SystemConfig.getInstance().isInSubQueryTransformToJoin()) {
                    // PreProcessor SubQuery ,transform in sub query to join
                    node = SubQueryPreProcessor.optimize(node);
                } else {
                    SubQueryPreNoTransformProcessor.optimize(node);
                }
            } else {
                // PreProcessor SubQuery ,transform in sub query to join; '/*!dble:plan=In2join*/ select...';
                node = SubQueryPreProcessor.optimize(node);
            }
            updateReferredTableNodes(node);
            int existGlobal = checkGlobalTable(node, new HashSet<>());
            if (node.type() == PlanNode.PlanNodeType.QUERY || node.isExistView() || existGlobal != 1 || node.isWithSubQuery() || node.isContainsSubQuery() || !PlanUtil.hasNoFakeNode(node)) {
                // optimizer sub query [Derived Tables (Subqueries in the FROM Clause)]
                //node = SubQueryProcessor.optimize(node);
                // transform right join to left join
                JoinPreProcessor.optimize(node);

                //  filter expr which is always true/false
                FilterPreProcessor.optimize(node);
                //  push down the filter which may contains ER KEY
                node = FilterJoinColumnPusher.optimize(node);


                if (SystemConfig.getInstance().isUseNewJoinOptimizer() || !hintPlanInfo.isZeroNode()) {
                    node = JoinProcessor.optimize(node, hintPlanInfo);
                } else {
                    node = JoinERProcessor.optimize(node);
                }
                if (existGlobal >= 0) {
                    GlobalTableProcessor.optimize(node);
                }
                //  push down filter
                node = FilterPusher.optimize(node);

                OrderByPusher.optimize(node);

                LimitPusher.optimize(node);

                node = SelectedProcessor.optimize(node);
                if (!hintPlanInfo.isZeroNode()) {
                    HintStrategyNestLoopProcessor.optimize(node, hintPlanInfo);
                } else {
                    JoinStrategyProcessor.chooser(node);
                }
            }
            return node;
        } catch (MySQLOutPutException e) {
            LoggerFactory.getLogger(MyOptimizer.class).error(node.toString(), e);
            throw e;
        } finally {
            TraceManager.finishSpan(traceObject);
        }
    }

    public static PlanNode managerOptimize(PlanNode node) {
        try {
            // PreProcessor SubQuery ,transform in sub query to join
            node = SubQueryPreProcessor.optimize(node);

            // transform right join to left join
            JoinPreProcessor.optimize(node);

            //  filter expr which is always true/false
            FilterPreProcessor.optimize(node);

            //  push down filter
            node = FilterPusher.optimize(node);

            node = OrderByPusher.optimize(node);

            LimitPusher.optimize(node);

            node = SelectedProcessor.optimize(node);

            return node;
        } catch (MySQLOutPutException e) {
            LoggerFactory.getLogger(MyOptimizer.class).error(node.toString(), e);
            throw e;
        }
    }

    private static List<TableNode> updateReferredTableNodes(PlanNode node) {
        List<TableNode> subTables = new ArrayList<>();
        for (PlanNode childNode : node.getChildren()) {
            List<TableNode> childSubTables = updateReferredTableNodes(childNode);
            node.getReferedTableNodes().addAll(childSubTables);
            subTables.addAll(childSubTables);
        }
        for (ItemSubQuery subQuery : node.getSubQueries()) {
            List<TableNode> childSubTables = subQuery.getPlanNode().getReferedTableNodes();
            node.getReferedTableNodes().addAll(childSubTables);
            subTables.addAll(childSubTables);
        }
        return subTables;
    }

    /**
     * existShardTable
     *
     * @param node PlanNode
     * @return return 1 if it's all no name table or all global table node;
     * return -1 if all the table is not global table,need not global optimizer;
     * return 0 for other ,may need to global optimizer ;
     */
    public static int checkGlobalTable(PlanNode node, Set<String> resultShardingNodes) {
        if (node.isWithSubQuery()) {
            return 0;
        }
        Set<String> shardingNodes = null;
        boolean isAllGlobal = true;
        boolean isContainGlobal = false;
        for (TableNode tn : node.getReferedTableNodes()) {
            if (tn.getUnGlobalTableCount() == 0) {
                isContainGlobal = true;
                if (isAllGlobal) {
                    if (shardingNodes == null) {
                        shardingNodes = new HashSet<>(tn.getNoshardNode());
                    } else {
                        shardingNodes.retainAll(tn.getNoshardNode());
                    }
                } else {
                    return 0;
                }
            } else {
                isAllGlobal = false;
                if (isContainGlobal) {
                    return 0;
                }
            }
        }


        if (isAllGlobal) {
            if (shardingNodes == null) { // all nonamenode
                String db = SchemaUtil.getRandomDb();
                SchemaConfig schemaConfig = DbleServer.getInstance().getConfig().getSchemas().get(db);
                node.setNoshardNode(schemaConfig.getAllShardingNodes());
                resultShardingNodes.addAll(schemaConfig.getAllShardingNodes());
                return 1;
            } else if (shardingNodes.size() > 0) { //all global table
                node.setNoshardNode(shardingNodes);
                resultShardingNodes.addAll(shardingNodes);
                String sql = node.getSql();
                for (TableNode tn : node.getReferedTableNodes()) {
                    sql = RouterUtil.removeSchema(sql, tn.getSchema());
                }
                node.setSql(sql);
                return 1;
            } else {
                return 0;
            }
        } else if (containsNoNameNode(node)) {
            return 0;
        }
        return -1;
    }

    private static boolean containsNoNameNode(PlanNode node) {
        if (node instanceof NoNameNode || node instanceof JoinInnerNode) {
            return true;
        }
        for (PlanNode child : node.getChildren()) {
            if (containsNoNameNode(child)) {
                return true;
            }
        }
        return false;
    }

}
