/*
 * This file is part of Nuts Framework.
 * Copyright (C) 2009 Nuts Develop Team.
 *
 * Nuts Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License any later version.
 *
 * Nuts Framework is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.exts.struts2.interceptor;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.http.HttpServletRequest;

import nuts.core.beans.PropertyUtils;
import nuts.core.lang.ArrayUtils;
import nuts.core.lang.ClassLoaderUtils;
import nuts.core.lang.StringUtils;
import nuts.core.orm.dao.DataAccessSession;
import nuts.exts.app.AppConstants;
import nuts.exts.exception.NoPermitException;
import nuts.exts.struts2.actions.NutsTextConstants;
import nuts.exts.struts2.util.StrutsContextUtils;
import nuts.exts.xwork2.ActionSupport;
import nuts.exts.xwork2.util.ContextUtils;

import org.apache.struts2.views.util.UrlHelper;

import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;

/**
 */
@SuppressWarnings("serial")
public class PermissionInterceptor extends AbstractInterceptor
		implements NutsTextConstants, AppConstants {

	/**
	 * action permit map
	 */
	private static Map<String, String[]> actionPermits = new HashMap<String, String[]>();

	/**
	 * action permit cache
	 */
	private static Map<String, String[]> actionPermitCache = new ConcurrentHashMap<String, String[]>();

	/**
	 * redirectResult
	 */
	protected String redirectResult = Action.LOGIN;

	/**
	 * needPassword
	 */
	protected boolean needPassword = false;

	/**
	 * @param redirectResult the redirectResult to set
	 */
	public void setRedirectResult(String redirectResult) {
		this.redirectResult = redirectResult;
	}

	/**
	 * @param needPassword the needPassword to set
	 */
	public void setNeedPassword(boolean needPassword) {
		this.needPassword = needPassword;
	}

	/**
	 * loadActionPermits
	 * @throws Exception if an error occurs
	 */
	public synchronized static void loadActionPermits(DataAccessSession das) throws Exception {
		actionPermits.clear();
		actionPermitCache.clear();

		Properties props = new Properties();
		props.load(ClassLoaderUtils.getResourceAsStream("action-permits.properties"));

		for (Entry<Object, Object> en : props.entrySet()) {
			actionPermits.put(en.getKey().toString(), StringUtils.split(en.getValue().toString()));
		}
	}

	/**
	 * hasPermission
	 * @param permits permits
	 * @param namespace namespace
	 * @param action action
	 * @return true if user has permit to access the action
	 */
	public static boolean hasPermission(String[] permits, String namespace, String action) {
		if (ArrayUtils.contains(permits, PERMISSION_ALL)) {
			return true;
		}

		String uri = ("/".equals(namespace) ? "" : namespace) + "/" + action;
		
		String[] ps = actionPermitCache.get(uri);
		if (ps == null) {
			ps = new String[0];
			for (Entry<String, String[]> e : actionPermits.entrySet()) {
				if (StringUtils.wildcardMatch(uri, e.getKey())) {
					ps = e.getValue();
					break;
				}
			}
			actionPermitCache.put(uri, ps);
		}

		if (ArrayUtils.isEmpty(ps)) {
			// uri is not in the check list, pass
			return true;
		}

		if (ArrayUtils.isEmpty(permits)) {
			return false;
		}
		
		if (ArrayUtils.contains(permits, PERMISSION_NONE)) {
			return false;
		}
		
		for (String p : ps) {
			if (ArrayUtils.contains(permits, p)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * add error to action
	 * @param invocation the action invocation
	 * @param id msgId
	 */
	protected void addActionError(ActionInvocation invocation, String id) {
		HttpServletRequest request = StrutsContextUtils.getServletRequest();
		String uri = request.getRequestURI();

		String msg = "";

		Object action = invocation.getAction();
		if (action instanceof ActionSupport) {
			ActionSupport a = (ActionSupport)action;
			msg = a.getText(id, new String[] { uri });
			a.addActionError(msg);
		}
		else {
			Map<String, String> m = new HashMap<String, String>();
			m.put("id", id);
			m.put("msg", msg);
			request.setAttribute("actionErrors", m);
		}
	}
	
	/**
	 * remove session error
	 * @param invocation the action invocation
	 * @param id msgId
	 */
	protected void removeSessionError(ActionInvocation invocation, String id) {
		Object action = invocation.getAction();
		if (action instanceof ActionSupport) {
			ActionSupport a = (ActionSupport)action;
			a.removeSessionError(id);
		}
	}
	
	/**
	 * clear session errors
	 * @param invocation the action invocation
	 * @param id msgId
	 */
	protected void clearSessionErrors(ActionInvocation invocation) {
		Object action = invocation.getAction();
		if (action instanceof ActionSupport) {
			ActionSupport a = (ActionSupport)action;
			a.clearSessionErrors();
		}
	}
	
	/**
	 * add error to session
	 * @param invocation the action invocation
	 * @param id msgId
	 */
	protected void addSessionError(ActionInvocation invocation, String id) {
		HttpServletRequest request = StrutsContextUtils.getServletRequest();
		String uri = request.getRequestURI();

		String msg = "";

		Object action = invocation.getAction();
		if (action instanceof ActionSupport) {
			ActionSupport a = (ActionSupport)action;
			msg = a.getText(id, new String[] { uri });
			a.addSessionError(id, msg);
		}
		else {
			Map<String, String> m = new HashMap<String, String>();
			m.put("id", id);
			m.put("msg", msg);
			request.setAttribute("sessionErrors", m);
		}
	}

	public static class SessionUser {
		private String password;
		private String[] permits;
		/**
		 * @return the password
		 */
		public String getPassword() {
			return password;
		}
		/**
		 * @param password the password to set
		 */
		public void setPassword(String password) {
			this.password = password;
		}
		/**
		 * @return the permits
		 */
		public String[] getPermits() {
			return permits;
		}
		/**
		 * @param permits the permits to set
		 */
		public void setPermits(String[] permits) {
			this.permits = permits;
		}
	}
	
	private String sessionUser = "user";
	
	protected SessionUser getSessionUser() {
		Object u = ContextUtils.getSession().get(sessionUser);
		if (u == null) {
			return null;
		}
		
		SessionUser user = new SessionUser();
		user.password = (String)PropertyUtils.getProperty(u, "password");
		user.permits = (String[])PropertyUtils.getProperty(u, "permits");
		
		return user;
	}
	
	/**
	 * Allows the Interceptor to do some processing on the request before and/or after the rest of the processing of the
	 * request by the {@link ActionInvocation} or to short-circuit the processing and just return a String return code.
	 *
	 * @param actionInvocation  the action invocation
	 * @return the return code, either returned from {@link ActionInvocation#invoke()}, or from the interceptor itself.
	 * @throws Exception any system-level error, as defined in {@link com.opensymphony.xwork2.Action#execute()}.
	 */
	public String intercept(ActionInvocation actionInvocation) throws Exception {
		SessionUser user = getSessionUser();
		if (user != null
				&& (!needPassword || StringUtils.isNotEmpty(user.password))) {
			String n = actionInvocation.getProxy().getNamespace();
			String a = actionInvocation.getInvocationContext().getName();
			if (hasPermission(user.permits, n, a)) {
				return actionInvocation.invoke();
			}
			else {
				throw new NoPermitException();
			}
		}
		else {
			HttpServletRequest request = StrutsContextUtils.getServletRequest();
			String requestUrl = UrlHelper.buildUrl(null, request, null, request.getParameterMap(),
				request.getScheme(), false, false, false, false);
			actionInvocation.getStack().getContext().put("redirect", requestUrl);
			addActionError(actionInvocation, ERROR_UNLOGIN);
			return redirectResult;
		}
	}

}
