GroupingCriteriaCollector.kt

/*
 *    Copyright 2016-2024 the original author or authors.
 *
 *    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
 *
 *       https://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 org.mybatis.dynamic.sql.util.kotlin

import org.mybatis.dynamic.sql.AndOrCriteriaGroup
import org.mybatis.dynamic.sql.BasicColumn
import org.mybatis.dynamic.sql.BindableColumn
import org.mybatis.dynamic.sql.ColumnAndConditionCriterion
import org.mybatis.dynamic.sql.CriteriaGroup
import org.mybatis.dynamic.sql.ExistsCriterion
import org.mybatis.dynamic.sql.NotCriterion
import org.mybatis.dynamic.sql.SqlBuilder
import org.mybatis.dynamic.sql.SqlCriterion
import org.mybatis.dynamic.sql.VisitableCondition

typealias GroupingCriteriaReceiver = GroupingCriteriaCollector.() -> Unit

fun GroupingCriteriaReceiver.andThen(after: SubCriteriaCollector.() -> Unit): GroupingCriteriaReceiver = {
    invoke(this)
    after(this)
}

@MyBatisDslMarker
sealed class SubCriteriaCollector {
    internal val subCriteria = mutableListOf<AndOrCriteriaGroup>()

    /**
     * Add sub criterion joined with "and" to the current context. If the receiver adds more than one
     * criterion that renders then parentheses will be added.
     *
     * This function may be called multiple times in a context.
     *
     * @param criteriaReceiver a function to create the contained criteria
     */
    fun and(criteriaReceiver: GroupingCriteriaReceiver): Unit =
        GroupingCriteriaCollector().apply(criteriaReceiver).let {
            subCriteria.add(
                AndOrCriteriaGroup.Builder().withConnector("and") //$NON-NLS-1$
                    .withInitialCriterion(it.initialCriterion)
                    .withSubCriteria(it.subCriteria)
                    .build()
            )
        }

    /**
     * Add a list of criteria joined with "and" to the current context. If the list contains more than
     * one criterion that renders then parentheses will be added. This function is distinguished from the
     * other overload in that it can accept a pre-created list of criteria and does not require any criterion
     * to be the initial criterion. The first criterion that renders will be rendered without the "and" or "or".
     *
     * This function may be called multiple times in a context.
     *
     * @param criteria a list of pre-created criteria
     *
     */
    fun and(criteria: List<AndOrCriteriaGroup>) {
        subCriteria.add(
            AndOrCriteriaGroup.Builder().withConnector("and") //$NON-NLS-1$
                .withSubCriteria(criteria)
                .build()
        )
    }

    /**
     * Add sub criterion joined with "or" to the current context. If the receiver adds more than one
     * criterion that renders then parentheses will be added.
     *
     * This function may be called multiple times in a context.
     *
     * @param criteriaReceiver a function to create the contained criteria
     */
    fun or(criteriaReceiver: GroupingCriteriaReceiver): Unit =
        GroupingCriteriaCollector().apply(criteriaReceiver).let {
            subCriteria.add(
                AndOrCriteriaGroup.Builder().withConnector("or") //$NON-NLS-1$
                    .withInitialCriterion(it.initialCriterion)
                    .withSubCriteria(it.subCriteria)
                    .build()
            )
        }

    /**
     * Add a list of criteria joined with "or" to the current context. If the list contains more than
     * one criterion that renders then parentheses will be added. This function is distinguished from the
     * other overload in that it can accept a pre-created list of criteria and does not require any criterion
     * to be the initial criterion. The first criterion that renders will be rendered without the "and" or "or".
     *
     * This function may be called multiple times in a context.
     *
     * @param criteria a list of pre-created criteria
     *
     */
    fun or(criteria: List<AndOrCriteriaGroup>) {
        subCriteria.add(
            AndOrCriteriaGroup.Builder().withConnector("or") //$NON-NLS-1$
                .withSubCriteria(criteria)
                .build()
        )
    }
}

/**
 * This class is used to gather criteria for a having or where clause. The class gathers two types of criteria:
 * an initial criterion, and sub-criteria connected by either an "and" or an "or".
 *
 * An initial criterion can be one of four types:
 * - A column and condition (called with the invoke operator on a column, or an infix function)
 * - An exists operator (called with the "exists" function)
 * - A criteria group which is essentially parenthesis within the where clause (called with the "group" function)
 * - A criteria group preceded with "not" (called with the "not" function)
 *
 * Only one of the initial criterion functions should be called within each scope. If you need more than one,
 * use a sub-criterion joined with "and" or "or"
 */
@Suppress("TooManyFunctions")
@MyBatisDslMarker
open class GroupingCriteriaCollector : SubCriteriaCollector() {
    internal var initialCriterion: SqlCriterion? = null
        private set(value) {
            assertNull(field, "ERROR.21") //$NON-NLS-1$
            field = value
        }

