package com.palmnest.webapp.listener;

import com.palmnest.model.User;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.util.LinkedHashSet;
import java.util.Set;


/**
 * UserCounterListener class used to count the current number
 * of active users for the applications.  Does this by counting
 * how many user objects are stuffed into the session.  It also grabs
 * these users and exposes them in the servlet context.
 *
 * @author <a href="mailto:chasel.java@gmail.com">Chasel Li</a>
 */
public class UserCounterListener implements ServletContextListener, HttpSessionAttributeListener, HttpSessionListener {
	/**
	 * Name of user counter variable
	 */
	public static final String         COUNT_KEY = "userCounter";
	/**
	 * Name of users Set in the ServletContext
	 */
	public static final String         USERS_KEY = "userNames";
	/**
	 * The default event we're looking to trap.
	 */
	public static final String         EVENT_KEY = HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
	private transient   ServletContext servletContext;
	private             int            counter;
	private             Set<User>      users;

	/**
	 * Initialize the context
	 *
	 * @param sce the event
	 */
	public synchronized void contextInitialized(ServletContextEvent sce) {
		servletContext = sce.getServletContext();
		servletContext.setAttribute((COUNT_KEY), Integer.toString(counter));
	}

	/**
	 * Set the servletContext, users and counter to null
	 *
	 * @param event The servletContextEvent
	 */
	public synchronized void contextDestroyed(ServletContextEvent event) {
		servletContext = null;
		users = null;
		counter = 0;
	}

	synchronized void incrementUserCounter() {
		counter = Integer.parseInt((String) servletContext.getAttribute(COUNT_KEY));
		counter++;
		servletContext.setAttribute(COUNT_KEY, Integer.toString(counter));
	}

	synchronized void decrementUserCounter() {
		int counter = Integer.parseInt((String) servletContext.getAttribute(COUNT_KEY));
		counter--;

		if (counter < 0) {
			counter = 0;
		}

		servletContext.setAttribute(COUNT_KEY, Integer.toString(counter));
	}

	@SuppressWarnings("unchecked")
	synchronized void addUsername(User user) {
		users = (Set<User>) servletContext.getAttribute(USERS_KEY);

		if (users == null) {
			users = new LinkedHashSet<User>();
		}

		if (!users.contains(user)) {
			users.add(user);
			servletContext.setAttribute(USERS_KEY, users);
			incrementUserCounter();
		}
	}

	@SuppressWarnings("unchecked")
	synchronized void removeUsername(User user) {
		users = (Set<User>) servletContext.getAttribute(USERS_KEY);

		if (users != null) {
			users.remove(user);
		}

		servletContext.setAttribute(USERS_KEY, users);
		decrementUserCounter();
	}

	/**
	 * This method is designed to catch when user's login and record their name
	 *
	 * @param event the event to process
	 * @see javax.servlet.http.HttpSessionAttributeListener#attributeAdded(javax.servlet.http.HttpSessionBindingEvent)
	 */
	public void attributeAdded(HttpSessionBindingEvent event) {
		if (event.getName().equals(EVENT_KEY) && !isAnonymous()) {
			SecurityContext securityContext = (SecurityContext) event.getValue();
			if (securityContext != null && securityContext.getAuthentication().getPrincipal() instanceof User) {
				User user = (User) securityContext.getAuthentication().getPrincipal();
				addUsername(user);
			}
		}
	}

	private boolean isAnonymous() {
		AuthenticationTrustResolver resolver = new AuthenticationTrustResolverImpl();
		SecurityContext             ctx      = SecurityContextHolder.getContext();
		if (ctx != null) {
			Authentication auth = ctx.getAuthentication();
			return resolver.isAnonymous(auth);
		}
		return true;
	}

	/**
	 * When user's logout, remove their name from the hashMap
	 *
	 * @param event the session binding event
	 * @see javax.servlet.http.HttpSessionAttributeListener#attributeRemoved(javax.servlet.http.HttpSessionBindingEvent)
	 */
	public void attributeRemoved(HttpSessionBindingEvent event) {
		if (event.getName().equals(EVENT_KEY) && !isAnonymous()) {
			SecurityContext securityContext = (SecurityContext) event.getValue();
			Authentication  auth            = securityContext.getAuthentication();
			if (auth != null && (auth.getPrincipal() instanceof User)) {
				User user = (User) auth.getPrincipal();
				removeUsername(user);
			}
		}
	}

	/**
	 * Needed for Acegi Security 1.0, as it adds an anonymous user to the session and
	 * then replaces it after authentication. http://forum.springframework.org/showthread.php?p=63593
	 *
	 * @param event the session binding event
	 * @see javax.servlet.http.HttpSessionAttributeListener#attributeReplaced(javax.servlet.http.HttpSessionBindingEvent)
	 */
	public void attributeReplaced(HttpSessionBindingEvent event) {
		if (event.getName().equals(EVENT_KEY) && !isAnonymous()) {
			final SecurityContext securityContext = (SecurityContext) event.getValue();
			if (securityContext.getAuthentication() != null
					&& securityContext.getAuthentication().getPrincipal() instanceof User) {
				final User user = (User) securityContext.getAuthentication().getPrincipal();
				addUsername(user);
			}
		}
	}

	public void sessionCreated(HttpSessionEvent se) {
	}

	public void sessionDestroyed(HttpSessionEvent se) {
		Object obj = se.getSession().getAttribute(EVENT_KEY);
		if (obj != null) {
			se.getSession().removeAttribute(EVENT_KEY);
		}
	}
}
