/* Copyright 2006-2009 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.codehaus.groovy.grails.plugins.springsecurity;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.WordUtils;
import org.codehaus.groovy.grails.commons.ApplicationHolder;
import org.codehaus.groovy.grails.commons.ControllerArtefactHandler;
import org.codehaus.groovy.grails.commons.GrailsApplication;
import org.codehaus.groovy.grails.commons.GrailsClass;
import org.codehaus.groovy.grails.commons.GrailsControllerClass;
import org.codehaus.groovy.grails.web.context.ServletContextHolder;
import org.codehaus.groovy.grails.web.mapping.UrlMappingInfo;
import org.codehaus.groovy.grails.web.mapping.UrlMappingsHolder;
import org.codehaus.groovy.grails.web.servlet.mvc.GrailsParameterMap;
import org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequest;
import org.codehaus.groovy.grails.web.util.WebUtils;
import org.springframework.security.ConfigAttributeDefinition;
import org.springframework.security.intercept.web.FilterInvocation;
import org.springframework.security.intercept.web.FilterInvocationDefinitionSource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * A {@link FilterInvocationDefinitionSource} that uses rules defined with Controller annotations
 * combined with static rules defined in <code>SecurityConfig.groovy</code>, e.g. for js, images, css
 * or for rules that cannot be expressed in a controller like '/**'.
 *
 * @author <a href='mailto:beckwithb@studentsonly.com'>Burt Beckwith</a>
 */
public class AnnotationFilterInvocationDefinition extends AbstractFilterInvocationDefinition {

	private UrlMappingsHolder _urlMappingsHolder;

	@Override
	protected String determineUrl(final FilterInvocation filterInvocation) {
		HttpServletRequest request = filterInvocation.getHttpRequest();
		HttpServletResponse response = filterInvocation.getHttpResponse();
		ServletContext servletContext = ServletContextHolder.getServletContext();
		GrailsApplication application = ApplicationHolder.getApplication();

		GrailsWebRequest existingRequest = WebUtils.retrieveGrailsWebRequest();

		String requestUrl = request.getRequestURI().substring(request.getContextPath().length());

		String url = null;
		try {
			GrailsWebRequest grailsRequest = new GrailsWebRequest(request, response, servletContext);
			WebUtils.storeGrailsWebRequest(grailsRequest);

			Map<String, Object> savedParams = copyParams(grailsRequest);

			for (UrlMappingInfo mapping : _urlMappingsHolder.matchAll(requestUrl)) {
				configureMapping(mapping, grailsRequest, savedParams);

				url = findGrailsUrl(mapping, application);
				if (url != null) {
					break;
				}
			}
		}
		finally {
			if (existingRequest == null) {
				WebUtils.clearGrailsWebRequest();
			}
			else {
				WebUtils.storeGrailsWebRequest(existingRequest);
			}
		}

		if (!StringUtils.hasLength(url)) {
			// probably css/js/image
			url = requestUrl;
		}

		return lowercaseAndStringQuerystring(url);
	}

	private String findGrailsUrl(final UrlMappingInfo mapping, final GrailsApplication application) {

		String actionName = mapping.getActionName();
		if (!StringUtils.hasLength(actionName)) {
			actionName = "";
		}

		String controllerName = mapping.getControllerName();

		if (isController(controllerName, actionName, application)) {
			if (!StringUtils.hasLength(actionName) || "null".equals(actionName)) {
				actionName = "index";
			}
			return ("/" + controllerName + "/" + actionName).trim();
		}

		return null;
	}

	private boolean isController(final String controllerName, final String actionName,
			final GrailsApplication application) {
		return application.getArtefactForFeature(ControllerArtefactHandler.TYPE,
				"/" + controllerName + "/" + actionName) != null;
	}

	private void configureMapping(final UrlMappingInfo mapping, final GrailsWebRequest grailsRequest,
			final Map<String, Object> savedParams) {

		// reset params since mapping.configure() sets values
		GrailsParameterMap params = grailsRequest.getParams();
		params.clear();
		params.putAll(savedParams);

		mapping.configure(grailsRequest);
	}

	@SuppressWarnings("unchecked")
	private Map<String, Object> copyParams(final GrailsWebRequest grailsRequest) {
		return new HashMap<String, Object>(grailsRequest.getParams());
	}

