/*
 * Copyright 2013 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
 *
 * http://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.socialsignin.spring.data.dynamodb.repository.support;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
import org.springframework.util.ReflectionUtils.MethodCallback;

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBIndexRangeKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMarshaller;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMarshalling;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBRangeKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBVersionAttribute;

/**
 * @author Michael Lavelle
 */
public class DynamoDBEntityMetadataSupport<T, ID extends Serializable> implements DynamoDBHashKeyExtractingEntityMetadata<T> {

	private final Class<T> domainType;
	private boolean hasRangeKey;
	private String hashKeyPropertyName;
	private List<String> globalIndexHashKeyPropertyNames;
	private List<String> globalIndexRangeKeyPropertyNames;

	private String dynamoDBTableName;
	private Map<String, String[]> globalSecondaryIndexNames = new HashMap<String, String[]>();

	@Override
	public String getDynamoDBTableName() {
		return dynamoDBTableName;
	}

	/**
	 * Creates a new {@link DynamoDBEntityMetadataSupport} for the given domain type.
	 *
	 * @param domainType
	 *            must not be {@literal null}.
	 */
	public DynamoDBEntityMetadataSupport(final Class<T> domainType) {

		Assert.notNull(domainType, "Domain type must not be null!");
		this.domainType = domainType;
		DynamoDBTable table = this.domainType.getAnnotation(DynamoDBTable.class);
		Assert.notNull(table, "Domain type must by annotated with DynamoDBTable!");
		this.dynamoDBTableName = table.tableName();
		this.globalSecondaryIndexNames = new HashMap<String, String[]>();
		this.globalIndexHashKeyPropertyNames = new ArrayList<String>();
		this.globalIndexRangeKeyPropertyNames = new ArrayList<String>();
		ReflectionUtils.doWithMethods(domainType, new MethodCallback() {
			@Override
            public void doWith(Method method) {
				if (method.getAnnotation(DynamoDBHashKey.class) != null) {
					hashKeyPropertyName = getPropertyNameForAccessorMethod(method);
				}
				if (method.getAnnotation(DynamoDBRangeKey.class) != null) {
					hasRangeKey = true;
				}
				DynamoDBIndexRangeKey dynamoDBRangeKeyAnnotation = method.getAnnotation(DynamoDBIndexRangeKey.class);
				DynamoDBIndexHashKey dynamoDBHashKeyAnnotation = method.getAnnotation(DynamoDBIndexHashKey.class);

				if (dynamoDBRangeKeyAnnotation != null) {
					addGlobalSecondaryIndexNames(method, dynamoDBRangeKeyAnnotation);
				}
				if (dynamoDBHashKeyAnnotation != null) {
					addGlobalSecondaryIndexNames(method, dynamoDBHashKeyAnnotation);
				}
			}
		});
		ReflectionUtils.doWithFields(domainType, new FieldCallback() {
			@Override
            public void doWith(Field field) {
				if (field.getAnnotation(DynamoDBHashKey.class) != null) {
					hashKeyPropertyName = getPropertyNameForField(field);
				}
				if (field.getAnnotation(DynamoDBRangeKey.class) != null) {
					hasRangeKey = true;
				}
				DynamoDBIndexRangeKey dynamoDBRangeKeyAnnotation = field.getAnnotation(DynamoDBIndexRangeKey.class);
				DynamoDBIndexHashKey dynamoDBHashKeyAnnotation = field.getAnnotation(DynamoDBIndexHashKey.class);

				if (dynamoDBRangeKeyAnnotation != null) {
					addGlobalSecondaryIndexNames(field, dynamoDBRangeKeyAnnotation);
				}
				if (dynamoDBHashKeyAnnotation != null) {
					addGlobalSecondaryIndexNames(field, dynamoDBHashKeyAnnotation);
				}
			}
		});
		Assert.notNull(hashKeyPropertyName, "Unable to find hash key field or getter method on " + domainType + "!");
	}

