package org.jboss.resteasy.jsapi;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.ws.rs.Consumes;
import javax.ws.rs.CookieParam;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.MatrixParam;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;

import org.jboss.resteasy.annotations.Form;
import org.jboss.resteasy.core.ResourceMethod;
import org.jboss.resteasy.jsapi.MethodParamMetaData.MethodParamType;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.util.FindAnnotation;

public class MethodMetaData
{
	private final static Logger logger = Logger
    .getLogger(MethodMetaData.class);

	private ResourceMethod resource;
	private Method method;
	private Class<?> klass;
	private String wants;
	private String consumesMIMEType;
	private String uri;
	private String functionName;
	private List<MethodParamMetaData> parameters = new ArrayList<MethodParamMetaData>();
	private Collection<String> httpMethods;
	private ServiceRegistry registry;
	private String functionPrefix;
	private boolean wantsForm;

	public MethodMetaData(ServiceRegistry serviceRegistry, ResourceMethod resource)
	{
		this.registry = serviceRegistry;
		this.resource = resource;
		this.method = resource.getMethod();
		this.klass = method.getDeclaringClass();
		Path methodPath = method.getAnnotation(Path.class);
		Path klassPath = klass.getAnnotation(Path.class);
		Produces produces = method.getAnnotation(Produces.class);
		if (produces == null)
			produces = klass.getAnnotation(Produces.class);
		this.wants = getWants(produces);
		Consumes consumes = method.getAnnotation(Consumes.class);
		if (consumes == null)
			consumes = klass.getAnnotation(Consumes.class);
		this.uri = appendURIFragments(registry, klassPath, methodPath);
		if(serviceRegistry.isRoot())
			this.functionPrefix = klass.getSimpleName();
		else
			this.functionPrefix = serviceRegistry.getFunctionPrefix();
		this.functionName = this.functionPrefix + "." + method.getName(); 
		httpMethods = resource.getHttpMethods();

		// we need to add all parameters from parent resource locators until the root
		List<Method> methodsUntilRoot = new ArrayList<Method>();
		methodsUntilRoot.add(method);
		serviceRegistry.collectResourceMethodsUntilRoot(methodsUntilRoot);
		for (Method method : methodsUntilRoot) {
			Annotation[][] allAnnotations = method.getParameterAnnotations();
			Class<?>[] parameterTypes = method.getParameterTypes();
			for (int i = 0; i < parameterTypes.length; i++)
			{
				processMetaData(parameterTypes[i], allAnnotations[i], true);
			}
		}
		// this must be after we scan the params in case of @Form
		this.consumesMIMEType = getConsumes(consumes);
		if(wantsForm && !"application/x-www-form-urlencoded".equals(consumesMIMEType)){
         logger.warn("Overriding @Consumes annotation in favour of application/x-www-form-urlencoded due to the presence of @FormParam");
			this.consumesMIMEType = "application/x-www-form-urlencoded";
		}
	}

