package org.jboss.resteasy.links.impl;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.annotation.security.RolesAllowed;
import javax.el.ELContext;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;

import org.jboss.resteasy.annotations.Form;
import org.jboss.resteasy.core.ResourceInvoker;
import org.jboss.resteasy.core.ResourceMethod;
import org.jboss.resteasy.core.ResourceMethodRegistry;
import org.jboss.resteasy.links.ELProvider;
import org.jboss.resteasy.links.LinkELProvider;
import org.jboss.resteasy.links.LinkResource;
import org.jboss.resteasy.links.LinkResources;
import org.jboss.resteasy.links.RESTServiceDiscovery;
import org.jboss.resteasy.links.ResourceFacade;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.specimpl.UriBuilderImpl;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.util.FindAnnotation;

public class RESTUtils {

	private final static Logger logger = Logger
	.getLogger(RESTUtils.class);

	public static <T> T addDiscovery(T entity, UriInfo uriInfo, ResourceMethodRegistry registry) {
		// find the field to inject first
		Field injectionField = findInjectionField(entity);
		if(injectionField == null)
			return entity;
		List<Method> methods = getServiceMethods(registry);
		RESTServiceDiscovery ret = new RESTServiceDiscovery();
		for(Method m : methods){
			processLinkResources(m, entity, uriInfo, ret);
		}
		// do not inject an empty service
		if(ret.isEmpty())
			return entity;
		// now inject
		injectionField.setAccessible(true);
		try {
			injectionField.set(entity, ret);
		} catch (Exception e) {
				logger.error("Failed to inject links in "+entity, e);
		}
		injectionField.setAccessible(false);
		return entity;
	}

	private static Field findInjectionField(Object entity) {
		Class<?> klass = entity.getClass();
		do{
			for(Field f : klass.getDeclaredFields()){
				if(f.getType().equals(RESTServiceDiscovery.class))
					return f;
			}
			klass = klass.getSuperclass();
		}while(klass != null);
		return null;
	}

	public static List<Method> getServiceMethods(ResourceMethodRegistry registry){
		ArrayList<Method> results = new ArrayList<Method>();
		for (Entry<String, List<ResourceInvoker>> entry : registry.getRoot()
				.getBounded().entrySet())
		{
			List<ResourceInvoker> invokers = entry.getValue();
			for (ResourceInvoker invoker : invokers)
			{
				if (invoker instanceof ResourceMethod)
				{
					ResourceMethod resourceMethod = (ResourceMethod)invoker;
					Method method = resourceMethod.getMethod();
					results.add(method);
				} else
				{
					// TODO: fix this?
				}
			}
		}
		return results;
	}

	private static void processLinkResources(Method m, Object entity, UriInfo uriInfo,
			RESTServiceDiscovery ret) {
		// find a single service
		LinkResource service = m
		.getAnnotation(LinkResource.class);
		if(service != null)
			processLinkResource(m, entity, uriInfo, ret, service);
		// find a multi-service
		LinkResources services = m
		.getAnnotation(LinkResources.class);
		if(services != null)
			for(LinkResource service2 : services.value())
				processLinkResource(m, entity, uriInfo, ret, service2);
	}

	private static void processLinkResource(Method m, Object entity, UriInfo uriInfo,
			RESTServiceDiscovery ret, LinkResource service) {
		String rel = service.rel();
		// if we have uri templates, we need a compatible instance
		Class<?> type = getServiceType(service, m);
		if(type.isInstance(entity)){
			if(checkConstraint(service, entity, m))
				addInstanceService(m, entity, uriInfo, ret, service, rel);
		}else if(entity instanceof ResourceFacade<?> && ((ResourceFacade<?>)entity).facadeFor() == type){
			if(checkConstraint(service, type, m))
				addService(m, (ResourceFacade<?>) entity, uriInfo, ret, service, rel);
		}
	}

