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

package com.actiontech.dble.plan.util;

import com.actiontech.dble.plan.common.item.Item;
import com.actiontech.dble.plan.common.item.ItemBoolean;
import com.actiontech.dble.plan.common.item.function.ItemFunc.Functype;
import com.actiontech.dble.plan.common.item.function.operator.cmpfunc.ItemFuncEqual;
import com.actiontech.dble.plan.common.item.function.operator.logic.ItemCond;
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.node.PlanNode;
import com.actiontech.dble.plan.node.TableNode;
import com.actiontech.dble.util.CollectionUtil;
import com.actiontech.dble.util.StringUtil;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

import java.util.*;

public final class FilterUtils {
    private FilterUtils() {
    }

    /**
     * split the filter until filter is 'boolean filer' or 'orfilter'
     *
     * @param filter
     * @return
     */
    public static List<Item> splitFilter(Item filter) {
        if (filter == null)
            throw new RuntimeException("check filter before split");
        Set<Item> filterSet = Sets.newHashSet();
        if (filter.type() == Item.ItemType.COND_ITEM) {
            ItemCond cond = (ItemCond) filter;
            if (cond.functype() == Functype.COND_AND_FUNC) {
                for (int index = 0; index < cond.getArgCount(); index++) {
                    Item subFilter = cond.arguments().get(index);
                    if (subFilter == null)
                        continue;
                    List<Item> subSplits = splitFilter(subFilter);
                    filterSet.addAll(subSplits);
                }
            } else if (cond.functype() == Functype.COND_OR_FUNC) {

                //step1 divide the args into base  part
                Set<List<Item>> saveSet = new LinkedHashSet<>();
                for (int index = 0; index < cond.getArgCount(); index++) {
                    Item subFilter = cond.arguments().get(index);
                    if (subFilter == null)
                        continue;
                    List<Item> subSplits = splitFilter(subFilter);
                    saveSet.add(subSplits);
                }

                Map<String, List<Item>> itemMap = gourpByRefertTable(filter, saveSet);
                if (!CollectionUtil.isEmpty(itemMap)) {
                    for (Map.Entry<String, List<Item>> entry : itemMap.entrySet()) {
                        ItemCondOr x = new ItemCondOr(entry.getValue());
                        x.getReferTables().addAll(entry.getValue().get(0).getReferTables());
                        x.setWithUnValAble(true);
                        filterSet.add(x);
                    }
                }
                filterSet.add(cond);
            } else {
                filterSet.add(cond);
            }
        } else {
            filterSet.add(filter);
        }
        return Lists.newArrayList(filterSet);
    }

