package com.infoclinika.pdx.specification;

import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.From;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Root;
import java.util.List;
import java.util.Objects;

import static java.util.stream.Collectors.toList;

/**
 * Utility methods to work with Spring JPA specifications.
 */
public final class SpecificationUtils {
    private static final String DELIMITER = ".";

    private SpecificationUtils() {
    }

    /**
     * Retrieves path from the root by provided attributes.
     *
     * @param root Query root corresponding to the given entity.
     * @param key  String with a list of attributes in format: {attr1}.{attr2}.{attr3}...
     * @param <T>  - type of Entity
     * @return Path by provided attributes.
     */
    public static <T> Path<Object> getPath(Root<T> root, String key) {
        if (key.contains(DELIMITER)) {
            final String[] keys = key.split("\\.");
            Join<?, ?> join = getOrCreateJoin(root, keys[0]);
            final int lastIndex = keys.length - 1;
            for (int i = 1; i < lastIndex; i++) {
                join = getOrCreateJoin(join, keys[i]);
            }
            return join.get(keys[lastIndex]);
        } else {
            return root.get(key);
        }
    }

    /**
     * Joins provided specifications into single specification using `OR` composition.
     *
     * @param specifications List of specifications.
     * @param <T>            Specification root class.
     * @return Composed specification of `null` if provided list is empty.
     */
    public static <T> Specification<T> joinWithOr(List<Specification<T>> specifications) {
        final List<Specification<T>> specs = specifications.stream()
                                                           .filter(Objects::nonNull)
                                                           .collect(toList());
        if (specs.isEmpty()) {
            return null;
        }

        Specification<T> result = specs.get(0);
        for (int i = 1; i < specs.size(); i++) {
            result = Specification.where(result)
                                  .or(specs.get(i));
        }
        return result;
    }

    /**
     * Joins provided specifications into single specification using `AND` composition.
     *
     * @param specifications List of specifications.
     * @param <T>            Specification root class.
     * @return Composed specification of `null` if provided list is empty.
     */
    public static <T> Specification<T> joinWithAnd(List<Specification<T>> specifications) {
        final List<Specification<T>> specs = specifications.stream()
                                                           .filter(Objects::nonNull)
                                                           .collect(toList());
        if (specs.isEmpty()) {
            return null;
        }

        Specification<T> result = specs.get(0);
        for (int i = 1; i < specs.size(); i++) {
            result = Specification.where(result)
                                  .and(specs.get(i));
        }
        return result;
    }

    private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute) {
        for (Join<?, ?> join : from.getJoins()) {
            final boolean sameName = join.getAttribute()
                                         .getName()
                                         .equals(attribute);
            if (sameName && join.getJoinType()
                                .equals(JoinType.INNER)) {
                return join;
            }
        }
        return from.join(attribute);
    }
}
