/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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 org.apache.causeway.persistence.querydsl.applib.services.repo;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

import jakarta.inject.Inject;

import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.NonUniqueResultException;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.EntityPathBase;

import org.jspecify.annotations.Nullable;

import org.apache.causeway.applib.exceptions.RecoverableException;
import org.apache.causeway.applib.services.repository.RepositoryService;
import org.apache.causeway.persistence.querydsl.applib.services.support.QueryDslSupport;
import org.apache.causeway.persistence.querydsl.applib.util.DslExpressions;

import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

/**
 * Provides default implementation and convenience methods for querying a specific entity (hierarchy), using
 * QueryDSL to construct the queries.
 *
 * <p>
 *     Internally, delegates to {@link QueryDslSupport} to actually submit the queries.
 * </p>
 *
 * @since 2.1 {@index}
 *
 * @param <T> is the entity type
 * @param <Q> is the Q type of the entity
 */
@SuperBuilder
@NoArgsConstructor
public abstract class QueryDslRepository<T extends Comparable, Q extends EntityPathBase<T>> {

    public static final OrderSpecifier<Comparable> ID_ORDER_SPECIFIER = new OrderSpecifier<>(Order.ASC, DslExpressions.constant("id"));

    @Inject protected QueryDslSupport queryDslSupport;
    @Inject protected RepositoryService repositoryService;

    /**
     * For testing
     */
    protected QueryDslRepository(
            final QueryDslSupport queryDslSupport,
            final RepositoryService repositoryService) {
        this.queryDslSupport = queryDslSupport;
        this.repositoryService = repositoryService;
    }

    private Class<T> entityClass;
    private Q entityPath;

    /**
     * Default ordering is by <code>id</code>, ascending.
     *
     * <p>
     *     Used as a default by certain methods that require an ordering, eg
     *     {@link #findAnyUsingDefaultOrder(Function...)} or
     *     {@link #findFieldsDistinctUsingDefaultOrder(Function, Function...)}.  All such methods have the suffix
     *     &quot;UsingDefaultOrder&quot; in their name.
     * </p>
     *
     * <p>
     *     Repository subclasses should override if they prefer an alternative default ordering of methods.
     * </p>
     *
     * <p>
     *     <B>NOTE</B>: Repository subclasses <i>must</i> override if the entity does not have an <code>id</code> field,
     *     otherwise calls to &quot;findXxxUsingDefaultOrder&quot; methods will fail.
     * </p>
     *
     * @return a list of {@link OrderSpecifier}s
     *
     * @see #findUsingDefaultOrder(Function[])
     * @see #findAllUsingDefaultOrder()
     * @see #findAnyUsingDefaultOrder(Function...)
     * @see #findFieldsDistinctUsingDefaultOrder(Function, Function...)
     */
    protected Function<Q, List<OrderSpecifier<? extends Comparable>>> getDefaultOrders() {
        return entity -> newList(ID_ORDER_SPECIFIER);
    }

    private Function<Q, OrderSpecifier<?>>[] getDefaultOrdersAsArray() {
        return getDefaultOrders().apply(getEntityPath())
                .stream()
                .map(orderSpecifier -> (Function<Q, OrderSpecifier<?>>) q -> orderSpecifier)
                .collect(Collectors.toList())
                .toArray(new Function[]{});
    }

    private OrderSpecifier<? extends Comparable>[] getDefaultOrdersUnwrapped() {
        try {
            var defaultOrdersFunc = getDefaultOrders();
            var defaultOrders = defaultOrdersFunc.apply(entity());
            return defaultOrders == null ? null : defaultOrders.toArray(new OrderSpecifier[0]);
        } catch (Exception e) {
            throw new RuntimeException("Invalid default order!", e);
        }
    }

    /**
     * @return the main entity class for which this repository implementation operates
     */
    public Class<T> getEntityClass() {
        if (entityClass == null) {
            entityClass = getTypeParameter(getClass(), 0);
        }
        return entityClass;
    }

