/*
 * Copyright 2014-2020 the original author or authors.
 *
 * Licensed 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
 *
 *      https://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.springframework.data.keyvalue.core;

import java.util.Collection;
import java.util.Optional;

import org.springframework.data.keyvalue.core.query.KeyValueQuery;
import org.springframework.lang.Nullable;

/**
 * Base implementation for accessing and executing {@link KeyValueQuery} against a {@link KeyValueAdapter}.
 *
 * @author Christoph Strobl
 * @author Mark Paluch
 * @param <ADAPTER>
 * @param <CRITERIA>
 * @param <SORT>
 */
public abstract class QueryEngine<ADAPTER extends KeyValueAdapter, CRITERIA, SORT> {

	private final Optional<CriteriaAccessor<CRITERIA>> criteriaAccessor;
	private final Optional<SortAccessor<SORT>> sortAccessor;

	private @Nullable ADAPTER adapter;

	public QueryEngine(@Nullable CriteriaAccessor<CRITERIA> criteriaAccessor, @Nullable SortAccessor<SORT> sortAccessor) {

		this.criteriaAccessor = Optional.ofNullable(criteriaAccessor);
		this.sortAccessor = Optional.ofNullable(sortAccessor);
	}

	/**
	 * Extract query attributes and delegate to concrete execution.
	 *
	 * @param query
	 * @param keyspace
	 * @return
	 */
	public Collection<?> execute(KeyValueQuery<?> query, String keyspace) {

		CRITERIA criteria = this.criteriaAccessor.map(it -> it.resolve(query)).orElse(null);
		SORT sort = this.sortAccessor.map(it -> it.resolve(query)).orElse(null);

		return execute(criteria, sort, query.getOffset(), query.getRows(), keyspace);
	}

	/**
	 * Extract query attributes and delegate to concrete execution.
	 *
	 * @param query
	 * @param keyspace
	 * @return
	 */
	public <T> Collection<T> execute(KeyValueQuery<?> query, String keyspace, Class<T> type) {

		CRITERIA criteria = this.criteriaAccessor.map(it -> it.resolve(query)).orElse(null);
		SORT sort = this.sortAccessor.map(it -> it.resolve(query)).orElse(null);

		return execute(criteria, sort, query.getOffset(), query.getRows(), keyspace, type);
	}

	/**
	 * Extract query attributes and delegate to concrete execution.
	 *
	 * @param query
	 * @param keyspace
	 * @return
	 */
	public long count(KeyValueQuery<?> query, String keyspace) {

		CRITERIA criteria = this.criteriaAccessor.map(it -> it.resolve(query)).orElse(null);
		return count(criteria, keyspace);
	}

	/**
	 * @param criteria
	 * @param sort
	 * @param offset
	 * @param rows
	 * @param keyspace
	 * @return
	 */
	public abstract Collection<?> execute(@Nullable CRITERIA criteria, @Nullable SORT sort, long offset, int rows,
			String keyspace);

	/**
	 * @param criteria
	 * @param sort
	 * @param offset
	 * @param rows
	 * @param keyspace
	 * @param type
	 * @return
	 * @since 1.1
	 */
	@SuppressWarnings("unchecked")
	public <T> Collection<T> execute(@Nullable CRITERIA criteria, @Nullable SORT sort, long offset, int rows,
			String keyspace, Class<T> type) {
		return (Collection<T>) execute(criteria, sort, offset, rows, keyspace);
	}

	/**
	 * @param criteria
	 * @param keyspace
	 * @return
	 */
	public abstract long count(@Nullable CRITERIA criteria, String keyspace);

	/**
	 * Get the {@link KeyValueAdapter} used.
	 *
	 * @return
	 */
	@Nullable
	protected ADAPTER getAdapter() {
		return this.adapter;
	}

	/**
	 * Get the required {@link KeyValueAdapter} used or throw {@link IllegalStateException} if the adapter is not set.
	 *
	 * @return the required {@link KeyValueAdapter}.
	 * @throws IllegalStateException if the adapter is not set.
	 */
	protected ADAPTER getRequiredAdapter() {

		ADAPTER adapter = getAdapter();

		if (adapter != null) {
			return adapter;
		}

		throw new IllegalStateException("Required KeyValueAdapter is not set!");
	}

	/**
	 * @param adapter
	 */
	@SuppressWarnings("unchecked")
	public void registerAdapter(KeyValueAdapter adapter) {

		if (this.adapter == null) {
			this.adapter = (ADAPTER) adapter;
		} else {
			throw new IllegalArgumentException("Cannot register more than one adapter for this QueryEngine.");
		}
	}
}