/*
 * Copyright 2015 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.rest.core.mapping;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

/**
 * {@link ResourceMappings} for {@link PersistentEntities}.
 * 
 * @author Oliver Gierke
 */
public class PersistentEntitiesResourceMappings implements ResourceMappings {

	private final PersistentEntities entities;
	private final SearchResourceMappings searchResourceMappings = new SearchResourceMappings(
			Collections.<MethodResourceMapping> emptyList());

	private final Map<Class<?>, ResourceMetadata> cache = new HashMap<Class<?>, ResourceMetadata>();
	private final Map<Class<?>, MappingResourceMetadata> mappingCache = new HashMap<Class<?>, MappingResourceMetadata>();
	private final Map<PersistentProperty<?>, ResourceMapping> propertyCache = new HashMap<PersistentProperty<?>, ResourceMapping>();

	/**
	 * Creates a new {@link PersistentEntitiesResourceMappings} from the given {@link PersistentEntities}.
	 * 
	 * @param entities must not be {@literal null}.
	 */
	public PersistentEntitiesResourceMappings(PersistentEntities entities) {
		this.entities = entities;
	}

	/* 
	 * (non-Javadoc)
	 * @see org.springframework.data.rest.core.mapping.ResourceMappings#getMappingFor(java.lang.Class)
	 */
	@Override
	public ResourceMetadata getMetadataFor(Class<?> type) {

		Assert.notNull(type, "Type must not be null!");

		type = ClassUtils.getUserClass(type);

		if (cache.containsKey(type)) {
			return cache.get(type);
		}

		MappingResourceMetadata metadata = getMappingMetadataFor(type);
		cache.put(type, metadata);

		return metadata;
	}

	/**
	 * Returns the {@link MappingResourceMetadata} for the given type.
	 * 
	 * @param type must not be {@literal null}.
	 * @return the {@link MappingResourceMetadata} if the given type is a {@link PersistentEntity}, {@literal null}
	 *         otherwise.
	 */
	MappingResourceMetadata getMappingMetadataFor(Class<?> type) {

		Assert.notNull(type, "Type must not be null!");
		type = ClassUtils.getUserClass(type);

		MappingResourceMetadata mappingMetadata = mappingCache.get(type);

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

		PersistentEntity<?, ?> entity = entities.getPersistentEntity(type);

		if (entity == null) {
			return null;
		}

		mappingMetadata = new MappingResourceMetadata(entity, this);
		mappingCache.put(type, mappingMetadata);
		mappingMetadata.init();

		return mappingMetadata;
	}

	/* 
	 * (non-Javadoc)
	 * @see org.springframework.data.rest.core.mapping.ResourceMappings#getSearchResourceMappings(java.lang.Class)
	 */
	@Override
	public SearchResourceMappings getSearchResourceMappings(Class<?> domainType) {
		return searchResourceMappings;
	}

	/* 
	 * (non-Javadoc)
	 * @see org.springframework.data.rest.core.mapping.ResourceMappings#exportsMappingFor(java.lang.Class)
	 */
	@Override
	public boolean exportsMappingFor(Class<?> type) {

		if (!hasMappingFor(type)) {
			return false;
		}

		ResourceMetadata metadata = getMetadataFor(type);
		return metadata.isExported();
	}

	/* 
	 * (non-Javadoc)
	 * @see org.springframework.data.rest.core.mapping.ResourceMappings#exportsTopLevelResourceFor(java.lang.String)
	 */
	@Override
	public boolean exportsTopLevelResourceFor(String path) {

		Assert.hasText(path);

		for (ResourceMetadata metadata : this) {
			if (metadata.getPath().matches(path)) {
				return metadata.isExported();
			}
		}

		return false;
	}

	/* 
	 * (non-Javadoc)
	 * @see org.springframework.data.rest.core.mapping.ResourceMappings#hasMappingFor(java.lang.Class)
	 */
	@Override
	public boolean hasMappingFor(Class<?> type) {

		if (cache.containsKey(type)) {
			return true;
		}

		return false;
	}

	/* 
	 * (non-Javadoc)
	 * @see org.springframework.data.rest.core.mapping.ResourceMetadataProvider#getMappingFor(org.springframework.data.mapping.PersistentProperty)
	 */
	public ResourceMapping getMappingFor(PersistentProperty<?> property) {

		ResourceMapping propertyMapping = propertyCache.get(property);

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

		propertyMapping = new PersistentPropertyResourceMapping(property, this);

		propertyCache.put(property, propertyMapping);

		return propertyMapping;
	}

	public boolean isMapped(PersistentProperty<?> property) {

		ResourceMapping metadata = getMappingFor(property);
		return metadata != null && metadata.isExported();
	}

	/* 
	 * (non-Javadoc)
	 * @see java.lang.Iterable#iterator()
	 */
	@Override
	public Iterator<ResourceMetadata> iterator() {

		Set<ResourceMetadata> metadata = new HashSet<ResourceMetadata>();

		for (ResourceMetadata candidate : cache.values()) {
			if (candidate != null) {
				metadata.add(candidate);
			}
		}

		return metadata.iterator();
	}

	/**
	 * Adds the given {@link ResourceMetadata} to the cache.
	 * 
	 * @param type must not be {@literal null}.
	 * @param metadata can be {@literal null}.
	 */
	protected final void addToCache(Class<?> type, ResourceMetadata metadata) {
		cache.put(type, metadata);
	}

	/**
	 * Returns whether we currently already have {@link ResourceMetadata} for the given type.
	 * 
	 * @param type must not be {@literal null}.
	 * @return
	 */
	protected final boolean hasMetadataFor(Class<?> type) {
		return cache.containsKey(type);
	}
}