    /**
     * Add an initial criterion preceded with "not" to the current context. If the receiver adds more than one
     * criterion that renders then parentheses will be added.
     *
     * This may only be called once per scope, and cannot be combined with "exists", "group", "invoke",
     * or any infix function in the same scope.
     *
     * @param criteriaReceiver a function to create the contained criteria
     */
    fun not(criteriaReceiver: GroupingCriteriaReceiver): Unit =
        GroupingCriteriaCollector().apply(criteriaReceiver).let {
            initialCriterion = NotCriterion.Builder()
                .withInitialCriterion(it.initialCriterion)
                .withSubCriteria(it.subCriteria)
                .build()
        }

    /**
     * Add an initial criterion preceded with "not" to the current context. If the list contains more than
     * one criterion that renders then parentheses will be added. This function is distinguished from the
     * other overload in that it can accept a pre-created list of criteria and does not require any criterion
     * to be the initial criterion. The first criterion that renders will be rendered without the "and" or "or".
     *
     * This may only be called once per scope, and cannot be combined with "exists", "group", "invoke",
     * or any infix function in the same scope.
     *
     * @param criteria a list of pre-created criteria
     *
     */
    fun not(criteria: List<AndOrCriteriaGroup>) {
        initialCriterion = NotCriterion.Builder().withSubCriteria(criteria).build()
    }

    /**
     * Add an initial criterion composed of a sub-query preceded with "exists" to the current context.
     *
     * This should only be specified once per scope, and cannot be combined with "invoke",
     * "group", "not", or any infix function in the same scope.
     *
     * @param kotlinSubQueryBuilder a function to create a select statement
     */
    fun exists(kotlinSubQueryBuilder: KotlinSubQueryBuilder.() -> Unit): Unit =
        KotlinSubQueryBuilder().apply(kotlinSubQueryBuilder).let {
            initialCriterion = ExistsCriterion.Builder().withExistsPredicate(SqlBuilder.exists(it)).build()
        }

    /**
     * Add an initial criterion to the current context. If the receiver adds more than one
     * criterion that renders at runtime then parentheses will be added.
     *
     * This may only be specified once per scope, and cannot be combined with "exists", "invoke",
     * "not", or any infix function in the same scope.
     *
     * This could "almost" be an operator invoke function. The problem is that
     * to call it a user would need to use "this" explicitly. We think that is too
     * confusing, so we'll stick with the function name of "group"
     *
     * @param criteriaReceiver a function to create the contained criteria
     */
    fun group(criteriaReceiver: GroupingCriteriaReceiver): Unit =
        GroupingCriteriaCollector().apply(criteriaReceiver).let {
            initialCriterion = CriteriaGroup.Builder()
                .withInitialCriterion(it.initialCriterion)
                .withSubCriteria(it.subCriteria)
                .build()
        }

    /**
     * Add an initial criterion preceded to the current context. If the list contains more than
     * one criterion that renders then parentheses will be added. This function is distinguished from the
     * other overload in that it can accept a pre-created list of criteria and does not require any criterion
     * to be the initial criterion. The first criterion that renders will be rendered without the "and" or "or".
     *
     * This may only be specified once per scope, and cannot be combined with "exists", "invoke",
     * "not", or any infix function in the same scope.
     *
     * @param criteria a list of pre-created criteria
     *
     */
    fun group(criteria: List<AndOrCriteriaGroup>) {
        initialCriterion = CriteriaGroup.Builder().withSubCriteria(criteria).build()
    }

    /**
     * Add an initial criterion to the current context based on a column and condition.
     * You can use it like "A.invoke(isEqualTo(3))" or "A (isEqualTo(3))".
     *
     * This is an extension function to a BindableColumn, but is scoped to the context of the
     * current collector.
     *
     * This should only be specified once per scope, and cannot be combined with "exists", "group",
     * "not", or any infix function in the same scope.
     *
     * @param condition the condition to be applied to this column, in this scope
     */
    operator fun <T> BindableColumn<T>.invoke(condition: VisitableCondition<T>) {
        initialCriterion = ColumnAndConditionCriterion.withColumn(this)
            .withCondition(condition)
            .build()
    }

    // infix functions...we may be able to rewrite these as extension functions once Kotlin solves the multiple
    // receivers problem (https://youtrack.jetbrains.com/issue/KT-42435)