	public DynamoDBEntityInformation<T, ID> getEntityInformation() {

		if (hasRangeKey) {
			DynamoDBHashAndRangeKeyExtractingEntityMetadataImpl<T, ID> metadata = new DynamoDBHashAndRangeKeyExtractingEntityMetadataImpl<T, ID>(
					domainType);
			return new DynamoDBIdIsHashAndRangeKeyEntityInformationImpl<T, ID>(domainType, metadata);
		} else {
			return new DynamoDBIdIsHashKeyEntityInformationImpl<T, ID>(domainType, this);
		}
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see
	 * org.springframework.data.repository.core.EntityMetadata#getJavaType()
	 */
	@Override
	public Class<T> getJavaType() {
		return domainType;
	}

	@Override
    public boolean isHashKeyProperty(String propertyName) {
		return hashKeyPropertyName.equals(propertyName);
	}

	protected boolean isFieldAnnotatedWith(final String propertyName, final Class<? extends Annotation> annotation) {

		Field field = findField(propertyName);
		return field != null && field.getAnnotation(annotation) != null;
	}

	private String toGetMethodName(String propertyName) {
		String methodName = propertyName.substring(0, 1).toUpperCase();
		if (propertyName.length() > 1) {
			methodName = methodName + propertyName.substring(1);
		}
		return "get" + methodName;
	}

	protected String toSetterMethodNameFromAccessorMethod(Method method) {
		String accessorMethodName = method.getName();
		if (accessorMethodName.startsWith("get")) {
			return "set" + accessorMethodName.substring(3);
		} else if (accessorMethodName.startsWith("is")) {
			return "is" + accessorMethodName.substring(2);
		}
		return null;
	}

	private String toIsMethodName(String propertyName) {
		String methodName = propertyName.substring(0, 1).toUpperCase();
		if (propertyName.length() > 1) {
			methodName = methodName + propertyName.substring(1);
		}
		return "is" + methodName;
	}

	private Method findMethod(String propertyName) {
		Method method = ReflectionUtils.findMethod(domainType, toGetMethodName(propertyName));
		if (method == null) {
			method = ReflectionUtils.findMethod(domainType, toIsMethodName(propertyName));
		}
		return method;

	}

	private Field findField(String propertyName) {
		return ReflectionUtils.findField(domainType, propertyName);
	}

	public String getOverriddenAttributeName(Method method) {

		if (method != null) {
			if (method.getAnnotation(DynamoDBAttribute.class) != null
					&& StringUtils.isNotEmpty(method.getAnnotation(DynamoDBAttribute.class).attributeName())) {
				return method.getAnnotation(DynamoDBAttribute.class).attributeName();
			}
			if (method.getAnnotation(DynamoDBHashKey.class) != null
					&& StringUtils.isNotEmpty(method.getAnnotation(DynamoDBHashKey.class).attributeName())) {
				return method.getAnnotation(DynamoDBHashKey.class).attributeName();
			}
			if (method.getAnnotation(DynamoDBRangeKey.class) != null
					&& StringUtils.isNotEmpty(method.getAnnotation(DynamoDBRangeKey.class).attributeName())) {
				return method.getAnnotation(DynamoDBRangeKey.class).attributeName();
			}
			if (method.getAnnotation(DynamoDBIndexRangeKey.class) != null
					&& StringUtils.isNotEmpty(method.getAnnotation(DynamoDBIndexRangeKey.class).attributeName())) {
				return method.getAnnotation(DynamoDBIndexRangeKey.class).attributeName();
			}
			if (method.getAnnotation(DynamoDBIndexHashKey.class) != null
					&& StringUtils.isNotEmpty(method.getAnnotation(DynamoDBIndexHashKey.class).attributeName())) {
				return method.getAnnotation(DynamoDBIndexHashKey.class).attributeName();
			}
			if (method.getAnnotation(DynamoDBVersionAttribute.class) != null
					&& StringUtils.isNotEmpty(method.getAnnotation(DynamoDBVersionAttribute.class).attributeName())) {
				return method.getAnnotation(DynamoDBVersionAttribute.class).attributeName();
			}
		}
		return null;

	}

	@Override
	public String getOverriddenAttributeName(final String propertyName) {

		Method method = findMethod(propertyName);
		if (method != null) {
			if (method.getAnnotation(DynamoDBAttribute.class) != null
					&& StringUtils.isNotEmpty(method.getAnnotation(DynamoDBAttribute.class).attributeName())) {
				return method.getAnnotation(DynamoDBAttribute.class).attributeName();
			}
			if (method.getAnnotation(DynamoDBHashKey.class) != null
					&& StringUtils.isNotEmpty(method.getAnnotation(DynamoDBHashKey.class).attributeName())) {
				return method.getAnnotation(DynamoDBHashKey.class).attributeName();
			}
			if (method.getAnnotation(DynamoDBRangeKey.class) != null
					&& StringUtils.isNotEmpty(method.getAnnotation(DynamoDBRangeKey.class).attributeName())) {
				return method.getAnnotation(DynamoDBRangeKey.class).attributeName();
			}
			if (method.getAnnotation(DynamoDBIndexRangeKey.class) != null
					&& StringUtils.isNotEmpty(method.getAnnotation(DynamoDBIndexRangeKey.class).attributeName())) {
				return method.getAnnotation(DynamoDBIndexRangeKey.class).attributeName();
			}
			if (method.getAnnotation(DynamoDBIndexHashKey.class) != null
					&& StringUtils.isNotEmpty(method.getAnnotation(DynamoDBIndexHashKey.class).attributeName())) {
				return method.getAnnotation(DynamoDBIndexHashKey.class).attributeName();
			}
			if (method.getAnnotation(DynamoDBVersionAttribute.class) != null
					&& StringUtils.isNotEmpty(method.getAnnotation(DynamoDBVersionAttribute.class).attributeName())) {
				return method.getAnnotation(DynamoDBVersionAttribute.class).attributeName();
			}
		}

		Field field = findField(propertyName);
		if (field != null) {
			if (field.getAnnotation(DynamoDBAttribute.class) != null
					&& StringUtils.isNotEmpty(field.getAnnotation(DynamoDBAttribute.class).attributeName())) {
				return field.getAnnotation(DynamoDBAttribute.class).attributeName();
			}
			if (field.getAnnotation(DynamoDBHashKey.class) != null
					&& StringUtils.isNotEmpty(field.getAnnotation(DynamoDBHashKey.class).attributeName())) {
				return field.getAnnotation(DynamoDBHashKey.class).attributeName();
			}
			if (field.getAnnotation(DynamoDBRangeKey.class) != null
					&& StringUtils.isNotEmpty(field.getAnnotation(DynamoDBRangeKey.class).attributeName())) {
				return field.getAnnotation(DynamoDBRangeKey.class).attributeName();
			}
			if (field.getAnnotation(DynamoDBIndexRangeKey.class) != null
					&& StringUtils.isNotEmpty(field.getAnnotation(DynamoDBIndexRangeKey.class).attributeName())) {
				return field.getAnnotation(DynamoDBIndexRangeKey.class).attributeName();
			}
			if (field.getAnnotation(DynamoDBIndexHashKey.class) != null
					&& StringUtils.isNotEmpty(field.getAnnotation(DynamoDBIndexHashKey.class).attributeName())) {
				return field.getAnnotation(DynamoDBIndexHashKey.class).attributeName();
			}
			if (field.getAnnotation(DynamoDBVersionAttribute.class) != null
					&& StringUtils.isNotEmpty(field.getAnnotation(DynamoDBVersionAttribute.class).attributeName())) {
				return field.getAnnotation(DynamoDBVersionAttribute.class).attributeName();
			}
		}
		return null;

	}

	@Override
	public DynamoDBMarshaller<?> getMarshallerForProperty(final String propertyName) {

		Method method = findMethod(propertyName);
		if (method != null && method.getAnnotation(DynamoDBMarshalling.class) != null) {
			try {
				return method.getAnnotation(DynamoDBMarshalling.class).marshallerClass().newInstance();
			} catch (InstantiationException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);

			}
		}

		return null;
	}

