/* $This file is distributed under the terms of the license in LICENSE$ */

package edu.cornell.mannlib.vitro.webapp.startup;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextListener;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Accumulates a list of messages from the StartupManager, and from the context
 * listeners that the run during startup.
 * 
 * This is thread-safe, with immutable items in the list and synchronized access
 * to the list.
 */
public class StartupStatus {
	private static final Log log = LogFactory.getLog(StartupStatus.class);

	protected static final String ATTRIBUTE_NAME = "STARTUP_STATUS";

	// ----------------------------------------------------------------------
	// static methods
	// ----------------------------------------------------------------------

	public static StartupStatus getBean(ServletContext ctx) {
		StartupStatus ss;

		Object o = ctx.getAttribute(ATTRIBUTE_NAME);
		if (o instanceof StartupStatus) {
			ss = (StartupStatus) o;
		} else {
			ss = new StartupStatus();
			ctx.setAttribute(ATTRIBUTE_NAME, ss);
		}

		return ss;
	}

	// ----------------------------------------------------------------------
	// methods to set status - note that these write to the log also.
	// ----------------------------------------------------------------------

	private SynchronizedStatusItemList itemList = new SynchronizedStatusItemList();

	public void info(ServletContextListener listener, String message) {
		addItem(StatusItem.Level.INFO, listener, message, null);
	}

	public void info(ServletContextListener listener, String message,
			Throwable cause) {
		addItem(StatusItem.Level.INFO, listener, message, cause);
	}

	public void warning(ServletContextListener listener, String message) {
		addItem(StatusItem.Level.WARNING, listener, message, null);
	}

	public void warning(ServletContextListener listener, String message,
			Throwable cause) {
		addItem(StatusItem.Level.WARNING, listener, message, cause);
	}

	public void fatal(ServletContextListener listener, String message) {
		addItem(StatusItem.Level.FATAL, listener, message, null);
	}

	public void fatal(ServletContextListener listener, String message,
			Throwable cause) {
		addItem(StatusItem.Level.FATAL, listener, message, cause);
	}

	/** Say that a previous fatal error prevented this listener from running. */
	public void listenerNotExecuted(ServletContextListener listener) {
		addItem(StatusItem.Level.NOT_EXECUTED,
				listener,
				"Not executed - startup was aborted by a previous fatal error.",
				null);
	}

	/** Create a simple item for this listener if no other exists. */
	public void listenerExecuted(ServletContextListener listener) {
		List<StatusItem> itemsForThisListener = getItemsForListener(listener);
		if (itemsForThisListener.isEmpty()) {
			addItem(StatusItem.Level.INFO, listener, "Ran successfully.", null);
		}
	}

	private void addItem(StatusItem.Level level, ServletContextListener source,
			String message, Throwable cause) {
		StatusItem item = new StatusItem(level, source, message, cause);
		itemList.add(item);

		String logMessage = "From " + item.getShortSourceName() + ": "
				+ item.getMessage();
		if (item.getLevel() == StatusItem.Level.FATAL) {
			if (cause == null) {
				log.fatal(logMessage);
			} else {
				log.fatal(logMessage, cause);
			}
		} else if (item.getLevel() == StatusItem.Level.WARNING) {
			if (cause == null) {
				log.warn(logMessage);
			} else {
				log.warn(logMessage, cause);
			}
		} else {
			if (cause == null) {
				log.info(logMessage);
			} else {
				log.info(logMessage, cause);
			}
		}
	}

	// ----------------------------------------------------------------------
	// methods to query status
	// ----------------------------------------------------------------------

	public boolean allClear() {
		return getErrorItems().isEmpty() && getWarningItems().isEmpty();
	}

	public boolean isStartupAborted() {
		return !getErrorItems().isEmpty();
	}

	public List<StatusItem> getStatusItems() {
		return itemList.filterItems(StatusItemFilter.ALL_ITEMS_FILTER);
	}

	public List<StatusItem> getErrorItems() {
		return itemList.filterItems(StatusItemFilter.ERROR_ITEMS_FILTER);
	}

	public List<StatusItem> getWarningItems() {
		return itemList.filterItems(StatusItemFilter.WARNING_ITEMS_FILTER);
	}

	public List<StatusItem> getItemsForListener(ServletContextListener listener) {
		return itemList.filterItems(StatusItemFilter.listenerFilter(listener));
	}