	protected void processMetaData(Class<?> type, Annotation[] annotations,
			boolean useBody)
	{
		QueryParam query;
		HeaderParam header;
		MatrixParam matrix;
		PathParam uriParam;
		CookieParam cookie;
		FormParam formParam;

		// boolean isEncoded = FindAnnotation.findAnnotation(annotations,
		// Encoded.class) != null;

		if ((query = FindAnnotation.findAnnotation(annotations, QueryParam.class)) != null)
		{
			addParameter(type, annotations, MethodParamType.QUERY_PARAMETER, query
					.value());
		} else if ((header = FindAnnotation.findAnnotation(annotations,
				HeaderParam.class)) != null)
		{
			addParameter(type, annotations, MethodParamType.HEADER_PARAMETER,
					header.value());
		} else if ((cookie = FindAnnotation.findAnnotation(annotations,
				CookieParam.class)) != null)
		{
			addParameter(type, annotations, MethodParamType.COOKIE_PARAMETER,
					cookie.value());
		} else if ((uriParam = FindAnnotation.findAnnotation(annotations,
				PathParam.class)) != null)
		{
			addParameter(type, annotations, MethodParamType.PATH_PARAMETER,
					uriParam.value());
		} else if ((matrix = FindAnnotation.findAnnotation(annotations,
				MatrixParam.class)) != null)
		{
			addParameter(type, annotations, MethodParamType.MATRIX_PARAMETER,
					matrix.value());
		} else if ((formParam = FindAnnotation.findAnnotation(annotations,
				FormParam.class)) != null)
		{
			addParameter(type, annotations, MethodParamType.FORM_PARAMETER,
					formParam.value());
			this.wantsForm = true;
		} else if (FindAnnotation.findAnnotation(annotations, Form.class) != null)
		{
			walkForm(type);
		} else if ((FindAnnotation.findAnnotation(annotations, Context.class)) != null)
		{
			// righfully ignore
		} else if (useBody)
		{
			addParameter(type, annotations, MethodParamType.ENTITY_PARAMETER, null);
		}
	}

	private void walkForm(Class<?> type)
	{
		for (Field field : type.getDeclaredFields())
		{
			processMetaData(field.getType(), field.getAnnotations(), false);
		}
		for (Method method : type.getDeclaredMethods())
		{
			if (method.getParameterTypes().length != 1
					|| !method.getReturnType().equals(Void.class))
				continue;
			processMetaData(method.getParameterTypes()[0],
					method.getAnnotations(), false);
		}
		if (type.getSuperclass() != null)
			walkForm(type.getSuperclass());
	}

	private void addParameter(Class<?> type, Annotation[] annotations,
			MethodParamType paramType, String value)
	{
		this.parameters.add(new MethodParamMetaData(type, annotations, paramType,
				value));
	}

	private String getWants(Produces produces)
	{
		if (produces == null)
			return null;
		String[] value = produces.value();
		if (value.length == 0)
			return null;
		if (value.length == 1)
			return value[0];
		StringBuffer buf = new StringBuffer();
		for (String mime : produces.value())
		{
			if (buf.length() != 0)
				buf.append(",");
			buf.append(mime);
		}
		return buf.toString();
	}

	private String getConsumes(Consumes consumes)
	{
		if (consumes == null)
			return "text/plain";
		if (consumes.value().length > 0)
			return consumes.value()[0];
		return "text/plain";
	}

	public static String appendURIFragments(String... fragments)
	{
		StringBuilder str = new StringBuilder();
		for (String fragment : fragments)
		{
			if (fragment == null || fragment.length() == 0 || fragment.equals("/"))
				continue;
			if (fragment.startsWith("/"))
				fragment = fragment.substring(1);
			if (fragment.endsWith("/"))
				fragment = fragment.substring(0, fragment.length() - 1);
			str.append('/').append(fragment);
		}
		if (str.length() == 0)
			return "/";
		return str.toString();
	}

	public ResourceMethod getResource()
	{
		return resource;
	}

	public Method getMethod()
	{
		return method;
	}

	public Class<?> getKlass()
	{
		return klass;
	}

	public String getWants()
	{
		return wants;
	}

	public String getConsumesMIMEType()
	{
		return consumesMIMEType;
	}

	public String getUri()
	{
		return uri;
	}

	public String getFunctionName()
	{
		return functionName;
	}

	public List<MethodParamMetaData> getParameters()
	{
		return parameters;
	}

	public Collection<String> getHttpMethods()
	{
		return httpMethods;
	}

	public static String appendURIFragments(ServiceRegistry registry, Path classPath, Path methodPath) {
		return appendURIFragments(registry == null ? null : registry.getUri(), 
				classPath != null ? classPath.value() : null,
				methodPath != null ? methodPath.value() : null);
	}

	public String getFunctionPrefix() {
		return functionPrefix;
	}
}