    // conditions for all data types
    fun BindableColumn<*>.isNull() = invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNull())

    fun BindableColumn<*>.isNotNull() = invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotNull())

    infix fun <T> BindableColumn<T>.isEqualTo(value: T & Any) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo(value))

    infix fun BindableColumn<*>.isEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo(subQuery))

    infix fun BindableColumn<*>.isEqualTo(column: BasicColumn) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isEqualTo(column))

    infix fun <T> BindableColumn<T>.isEqualToWhenPresent(value: T?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isEqualToWhenPresent(value))

    infix fun <T> BindableColumn<T>.isNotEqualTo(value: T & Any) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotEqualTo(value))

    infix fun BindableColumn<*>.isNotEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotEqualTo(subQuery))

    infix fun BindableColumn<*>.isNotEqualTo(column: BasicColumn) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotEqualTo(column))

    infix fun <T> BindableColumn<T>.isNotEqualToWhenPresent(value: T?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotEqualToWhenPresent(value))

    infix fun <T> BindableColumn<T>.isGreaterThan(value: T & Any) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThan(value))

    infix fun BindableColumn<*>.isGreaterThan(subQuery: KotlinSubQueryBuilder.() -> Unit) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThan(subQuery))

    infix fun BindableColumn<*>.isGreaterThan(column: BasicColumn) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThan(column))

    infix fun <T> BindableColumn<T>.isGreaterThanWhenPresent(value: T?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanWhenPresent(value))

    infix fun <T> BindableColumn<T>.isGreaterThanOrEqualTo(value: T & Any) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanOrEqualTo(value))

    infix fun BindableColumn<*>.isGreaterThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanOrEqualTo(subQuery))

    infix fun BindableColumn<*>.isGreaterThanOrEqualTo(column: BasicColumn) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanOrEqualTo(column))

    infix fun <T> BindableColumn<T>.isGreaterThanOrEqualToWhenPresent(value: T?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isGreaterThanOrEqualToWhenPresent(value))

    infix fun <T> BindableColumn<T>.isLessThan(value: T & Any) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThan(value))

    infix fun BindableColumn<*>.isLessThan(subQuery: KotlinSubQueryBuilder.() -> Unit) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThan(subQuery))

    infix fun BindableColumn<*>.isLessThan(column: BasicColumn) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThan(column))

    infix fun <T> BindableColumn<T>.isLessThanWhenPresent(value: T?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanWhenPresent(value))

    infix fun <T> BindableColumn<T>.isLessThanOrEqualTo(value: T & Any) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanOrEqualTo(value))

    infix fun BindableColumn<*>.isLessThanOrEqualTo(subQuery: KotlinSubQueryBuilder.() -> Unit) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanOrEqualTo(subQuery))

    infix fun BindableColumn<*>.isLessThanOrEqualTo(column: BasicColumn) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanOrEqualTo(column))

    infix fun <T> BindableColumn<T>.isLessThanOrEqualToWhenPresent(value: T?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLessThanOrEqualToWhenPresent(value))

    fun <T> BindableColumn<T>.isIn(vararg values: T & Any) = isIn(values.asList())

    @JvmName("isInArray")
    infix fun <T> BindableColumn<T>.isIn(values: Array<out T & Any>) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isIn(values))

    infix fun <T> BindableColumn<T>.isIn(values: Collection<T & Any>) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isIn(values))

    infix fun BindableColumn<*>.isIn(subQuery: KotlinSubQueryBuilder.() -> Unit) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isIn(subQuery))

    fun <T> BindableColumn<T>.isInWhenPresent(vararg values: T?) = isInWhenPresent(values.asList())

    @JvmName("isInArrayWhenPresent")
    infix fun <T> BindableColumn<T>.isInWhenPresent(values: Array<out T?>?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isInWhenPresent(values))

    infix fun <T> BindableColumn<T>.isInWhenPresent(values: Collection<T?>?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isInWhenPresent(values))

    fun <T> BindableColumn<T>.isNotIn(vararg values: T & Any) = isNotIn(values.asList())

    @JvmName("isNotInArray")
    infix fun <T> BindableColumn<T>.isNotIn(values: Array<out T & Any>) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotIn(values))

    infix fun <T> BindableColumn<T>.isNotIn(values: Collection<T & Any>) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotIn(values))

    infix fun BindableColumn<*>.isNotIn(subQuery: KotlinSubQueryBuilder.() -> Unit) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotIn(subQuery))

    fun <T> BindableColumn<T>.isNotInWhenPresent(vararg values: T?) = isNotInWhenPresent(values.asList())

    @JvmName("isNotInArrayWhenPresent")
    infix fun <T> BindableColumn<T>.isNotInWhenPresent(values: Array<out T?>?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotInWhenPresent(values))

    infix fun <T> BindableColumn<T>.isNotInWhenPresent(values: Collection<T?>?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotInWhenPresent(values))

    infix fun <T> BindableColumn<T>.isBetween(value1: T & Any) =
        SecondValueCollector<T & Any> {
            invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isBetween(value1).and(it))
        }

    infix fun <T> BindableColumn<T>.isBetweenWhenPresent(value1: T?) =
        NullableSecondValueCollector<T> {
            invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isBetweenWhenPresent(value1).and(it))
        }

    infix fun <T> BindableColumn<T>.isNotBetween(value1: T & Any) =
        SecondValueCollector<T & Any> {
            invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotBetween(value1).and(it))
        }

    infix fun <T> BindableColumn<T>.isNotBetweenWhenPresent(value1: T?) =
        NullableSecondValueCollector<T> {
            invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotBetweenWhenPresent(value1).and(it))
        }

    // for string columns, but generic for columns with type handlers
    infix fun <T> BindableColumn<T>.isLike(value: T & Any) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLike(value))

    infix fun <T> BindableColumn<T>.isLikeWhenPresent(value: T?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLikeWhenPresent(value))

    infix fun <T> BindableColumn<T>.isNotLike(value: T & Any) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotLike(value))

    infix fun <T> BindableColumn<T>.isNotLikeWhenPresent(value: T?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotLikeWhenPresent(value))

    // shortcuts for booleans
    fun BindableColumn<Boolean>.isTrue() = isEqualTo(true)

    fun BindableColumn<Boolean>.isFalse() = isEqualTo(false)

    // conditions for strings only
    infix fun BindableColumn<String>.isLikeCaseInsensitive(value: String) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLikeCaseInsensitive(value))

    infix fun BindableColumn<String>.isLikeCaseInsensitiveWhenPresent(value: String?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isLikeCaseInsensitiveWhenPresent(value))

    infix fun BindableColumn<String>.isNotLikeCaseInsensitive(value: String) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotLikeCaseInsensitive(value))

    infix fun BindableColumn<String>.isNotLikeCaseInsensitiveWhenPresent(value: String?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotLikeCaseInsensitiveWhenPresent(value))

    fun BindableColumn<String>.isInCaseInsensitive(vararg values: String) = isInCaseInsensitive(values.asList())

    @JvmName("isInArrayCaseInsensitive")
    infix fun BindableColumn<String>.isInCaseInsensitive(values: Array<out String>) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isInCaseInsensitive(values))

    infix fun BindableColumn<String>.isInCaseInsensitive(values: Collection<String>) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isInCaseInsensitive(values))

    fun BindableColumn<String>.isInCaseInsensitiveWhenPresent(vararg values: String?) =
        isInCaseInsensitiveWhenPresent(values.asList())

    @JvmName("isInArrayCaseInsensitiveWhenPresent")
    infix fun BindableColumn<String>.isInCaseInsensitiveWhenPresent(values: Array<out String?>?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isInCaseInsensitiveWhenPresent(values))

    infix fun BindableColumn<String>.isInCaseInsensitiveWhenPresent(values: Collection<String?>?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isInCaseInsensitiveWhenPresent(values))

    fun BindableColumn<String>.isNotInCaseInsensitive(vararg values: String) =
        isNotInCaseInsensitive(values.asList())

    @JvmName("isNotInArrayCaseInsensitive")
    infix fun BindableColumn<String>.isNotInCaseInsensitive(values: Array<out String>) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotInCaseInsensitive(values))

    infix fun BindableColumn<String>.isNotInCaseInsensitive(values: Collection<String>) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotInCaseInsensitive(values))

    fun BindableColumn<String>.isNotInCaseInsensitiveWhenPresent(vararg values: String?) =
        isNotInCaseInsensitiveWhenPresent(values.asList())

    @JvmName("isNotInArrayCaseInsensitiveWhenPresent")
    infix fun BindableColumn<String>.isNotInCaseInsensitiveWhenPresent(values: Array<out String?>?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotInCaseInsensitiveWhenPresent(values))

    infix fun BindableColumn<String>.isNotInCaseInsensitiveWhenPresent(values: Collection<String?>?) =
        invoke(org.mybatis.dynamic.sql.util.kotlin.elements.isNotInCaseInsensitiveWhenPresent(values))

    companion object {
        fun having(receiver: GroupingCriteriaReceiver): GroupingCriteriaReceiver = receiver

        /**
         * Function for code simplification. This allows creation of an independent where clause
         * that can be reused in different statements. For example:
         *
         * val whereClause = where { id isEqualTo 3 }
         *
         * val rows = countFrom(foo) {
         *   where(whereClause)
         * }
         *
         * Use of this function is optional. You can also write code like this:
         *
         * val whereClause: GroupingCriteriaReceiver = { id isEqualTo 3 }
         *
         */
        fun where(receiver: GroupingCriteriaReceiver): GroupingCriteriaReceiver = receiver
    }
}

class SecondValueCollector<T> (private val consumer: (T) -> Unit) {
    infix fun and(value2: T) = consumer.invoke(value2)
}

class NullableSecondValueCollector<T> (private val consumer: (T?) -> Unit) {
    infix fun and(value2: T?) = consumer.invoke(value2)
}