package org.springframework.data.mybatis.repository.query;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import org.apache.ibatis.mapping.SqlCommandType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.mybatis.repository.Modifying;
import org.springframework.data.mybatis.repository.Query;
import org.springframework.data.mybatis.repository.ResultMap;
import org.springframework.data.mybatis.repository.SelectColumns;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

public class MybatisQueryMethod extends QueryMethod {

	private static final Set<Class<?>> NATIVE_ARRAY_TYPES;

	static {

		Set<Class<?>> types = new HashSet<>();
		types.add(byte[].class);
		types.add(Byte[].class);
		types.add(char[].class);
		types.add(Character[].class);

		NATIVE_ARRAY_TYPES = Collections.unmodifiableSet(types);
	}

	private final Method method;

	private final RepositoryMetadata metadata;

	private final String namespace;

	private final String statementName;

	private Integer limitSize;

	/**
	 * Creates a new {@link QueryMethod} from the given parameters. Looks up the correct
	 * query to use for following invocations of the method given.
	 * @param method must not be {@literal null}.
	 * @param metadata must not be {@literal null}.
	 * @param factory must not be {@literal null}.
	 */
	public MybatisQueryMethod(Method method, RepositoryMetadata metadata,
			ProjectionFactory factory) {

		super(method, metadata, factory);

		Assert.notNull(method, "Method must not be null!");

		this.method = method;

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

		this.metadata = metadata;
		String namespace = getAnnotationValue("namespace", String.class);
		String statementName = getAnnotationValue("statement", String.class);
		this.namespace = StringUtils.hasText(namespace) ? namespace
				: metadata.getRepositoryInterface().getName();
		this.statementName = StringUtils.hasText(statementName) ? statementName
				: (method.getName() + UUID.randomUUID().toString().replace("-", ""));

	}

	@Override
	public MybatisEntityMetadata<?> getEntityInformation() {
		return new DefaultMybatisEntityMetadata<>(getDomainClass());
	}

	@Override
	public boolean isModifyingQuery() {
		return null != AnnotationUtils.findAnnotation(method, Modifying.class);
	}

	public SqlCommandType getModifyingType() {
		if (!isModifyingQuery()) {
			throw new IllegalStateException(String.format(
					"No annotated @Modifying found for query method %s!", getName()));
		}

		String value = getMergedOrDefaultAnnotationValue("value", Modifying.class,
				String.class);
		if (StringUtils.isEmpty(value)) {
			return null;
		}
		if ("insert".equalsIgnoreCase(value)) {
			return SqlCommandType.INSERT;
		}
		if ("update".equalsIgnoreCase(value)) {
			return SqlCommandType.UPDATE;
		}
		if ("delete".equalsIgnoreCase(value)) {
			return SqlCommandType.DELETE;
		}
		return null;
	}

	@Override
	protected MybatisParameters createParameters(Method method) {
		return new MybatisParameters(method);
	}

	@Override
	public MybatisParameters getParameters() {
		return (MybatisParameters) super.getParameters();
	}

	@Override
	public boolean isCollectionQuery() {
		return super.isCollectionQuery()
				&& !NATIVE_ARRAY_TYPES.contains(method.getReturnType());
	}

	Class<?> getReturnType() {

		return method.getReturnType();
	}

	private <T> T getAnnotationValue(String attribute, Class<T> type) {
		return getMergedOrDefaultAnnotationValue(attribute, Query.class, type);
	}

	private <T> T getMergedOrDefaultAnnotationValue(String attribute,
			Class annotationType, Class<T> targetType) {

		Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(method,
				annotationType);
		if (annotation == null) {
			return targetType
					.cast(AnnotationUtils.getDefaultValue(annotationType, attribute));
		}

		return targetType.cast(AnnotationUtils.getValue(annotation, attribute));
	}

	/**
	 * If has {@link Query} annotation, will be a Simple Query. <br/>
	 * Strategy: if just has value, will use value as SQL will generate a statement with
	 * namespace as repository'name and statement name as method name + UUID; if no value
	 * set, will scan mapper with defined statement.
	 * @return
	 */
	public boolean isAnnotatedQuery() {
		return null != AnnotationUtils.findAnnotation(method, Query.class);
	}

	public String getNamedQueryName() {
		String annotatedName = getAnnotationValue("name", String.class);
		return StringUtils.hasText(annotatedName) ? annotatedName
				: super.getNamedQueryName();
	}

	@Nullable
	public String getAnnotatedQuery() {

		String query = getAnnotationValue("value", String.class);
		return StringUtils.hasText(query) ? query : null;
	}

	public String getRequiredAnnotatedQuery() throws IllegalStateException {

		String query = getAnnotatedQuery();

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

		throw new IllegalStateException(String
				.format("No annotated query found for query method %s!", getName()));
	}

	@Nullable
	public String getCountQuery() {

		String countQuery = getAnnotationValue("countQuery", String.class);
		return StringUtils.hasText(countQuery) ? countQuery : null;
	}

	public String getNamedCountQueryName() {

		String annotatedName = getAnnotationValue("countName", String.class);
		return StringUtils.hasText(annotatedName) ? annotatedName
				: getNamedQueryName() + ".count";
	}

	@Nullable
	public String getQueryCountNamespace() {

		String annotatedName = getAnnotationValue("countNamespace", String.class);
		return StringUtils.hasText(annotatedName) ? annotatedName
				: getNamespace() + ".count";

	}

	@Nullable
	public String getQueryCountStatement() {
		String annotatedName = getAnnotationValue("countStatement", String.class);
		return StringUtils.hasText(annotatedName) ? annotatedName
				: getStatementName() + ".count";
	}

	public RepositoryMetadata getMetadata() {
		return metadata;
	}

	public String getNamespace() {

		String namespace = getAnnotationValue("namespace", String.class);
		return StringUtils.hasText(namespace) ? namespace : this.namespace;

	}

	public String getStatementName() {
		String statement = getAnnotationValue("statement", String.class);
		return StringUtils.hasText(statement) ? statement
				: (isAnnotatedQuery() ? method.getName() : this.statementName);
	}

	public String getStatementId() {
		return getNamespace() + '.' + getStatementName();
	}

	public Integer getLimitSize() {
		return limitSize;
	}

	public void setLimitSize(Integer limitSize) {
		this.limitSize = limitSize;
	}

	public String getCountStatementName() {
		String statementName = getAnnotationValue("countStatement", String.class);
		return StringUtils.hasText(statementName) ? statementName
				: ("count_" + getStatementName());
	}

	public String getCountStatementId() {
		return getNamespace() + "." + getCountStatementName();
	}

	public String getSelectColumns() {

		SelectColumns columns = method.getAnnotation(SelectColumns.class);
		if (null == columns || StringUtils.isEmpty(columns.value())) {
			return null;
		}

		return columns.value();

	}

	public String getResultMap() {
		ResultMap resultMap = method.getAnnotation(ResultMap.class);
		if (null == resultMap || StringUtils.isEmpty(resultMap.value())) {
			return null;
		}
		return resultMap.value();
	}

}