    /**
     * The main entity Q instance for which this repository implementation operates.
     *
     * <p>
     *     The default implementation instantiates this reflectively, based on the conventions of the query-dsl
     *     annotation processor; eg <code>QCustomer</code> has a 1-arg string constructor.  In most cases there is
     *     no reason to override this.
     * </p>
     *
     * <p>
     *     However, for some complicated orderings, we have found it necessary to instead use the Q instance created
     *     by the annotation processor, called eg <code>QCustomer.customer</code>.  It is perfectly acceptable to
     *     override this method and just return the appropriate for the entity in question.
     * </p>
     *
     * @return the main entity Q instance for which this repository implementation operates
     *
     * @see #entity()
     * @see #q()
     */
    public Q getEntityPath() {
        if (entityPath == null) {
            entityPath = getEntityPathInstance();
        }
        return entityPath;
    }

    /**
     * @return the main entity Q instance for which this repository implementation operates
     *
     * @see #entity()
     * @see #getEntityPath()
     */
    public Q q() {
        return getEntityPath();
    }

    /**
     * @return the main entity Q instance for which this repository implementation operates
     *
     * @see #getEntityPath()
     * @see #q()
     */
    public Q entity() {
        return getEntityPath();
    }

    private Q getEntityPathInstance() {
        Class<Q> qClass = getTypeParameter(getClass(), 1);
        if (qClass == null) {
            throw new RecoverableException("Could not find Q type for this entity");
        }
        try {
            String alias = "e";
            if (getEntityClass() != null) {
                alias = getEntityClass().getSimpleName();
                alias = alias.substring(0, 1).toLowerCase() + alias.substring(1);
            }
            return qClass.getConstructor(String.class).newInstance(alias);
        } catch (Exception e) {
            throw new RecoverableException("Could not instantiate Q type " + qClass.getName(), e);
        }
    }

    /**
     * Returns all the instances of this entity, in the preferred {@link OrderSpecifier ordering}.
     *
     * @param orderSpecifiers    the ordering to apply
     *
     * @return all instances of this entity in the preferred order
     *
     * @see #findAllUsingDefaultOrder()
     */
    public List<T> findAll(final Function<Q, OrderSpecifier<?>>... orderSpecifiers) {
        return queryDslSupport
                .selectFrom(getEntityPath())
                .orderBy(unwrapOrderSpecifiers(orderSpecifiers))
                .fetch();
    }

    /**
     * Returns all the instances of this entity, in the {@link #getDefaultOrders() default order}.
     *
     * <p>
     *     NOTE: the default implementation of {@link #getDefaultOrders()} requires that the <code>id</code> field
     *     exists.  If this is not the case, then the method must be overridden.
     * </p>
     *
     * @return all instances of this entity in the {@link #getDefaultOrders() default order}.
     *
     * @see #findAll(Function[])
     * @see #getDefaultOrders()
     */
    public List<T> findAllUsingDefaultOrder() {
        return findAll(getDefaultOrdersAsArray());
    }

    /**
     * Based on the given predicate(s), search for exactly zero or one entity instance.
     *
     * @param predicates the predicates to apply as a filter
     *
     * @return exactly one result or null
     * @throws NonUniqueResultException if there is more than one matching result
     */
    public Optional<T> findUnique(
            final Function<Q, Predicate>... predicates
    ) throws NonUniqueResultException {
        return Optional.ofNullable(queryDslSupport
                .selectFrom(getEntityPath())
                .where(unwrapPredicates(predicates))
                .fetchOne());
    }

    /**
     * Based on the given predicates search for the first entity instance based on the provided ordering.
     *
     * @param predicates the predicate(s) to apply as a filter (can be null)
     * @return the first result or null
     *
     * @see #findAny(Function, Function[])
     * @see #findAnyUsingDefaultOrder(Function...)
     */
    public Optional<T> findAny(
            final @Nullable Function<Q, Predicate>[] predicates,
            final Function<Q, OrderSpecifier<?>>... orderSpecifiers
    ) {
        return Optional.ofNullable(queryDslSupport
                .selectFrom(getEntityPath())
                .where(unwrapPredicates(predicates))
                .orderBy(unwrapOrderSpecifiers(orderSpecifiers))
                .fetchFirst());
    }

    /**
     * Based on the given predicate, search for the first entity instance based on the provided ordering.
     *
     * @param predicate      the predicate to apply as a filter (can be null)
     *
     * @return the first result or null
     *
     * @see #findAny(Function[], Function[])
     * @see #findAnyUsingDefaultOrder(Function...)
     */
    public Optional<T> findAny(
            final Function<Q, Predicate> predicate,
            final Function<Q, OrderSpecifier<?>>... orderSpecifiers
    ) {
        return findAny(asArray(predicate), orderSpecifiers);
    }