	private static Class<?> getServiceType(LinkResource service, Method m) {
		Class<?> type = service.value();
		if(type != Void.class)
			return type;
		// are we looking at the return type or the body type?
		type = findBodyType(m);
		if(type == null){
			// our next best bet is the return type
			type = m.getReturnType();
		}
		if(Void.TYPE == type)
			throw new ServiceDiscoveryException(m, "Cannot guess resource type for service discovery");
		if(Collection.class.isAssignableFrom(type))
			throw new ServiceDiscoveryException(m, "Cannot guess collection type for service discovery");
		if(Response.class.isAssignableFrom(type))
			throw new ServiceDiscoveryException(m, "Cannot guess type for Response");
		return type;
	}

	private static Class<?> findBodyType(Method m) {
		Annotation[][] annotations = m.getParameterAnnotations();
		Class<?>[] types = m.getParameterTypes();
		for (int i = 0; i < types.length; i++) {
			// if there's no JAXRS annotation nor @Form, it's a body right?
			if(FindAnnotation.findJaxRSAnnotations(annotations[i]).length == 0
					&& FindAnnotation.findAnnotation(annotations[i], Form.class) == null)
				return types[i];
		}
		return null;
	}

	private static boolean checkConstraint(LinkResource service, Object object, Method m) {
		String constraint = service.constraint();
		if(constraint == null || constraint.length() == 0)
			return checkEJBConstraint(m);
		Boolean ret = evaluateELBoolean(m, getELContext(m, object), object, constraint);
		return ret != null && ret.booleanValue();
	}

	private static boolean checkEJBConstraint(Method m) {
		// Use dynamic class loading here since if the EJB annotation class is not present
		// it cannot be on the method, so we don't have to check for it
		try {
			Class.forName("javax.annotation.security.RolesAllowed");
		} catch (ClassNotFoundException e) {
			// class not here, therefore not on method either
			return true;
		}
		// From now on we can use this class since it's there. I (Stef Epardaud) don't think we need to 
		// remove the reference here and use reflection.
		RolesAllowed rolesAllowed = m.getAnnotation(RolesAllowed.class);
		if(rolesAllowed == null)
			return true;
		SecurityContext context = ResteasyProviderFactory.getContextData(SecurityContext.class);
		for(String role : rolesAllowed.value())
			if(context.isUserInRole(role))
				return true;
		return false;
	}

	private static void addService(Method m, ResourceFacade<?> entity, UriInfo uriInfo,
			RESTServiceDiscovery ret, LinkResource service, String rel) {
		Map<String, ? extends Object> pathParameters = entity.pathParameters();
		// do we need any path parameters?
		UriBuilder uriBuilder = uriInfo.getBaseUriBuilder().path(m.getDeclaringClass());
		if(m.isAnnotationPresent(Path.class))
			uriBuilder.path(m);
		URI uri;
		List<String> paramNames = ((UriBuilderImpl)uriBuilder).getPathParamNamesInDeclarationOrder();
		if(paramNames.isEmpty())
			uri = uriBuilder.build();
		else if(pathParameters.size() >= paramNames.size())
			uri = uriBuilder.buildFromMap(pathParameters);
		else
			// just bail out since we don't have enough parameters, that must be an instance service
			return;
		if(rel.length() == 0){
			if (m.isAnnotationPresent(GET.class))
				rel = "list";
			else if (m.isAnnotationPresent(POST.class))
				rel = "add";
		}
		ret.addLink(uri, rel);
	}

	private static void addInstanceService(Method m, Object entity,
			UriInfo uriInfo, RESTServiceDiscovery ret, LinkResource service,
			String rel) {
		UriBuilder uriBuilder = uriInfo.getBaseUriBuilder().path(m.getDeclaringClass());
		if(m.isAnnotationPresent(Path.class))
			uriBuilder.path(m);
		URI uri = buildURI(uriBuilder, service, entity, m);

		if (rel.length() == 0) {
			if (m.isAnnotationPresent(GET.class)){
				Class<?> type = m.getReturnType();
				if(Collection.class.isAssignableFrom(type))
					rel = "list";
				else
					rel = "self";
			}else if (m.isAnnotationPresent(PUT.class))
				rel = "update";
			else if (m.isAnnotationPresent(POST.class))
				rel = "add";
			else if (m.isAnnotationPresent(DELETE.class))
				rel = "remove";
		}
		ret.addLink(uri, rel);
	}

