/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.trino.util;

import io.trino.sql.planner.Symbol;
import io.trino.sql.planner.plan.PlanNode;
import io.trino.sql.tree.ComparisonExpression;
import io.trino.sql.tree.Expression;
import io.trino.sql.tree.FunctionCall;
import io.trino.sql.tree.Literal;
import io.trino.sql.tree.SymbolReference;

import java.util.Collection;
import java.util.List;
import java.util.Set;

import static com.google.common.base.Verify.verify;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static io.trino.metadata.ResolvedFunction.extractFunctionName;
import static io.trino.sql.ExpressionUtils.extractConjuncts;
import static io.trino.sql.tree.ComparisonExpression.Operator.LESS_THAN;
import static io.trino.sql.tree.ComparisonExpression.Operator.LESS_THAN_OR_EQUAL;

public final class SpatialJoinUtils
{
    public static final String ST_CONTAINS = "st_contains";
    public static final String ST_WITHIN = "st_within";
    public static final String ST_INTERSECTS = "st_intersects";
    public static final String ST_DISTANCE = "st_distance";

    private SpatialJoinUtils() {}

    /**
     * Returns a subset of conjuncts matching one of the following shapes:
     * - ST_Contains(...)
     * - ST_Within(...)
     * - ST_Intersects(...)
     * <p>
     * Doesn't check or guarantee anything about function arguments.
     */
    public static List<FunctionCall> extractSupportedSpatialFunctions(Expression filterExpression)
    {
        return extractConjuncts(filterExpression).stream()
                .filter(FunctionCall.class::isInstance)
                .map(FunctionCall.class::cast)
                .filter(SpatialJoinUtils::isSupportedSpatialFunction)
                .collect(toImmutableList());
    }

    private static boolean isSupportedSpatialFunction(FunctionCall functionCall)
    {
        String functionName = extractFunctionName(functionCall.getName());
        return functionName.equalsIgnoreCase(ST_CONTAINS) ||
                functionName.equalsIgnoreCase(ST_WITHIN) ||
                functionName.equalsIgnoreCase(ST_INTERSECTS);
    }

    /**
     * Returns a subset of conjuncts matching one the following shapes:
     * - ST_Distance(...) <= ...
     * - ST_Distance(...) < ...
     * - ... >= ST_Distance(...)
     * - ... > ST_Distance(...)
     * <p>
     * Doesn't check or guarantee anything about ST_Distance functions arguments
     * or the other side of the comparison.
     */
    public static List<ComparisonExpression> extractSupportedSpatialComparisons(Expression filterExpression)
    {
        return extractConjuncts(filterExpression).stream()
                .filter(ComparisonExpression.class::isInstance)
                .map(ComparisonExpression.class::cast)
                .filter(SpatialJoinUtils::isSupportedSpatialComparison)
                .collect(toImmutableList());
    }

    private static boolean isSupportedSpatialComparison(ComparisonExpression expression)
    {
        switch (expression.getOperator()) {
            case LESS_THAN:
            case LESS_THAN_OR_EQUAL:
                return isSTDistance(expression.getLeft());
            case GREATER_THAN:
            case GREATER_THAN_OR_EQUAL:
                return isSTDistance(expression.getRight());
            default:
                return false;
        }
    }

    private static boolean isSTDistance(Expression expression)
    {
        if (expression instanceof FunctionCall) {
            return extractFunctionName(((FunctionCall) expression).getName()).equalsIgnoreCase(ST_DISTANCE);
        }

        return false;
    }