    /**
     * group the or filter into Map<table,List<condition>>
     * try to optimize where with 'OR'
     * <p>
     * eg1: tabl1.join(table2).query((table1.id>5 && table1.name = table2.name)or (table2.id<10 && table1.name = table2.name)")
     * after optimized:
     * table1.query("table1.id>5").or(table2.query("table2.id<10").on("table1.name = table2.name")
     * <p>
     * eg2: tabl1.join(table2).query(table1.id = table2.id && (table1.name = a or table2.name = b))
     * after optimized:
     * table1.query("table1.name = a").or(table2.query("table2.name = b").on("table1.name = table2.name")
     *
     * @param self
     * @param saveSet
     * @return
     */
    public static Map<String, List<Item>> gourpByRefertTable(Item self, Set<List<Item>> saveSet) {
        Map<String, List<Item>> itemMap = new HashMap<>();

        //step2 turned to group the base filter by refertTable
        //only when a table has filter in every args of condition OR ,the table can be limit
        //if a refertTable meet the criteria ,the table and all it's filter will be
        jumpOut:
        for (PlanNode refertTable : self.getReferTables()) { // Traversing related tables
            if (refertTable instanceof TableNode) {
                String tableName = ((TableNode) refertTable).getTableName();
                if (!StringUtil.isEmpty(refertTable.getAlias())) {
                    tableName += refertTable.getAlias();
                }

                //this loop is to check wether the table has filter in every Subconditions (....where subcondition1 or subcondition2 )
                for (List<Item> singleList : saveSet) {
                    boolean hasOutTable = false;
                    //in every subcondition the default relationship is 'AND'
                    //so the condition in subconditions also need be group by table
                    Map<String, List<Item>> itemMapForSingle = new HashMap<String, List<Item>>();
                    for (Item x : singleList) {
                        if (x.getReferTables().size() == 1) {
                            for (PlanNode backTable : x.getReferTables()) {
                                if (backTable instanceof TableNode) {
                                    String backTableName = ((TableNode) backTable).getTableName();
                                    if (!StringUtil.isEmpty(backTable.getAlias())) {
                                        backTableName += backTable.getAlias();
                                    }
                                    if (backTableName.equals(tableName)) {
                                        hasOutTable = true;
                                        if (!itemMapForSingle.containsKey(tableName)) {
                                            itemMapForSingle.put(tableName, new ArrayList<Item>());
                                        }
                                        itemMapForSingle.get(tableName).add(x);
                                    }
                                }
                            }
                        }
                    }
                    if (!hasOutTable) {
                        //it the refertTable
                        itemMap.remove(tableName);
                        continue jumpOut;
                    }

                    //in every single list the sub expr  relationship default as 'AND'
                    //so just group the relationship in every sub expr
                    for (Map.Entry<String, List<Item>> entry : itemMapForSingle.entrySet()) {
                        if (!itemMap.containsKey(entry.getKey())) {
                            itemMap.put(tableName, new ArrayList<Item>());
                        }
                        if (entry.getValue().size() != 1) {
                            ItemCondAnd x = new ItemCondAnd(entry.getValue());
                            x.getReferTables().addAll(entry.getValue().get(0).getReferTables());
                            itemMap.get(tableName).add(x);
                        } else {
                            itemMap.get(tableName).add(entry.getValue().get(0));
                        }
                    }
                }
            }
        }
        return itemMap;
    }


    public static ItemCond createLogicalFilterNoName(boolean and, List<Item> subFilters) {
        ItemCond cond = null;
        if (and)
            cond = new ItemCondAnd(subFilters);
        else
            cond = new ItemCondOr(subFilters);
        PlanUtil.refreshReferTables(cond);
        return cond;
    }

    public static Item and(List<Item> filters) {
        if (filters == null || filters.isEmpty())
            return null;
        List<Item> subFilters = new ArrayList<>();
        for (Item filter : filters) {
            if (filter == null)
                continue;
            if (filter.type() == Item.ItemType.COND_ITEM && ((ItemCond) filter).functype() == Functype.COND_AND_FUNC) {
                subFilters.addAll(filter.arguments());
            } else
                subFilters.add(filter);
        }
        if (subFilters.isEmpty())
            return null;
        else if (subFilters.size() == 1)
            return subFilters.get(0);
        else {
            return createLogicalFilterNoName(true, subFilters);
        }
    }

    /**
     * create and condition
     */
    public static Item and(Item root, Item o) {
        List<Item> list = new ArrayList<>();
        list.add(root);
        list.add(o);
        return and(list);
    }

    public static Item or(List<Item> filters) {
        if (filters == null)
            return null;
        List<Item> subFilters = new ArrayList<>();
        for (Item filter : filters) {
            if (filter == null)
                continue;
            if (filter.type() == Item.ItemType.COND_ITEM && ((ItemCond) filter).functype() == Functype.COND_OR_FUNC) {
                subFilters.addAll(filter.arguments());
            } else
                subFilters.add(filter);
        }
        if (subFilters.isEmpty())
            return new ItemBoolean(false);
        else if (subFilters.size() == 1)
            return subFilters.get(0);
        else {
            return createLogicalFilterNoName(false, subFilters);
        }
    }

    /**
     * create or condition
     */
    public static Item or(Item root, Item o) {
        List<Item> list = new ArrayList<>();
        list.add(root);
        list.add(o);
        return or(list);
    }

    /**
     * create equal filter
     */
    public static ItemFuncEqual equal(Item column, Item value, int charsetIndex) {
        ItemFuncEqual f = new ItemFuncEqual(column, value, charsetIndex);
        PlanUtil.refreshReferTables(f);
        return f;
    }
}