	protected String getPropertyNameForAccessorMethod(Method method) {
		String methodName = method.getName();
		String propertyName = null;
		if (methodName.startsWith("get")) {
			propertyName = methodName.substring(3);
		} else if (methodName.startsWith("is")) {
			propertyName = methodName.substring(2);
		}
		Assert.notNull(propertyName, "Hash or range key annotated accessor methods must start with 'get' or 'is'");

		String firstLetter = propertyName.substring(0, 1);
		String remainder = propertyName.substring(1);
		return firstLetter.toLowerCase() + remainder;
	}

	protected String getPropertyNameForField(Field field) {
		return field.getName();
	}

	@Override
	public String getHashKeyPropertyName() {
		return hashKeyPropertyName;
	}

	private void addGlobalSecondaryIndexNames(Method method, DynamoDBIndexRangeKey dynamoDBIndexRangeKey) {

		if (dynamoDBIndexRangeKey.globalSecondaryIndexNames() != null
				&& dynamoDBIndexRangeKey.globalSecondaryIndexNames().length > 0) {
			String propertyName = getPropertyNameForAccessorMethod(method);

			globalSecondaryIndexNames.put(propertyName, method.getAnnotation(DynamoDBIndexRangeKey.class)
					.globalSecondaryIndexNames());
			globalIndexRangeKeyPropertyNames.add(propertyName);

		}
		if (dynamoDBIndexRangeKey.globalSecondaryIndexName() != null
				&& dynamoDBIndexRangeKey.globalSecondaryIndexName().trim().length() > 0) {
			String propertyName = getPropertyNameForAccessorMethod(method);
			globalSecondaryIndexNames.put(propertyName,
					new String[] { method.getAnnotation(DynamoDBIndexRangeKey.class).globalSecondaryIndexName() });
			globalIndexRangeKeyPropertyNames.add(propertyName);

		}

	}

