package org.springframework.data.simpledb.query;

import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.simpledb.annotation.Query;
import org.springframework.data.simpledb.core.SimpleDbDomain;
import org.springframework.data.simpledb.query.parser.QueryParserUtils;
import org.springframework.data.simpledb.reflection.ReflectionUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Iterator;
import java.util.List;

/**
 * SimpleDB specific extension of {@link org.springframework.data.repository.query.QueryMethod}. <br/>
 * Adds query extraction based on custom Query annotation and query validation helper methods.
 */
public class SimpleDbQueryMethod extends QueryMethod {

	// TODO add here custom query extractor
	private final Method method;
	private final SimpleDbDomain simpleDbDomain;

	/**
	 * Creates a new {@link org.springframework.data.simpledb.query.SimpleDbQueryMethod}
	 * 
	 * @param method
	 *            must not be {@literal null}
	 * @param metadata
	 *            must not be {@literal null}
	 * @param simpleDbDomain
	 */
	public SimpleDbQueryMethod(Method method, RepositoryMetadata metadata, SimpleDbDomain simpleDbDomain) {
		super(method, metadata);
		
		this.method = method;
		this.simpleDbDomain = simpleDbDomain;

		Assert.isTrue(!(isModifyingQuery() && getParameters().hasSpecialParameter()),
                String.format("Modifying method must not contain %s!", Parameters.TYPES));
	}

	private void assertParameterNamesInAnnotatedQuery(String annotatedQuery) {

		if(!StringUtils.hasText(annotatedQuery)) {
			return;
		}

		for(Parameter parameter : getParameters()) {

			if(!parameter.isNamedParameter()) {
				continue;
			}

			if(!annotatedQuery.contains(String.format(":%s", parameter.getName()))) {
				throw new IllegalStateException(String.format(
						"Using named parameters for method %s but parameter '%s' not found in annotated query '%s'!",
						method, parameter.getName(), annotatedQuery));
			}
		}
	}

	/**
	 * Returns the query string declared in a {@link Query} annotation or {@literal null} if neither the annotation
	 * found nor the attribute was specified.
	 * 
	 * @return a Query String
	 */
	public final String getAnnotatedQuery() {
		String valueParameter = getValueParameters();
		String whereParameters = getWhereParameters();
		String[] selectParameters = getSelectParameters();
		
		String result = QueryParserUtils.buildQueryFromQueryParameters(valueParameter, selectParameters, whereParameters,
				simpleDbDomain.getDomain(getDomainClass()));
		
		assertParameterNamesInAnnotatedQuery(result);
		
		return result;
	}

	protected String[] getSelectParameters() {
		return getAnnotationValue(Query.QueryClause.SELECT.getQueryClause(), String[].class);
	}
	protected String getWhereParameters() {
		return getAnnotationValue(Query.QueryClause.WHERE.getQueryClause(), String.class);
	}
	protected String getValueParameters() {
		return getAnnotationValue(Query.QueryClause.VALUE.getQueryClause(), String.class);
	}
	
	/**
	 * Returns the {@link Query} annotation's attribute casted to the given type or default value if no annotation
	 * available.
	 * 
	 * @param attribute
	 * @param type
	 * @return
	 */
	private <T> T getAnnotationValue(String attribute, Class<T> type) {

		Query annotation = method.getAnnotation(Query.class);
		Object value = annotation == null ? AnnotationUtils.getDefaultValue(Query.class, attribute) : AnnotationUtils
				.getValue(annotation, attribute);

		return type.cast(value);
	}

	public Class<?> getDomainClazz() {
		return super.getDomainClass();
	}

	public Class<?> getReturnType() {
		return method.getReturnType();
	}

	public boolean returnsFieldOfTypeCollection() {
		String query = getAnnotatedQuery();
		if(!isCollectionQuery()) {
			return false;
		}
		List<String> attributesFromQuery = QueryUtils.getQueryPartialFieldNames(query);
		if(attributesFromQuery.size() != 1) {
			return false;
		}
		String attributeName = attributesFromQuery.get(0);
		return ReflectionUtils.isOfType(method.getGenericReturnType(), getDomainClass(), attributeName);
	}

	public boolean returnsListOfListOfObject() {
		Type returnedGenericType = getCollectionGenericType();
		return ReflectionUtils.isListOfListOfObject(returnedGenericType);
	}

	public boolean returnsCollectionOfDomainClass() {
		Type returnedGenericType = getCollectionGenericType();
		return returnedGenericType.equals(getDomainClass());
	}

	private Type getCollectionGenericType() {
		Type returnType = method.getGenericReturnType();
		if(isCollectionQuery()) {
			return ((ParameterizedType) returnType).getActualTypeArguments()[0];
		} else {
			return returnType;
		}
	}

	/**
	 * @return whether or not the query method contains a {@link Pageable} parameter in its signature.
	 */
	public boolean isPagedQuery() {
		boolean isPaged = false;

		final Iterator<Parameter> it = getParameters().iterator();

		while(it.hasNext()) {
			final Parameter param = it.next();

			if(Pageable.class.isAssignableFrom(param.getType())) {
				isPaged = true;
				break;
			}
		}

		return isPaged;
	}
	
	public static boolean isAnnotatedQuery(final Method method) {
		return method.getAnnotation(Query.class) != null;
	}

}