    /**
     * Based on the given predicate function search for the first entity instance based on the
     * {@link #getDefaultOrders() default ordering}.
     *
     * <p>
     *     NOTE: the default implementation of {@link #getDefaultOrders()} requires that the <code>id</code> field
     *     exists.  If this is not the case, then that method must be overridden.
     * </p>
     *
     * @param predicates the predicate(s) to apply as a filter
     * @return the first result or null
     *
     * @see #findAny(Function[], Function[])
     * @see #findAny(Function, Function[])
     * @see #getDefaultOrdersUnwrapped()
     */
    public Optional<T> findAnyUsingDefaultOrder(final Function<Q, Predicate>... predicates) {
        return findAny(predicates, getDefaultOrdersAsArray());
    }

    /**
     * Based on the given predicate(s), search for the first entity instance based on specified
     * {@link OrderSpecifier ordering}.
     *
     * @param predicates        the predicate(s) to apply as a filter (can be null)
     * @param orderSpecifiers   the ordering to apply
     *
     * @return the first result or null
     *
     * @see #findFirstUsingDefaultOrder(Function[]) (Predicate[], OrderSpecifier[])
     * @see #findFirst(Function, Function[])
     */
    public Optional<T> findFirst(
            final @Nullable Function<Q, Predicate>[] predicates,
            final Function<Q, OrderSpecifier<?>>... orderSpecifiers
    ) {
        return Optional.ofNullable(queryDslSupport
                .selectFrom(getEntityPath())
                .where(unwrapPredicates(predicates))
                .orderBy(unwrapOrderSpecifiers(orderSpecifiers))
                .fetchFirst());
    }

    /**
     * Based on the given predicate, search for the first entity instance based on specified
     * {@link OrderSpecifier ordering}.
     *
     * @param predicate        the predicate(s) to apply as a filter
     * @param orderSpecifiers   the ordering to apply
     *
     * @return the first result or null
     *
     * @see #findFirstUsingDefaultOrder(Function[]) (Predicate[], OrderSpecifier[])
     * @see #findFirst(Function[], Function[])
     */
    public Optional<T> findFirst(
            final Function<Q, Predicate> predicate,
            final Function<Q, OrderSpecifier<?>>... orderSpecifiers) {
        return findFirst(asArray(predicate), orderSpecifiers);
    }

    /**
     * Based on the given predicate(s) search for the first entity instance based on specified
     * {@link OrderSpecifier ordering}.
     *
     * @param predicates the predicate(s) to apply as a filter
     *
     * @return the first result or null
     *
s     * @see #findFirst(Function[], Function[])
     * @see #findFirst(Function, Function[])
     */
    public Optional<T> findFirstUsingDefaultOrder(
            final Function<Q, Predicate>... predicates) {
        return findFirst(predicates, getDefaultOrdersAsArray());
    }

    /**
     * Based on the given predicate(s), search for applicable entity instances and return the distinct projection
     * (a subset of fields).
     *
     * <p>
     * <b>CAUTION</b>: when the supplied {@link OrderSpecifier ordering} is not aligned to the projection one might get
     * unexpected results, because the elimination of duplicates is based on the ordering, not the projection!
     * </p>
     *
     * @param projection        the projection that defines the field to return
     * @param predicates        the predicate(s) to apply as a filter (can be null)
     * @param orderSpecifiers   the ordering to apply; make sure aligns with the projection.
     *
     * @return the projection result ordered as specified
     *
     * @see #findFieldsDistinct(Function, Function[], Function[])
     * @see #findFieldsDistinctUsingDefaultOrder(Function, Function...)
     */
    public <F> List<F> findFieldsDistinct(
            final Function<Q, Expression<F>> projection,
            final @Nullable Function<Q, Predicate>[] predicates,
            final Function<Q, OrderSpecifier<?>>... orderSpecifiers) {
        return queryDslSupport
                .select(projection.apply(getEntityPath()))
                .distinct()
                .from(getEntityPath())
                .where(unwrapPredicates(predicates))
                .orderBy(unwrapOrderSpecifiers(orderSpecifiers))
                .fetch();
    }