	private static URI buildURI(UriBuilder uriBuilder, LinkResource service,
			Object entity, Method m) {
		// see if we need parameters
		String[] uriTemplates = service.pathParameters();
		if (uriTemplates.length > 0) {
			Object[] values = new Object[uriTemplates.length];
			for (int i = 0; i < values.length; i++)
				values[i] = evaluateEL(m, getELContext(m, entity), entity, uriTemplates[i]);
			return uriBuilder.build(values);
		} 
		// do we need any path parameters?
		List<String> paramNames = ((UriBuilderImpl)uriBuilder).getPathParamNamesInDeclarationOrder();
		if(paramNames.isEmpty())
			return uriBuilder.build();
		// try to find the IDs
		List<Object> params = findURIParamsFromResource(entity);
		if(params.size() == paramNames.size())
			return uriBuilder.build(params.toArray());
		// if we have too many, ignore the last ones
		if(params.size() > paramNames.size())
			return uriBuilder.build(params.subList(0, paramNames.size()).toArray());
		throw new ServiceDiscoveryException(m, "Not enough URI parameters: expecting "+paramNames.size()+" but only found "+params.size());
	}

	private static List<Object> findURIParamsFromResource(Object entity) {
		List<Object> ids = new ArrayList<Object>();
		do{
			List<Object> theseIDs = BeanUtils.findIDs(entity);
			ids.addAll(0, theseIDs);
		}while((entity = BeanUtils.findParentResource(entity)) != null);
		return ids;
	}

	private static LinkELProvider findLinkELProvider(Method m){
		if(m.isAnnotationPresent(LinkELProvider.class))
			return m.getAnnotation(LinkELProvider.class);
		Class<?> c = m.getDeclaringClass();
		if(c.isAnnotationPresent(LinkELProvider.class))
			return c.getAnnotation(LinkELProvider.class);
		Package p = c.getPackage();
		if(p != null && p.isAnnotationPresent(LinkELProvider.class))
			return p.getAnnotation(LinkELProvider.class);
		return null;
	}

	private static ELProvider getELProvider(Method m){
		LinkELProvider linkElProvider = findLinkELProvider(m);
		if(linkElProvider == null)
			return null;
		Class<? extends ELProvider> elProviderClass = linkElProvider.value();
		try{
			return elProviderClass.newInstance();
		}catch(Exception x){
				logger.error("Could not instantiate ELProvider class "+elProviderClass.getName(), x);
			throw new ServiceDiscoveryException(m, "Failed to instantiate ELProvider: "+elProviderClass.getName(), x);
		}
	}

	private static ELContext getELContext(Method m, Object base){
		ELContext ours = EL.createELContext(base);
		ELProvider elProvider = getELProvider(m);
		if(elProvider != null)
			return elProvider.getContext(ours);
		return ours;
	}

	public static Map<String, ? extends Object> derivePathParameters(UriInfo uriInfo){
		MultivaluedMap<String, String> pathParameters = uriInfo.getPathParameters();
		Map<String, String> ret = new HashMap<String,String>();
		for(Entry<String, List<String>> entry:  pathParameters.entrySet()){
			ret.put(entry.getKey(), entry.getValue().get(0));
		}
		return ret;
	}

	public static Object evaluateEL(Method m, ELContext context, Object base, String expression) {
		try{
			return EL.EXPRESSION_FACTORY.createValueExpression(context, expression,
					Object.class).getValue(context);
		}catch(Exception x){
			throw new ServiceDiscoveryException(m, "Failed to evaluate EL expression: "+expression, x);

		}
	}

	public static Boolean evaluateELBoolean(Method m, ELContext context, Object base, String expression) {
		try{
			return (Boolean) EL.EXPRESSION_FACTORY.createValueExpression(context, expression,
					Boolean.class).getValue(context);
		}catch(Exception x){
			throw new ServiceDiscoveryException(m, "Failed to evaluate EL expression: "+expression, x);

		}
	}

}
