/**
 * 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.cxf.jaxrs.ext.search.jpa;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CompoundSelection;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Selection;
import javax.persistence.metamodel.SingularAttribute;

public class JPACriteriaQueryVisitor<T, E> extends AbstractJPATypedQueryVisitor<T, E, CriteriaQuery<E>> {

    public JPACriteriaQueryVisitor(EntityManager em,
                                   Class<T> tClass,
                                   Class<E> queryClass) {
        this(em, tClass, queryClass, null, null);
    }

    public JPACriteriaQueryVisitor(EntityManager em,
                                   Class<T> tClass,
                                   Class<E> queryClass,
                                   List<String> joinProps) {
        this(em, tClass, queryClass, null, joinProps);
    }

    public JPACriteriaQueryVisitor(EntityManager em,
                                   Class<T> tClass,
                                   Class<E> queryClass,
                                   Map<String, String> fieldMap) {
        super(em, tClass, queryClass, fieldMap);
    }

    public JPACriteriaQueryVisitor(EntityManager em,
                                   Class<T> tClass,
                                   Class<E> queryClass,
                                   Map<String, String> fieldMap,
                                   List<String> joinProps) {
        super(em, tClass, queryClass, fieldMap, joinProps);
    }

    public CriteriaQuery<E> getQuery() {
        return getCriteriaQuery();
    }

    public Long count() {
        if (super.getQueryClass() != Long.class) {
            throw new IllegalStateException("Query class needs to be of type Long");
        }
        @SuppressWarnings("unchecked")
        CriteriaQuery<Long> countQuery = (CriteriaQuery<Long>)getCriteriaQuery();
        countQuery.select(getCriteriaBuilder().count(getRoot()));
        return super.getEntityManager().createQuery(countQuery).getSingleResult();
    }

    public TypedQuery<E> getOrderedTypedQuery(List<SingularAttribute<T, ?>> attributes, boolean asc) {
        CriteriaQuery<E> cQuery = orderBy(attributes, asc);
        return getTypedQuery(cQuery);
    }

    public CriteriaQuery<E> orderBy(List<SingularAttribute<T, ?>> attributes, boolean asc) {
        CriteriaBuilder cb = getCriteriaBuilder();

        List<Order> orders = new ArrayList<>();
        for (SingularAttribute<T, ?> attribute : attributes) {
            Path<?> selection = getRoot().get(attribute);
            Order order = asc ? cb.asc(selection) : cb.desc(selection);
            orders.add(order);
        }
        return getCriteriaQuery().orderBy(orders);
    }

    public TypedQuery<E> getArrayTypedQuery(List<SingularAttribute<T, ?>> attributes) {
        CriteriaQuery<E> cQuery = selectArraySelections(toSelectionsArray(toSelectionsList(attributes, false)));
        return getTypedQuery(cQuery);
    }

    public CriteriaQuery<E> selectArray(List<SingularAttribute<T, ?>> attributes) {
        return selectArraySelections(toSelectionsArray(toSelectionsList(attributes, false)));
    }

    private CriteriaQuery<E> selectArraySelections(Selection<?>... selections) {
        @SuppressWarnings("unchecked")
        CompoundSelection<E> selection = (CompoundSelection<E>)getCriteriaBuilder().array(selections);
        getQuery().select(selection);
        return getQuery();
    }

    public CriteriaQuery<E> selectConstruct(List<SingularAttribute<T, ?>> attributes) {
        return selectConstructSelections(toSelectionsArray(toSelectionsList(attributes, false)));
    }

    public TypedQuery<E> getConstructTypedQuery(List<SingularAttribute<T, ?>> attributes) {
        CriteriaQuery<E> cQuery = selectConstructSelections(toSelectionsArray(toSelectionsList(attributes, false)));
        return getTypedQuery(cQuery);
    }

    private CriteriaQuery<E> selectConstructSelections(Selection<?>... selections) {
        getQuery().select(getCriteriaBuilder().construct(getQueryClass(), selections));
        return getQuery();
    }

    public CriteriaQuery<E> selectTuple(List<SingularAttribute<T, ?>> attributes) {
        return selectTupleSelections(toSelectionsArray(toSelectionsList(attributes, true)));
    }

    public TypedQuery<E> getTupleTypedQuery(List<SingularAttribute<T, ?>> attributes) {
        CriteriaQuery<E> cQuery = selectTupleSelections(toSelectionsArray(toSelectionsList(attributes, true)));
        return getTypedQuery(cQuery);
    }

    private CriteriaQuery<E> selectTupleSelections(Selection<?>... selections) {
        @SuppressWarnings("unchecked")
        CompoundSelection<E> selection =
            (CompoundSelection<E>)getCriteriaBuilder().tuple(selections);
        getQuery().select(selection);
        return getQuery();
    }

    private List<Selection<?>> toSelectionsList(List<SingularAttribute<T, ?>> attributes, boolean setAlias) {
        List<Selection<?>> selections = new ArrayList<Selection<?>>(attributes.size());
        for (SingularAttribute<T, ?> attr : attributes) {
            Path<?> path = getRoot().get(attr);
            path.alias(attr.getName());
            selections.add(path);
        }
        return selections;
    }

    private static Selection<?>[] toSelectionsArray(List<Selection<?>> selections) {
        return selections.toArray(new Selection[0]);
    }

    private TypedQuery<E> getTypedQuery(CriteriaQuery<E> theCriteriaQuery) {
        return super.getEntityManager().createQuery(theCriteriaQuery);
    }
}