    public static boolean isSpatialJoinFilter(PlanNode left, PlanNode right, Expression filterExpression)
    {
        List<FunctionCall> functionCalls = extractSupportedSpatialFunctions(filterExpression);
        for (FunctionCall functionCall : functionCalls) {
            if (isSpatialJoinFilter(left, right, functionCall)) {
                return true;
            }
        }

        List<ComparisonExpression> spatialComparisons = extractSupportedSpatialComparisons(filterExpression);
        for (ComparisonExpression spatialComparison : spatialComparisons) {
            if (spatialComparison.getOperator() == LESS_THAN || spatialComparison.getOperator() == LESS_THAN_OR_EQUAL) {
                // ST_Distance(a, b) <= r
                Expression radius = spatialComparison.getRight();
                if (radius instanceof Literal || (radius instanceof SymbolReference && getSymbolReferences(right.getOutputSymbols()).contains(radius))) {
                    if (isSpatialJoinFilter(left, right, (FunctionCall) spatialComparison.getLeft())) {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    private static boolean isSpatialJoinFilter(PlanNode left, PlanNode right, FunctionCall spatialFunction)
    {
        List<Expression> arguments = spatialFunction.getArguments();
        verify(arguments.size() == 2);
        if (!(arguments.get(0) instanceof SymbolReference) || !(arguments.get(1) instanceof SymbolReference)) {
            return false;
        }

        SymbolReference firstSymbol = (SymbolReference) arguments.get(0);
        SymbolReference secondSymbol = (SymbolReference) arguments.get(1);

        Set<SymbolReference> probeSymbols = getSymbolReferences(left.getOutputSymbols());
        Set<SymbolReference> buildSymbols = getSymbolReferences(right.getOutputSymbols());

        if (probeSymbols.contains(firstSymbol) && buildSymbols.contains(secondSymbol)) {
            return true;
        }

        if (probeSymbols.contains(secondSymbol) && buildSymbols.contains(firstSymbol)) {
            return true;
        }

        return false;
    }

    private static Set<SymbolReference> getSymbolReferences(Collection<Symbol> symbols)
    {
        return symbols.stream().map(Symbol::toSymbolReference).collect(toImmutableSet());
    }
}

@@ -583,87 +583,8 @@
 l;%0A%0A
-import io.trino.sql.planner.Symbol;%0Aimport io.trino.sql.planner.plan.PlanNode;%0A
 impo
@@ -706,211 +706,32 @@
 ll;%0A
-import io.trino.sql.tree.Literal;%0Aimport io.trino.sql.tree.SymbolReference;%0A%0Aimport java.util.Collection;%0Aimport java.util.List;%0Aimport java.util.Set;%0A%0Aimport static com.google.common.base.Verify.verify;
+%0Aimport java.util.List;%0A
 %0Aimp
@@ -802,77 +802,8 @@
 st;%0A
-import static com.google.common.collect.ImmutableSet.toImmutableSet;%0A
 impo
@@ -932,163 +932,8 @@
 cts;
-%0Aimport static io.trino.sql.tree.ComparisonExpression.Operator.LESS_THAN;%0Aimport static io.trino.sql.tree.ComparisonExpression.Operator.LESS_THAN_OR_EQUAL;
 %0A%0Apu
@@ -3737,2369 +3737,7 @@
    %7D
-%0A%0A    public static boolean isSpatialJoinFilter(PlanNode left, PlanNode right, Expression filterExpression)%0A    %7B%0A        List%3CFunctionCall%3E functionCalls = extractSupportedSpatialFunctions(filterExpression);%0A        for (FunctionCall functionCall : functionCalls) %7B%0A            if (isSpatialJoinFilter(left, right, functionCall)) %7B%0A                return true;%0A            %7D%0A        %7D%0A%0A        List%3CComparisonExpression%3E spatialComparisons = extractSupportedSpatialComparisons(filterExpression);%0A        for (ComparisonExpression spatialComparison : spatialComparisons) %7B%0A            if (spatialComparison.getOperator() == LESS_THAN %7C%7C spatialComparison.getOperator() == LESS_THAN_OR_EQUAL) %7B%0A                // ST_Distance(a, b) %3C= r%0A                Expression radius = spatialComparison.getRight();%0A                if (radius instanceof Literal %7C%7C (radius instanceof SymbolReference && getSymbolReferences(right.getOutputSymbols()).contains(radius))) %7B%0A                    if (isSpatialJoinFilter(left, right, (FunctionCall) spatialComparison.getLeft())) %7B%0A                        return true;%0A                    %7D%0A                %7D%0A            %7D%0A        %7D%0A%0A        return false;%0A    %7D%0A%0A    private static boolean isSpatialJoinFilter(PlanNode left, PlanNode right, FunctionCall spatialFunction)%0A    %7B%0A        List%3CExpression%3E arguments = spatialFunction.getArguments();%0A        verify(arguments.size() == 2);%0A        if (!(arguments.get(0) instanceof SymbolReference) %7C%7C !(arguments.get(1) instanceof SymbolReference)) %7B%0A            return false;%0A        %7D%0A%0A        SymbolReference firstSymbol = (SymbolReference) arguments.get(0);%0A        SymbolReference secondSymbol = (SymbolReference) arguments.get(1);%0A%0A        Set%3CSymbolReference%3E probeSymbols = getSymbolReferences(left.getOutputSymbols());%0A        Set%3CSymbolReference%3E buildSymbols = getSymbolReferences(right.getOutputSymbols());%0A%0A        if (probeSymbols.contains(firstSymbol) && buildSymbols.contains(secondSymbol)) %7B%0A            return true;%0A        %7D%0A%0A        if (probeSymbols.contains(secondSymbol) && buildSymbols.contains(firstSymbol)) %7B%0A            return true;%0A        %7D%0A%0A        return false;%0A    %7D%0A%0A    private static Set%3CSymbolReference%3E getSymbolReferences(Collection%3CSymbol%3E symbols)%0A    %7B%0A        return symbols.stream().map(Symbol::toSymbolReference).collect(toImmutableSet());%0A    %7D
 %0A%7D%0A

Remove unused method in SpatialJoinUtils