    /**
     * Based on the given predicate, search for applicable entity instances and return the distinct projection
     * (a subset of fields).
     *
     * <p>
     * <b>CAUTION</b>: when the supplied {@link OrderSpecifier ordering} is not aligned to the projection one might get
     * unexpected results, because the elimination of duplicates is based on the ordering, not the projection!
     * </p>
     *
     * @param projection        the projection that defines the field to return
     * @param predicate the predicate function to apply as a filter (can be null).
     * @param orderSpecifiers    the ordering to apply; make sure aligns with the projection.
     *
     * @return the projection result ordered as specified
     *
     * @see #findFieldsDistinct(Function, Function[], Function[])
     * @see #findFieldsDistinctUsingDefaultOrder(Function, Function...)
     */
    public <F> List<F> findFieldsDistinct(
            final Function<Q, Expression<F>> projection,
            final @Nullable Function<Q, Predicate> predicate,
            final Function<Q, OrderSpecifier<?>>... orderSpecifiers) {
        return findFieldsDistinct(projection, asArray(predicate), orderSpecifiers);
    }

    /**
     * Based on the given predicate function search for applicable entity instances and return the distinct projection,
     * using the {@link #getDefaultOrders() default ordering}.
     *
     * <p>
     * <b>CAUTION</b>: when the {@link #getDefaultOrders() ordering} is not aligned to the projection one might get
     * unexpected results, because the elimination of duplicates is based on the ordering, not the projection!
     * </p>
     *
     * <p>
     *     NOTE: the default implementation of {@link #getDefaultOrders()} requires that the <code>id</code> field
     *     exists.  If this is not the case, then that method must be overridden.
     * </p>
     *
     * @param projection the projection that defines the field to return
     * @param predicates the predicate(s) to apply as a filter (can be null)
     * @return the ordered projection result
     *
     * @see #findFieldsDistinct(Function, Function[], Function[])
     * @see #findFieldsDistinct(Function, Function, Function[])
     */
    public <F> List<F> findFieldsDistinctUsingDefaultOrder(
            final Function<Q, Expression<F>> projection,
            final @Nullable Function<Q, Predicate>... predicates
    ) {
        return queryDslSupport
                .select(projection.apply(getEntityPath()))
                .distinct()
                .from(getEntityPath())
                .where(unwrapPredicates(predicates))
                .orderBy(getDefaultOrdersUnwrapped())
                .fetch();
    }

    /**
     * Based on the given predicate function search for applicable entity instances and return the first projection
     * based on specified {@link OrderSpecifier ordering}.
     *
     * <p>
     * <b>CAUTION</b>: when the supplied {@link OrderSpecifier ordering} is not aligned to the projection one might get
     * unexpected results, because the elimination of duplicates is based on the ordering, not the projection!
     * </p>
     *
     * @param projection        the projection that defines the field to return
     * @param predicates        the predicate(s) to apply as a filter (can be null)
     * @param orderSpecifiers    the ordering to apply
     * @return the first projection result based on the given order
     *
     * @see #findFirstFields(Function, Function, Function[])
     */
    public <F> Optional<F> findFirstFields(
            final Function<Q, Expression<F>> projection,
            final @Nullable Function<Q, Predicate>[] predicates,
            final Function<Q, OrderSpecifier<?>>... orderSpecifiers
    ) {
        return Optional.ofNullable(queryDslSupport
                .select(projection.apply(getEntityPath()))
                .distinct()
                .from(getEntityPath())
                .where(unwrapPredicates(predicates))
                .orderBy(unwrapOrderSpecifiers(orderSpecifiers))
                .fetchFirst());
    }

    /**
     * Based on the given predicate function search for applicable entity instances and return the first projection
     * based on specified {@link OrderSpecifier ordering}.
     *
     * <p>
     * <b>CAUTION</b>: when the supplied {@link OrderSpecifier ordering} is not aligned to the projection one might get
     * unexpected results, because the elimination of duplicates is based on the ordering, not the projection!
     * </p>
     *
     * @param projection        the projection that defines the field to return
     * @param predicate         the predicate to apply as a filter
     * @param orderSpecifiers    the ordering to apply
     * @return the first projection result based on the given order
     *
     * @see #findFirstFields(Function, Function[], Function[])
     */
    public <F> Optional<F> findFirstFields(
            final Function<Q, Expression<F>> projection,
            final @Nullable Function<Q, Predicate> predicate,
            final Function<Q, OrderSpecifier<?>>... orderSpecifiers
    ) {
        return findFirstFields(projection, asArray(predicate), orderSpecifiers);
    }

