package com.infoclinika.pdx.specification;

import org.springframework.data.jpa.domain.Specification;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Builder to create a sample specification.
 *
 * @param <T> type of Entity.
 */
@SuppressWarnings("ReturnOfNull")
public class SpecificationBuilder<T> {
    private static final Pattern PATTERN = Pattern.compile("(\\S+?)([:<>])(.+?);");
    private final List<SearchCriteria> params;

    public SpecificationBuilder() {
        params = new ArrayList<>();
    }

    /**
     * Parses query string as multiple search criteria and adds it to searching params.
     *
     * @param queryString string in format {field}{operation}{value};{field}{operation}{value}...
     *                    See {@link SearchCriteria}
     */
    public SpecificationBuilder<T> with(String queryString) {
        final Matcher matcher = PATTERN.matcher(queryString + ';');
        while (matcher.find()) {
            with(matcher.group(1), matcher.group(2), matcher.group(3));
        }
        return this;
    }

    /**
     * Parses query string as search criteria and adds it to searching params.
     * See {@link SearchCriteria}
     */
    public SpecificationBuilder<T> with(String key, String operation, String value) {
        params.add(new SearchCriteria(key, operation, value));
        return this;
    }

    /**
     * Removes search criteria with specified key.
     * See {@link SearchCriteria}
     */
    public SpecificationBuilder<T> without(String key) {
        final List<SearchCriteria> paramsToRemove = this.params.stream()
                                                               .filter(criteria -> criteria.getKey()
                                                                                           .equals(key))
                                                               .collect(Collectors.toList());
        this.params.removeAll(paramsToRemove);
        return this;
    }

    /**
     * Builds Specification.
     */
    public Specification<T> build() {
        if (params.isEmpty()) {
            return null;
        }

        final List<Specification<T>> specs = new ArrayList<>();
        for (SearchCriteria param : params) {
            specs.add(new EntitySpecification<>(param));
        }

        return concatenateWithAnd(specs);
    }

    /**
     * Builds Specification.
     */
    public Specification<T> build(FilterQuery filterQuery) {
        final List<SearchCriteria> params = filterQuery.getParams();
        if (params.isEmpty()) {
            return null;
        }

        final List<Specification<T>> specs = new ArrayList<>();
        for (SearchCriteria param : params) {
            specs.add(new EntitySpecification<>(param));
        }

        return concatenateWithAnd(specs);
    }

    private Specification<T> concatenateWithAnd(List<Specification<T>> specs) {
        Specification<T> result = specs.get(0);
        for (int i = 1; i < specs.size(); i++) {
            result = Specification.where(result)
                                  .and(specs.get(i));
        }
        return result;
    }
}