	// ----------------------------------------------------------------------
	// helper classes
	// ----------------------------------------------------------------------

	/**
	 * An immutable item that can't throw an exception during construction and
	 * will always contain suitable, non-null values.
	 */
	public static class StatusItem {
		public enum Level {
			INFO, WARNING, FATAL, NOT_EXECUTED
		}

		private final Level level;
		private final String sourceName;
		private final String shortSourceName;
		private final String message;
		private final String cause;

		private boolean unexpectedArguments;

		public StatusItem(Level level, ServletContextListener source,
				String message, Throwable cause) {
			this.level = figureLevel(level);
			this.sourceName = figureSourceName(source);
			this.shortSourceName = figureShortSourceName(source);
			this.message = message;
			this.cause = figureCauseString(cause);

			if (unexpectedArguments) {
				log.error("Unexpected arguments to "
						+ StatusItem.class.getName() + ": level=" + level
						+ ", source=" + source + ", message=" + message
						+ ", cause=" + cause);
			}
		}

		/** Level should never be null: we have a problem. */
		private Level figureLevel(Level newLevel) {
			if (newLevel == null) {
				unexpectedArguments = true;
				return Level.FATAL;
			} else {
				return newLevel;
			}
		}

		private String figureSourceName(ServletContextListener source) {
			if (source == null) {
				unexpectedArguments = true;
				return "UNKNOWN SOURCE";
			} else {
				return source.getClass().getName();
			}
		}

		/**
		 * Don't just use getSimpleName(): on an inner class we'd like to see
		 * the parent also.
		 */
		private String figureShortSourceName(ServletContextListener source) {
			if (source == null) {
				unexpectedArguments = true;
				return "UNKNOWN_SOURCE";
			} else {
				String sourceClassName = source.getClass().getName();
				int lastPeriodHere = sourceClassName.lastIndexOf('.');
				return sourceClassName.substring(lastPeriodHere + 1);
			}
		}

		/** Cause may be null - that's not unexpected. */
		private String figureCauseString(Throwable newCause) {
			if (newCause == null) {
				return "";
			} else {
				StringWriter sw = new StringWriter();
				PrintWriter pw = new PrintWriter(sw);
				newCause.printStackTrace(pw);
				return sw.toString();
			}
		}

		public Level getLevel() {
			return level;
		}

		public String getSourceName() {
			return sourceName;
		}

		public String getShortSourceName() {
			return shortSourceName;
		}

		public String getMessage() {
			return message;
		}

		public String getCause() {
			return cause;
		}

	}

	/**
	 * A filter class and some basic instances.
	 */
	private static abstract class StatusItemFilter {
		public abstract boolean accept(StatusItem item);

		public static final StatusItemFilter ALL_ITEMS_FILTER = new StatusItemFilter() {
			@Override
			public boolean accept(StatusItem item) {
				return true;
			}
		};

		public static final StatusItemFilter ERROR_ITEMS_FILTER = new StatusItemFilter() {
			@Override
			public boolean accept(StatusItem item) {
				return item.level == StatusItem.Level.FATAL;
			}
		};

		public static final StatusItemFilter WARNING_ITEMS_FILTER = new StatusItemFilter() {
			@Override
			public boolean accept(StatusItem item) {
				return item.level == StatusItem.Level.WARNING;
			}
		};

		public static StatusItemFilter listenerFilter(
				ServletContextListener listener) {
			final String listenerName = listener.getClass().getName();

			return new StatusItemFilter() {
				@Override
				public boolean accept(StatusItem item) {
					return item.getSourceName().equals(listenerName);
				}
			};

		}
	}

	/**
	 * A list, with synchronized methods for adding to it, and for getting a
	 * filtered subset of it.
	 */
	private class SynchronizedStatusItemList {
		private final List<StatusItem> list = new ArrayList<StatusItem>();

		public void add(StatusItem item) {
			synchronized (list) {
				list.add(item);
			}
		}

		public List<StatusItem> filterItems(StatusItemFilter filter) {
			List<StatusItem> filteredList = new ArrayList<StatusItem>();
			synchronized (list) {
				for (StatusItem item : list) {
					if (filter.accept(item)) {
						filteredList.add(item);
					}
				}
			}
			return filteredList;
		}
	}
}
