/*
 * 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.Locale;
import java.util.Map;

import javax.servlet.http.Cookie;

import nuts.core.i18n.LocaleUtils;
import nuts.core.lang.StringUtils;
import nuts.exts.struts2.NutsStrutsConstants;
import nuts.exts.xwork2.util.LocalizedTextUtils;

import org.apache.struts2.ServletActionContext;

import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
import com.opensymphony.xwork2.interceptor.I18nInterceptor;

/**
 * <!-- START SNIPPET: description -->
 * 
 * An interceptor that handles setting the locale specified in a session as the locale for the
 * current action request. In addition, this interceptor will look for a specific HTTP request
 * parameter and set the locale to whatever value is provided. This means that this interceptor can
 * be used to allow for your application to dynamically change the locale for the user's session.
 * This is very useful for applications that require multi-lingual support and want the user to be
 * able to set his or her language preference at any point. The locale parameter is removed during
 * the execution of this interceptor, ensuring that properties aren't set on an action (such as
 * __locale) that have no typical corresponding setter in your action.
 * 
 * <p/>
 * For example, using the default parameter name, a request to
 * <b>foo.action?__locale=en_US</b>, then the locale for US English is saved in the user's
 * session and will be used for all future requests.
 * 
 * <!-- END SNIPPET: description -->
 * 
 * <p/>
 * <u>Interceptor parameters:</u>
 * 
 * <!-- START SNIPPET: parameters -->
 * 
 * <ul>
 * 
 * <li>parameterName (optional) - the name of the HTTP request parameter that dictates the locale to
 * switch to and save in the session. By default this is <b>__locale</b></li>
 * 
 * <li>attributeName (optional) - the name of the session key to store the selected locale. By
 * default this is <b>WW_TRANS_I18N_LOCALE</b></li>
 * 
 * </ul>
 * 
 * <!-- END SNIPPET: parameters -->
 * 
 * <p/>
 * <u>Extending the interceptor:</u>
 * 
 * <p/>
 * 
 * <!-- START SNIPPET: extending -->
 * 
 * There are no known extensions points for this interceptor.
 * 
 * <!-- END SNIPPET: extending -->
 * 
 * <p/>
 * <u>Example code:</u>
 * 
 * <pre>
 * &lt;!-- START SNIPPET: example --&gt;
 * &lt;action name=&quot;someAction&quot; class=&quot;com.examples.SomeAction&quot;&gt;
 *     &lt;interceptor-ref name=&quot;locale&quot;/&gt;
 *     &lt;interceptor-ref name=&quot;basicStack&quot;/&gt;
 *     &lt;result name=&quot;success&quot;&gt;good_result.ftl&lt;/result&gt;
 * &lt;/action&gt;
 * &lt;!-- END SNIPPET: example --&gt;
 * </pre>
 */
@SuppressWarnings("serial")
public class LocaleInterceptor extends AbstractInterceptor {

	/**
	 * DEFAULT_SESSION_ATTRIBUTE = I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE;
	 */
	public static final String DEFAULT_SESSION_ATTRIBUTE = I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE;

	/**
	 * DEFAULT_PARAMETER = "__locale";
	 */
	public static final String DEFAULT_PARAMETER = "__locale";

	/**
	 * DEFAULT_REQUEST_PARAMETER = "__req_locale";
	 */
    public static final String DEFAULT_REQUEST_PARAMETER = "__req_locale";

    /**
	 * DEFAULT_COOKIE = DEFAULT_SESSION_ATTRIBUTE;
	 */
	public static final String DEFAULT_COOKIE = DEFAULT_SESSION_ATTRIBUTE;

	/**
	 * DEFAULT_COOKIE_MAXAGE = 60 * 60 * 24 * 30; //1M
	 */
	public static final Integer DEFAULT_COOKIE_MAXAGE = 60 * 60 * 24 * 30; //1M

	protected String attributeName = DEFAULT_SESSION_ATTRIBUTE;
	protected String parameterName = DEFAULT_PARAMETER;
	protected String requestName = DEFAULT_REQUEST_PARAMETER;
	protected String cookieName = DEFAULT_COOKIE;
	protected String cookieDomain;
	protected String cookiePath;
	protected Integer cookieMaxAge = DEFAULT_COOKIE_MAXAGE;
	protected String[] validLocales;
	protected boolean fromAcceptLanguage = true;

	/**
	 * Constructor
	 */
	public LocaleInterceptor() {
	}

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

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

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

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

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

	/**
	 * @param cookieMaxAge the cookieMaxAge to set
	 */
	public void setCookieMaxAge(Integer cookieMaxAge) {
		this.cookieMaxAge = cookieMaxAge;
	}