    /**
     * Based on the given predicate function search for applicable entity instances and return the first projection
     * based on the {@link #getDefaultOrders() default ordering}.
     *
     * <p>
     * <b>CAUTION</b>: when the {@link #getDefaultOrders() ordering} is not aligned to the projection one might get
     * unexpected results, because the elimination of duplicates is based on the ordering, not the projection!
     * </p>
     *
     * <p>
     *     NOTE: the default implementation of {@link #getDefaultOrders()} requires that the <code>id</code> field
     *     exists.  If this is not the case, then that method must be overridden.
     * </p>
     *
     * @param projection    the projection that defines the field to return
     * @param predicates    the predicate(s) to apply as a filter
     * @return the first projection result based on the default order
     *
     * @see #getDefaultOrders()
     */
    public <F> Optional<F> findFirstFieldsUsingDefaultOrder(
            final Function<Q, Expression<F>> projection,
            final @Nullable Function<Q, Predicate>... predicates
    ) {
        return findFirstFields(projection, predicates, getDefaultOrdersAsArray());
    }

    /**
     * Based on the given predicates search for exactly one or zero entity instance and return the projection.
     *
     * @param projection the projection that defines the field to return
     * @param predicates the predicates to apply as a filter
     * @return the projection result
     * @throws NonUniqueResultException if there is more than one matching result
     */
    public <F> Optional<F> findUniqueFields(
            final Function<Q, Expression<F>> projection,
            final Function<Q, Predicate>... predicates
    ) throws NonUniqueResultException {
        return Optional.ofNullable(queryDslSupport
                .select(projection.apply(getEntityPath()))
                .from(getEntityPath())
                .where(unwrapPredicates(predicates))
                .fetchOne());
    }

    /**
     * Based on the given predicate(s), search for applicable entity instances and apply the given
     * {@link OrderSpecifier ordering}.
     *
     * @param predicates the predicate(s) to apply as a filter
     * @param orderSpecifiers    the ordering to apply
     * @return the ordered instances
     *
     * @see #find(Function, Function[])
     * @see #findUsingDefaultOrder(Function[])
     */
    public List<T> find(
            final @Nullable Function<Q, Predicate>[] predicates,
            final Function<Q, OrderSpecifier<?>>... orderSpecifiers
    ) {
        return queryDslSupport
                .selectFrom(getEntityPath())
                .where(unwrapPredicates(predicates))
                .orderBy(unwrapOrderSpecifiers(orderSpecifiers))
                .fetch();
    }

    /**
     * Based on the given predicate(s), search for applicable entity instances and apply the given
     * {@link OrderSpecifier ordering}.
     *
     * @param predicate the predicate to apply as a filter (can be null)
     * @param orderSpecifiers    the ordering to apply
     * @return the ordered instances
     *
     * @see #find(Function[], Function[])
     * @see #findUsingDefaultOrder(Function[])
     */
    public List<T> find(
            final @Nullable Function<Q, Predicate> predicate,
            final Function<Q, OrderSpecifier<?>>... orderSpecifiers
    ) {
        return find(asArray(predicate), orderSpecifiers);
    }

    /**
     * Based on the given predicate function search for applicable entity instances and apply the
     * {@link #getDefaultOrders() default ordering}.
     *
     * <p>
     *     NOTE: the default implementation of {@link #getDefaultOrders()} requires that the <code>id</code> field
     *     exists.  If this is not the case, then that method must be overridden.
     * </p>
     *
     * @param predicates the predicate function to apply as a filter
     * @return the ordered instances
     *
     * @see #find(Function[], Function[])
     * @see #find(Function, Function[])
     */
    public List<T> findUsingDefaultOrder(final Function<Q, Predicate>... predicates) {
        return find(predicates, getDefaultOrdersAsArray());
    }