	private void addGlobalSecondaryIndexNames(Field field, DynamoDBIndexRangeKey dynamoDBIndexRangeKey) {

		if (dynamoDBIndexRangeKey.globalSecondaryIndexNames() != null
				&& dynamoDBIndexRangeKey.globalSecondaryIndexNames().length > 0) {
			String propertyName = getPropertyNameForField(field);

			globalSecondaryIndexNames.put(propertyName, field.getAnnotation(DynamoDBIndexRangeKey.class)
					.globalSecondaryIndexNames());
			globalIndexRangeKeyPropertyNames.add(propertyName);

		}
		if (dynamoDBIndexRangeKey.globalSecondaryIndexName() != null
				&& dynamoDBIndexRangeKey.globalSecondaryIndexName().trim().length() > 0) {
			String propertyName = getPropertyNameForField(field);
			globalSecondaryIndexNames.put(propertyName,
					new String[] { field.getAnnotation(DynamoDBIndexRangeKey.class).globalSecondaryIndexName() });
			globalIndexRangeKeyPropertyNames.add(propertyName);

		}

	}

	private void addGlobalSecondaryIndexNames(Method method, DynamoDBIndexHashKey dynamoDBIndexHashKey) {

		if (dynamoDBIndexHashKey.globalSecondaryIndexNames() != null
				&& dynamoDBIndexHashKey.globalSecondaryIndexNames().length > 0) {
			String propertyName = getPropertyNameForAccessorMethod(method);

			globalSecondaryIndexNames.put(propertyName, method.getAnnotation(DynamoDBIndexHashKey.class)
					.globalSecondaryIndexNames());
			globalIndexHashKeyPropertyNames.add(propertyName);

		}
		if (dynamoDBIndexHashKey.globalSecondaryIndexName() != null
				&& dynamoDBIndexHashKey.globalSecondaryIndexName().trim().length() > 0) {
			String propertyName = getPropertyNameForAccessorMethod(method);

			globalSecondaryIndexNames.put(propertyName,
					new String[] { method.getAnnotation(DynamoDBIndexHashKey.class).globalSecondaryIndexName() });
			globalIndexHashKeyPropertyNames.add(propertyName);

		}
	}

	private void addGlobalSecondaryIndexNames(Field field, DynamoDBIndexHashKey dynamoDBIndexHashKey) {

		if (dynamoDBIndexHashKey.globalSecondaryIndexNames() != null
				&& dynamoDBIndexHashKey.globalSecondaryIndexNames().length > 0) {
			String propertyName = getPropertyNameForField(field);

			globalSecondaryIndexNames.put(propertyName, field.getAnnotation(DynamoDBIndexHashKey.class)
					.globalSecondaryIndexNames());
			globalIndexHashKeyPropertyNames.add(propertyName);

		}
		if (dynamoDBIndexHashKey.globalSecondaryIndexName() != null
				&& dynamoDBIndexHashKey.globalSecondaryIndexName().trim().length() > 0) {
			String propertyName = getPropertyNameForField(field);

			globalSecondaryIndexNames.put(propertyName,
					new String[] { field.getAnnotation(DynamoDBIndexHashKey.class).globalSecondaryIndexName() });
			globalIndexHashKeyPropertyNames.add(propertyName);

		}
	}

	@Override
	public Map<String, String[]> getGlobalSecondaryIndexNamesByPropertyName() {
		return globalSecondaryIndexNames;
	}

	@Override
	public boolean isGlobalIndexHashKeyProperty(String propertyName) {
		return globalIndexHashKeyPropertyNames.contains(propertyName);
	}

	@Override
	public boolean isGlobalIndexRangeKeyProperty(String propertyName) {
		return globalIndexRangeKeyPropertyNames.contains(propertyName);
	}

}