	/**
	 * @param validLocale the validLocale to set
	 */
    @Inject(value=NutsStrutsConstants.NUTS_LOCALE_VALID, required=false)
	public void setValidLocale(String validLocale) {
		this.validLocales = StringUtils.split(validLocale, ',');
	}

	/**
	 * @param fromAcceptLanguage the fromAcceptLanguage to set
	 */
	public void setFromAcceptLanguage(String fromAcceptLanguage) {
		this.fromAcceptLanguage = Boolean.parseBoolean(fromAcceptLanguage);
	}

	/**
	 * 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 {
		Map<String, Object> params = actionInvocation.getInvocationContext().getParameters();
		Map<String, Object> session = actionInvocation.getInvocationContext().getSession();

		boolean saveToSession = true;
		boolean saveToCookie = true;
		
        Locale locale = null;

		locale = getLocaleFromParameter(params, parameterName);
		if (locale == null) {
			locale = getLocaleFromParameter(params, requestName);
			if (locale != null) {
				saveToSession = false;
				saveToCookie = false;
			}
		}

        if (locale == null && session != null) {
			Object obj = session.get(attributeName);
			if (obj != null && obj instanceof Locale) {
				locale = (Locale)obj;
				saveToSession = false;
				saveToCookie = false;
			}
		}
		
		if (locale == null && StringUtils.isNotEmpty(cookieName)) {
			locale = getLocaleFromCookie();
			saveToCookie = false;
		}
		
		if (locale == null && fromAcceptLanguage) {
			locale = getLocaleFromAcceptLanguage();
			saveToCookie = false;
		}

		// save locale
		if (locale != null) {
			saveLocale(actionInvocation, locale);

			if (saveToSession && session != null) {
				session.put(attributeName, locale);
			}
			if (saveToCookie && StringUtils.isNotEmpty(cookieName)) {
				saveLocaleToCookie(locale);
			}
		}
		
		return actionInvocation.invoke();
	}

	protected Locale getLocaleFromCookie() {
		Cookie[] cookies = ServletActionContext.getRequest().getCookies();
		for (int i = 0; cookies != null && i < cookies.length; i++) {
			Cookie c = cookies[i];
			if (cookieName.equals(c.getName())) {
				Locale locale = LocalizedTextUtils.localeFromString(c.getValue(), null);
				return isValidLocale(locale) ? locale : null;
			}
		}
		return null;
    }

	protected Locale getLocaleFromParameter(Map<String, Object> params,
			String parameterName) {
		Object locale = params.remove(parameterName);
		if (locale != null && locale.getClass().isArray() && ((Object[])locale).length == 1) {
			locale = ((Object[])locale)[0];
		}

		if (locale != null && !(locale instanceof Locale)) {
			locale = LocalizedTextUtils.localeFromString(locale.toString(), null);
		}
		return isValidLocale((Locale)locale) ? (Locale)locale : null;
	}

	protected Locale getLocaleFromAcceptLanguage() {
		String al = ServletActionContext.getRequest().getHeader("Accept-Language");
		if (StringUtils.isNotEmpty(al)) {
			String[] als = StringUtils.split(StringUtils.replaceChars(al, '-', '_'), ",;");
			for (String str : als) {
				try {
					Locale locale = LocaleUtils.toLocale(str);
					if (isValidLocale(locale)) {
						return locale;
					}
				}
				catch (Exception e) {
					
				}
			}
		}
		return null;
	}

    protected void saveLocaleToCookie(Locale locale) {
		Cookie c = new Cookie(cookieName, locale.toString());
		if (StringUtils.isNotEmpty(cookieDomain)) {
			c.setDomain(cookieDomain);
		}
		
		if (StringUtils.isNotEmpty(cookiePath)) {
			c.setPath(cookiePath);
		}
		else {
			c.setPath(ServletActionContext.getRequest().getContextPath());
		}
		
		c.setMaxAge(cookieMaxAge);
		
		ServletActionContext.getResponse().addCookie(c);
    }
    
	/**
	 * Save the given locale to the ActionInvocation.
	 * 
	 * @param invocation The ActionInvocation.
	 * @param locale The locale to save.
	 */
	protected void saveLocale(ActionInvocation invocation, Locale locale) {
		invocation.getInvocationContext().setLocale(locale);
	}

	protected boolean isValidLocale(Locale locale) {
		if (locale != null) {
			if (validLocales == null) {
				return true;
			}
			String l = locale.toString();
			for (String p : validLocales) {
				if (l.startsWith(p)) {
					return true;
				}
			}
		}
		return false;
	}
}
