package internal.org.springframework.content.rest.links;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;

import internal.org.springframework.content.rest.utils.ContentStoreUtils;
import internal.org.springframework.content.rest.utils.DomainObjectUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.InvalidPropertyException;
import org.springframework.content.commons.annotations.ContentId;
import org.springframework.content.commons.storeservice.ContentStoreInfo;
import org.springframework.content.commons.storeservice.ContentStoreService;
import org.springframework.content.commons.utils.BeanUtils;
import org.springframework.content.rest.config.RestConfiguration;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.rest.core.mapping.RepositoryResourceMappings;
import org.springframework.data.rest.core.mapping.ResourceMetadata;
import org.springframework.data.rest.webmvc.BaseUri;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
import org.springframework.data.rest.webmvc.support.RepositoryLinkBuilder;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkBuilder;
import org.springframework.hateoas.ResourceProcessor;
import org.springframework.hateoas.mvc.BasicLinkBuilder;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

/**
 * Adds content and content collection links to Spring Data REST Entity Resources.
 * 
 * @author warrep
 *
 */
public class ContentLinksResourceProcessor
		implements ResourceProcessor<PersistentEntityResource> {

	private static final Log log = LogFactory.getLog(ContentLinksResourceProcessor.class);

	private ContentStoreService stores;
	private RestConfiguration config;
	private RepositoryResourceMappings mappings;

	public ContentLinksResourceProcessor(Repositories repos, ContentStoreService stores, RestConfiguration config, RepositoryResourceMappings mappings) {
		this.stores = stores;
		this.config = config;
		this.mappings = mappings;
	}

	public PersistentEntityResource process(final PersistentEntityResource resource) {

		Object object = resource.getContent();
		if (object == null)
			return resource;

		Object entityId = DomainObjectUtils.getId(object);

		ContentStoreInfo store = ContentStoreUtils.findContentStore(stores, object.getClass());

		Field[] fields = BeanUtils.findFieldsWithAnnotation(object.getClass(), ContentId.class, new BeanWrapperImpl(object));
		if (fields.length == 1) {
			if (store != null) {

				// for compatibility with v0.x.0 versions
				originalLink(config.getBaseUri(), store, entityId).ifPresent((l) -> resource.add(l));

				resource.add(shortcutLink(config.getBaseUri(), store, entityId, StringUtils.uncapitalize(ContentStoreUtils.getSimpleName(store))));
			}
		} else if (fields.length > 1) {
			for (Field field : fields) {
				resource.add(fullyQualifiedLink(config.getBaseUri(), store, entityId, field.getName()));
			}
		}

		List<Field> processed = new ArrayList<>();

		ResourceMetadata md = mappings.getMetadataFor(object.getClass());

		// public fields
		for (Field field : object.getClass().getFields()) {
			processed.add(field);
			handleField(field, resource, md, config.getBaseUri(), entityId);
		}

		// handle properties
		BeanWrapper wrapper = new BeanWrapperImpl(object);
		for (PropertyDescriptor descriptor : wrapper.getPropertyDescriptors()) {
			Field field = null;
			try {
				field = object.getClass().getDeclaredField(descriptor.getName());
				if (processed.contains(field) == false) {
					handleField(field, resource, md, config.getBaseUri(), entityId);
				}
			}
			catch (NoSuchFieldException nsfe) {
				log.trace(String.format("No field for property %s, ignoring",
						descriptor.getName()));
			}
			catch (SecurityException se) {
				log.warn(String.format(
						"Unexpected security error while handling content links for property %s",
						descriptor.getName()));
			}
		}

		return resource;
	}

	private void handleField(Field field, final PersistentEntityResource resource, ResourceMetadata metadata, URI baseUri, Object entityId) {

		Class<?> fieldType = field.getType();
		if (fieldType.isArray()) {
			fieldType = fieldType.getComponentType();

			ContentStoreInfo store = ContentStoreUtils.findContentStore(stores, fieldType);
			if (store != null) {
				resource.add(propertyLink(metadata, baseUri, entityId, field.getName(), null));
			}
		}
		else if (Collection.class.isAssignableFrom(fieldType)) {
			Type type = field.getGenericType();

			if (type instanceof ParameterizedType) {

				ParameterizedType pType = (ParameterizedType) type;
				Type[] arr = pType.getActualTypeArguments();

				for (Type tp : arr) {
					fieldType = (Class<?>) tp;
				}

				ContentStoreInfo store = ContentStoreUtils.findContentStore(stores,
						fieldType);
				if (store != null) {
					Object object = resource.getContent();
					BeanWrapper wrapper = new BeanWrapperImpl(object);
					Object value = null;
					try {
						value = wrapper.getPropertyValue(field.getName());
					}
					catch (InvalidPropertyException ipe) {
						try {
							value = ReflectionUtils.getField(field, object);
						}
						catch (IllegalStateException ise) {
							log.trace(String.format("Didn't get value for property %s", field.getName()));
						}
					}
					if (value != null) {
						Iterator iter = ((Collection) value).iterator();
						while (iter.hasNext()) {
							Object o = iter.next();
							if (BeanUtils.hasFieldWithAnnotation(o, ContentId.class)) {
								String cid = BeanUtils.getFieldWithAnnotation(o, ContentId.class).toString();
								if (cid != null) {
									resource.add(propertyLink(metadata, baseUri, entityId, field.getName(), cid));
								}
							}
						}
					}
				}
			}
		}
		else {
			ContentStoreInfo store = ContentStoreUtils.findContentStore(stores,
					fieldType);
			if (store != null) {
				Object object = resource.getContent();
				BeanWrapper wrapper = new BeanWrapperImpl(object);
				Object value = null;
				try {
					value = wrapper.getPropertyValue(field.getName());
				}
				catch (InvalidPropertyException ipe) {
					try {
						value = ReflectionUtils.getField(field, object);
					}
					catch (IllegalStateException ise) {
						log.trace(String.format("Didn't get value for property %s", field.getName()));
					}
				}
				if (value != null) {
					String cid = BeanUtils.getFieldWithAnnotation(value, ContentId.class).toString();
					if (cid != null) {
						resource.add(propertyLink(metadata, baseUri, entityId, field.getName(), cid));
					}
				}
			}
		}
	}

	private Optional<Link> originalLink(URI baseUri, ContentStoreInfo store, Object id) {

		if (id == null) {
			return Optional.empty();
		}

		return Optional.of(shortcutLink(baseUri, store, id, ContentStoreUtils.storePath(store)));
	}

	private Link shortcutLink(URI baseUri, ContentStoreInfo store, Object id, String linkRel) {
		LinkBuilder builder = BasicLinkBuilder.linkToCurrentMapping();

		if (baseUri != null) {
			builder = builder.slash(baseUri);
		}

		return builder.slash(ContentStoreUtils.storePath(store))
				.slash(id)
				.withRel(linkRel);
	}

	private Link fullyQualifiedLink(URI baseUri, ContentStoreInfo store, Object id, String fieldName) {
		LinkBuilder builder = BasicLinkBuilder.linkToCurrentMapping();

		if (baseUri != null) {
			builder = builder.slash(baseUri);
		}

		String property = StringUtils.uncapitalize(ContentStoreUtils.propertyName(fieldName));
		return builder.slash(ContentStoreUtils.storePath(store))
				.slash(id)
				.slash(property)
				.withRel(property);
	}

	private Link propertyLink(ResourceMetadata md, URI baseUri, Object id, String property, String contentId) {
		LinkBuilder builder = new RepositoryLinkBuilder(md, new BaseUri(baseUri));

		builder = builder.slash(id).slash(property);

		if (contentId != null) {
			builder = builder.slash(contentId);
		}

		return builder.withRel(property);
	}

	protected Object invokeField(Field field, Object object) {

		try {
			return field.get(object);
		}
		catch (IllegalAccessException e) {
			return null;
		}
	}
}