
package org.callimachusproject.server.chain;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Future;

import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.concurrent.BasicFuture;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.protocol.HttpContext;
import org.callimachusproject.annotations.header;
import org.callimachusproject.annotations.method;
import org.callimachusproject.client.HttpUriResponse;
import org.callimachusproject.server.AsyncExecChain;
import org.callimachusproject.server.helpers.CalliContext;
import org.callimachusproject.server.helpers.ResourceOperation;
import org.callimachusproject.server.helpers.ResponseBuilder;
import org.callimachusproject.server.helpers.ResponseCallback;

/**
 * Responds for OPTIONS requests.
 * 
 * @author James Leigh
 * 
 */
public class OptionsHandler implements AsyncExecChain {
	private static final String REQUEST_METHOD = "Access-Control-Request-Method";
	private static final Set<String> ALLOW_HEADERS = new TreeSet<String>(
			Arrays.asList("Authorization", "Cache-Control", "Location",
					"Range", "Accept", "Accept-Charset", "Accept-Encoding",
					"Accept-Language", "Content-Encoding", "Content-Language",
					"Content-Length", "Content-Location", "Content-MD5",
					"Content-Type", "If-Match", "If-Modified-Since",
					"If-None-Match", "If-Range", "If-Unmodified-Since"));
	private final AsyncExecChain delegate;

	public OptionsHandler(AsyncExecChain delegate) {
		this.delegate = delegate;
	}

	@Override
	public Future<HttpResponse> execute(HttpHost target,
			HttpRequest request, HttpContext context,
			final FutureCallback<HttpResponse> callback) {
		if ("OPTIONS".equals(request.getRequestLine().getMethod())) {
			ResourceOperation trans = CalliContext.adapt(context).getResourceTransaction();
			StringBuilder sb = new StringBuilder();
			sb.append("OPTIONS, TRACE");
			for (String method : trans.getAllowedMethods()) {
				sb.append(", ").append(method);
			}
			String allow = sb.toString();
			HttpUriResponse rb = new ResponseBuilder(request, context).noContent();
			rb.addHeader("Allow", allow);
			String m = trans.getVaryHeader(REQUEST_METHOD);
			if (m == null) {
				rb.addHeader("Access-Control-Allow-Methods", allow);
			} else {
				rb.addHeader("Access-Control-Allow-Methods", m);
			}
			StringBuilder headers = new StringBuilder();
			for (String header : ALLOW_HEADERS) {
				if (headers.length() > 0) {
					headers.append(",");
				}
				headers.append(header);
			}
			for (String header : getAllowedHeaders(m, trans)) {
				if (!ALLOW_HEADERS.contains(header)) {
					headers.append(",");
					headers.append(header);
				}
			}
			rb.addHeader("Access-Control-Allow-Headers", headers.toString());
			String max = getMaxAge(trans.getRequestedResource().getClass());
			if (max != null) {
				rb.addHeader("Access-Control-Max-Age", max);
			}
			BasicFuture<HttpResponse> future;
			future = new BasicFuture<HttpResponse>(callback);
			future.completed(rb);
			return future;
		} else {
			return delegate.execute(target, request, context, new ResponseCallback(callback) {
				public void completed(HttpResponse result) {
					try {
						allow(result);
						super.completed(result);
					} catch (RuntimeException ex) {
						super.failed(ex);
					}
				}
			});
		}
	}

	void allow(HttpResponse resp) {
		if (resp != null && resp.getStatusLine().getStatusCode() == 405) {
			if (resp.containsHeader("Allow")) {
				String allow = resp.getFirstHeader("Allow").getValue();
				resp.setHeader("Allow", allow + ",OPTIONS");
			} else {
				resp.setHeader("Allow", "OPTIONS");
			}
		}
	}

	private Collection<String> getAllowedHeaders(String m, ResourceOperation request) {
		Collection<String> result = null;
		Class<?> type = request.getRequestedResource().getClass();
		for (Method method : type.getMethods()) {
			if (m != null && method.isAnnotationPresent(method.class)) {
				String[] mm = method.getAnnotation(method.class).value();
				if (!Arrays.asList(mm).contains(m))
					continue;
			}
			for (Annotation[] anns : method.getParameterAnnotations()) {
				for (Annotation ann : anns) {
					if (ann.annotationType().equals(header.class)) {
						if (result == null) {
							result = new TreeSet<String>();
						}
						result.addAll(Arrays.asList(((header) ann).value()));
					}
				}
			}
		}
		if (result == null)
			return Collections.emptyList();
		return result;
	}

	private String getMaxAge(Class<?> type) {
		if (type.isAnnotationPresent(header.class)) {
			for (String value : type.getAnnotation(header.class).value()) {
				int m = value.indexOf("max-age=");
				if (m >= 0) {
					int idx = value.indexOf(':');
					if (idx < 0)
						continue;
					String name = value.substring(0, idx);
					if (!name.equalsIgnoreCase("cache-control"))
						continue;
					int c = value.indexOf(';', m);
					if (c < 0) {
						c = value.length();
					}
					String max = value.substring(m, c);
					return max.trim();
				}
			}
		} else {
			if (type.getSuperclass() != null) {
				String max = getMaxAge(type.getSuperclass());
				if (max != null) {
					return max;
				}
			}
			for (Class<?> face : type.getInterfaces()) {
				String max = getMaxAge(face);
				if (max != null) {
					return max;
				}
			}
		}
		return null;
	}

}
