/*
 * Copyright 2002-2014 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.springframework.data.crate.core.mapping;

import static java.lang.String.format;
import static java.util.Arrays.asList;
import static org.springframework.util.StringUtils.hasText;
import static org.springframework.util.StringUtils.replace;

import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.BeanFactoryAccessor;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.data.crate.core.mapping.annotations.Table;
import org.springframework.data.crate.core.mapping.schema.TableParameters;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.model.BasicPersistentEntity;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.util.TypeInformation;
import org.springframework.expression.spel.support.StandardEvaluationContext;


/**
 * Crate specific {@link org.springframework.data.mapping.PersistentEntity} implementation holding
 *
 * @param <T>
 * @author Rizwan Idrees
 * @author Hasnain Javed
 */
public class SimpleCratePersistentEntity<T> extends BasicPersistentEntity<T, CratePersistentProperty>
		implements CratePersistentEntity<T>, ApplicationContextAware {

	private final static String VERSION_TYPE = "Version property '%s' must be of type java.lang.Long";
	
	private final StandardEvaluationContext context;
	
	private final String tableName;
	private final TableParameters parameters;

	public SimpleCratePersistentEntity(TypeInformation<T> typeInformation) {
		super(typeInformation);
		this.context = new StandardEvaluationContext();
		this.tableName = resolveTableName(typeInformation);
		this.parameters = resolveTableParameters();
 	}
	
	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		context.addPropertyAccessor(new BeanFactoryAccessor());
		context.setBeanResolver(new BeanFactoryResolver(applicationContext));
		context.setRootObject(applicationContext);
	}
	
	@Override
	public void addPersistentProperty(CratePersistentProperty property) {
		
		super.addPersistentProperty(property);
		
		if(property.isVersionProperty() && !isLongType(property.getType())) {
			throw new MappingException(format(VERSION_TYPE, property.getFieldName()));
		}
	}
	
	@Override
	public String getTableName() {
		return tableName;
	}
	
	@Override
	public TableParameters getTableParameters() {
		return parameters;
	}

	@Override
	public Set<String> getPropertyNames(String... exclude) {
		
		Set<CratePersistentProperty> properties = getPersistentProperties();
		
		Set<String> excluded = new HashSet<>(asList(exclude));
		
		Set<String> propertyNames = new TreeSet<>();
		
		for(CratePersistentProperty property : properties) {
			if(!excluded.contains(property.getFieldName())) {
				propertyNames.add(property.getFieldName());
			}
		}
		
		return propertyNames;
	}
	
	/**
	 * Returns all fields excluding static and transient fields
	 */
	@Override
	public Set<CratePersistentProperty> getPersistentProperties() {

		final Set<CratePersistentProperty> properties = new LinkedHashSet<>();
		
		doWithProperties(new PropertyHandler<CratePersistentProperty>() {
			@Override
			public void doWithPersistentProperty(CratePersistentProperty persistentProperty) {
				properties.add(persistentProperty);
			}
		});
		
		return properties;
	}
	
	/**
	 * Returns all primitive fields (String, int, etc)
	 */
	@Override
	public Set<CratePersistentProperty> getPrimitiveProperties() {
		
		Set<CratePersistentProperty> properties = getPersistentProperties();
		Set<CratePersistentProperty> simpleProperties = new LinkedHashSet<>();
		
		for(CratePersistentProperty property : properties) {
			
			boolean isCollectionLike = property.isCollectionLike();
			boolean isMap = property.isMap();
			boolean isEntity = property.isEntity();
			
			if(!isCollectionLike && !isMap && !isEntity) {
				simpleProperties.add(property);
			}
		}
		
		return simpleProperties;
	}

	/**
	 * Returns all non primitive (entity) type fields
	 */
	@Override
	public Set<CratePersistentProperty> getEntityProperties() {
		
		Set<CratePersistentProperty> properties = getPersistentProperties();
		Set<CratePersistentProperty> compositeProperties = new LinkedHashSet<>();
		
		for(CratePersistentProperty property : properties) {
			
			boolean isCollectionLike = property.isCollectionLike();
			boolean isMap = property.isMap();
			boolean isEntity = property.isEntity();
			
			if(!isCollectionLike && !isMap && isEntity) {
				compositeProperties.add(property);
			}
		}
		
		return compositeProperties;
	}
	
	/**
	 * Returns all java.util.Collection implementations
	 */
	@Override
	public Set<CratePersistentProperty> getCollectionProperties() {
		
		Set<CratePersistentProperty> properties = getPersistentProperties();
		Set<CratePersistentProperty> collectionProperties = new LinkedHashSet<>();
		
		for(CratePersistentProperty property : properties) {
			
			boolean isCollectionLike = property.isCollectionLike();
			boolean isArray = property.isArray();
			
			if(isCollectionLike && !isArray) {
				collectionProperties.add(property);
			}
		}
		
		return collectionProperties;
	}
	
	/**
	 * Returns all java.util.Map implementations
	 */
	@Override
	public Set<CratePersistentProperty> getMapProperties() {
		
		Set<CratePersistentProperty> properties = getPersistentProperties();
		Set<CratePersistentProperty> mapProperties = new LinkedHashSet<>();
		
		for(CratePersistentProperty property : properties) {
			
			if(property.isMap()) {
				mapProperties.add(property);
			}
		}
		
		return mapProperties;
	}
	
	/**
	 * Returns all array fields
	 */
	@Override
	public Set<CratePersistentProperty> getArrayProperties() {
		
		Set<CratePersistentProperty> properties = getPersistentProperties();
		Set<CratePersistentProperty> arrayProperties = new LinkedHashSet<>();
		
		for(CratePersistentProperty property : properties) {
			
			if(property.isArray()) {
				arrayProperties.add(property);
			}
		}
		
		return arrayProperties;
	}
	
	private String resolveTableName(TypeInformation<T> typeInformation) {
		
		String fallback = replace(typeInformation.getType().getName(), ".", "_");
		
		String tableName;
		
		Table annotation = findAnnotation(Table.class);
		
		if(annotation != null) {
			tableName = hasText(annotation.name()) ? replace(annotation.name(), " ", "_") : fallback;
		}else {
			tableName = fallback;
		}
		
		return tableName;
 	}
	
	private TableParameters resolveTableParameters() {
		
		TableParameters parameters = null;
		
		Table annotation = findAnnotation(Table.class); 
		
		if(annotation != null) {
			parameters = new TableParameters(annotation.numberOfReplicas(), 
											 annotation.refreshInterval(), 
											 annotation.columnPolicy());
		}
		
		return parameters;
	}
	
	private boolean isLongType(Class<?> clazz) {
		return Long.class.equals(clazz) || Long.TYPE.equals(clazz);
	}
}