    /**
     * Based on the given predicate search for applicable entity instances,
     * then transform the results to the given bean using the given projections and apply the given ordering.
     *
     * @param projections       the projections which define which information to deliver to the bean creation
     * @param predicates        the predicate(s) to apply as a filter (can be null)
     * @param orderSpecifiers    the ordering to apply
     * @param bean              the bean class to use as a destination for the projection results
     * @param <B>               the bean type
     * @return the information defined by the projections as ordered beans
     *
     * @see #findAsBean(Function, Function, Class, Function[])
     */
    public <B> List<B> findAsBean(
            final Function<Q, Expression<?>>[] projections,
            final Class<? extends B> bean,
            final @Nullable Function<Q, Predicate>[] predicates,
            final Function<Q, OrderSpecifier<?>>... orderSpecifiers
    ) {
        return (List<B>) queryDslSupport
                .from(getEntityPath())
                .projection(Projections.bean(bean, unwrapProjections(projections)))
                .where(unwrapPredicates(predicates))
                .orderBy(unwrapOrderSpecifiers(orderSpecifiers))
                .fetch();
    }

    /**
     * Based on the given predicate search for applicable entity instances,
     * then transform the results to the given bean using the given projections and apply the given ordering.
     *
     * @param predicate         the predicate to apply as a filter (can be null)
     * @param orderSpecifier    the ordering to apply
     * @param bean              the bean class to use as a destination for the projection results
     * @param projections       the projections which define which information to deliver to the bean creation
     * @param <B>               the bean type
     * @return the information defined by the projections as ordered beans
     */
    public <B> List<B> findAsBean(
            final @Nullable Function<Q, Predicate> predicate,
            final Function<Q, OrderSpecifier<?>> orderSpecifier,
            final Class<? extends B> bean,
            final Function<Q, Expression<?>>... projections) {
        return (List<B>) queryDslSupport
                .from(getEntityPath())
                .projection(Projections.bean(bean, unwrapProjections(projections)))
                .where(unwrapPredicates(predicate))
                .orderBy(orderSpecifier.apply(getEntityPath()))
                .fetch();
    }

//not used
//    private <F> Expression<F>[] unwrapExpressions(Function<Q, Expression<F>>... expressions) {
//        return _Casts.uncheckedCast(Arrays.stream(expressions)
//                .map(x -> x.apply(getEntityPath()))
//                .collect(Collectors.toUnmodifiableList())
//                .toArray(new Expression[0]));
//    }

    private Expression<?>[] unwrapProjections(final Function<Q, Expression<?>>... projections) {
        return Arrays.stream(projections)
                .map(x -> x.apply(getEntityPath()))
                .collect(Collectors.toUnmodifiableList())
                .toArray(new Expression[0]);
    }

    private Predicate[] unwrapPredicates(final Function<Q, Predicate>... predicates) {
        if(predicates == null) {
            return new Predicate[]{new BooleanBuilder()};
        }
        if(predicates.length == 0) {
            return new Predicate[]{new BooleanBuilder()};
        }
        if(predicates.length == 1 && predicates[0] == null) {
            return new Predicate[]{new BooleanBuilder()};
        }
        return Arrays.stream(predicates)
                .map(x -> x.apply(getEntityPath()))
                .collect(Collectors.toUnmodifiableList())
                .toArray(new Predicate[0]);
    }

    private OrderSpecifier<?>[] unwrapOrderSpecifiers(final Function<Q, OrderSpecifier<?>>... orderSpecifiers) {
        return Arrays.stream(orderSpecifiers)
                .map(x -> x.apply(getEntityPath()))
                .collect(Collectors.toUnmodifiableList())
                .toArray(new OrderSpecifier[0]);
    }

    private static <T> Class<T> getTypeParameter(final Class<?> parameterizedType, final int index){
        if(parameterizedType==null) return null;

        ParameterizedType pType= (ParameterizedType) parameterizedType.getGenericSuperclass();
        if(pType==null) return null;

        Type[] types= pType.getActualTypeArguments();
        if(types==null || types.length<=index || types[index] instanceof ParameterizedType) return null;

        return (Class<T>) types[index];
    }

    private static <T extends Comparable, Q extends EntityPathBase<T>> Function<Q, Predicate>[] asArray(final Function<Q, Predicate> predicate) {
        return predicate != null
                ? (Function<Q, Predicate>[]) new Function[] {predicate}
                : new Function[0];
    }

    static <T> List<T> newList(final T... objs) {
        return newArrayList(objs);
    }

    static <T> ArrayList<T> newArrayList(final T... objs) {
        ArrayList<T> result = new ArrayList<>();
        Collections.addAll(result, objs);
        return result;
    }

}