	/**
	 * Called by the plugin to set controller role info.<br/>
	 *
	 * Reinitialize by calling <code>ctx.objectDefinitionSource.initialize(
	 * 	ctx.authenticateService.securityConfig.security.annotationStaticRules,
	 * 	ctx.grailsUrlMappingsHolder,
	 * 	ApplicationHolder.application.controllerClasses)</code>
	 *
	 * @param staticRules  keys are URL patterns, values are role names for that pattern
	 * @param urlMappingsHolder  mapping holder
	 * @param controllerClasses  all controllers
	 */
	public void initialize(
			final Map<String, Collection<String>> staticRules,
			final UrlMappingsHolder urlMappingsHolder,
			final GrailsClass[] controllerClasses) {

		Map<String, Map<String, Set<String>>> actionRoleMap = new HashMap<String, Map<String,Set<String>>>();
		Map<String, Set<String>> classRoleMap = new HashMap<String, Set<String>>();

		Assert.notNull(staticRules, "staticRules map is required");
		Assert.notNull(urlMappingsHolder, "urlMappingsHolder is required");

		_compiled.clear();

		_urlMappingsHolder = urlMappingsHolder;

		for (GrailsClass controllerClass : controllerClasses) {
			findControllerAnnotations((GrailsControllerClass)controllerClass, actionRoleMap, classRoleMap);
		}

		compileActionMap(actionRoleMap);
		compileClassMap(classRoleMap);
		compileStaticRules(staticRules);

		if (_log.isTraceEnabled()) {
			_log.trace("configs: " + _compiled);
		}
	}

	private void compileActionMap(final Map<String, Map<String, Set<String>>> map) {
		for (Map.Entry<String, Map<String, Set<String>>> controllerEntry : map.entrySet()) {
			String controllerName = controllerEntry.getKey();
			Map<String, Set<String>> actionRoles = controllerEntry.getValue();
			for (Map.Entry<String, Set<String>> actionEntry : actionRoles.entrySet()) {
				String actionName = actionEntry.getKey();
				Set<String> roles = actionEntry.getValue();
				storeMapping(controllerName, actionName, roles, false);
			}
		}
	}

	private void compileClassMap(final Map<String, Set<String>> classRoleMap) {
		for (Map.Entry<String, Set<String>> entry : classRoleMap.entrySet()) {
			String controllerName = entry.getKey();
			Set<String> roles = entry.getValue();
			storeMapping(controllerName, null, roles, false);
		}
	}

	private void compileStaticRules(final Map<String, Collection<String>> staticRules) {
		for (Map.Entry<String, Collection<String>> entry : staticRules.entrySet()) {
			String pattern = entry.getKey();
			Collection<String> roles = entry.getValue();
			storeMapping(pattern, null, roles, true);
		}
	}

	private void storeMapping(final String controllerNameOrPattern, final String actionName,
			final Collection<String> roles, final boolean isPattern) {

		String fullPattern;
		if (isPattern) {
			fullPattern = controllerNameOrPattern;
		}
		else {
			StringBuilder sb = new StringBuilder();
			sb.append('/').append(controllerNameOrPattern);
			if (actionName != null) {
				sb.append('/').append(actionName);
			}
			sb.append("/**");
			fullPattern = sb.toString();
		}

		ConfigAttributeDefinition configAttribute = new ConfigAttributeDefinition(
				roles.toArray(new String[roles.size()]));

		Object key = getUrlMatcher().compile(fullPattern);
		ConfigAttributeDefinition replaced = _compiled.put(key, configAttribute);
		if (replaced != null) {
			_log.warn("replaced rule for '" + key + "' with roles " + replaced.getConfigAttributes()
					+ " with roles " + configAttribute.getConfigAttributes());
		}
	}

	private void findControllerAnnotations(final GrailsControllerClass controllerClass,
			final Map<String, Map<String, Set<String>>> actionRoleMap,
			final Map<String, Set<String>> classRoleMap) {

		Class<?> clazz = controllerClass.getClazz();
		String controllerName = WordUtils.uncapitalize(controllerClass.getName());

		Secured annotation = clazz.getAnnotation(Secured.class);
		if (annotation != null) {
			classRoleMap.put(controllerName, asSet(annotation.value()));
		}

		Map<String, Set<String>> annotatedClosureNames = findActionRoles(clazz);
		if (annotatedClosureNames != null) {
			actionRoleMap.put(controllerName, annotatedClosureNames);
		}
	}

	private Map<String, Set<String>> findActionRoles(final Class<?> clazz) {
		// since action closures are defined as "def foo = ..." they're
		// fields, but they end up as private
		Map<String, Set<String>> actionRoles = new HashMap<String, Set<String>>();
		for (Field field : clazz.getDeclaredFields()) {
			Secured annotation = field.getAnnotation(Secured.class);
			if (annotation != null) {
				actionRoles.put(field.getName(), asSet(annotation.value()));
			}
		}
		return actionRoles;
	}

	private Set<String> asSet(final String[] strings) {
		Set<String> set = new HashSet<String>();
		for (String string : strings) {
			set.add(string);
		}
		return set;
	}
}
