package uk.ac.ebi.quickgo.rest.search.query;

import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;

/**
 * <p>This class defines an algorithm for serializing {@link QuickGOQuery}s into a corresponding
 * Solr query String. The implementation creates a corresponding String query that returns
 * results that cannot be guaranteed to be in the order of relevance: instead, results
 * may be returned in the order the entities were inserted into the underlying repository.
 *
 * <p>A reason for using this implementation would be for improved performance when searching for
 * large numbers of Solr documents by an indexed value, e.g. "find me all documents whose field1
 * has value id1 OR id2 OR ....".
 *
 * <p>Specifically, this class defines specific behaviour for {@link CompositeQuery}s containing ORs and
 * defers control to {@link SortedSolrQuerySerializer} otherwise.
 * {@link FieldQuery}s.
 *
 * Created 02/08/16
 *
 * @author Edd
 */
public class UnsortedSolrQuerySerializer implements QueryVisitor<String> {
    static final String TERMS_LOCAL_PARAMS_QUERY_FORMAT = "({!terms f=%s}%s)";

    private final SortedSolrQuerySerializer sortedQuerySerializer;
    private final Set<String> termsQueryCompatibleFields;

    public UnsortedSolrQuerySerializer(Set<String> termsQueryCompatibleFields) {
        this.sortedQuerySerializer = new SortedSolrQuerySerializer();

        this.termsQueryCompatibleFields = termsQueryCompatibleFields;
    }

    @Override
    public String visit(FieldQuery query) {
        if (isTermsQueryCompatible(query)) {
            return buildTermsQuery(query.field(), query.value());
        } else {
            return sortedQuerySerializer.visit(query);
        }
    }

    /**
     * Handles {@link CompositeQuery} instances identically to the behaviour in
     * {@link SortedSolrQuerySerializer}, except for when handling disjunctions (ORs). If
     * all of the queries associated with the disjunction are {@link FieldQuery}s, and
     * each are on the same field, then a Solr "LocalParams" terms query is created, which is a
     * more performant disjunction performed directly on the index, by-passing any scoring.
     * This becomes important if one constructs a query with many (e.g., thousands) of disjunctions.
     *
     * @param query the {@link CompositeQuery} which is to be serialized
     * @return the serialized String representation of the supplied {@code query}
     */
    @Override
    public String visit(CompositeQuery query) {
        CompositeQuery.QueryOp operator = query.queryOperator();
        Set<QuickGOQuery> queries = query.queries();

        if (queries.size() == 1 && operator.equals(CompositeQuery.QueryOp.NOT)) {
            String singletonQuery = queries.iterator().next().accept(this);
            return CompositeQuery.QueryOp.NOT + " (" + singletonQuery + ")";
        } else {
            String operatorText = " " + operator.name() + " ";

            if (operator.equals(CompositeQuery.QueryOp.AND)) {
                return queries.stream()
                        .map(q -> q.accept(this))
                        .collect(Collectors.joining(operatorText, "(", ")"));
            } else {
                // assume all queries in this OR are on the same field, and proceed to construct a terms query
                NestedOrSerializer nestedOrSerializer = new NestedOrSerializer();

                StringJoiner termsValuesJoiner = new StringJoiner(",");
                boolean allTransformed = true;
                for (QuickGOQuery q : queries) {
                    TermQueryTransformationResult transformResult = q.accept(nestedOrSerializer);
                    if (transformResult.successful) {
                        termsValuesJoiner.add(transformResult.value);
                    } else {
                        allTransformed = false;
                        break;
                    }
                }
                if (allTransformed) {
                    return buildTermsQuery(nestedOrSerializer.field, termsValuesJoiner.toString());
                } else {
                    // otherwise, re-use the default sorted serializer
                    return queries.stream()
                            .map(q -> q.accept(this))
                            .collect(Collectors.joining(operatorText, "(", ")"));
                }
            }
        }
    }

    private class TermQueryTransformationResult {
        private static final String DEFAULT_TRANSFORMATION_VALUE = "TransformationFailed";

        TermQueryTransformationResult(boolean successful, String value) {
            this.successful = successful;
            this.value = value;
        }

        TermQueryTransformationResult(boolean successful) {
            this(successful, DEFAULT_TRANSFORMATION_VALUE);
        }

        boolean successful;
        String value;
    }

    /**
     * The serializer that handles nested {@link CompositeQuery}s involving ORs, referred to in
     * {@link UnsortedSolrQuerySerializer#visit(uk.ac.ebi.quickgo.rest.search.query.CompositeQuery)}.
     * Specifically, this OR serializer, handles (visits) only simple {@link FieldQuery} instances. No
     * other {@link QuickGOQuery} subclass is handled.
     */
    private class NestedOrSerializer implements QueryVisitor<TermQueryTransformationResult> {
        private String field;

        @Override
        public TermQueryTransformationResult visit(FieldQuery query) {
            if (isTermsQueryCompatible(query)) {
                if (field != null && !field.equals(query.field())) {
                    throw new IllegalArgumentException(
                            "Fields must all be the same. Encountered: " + field + ", and " + query.field());
                } else {
                    field = query.field();
                }
                return new TermQueryTransformationResult(true, query.value());
            } else {
                return new TermQueryTransformationResult(false);
            }
        }

        @Override
        public TermQueryTransformationResult visit(CompositeQuery query) {
            return new TermQueryTransformationResult(false);
        }

        @Override
        public TermQueryTransformationResult visit(NoFieldQuery query) {
            return new TermQueryTransformationResult(false);
        }

        @Override
        public TermQueryTransformationResult visit(AllQuery query) {
            return new TermQueryTransformationResult(false);
        }

        @Override
        public TermQueryTransformationResult visit(JoinQuery query) {
            return new TermQueryTransformationResult(false);
        }
    }

    @Override
    public String visit(NoFieldQuery query) {
        return sortedQuerySerializer.visit(query);
    }

    @Override
    public String visit(AllQuery query) {
        return sortedQuerySerializer.visit(query);
    }

    @Override
    public String visit(JoinQuery query) {
        return sortedQuerySerializer.visit(query);
    }

    private String buildTermsQuery(String field, String... values) {
        StringJoiner stringJoiner = new StringJoiner(",");
        for (String value : values) {
            stringJoiner.add(value.toLowerCase());
        }
        return String.format(TERMS_LOCAL_PARAMS_QUERY_FORMAT, field, stringJoiner.toString());
    }

    private boolean isTermsQueryCompatible(FieldQuery query) {
        return termsQueryCompatibleFields.contains(query.field());
    }
}

@@ -729,16 +729,43 @@
 OR ....%22
+, i.e., a large disjunction
 .%0A *%0A * 
@@ -796,171 +796,213 @@
 ass 
-defines specific behaviour for %7B@link CompositeQuery%7Ds containing ORs and%0A * defers control to %7B@link SortedSolrQuerySerializer%7D otherwise.%0A * %7B@link FieldQuery%7Ds.
+produces queries that use the %22LocalParams%22 style of query, as documented here:%0A * %3Cul%3E%0A *     %3Cli%3Ehttps://cwiki.apache.org/confluence/display/solr/Other+Parsers#OtherParsers-TermsQueryParser%3C/li%3E%0A * %3C/ul%3E
 %0A *%0A

clarify javadoc