public class AbsenceManager extends AbstractManager {

	private DataTrain train;

	private static final Logger LOG = Logger.getLogger(AbsenceManager.class
			.getName());

	public AbsenceManager(DataTrain dataTrain) {
		this.train = dataTrain;
	}

	/**
	 * This does not store any changes in the database!
	 * 
	 * Note that this is only valid for forms A, B, and C. Form D has separate
	 * validation that occurs when a Form D is approved AND verified.
	 * 
	 * @param absence
	 * @return
	 */
	private boolean shouldBeAutoApproved(Absence absence) {
		Event linked = absence.getEvent();
		User student = absence.getStudent();

		if (absence.getStatus() != Absence.Status.Pending) {
			// only pending absences can be auto-approved
			return false;
		}
		if (linked == null) {
			// Can only auto-approve linked absences
			return false;
		}

		if (student == null) {
			// Has to have a student associated
			return false;
		}

		// True if approved by any form
		for (Form form : train.forms().get(student)) {
			if (shouldBeAutoApproved(absence, form)) {
				return true;
			}
		}

		return false;
	}

	Interval getAbsenceInterval(Absence absence, Event event, DateTimeZone zone) {
		// this assumes that a tardy is the /first/ checkin
		// and that an eco is the /last/ checkout
		switch (absence.getType()) {
		case Absence:
			// should be the whole thing
			Interval i = absence.getInterval(zone).overlap(
					event.getInterval(zone));
			if (i == null) {
				return null;
			}
			return i;
		case EarlyCheckOut:
			DateTime checkout = absence.getCheckout(zone);
			if (!event.getInterval(zone).contains(checkout)) {
				throw new IllegalArgumentException();
			}
			return new Interval(checkout, event.getInterval(zone).getEnd());
		case Tardy:
			DateTime checkin = absence.getCheckin(zone);
			if (!event.getInterval(zone).contains(checkin)) {
				throw new IllegalArgumentException();
			}
			return new Interval(event.getInterval(zone).getStart(), checkin);
		default:
			throw new UnsupportedOperationException();
		}

	}

	Interval getEffectiveFormInterval(Absence absence, Form form,
			DateTimeZone zone) {
		int formDayOfWeek = form.getDayOfWeek().DayOfWeek;
		DateMidnight formDayOnAbsenceWeek;
		switch (absence.getType()) {
		case Absence:

			DateTime aStart = absence.getInterval(zone).getStart();
			DateTime aEnd = absence.getInterval(zone).getEnd();

			if (aStart.getDayOfWeek() != aEnd.getDayOfWeek()) {
				LOG.warning("BLERG");
			}

			formDayOnAbsenceWeek = aStart.withDayOfWeek(formDayOfWeek)
					.toDateMidnight();
			break;
		case Tardy:
			DateTime checkin = absence.getCheckin(zone);
			formDayOnAbsenceWeek = checkin.withDayOfWeek(formDayOfWeek)
					.toDateMidnight();
			break;
		case EarlyCheckOut:
			DateTime checkout = absence.getCheckout(zone);
			formDayOnAbsenceWeek = checkout.withDayOfWeek(formDayOfWeek)
					.toDateMidnight();
			break;
		default:
			throw new UnsupportedOperationException();
		}

		int minutesToOrFrom = form.getMinutesToOrFrom();
		DateTime effectiveStartTime = form.getStartTime()
				.toDateTime(formDayOnAbsenceWeek).minusMinutes(minutesToOrFrom);
		DateTime effectiveEndTime = form.getEndTime()
				.toDateTime(formDayOnAbsenceWeek).plusMinutes(minutesToOrFrom);

		Interval effectiveFormInterval = new Interval(effectiveStartTime,
				effectiveEndTime);
		return effectiveFormInterval;

	}

	/**
	 * This does not store any changes in the database!
	 * 
	 * Note that this is only valid for forms A, B, and C. Form D has separate
	 * validation that occurs when a Form D is approved AND verified.
	 * 
	 * @param absence
	 * @param form
	 * @return
	 */
	private boolean shouldBeAutoApproved(Absence absence, Form form) {

		DateTimeZone zone = this.train.appData().get().getTimeZone();

		if (form.getStatus() != Form.Status.Approved) {
			// form must be approved!
			return false;
		}
		if (absence.getStatus() != Absence.Status.Pending) {
			// only pending absences can be auto-approved
			return false;
		}

		if (form.getStudent() == null || absence.getStudent() == null) {
			throw new IllegalArgumentException(
					"Student was null in the absence or form.");
		}

		if (absence.getEvent() == null) {
			throw new IllegalArgumentException("Absence had a null event.");
		}

		if (!form.getStudent().equals(absence.getStudent())) {
			throw new IllegalArgumentException(
					"Can't check absence against a form from another student.");
		}

		switch (form.getType()) {
		case PerformanceAbsence:
			// Performance absence request
			if (absence.getEvent().getType() != Event.Type.Performance) {
				// nope!
				return false;
			} else {
				// TODO use Absence.isContainedIn(...)?
				if (form.getInterval(zone).contains(
						absence.getEvent().getInterval(zone))) {
					return true;
				}
			}
			break;
		case ClassConflict:
			Event e = absence.getEvent();

			if (e.getType() != Event.Type.Rehearsal) {
				return false;
			}

			Interval effectiveFormInterval = getEffectiveFormInterval(absence,
					form, zone);
			return Absence.isContainedIn(absence, effectiveFormInterval);
		case TimeWorked:
			// this does not auto-approve here. It does that upon an upDateTime
			// of a
			// Form D in the forms controller
			break;
		}

		return false;
	}

	/**
	 * This is deprecated because the start and end DateTime are not sufficient
	 * to identify a single event. Type must also be specified. This is needed
	 * in the current implementation of the mobile app, though.
	 * 
	 * @param student
	 * @param start
	 * @param end
	 * @return
	 */
	@Deprecated
	public Absence createOrUpdateAbsence(User student, Interval interval) {

		if (student == null) {
			throw new IllegalArgumentException(Lang.ERROR_ABSENCE_FOR_NULL_USER);
		}

		Absence absence = ModelFactory
				.newAbsence(Absence.Type.Absence, student);
		absence.setInterval(interval);
		absence.setStatus(Absence.Status.Pending);

		// Associate with event
		if (!tryLink(absence)) {
			LOG.log(Level.WARNING,
					"Orphaned absence being created for timespan: "
							+ interval.getStart() + " to " + interval.getEnd());
		}

		return storeAbsence(absence, student);
	}

	/**
	 * 
	 * @param student
	 * @param e
	 * @return
	 */
	public Absence createOrUpdateAbsence(User student, Event e) {

		if (student == null) {
			throw new IllegalArgumentException(Lang.ERROR_ABSENCE_FOR_NULL_USER);
		}

		DateTimeZone zone = this.train.appData().get().getTimeZone();

		Absence absence = ModelFactory
				.newAbsence(Absence.Type.Absence, student);
		absence.setStatus(Absence.Status.Pending);

		if (e != null) {
			absence.setEvent(e);
			absence.setInterval(e.getInterval(zone));
			// associated with this event for this student
		} else {
			LOG.log(Level.SEVERE, "Orphaned absence being created, bad event passed in, its id was null");
		}

		return storeAbsence(absence, student);
	}

	public Absence createOrUpdateEarlyCheckout(User student, DateTime time) {

		if (student == null)
			throw new IllegalArgumentException(Lang.ERROR_ABSENCE_FOR_NULL_USER);

		Absence absence = ModelFactory.newAbsence(Absence.Type.EarlyCheckOut,
				student);
		absence.setCheckout(time);
		absence.setStatus(Absence.Status.Pending);

		// Associate with event
		if (!tryLink(absence)) {
			LOG.log(Level.WARNING,
					"Orphaned early checkout being created for time: " + time);
		}

		return storeAbsence(absence, student);
	}

	public Absence createOrUpdateTardy(User student, DateTime time) {

		if (student == null) {
			throw new IllegalArgumentException(Lang.ERROR_ABSENCE_FOR_NULL_USER);
		}

		Absence absence = ModelFactory.newAbsence(Absence.Type.Tardy, student);
		absence.setCheckin(time);
		// Associate with event
		if (!tryLink(absence)) {
			LOG.log(Level.WARNING, "Orphaned tardy being created at time: "
					+ time);
		}

		return storeAbsence(absence, student);
	}

	void delete(User student) {
		List<Absence> absences = this.get(student);
		train.getDataStore().deleteAll(absences);
	}

	public List<Absence> get(Absence.Type... types) {

		if (types == null || types.length == 0)
			throw new IllegalArgumentException(
					"Must pass at least one type to get by type");

		RootFindCommand<Absence> find = this.train.find(Absence.class);
		find.addFilter(Absence.FIELD_TYPE, FilterOperator.IN,
				Arrays.asList(types));

		return find.returnAll().now();
	}

	public Absence get(long id) {
		return this.train.getDataStore().load(
				this.train.getTie(Absence.class, id));
	}

	public List<Absence> get(User student) {
		List<Absence> absences = this.train
				.find(Absence.class)
				.addFilter(Absence.FIELD_STUDENT, FilterOperator.EQUAL, student)
				.returnAll().now();
		return absences;
	}

	public List<Absence> getAll() {
		return this.train.find(Absence.class).returnAll().now();
	}

	/**
	 * Returns a list of all Absences that have the given Event associate with
	 * them.
	 * 
	 * @param associated
	 * @return
	 */
	public List<Absence> getAll(Event event) {

		return this.train.find(Absence.class)
				.addFilter(Absence.FIELD_EVENT, FilterOperator.EQUAL, event)
				.returnAll().now();
	}

	public List<Absence> getAll(Event event, User student) {

		return this.train
				.find(Absence.class)
				.addFilter(Absence.FIELD_STUDENT, FilterOperator.EQUAL, student)
				.addFilter(Absence.FIELD_EVENT, FilterOperator.EQUAL, event)
				.returnAll().now();
	}

	public Integer getCount() {
		return this.train.find(Absence.class).returnCount().now();
	}

	public Integer getCount(Absence.Type type) {

		RootFindCommand<Absence> find = this.train.find(Absence.class);
		find.addFilter(Absence.FIELD_TYPE, FilterOperator.EQUAL, type);

		return find.returnCount().now();
	}

	public List<Absence> getUnanchored() {
		List<Absence> absences = this.train.find(Absence.class)
				.addFilter(Absence.FIELD_EVENT, FilterOperator.EQUAL, null)
				.returnAll().now();
		return absences;
	}

	/**
	 * Note that if the parameter is not in the database, this will not throw an
	 * exception!
	 * 
	 * @param todie
	 */
	public void remove(Absence todie) {
		ObjectDatastore od = this.train.getDataStore();

		od.delete(todie);

		// Finally check for side-effects caused by absence
		// this also checks the students grade
		train.users().update(todie.getStudent());
	}

	public void remove(List<Absence> todie) {
		ObjectDatastore od = this.train.getDataStore();
		UserManager uc = this.train.users();

		HashSet<User> users = new HashSet<User>();
		for (Absence a : todie) {
			User u = a.getStudent();
			users.add(u);
		}
		od.deleteAll(todie);

		// Finally check for side-effects caused by absence
		for (User u : users) {
			uc.update(u);
		}
	}

	/**
	 * Note! This method causes destructive edits to the database that cannot be
	 * fixed after releasing a reference to both parameters!
	 * 
	 * Given two absences, it checks to make sure that they're linked to the
	 * same event and that they have the same student, then applies the ruleset
	 * that dictates which absence takes precedence and returns it after
	 * removing the conflicting one.
	 * 
	 * The Absence returned will still need to be stored in the database!
	 * 
	 * @param current
	 *            New absence, which is being resolved. Could be modified
	 *            slightly during the resolving process, so if was already in
	 *            the database it should be updated
	 * @param contester
	 *            An existing absence in the database, which could be removed
	 *            from the database if the current one overrides it
	 * 
	 * @return True if current absence is non-conflicting, or could be modified
	 *         to be non-conflicting, otherwise returns false to indicate the
	 *         current absence should not be stored in the database
	 */
	private boolean resolveConflict(Absence current, Absence contester) {

		if (current.equals(contester)) {
			return true;
			// not a conflict
		}

		if (!contester.getEvent().equals(current.getEvent())) {
			// not a conflict
			return true;
		}
		if (!contester.getStudent().equals(current.getStudent())) {
			// not a conflict
			return true;
		}

		DateTimeZone zone = train.appData().get().getTimeZone();

		// Basic tenants:
		// - Anything beats an absence
		// - Later tardy beats an earlier one
		// - Earlier EarlyCheckOut beats a later one
		// - An EarlyCheckOut before and Tardy after an event
		// combines into a new super-powerful being, an absence

		switch (current.getType()) {
		case Absence:
			switch (contester.getType()) {
			case Absence:
				return false;
			case Tardy:
				return false;
			case EarlyCheckOut:
				// No conflict
				return true;
			}
			break;
		case Tardy:
			switch (contester.getType()) {
			case Absence:
				// Absence loses to tardy
				remove(contester);
				return true;
			case Tardy:
				if (contester.getCheckin(zone).equals(current.getCheckin(zone))) {
					if (contester.getStatus() == Absence.Status.Approved) {
						return false;
					} else {
						remove(contester);
						return true;
					}
				}
				// both allowed
				return true;
			case EarlyCheckOut:
				// No conflict
				return true;
			}
			break;
		case EarlyCheckOut:
			switch (contester.getType()) {
			case Absence:
				return true;
			case Tardy:
				return true;
			case EarlyCheckOut:
				if (contester.getCheckout(zone).equals(
						current.getCheckout(zone))) {
					if (contester.getStatus() == Absence.Status.Approved) {
						return false;
					} else {
						remove(contester);
						return true;
					}
				}
				return true;
			}
			break;
		}

		// should never reach this point!
		throw new IllegalArgumentException(
				"Types of absences were somehow wrong.");
	}

	private Absence storeAbsence(Absence absence, User student) {
		train.getDataStore().store(absence);

		// Then do some validation
		Absence resolvedAbsence = validateAbsence(absence);
		if (resolvedAbsence != null) {
			if (shouldBeAutoApproved(resolvedAbsence)) {
				resolvedAbsence.setStatus(Absence.Status.Approved);
			}

			// Then do actual store
			this.train.getDataStore().storeOrUpdate(resolvedAbsence);

			// Finally check for side-effects caused by absence
			train.users().update(student);

			// Done.
			return resolvedAbsence;
		} else {
			train.getDataStore().delete(absence);
		}

		// Invalid absence returns null because it doesn't store
		return null;
	}

	private boolean tryLink(Absence absence) {
		DateTimeZone zone = train.appData().get().getTimeZone();
		List<Event> events;

		switch (absence.getType()) {
		case Absence:
			Interval interval = absence.getInterval(zone);

			// Associate with event
			// else the absence is orphaned
			events = train.events().getExactlyAt(interval);
			if (events.size() == 1) {
				absence.setEvent(events.get(0));
				// associated with this event for this student
				return true;
			} else {
				return false;
			}
		case EarlyCheckOut:
			DateTime checkout = absence.getCheckout(zone);

			// Associate with event
			events = this.train.events().getContaining(checkout, 2);

			if (events.size() == 1) {
				absence.setEvent(events.get(0));
				// associated with this event for this student
				return true;
			} else {
				return false;
			}
		case Tardy:
			DateTime checkin = absence.getCheckin(zone);

			// Associate with event
			events = train.events().getContaining(checkin, 2);
			absence.setStatus(Absence.Status.Pending);

			// else the absence is orphaned, because we can't know which one is
			// best. It'll show in unanchored and they'll have to fix it.
			if (events.size() == 1) {
				Event toLink = events.get(0);
				// now link
				absence.setEvent(toLink);

				return true;
			} else {
				return false;
			}
		default:
			throw new UnsupportedOperationException();
		}
	}

	/**
	 * Method to be used by the Event Controller to upDateTime absences when an
	 * event is deleted. This will try to link together unanchored absences and
	 * events
	 * 
	 * @param events
	 *            a list of all the events in the database
	 * @param eventStart
	 *            the starting time for the event deleted
	 * @param eventEnd
	 *            the ending time for the event just deleted
	 */
	public void tryLinkUnanchoredInInterval(Interval eventInterval) {
		for (Absence a : getUnanchored()) {
			if (Absence.isContainedIn(a, eventInterval)) {
				if (tryLink(a)) {
					// Then do actual store
					this.train.getDataStore().storeOrUpdate(a);

					// Finally check for side-effects caused by absence
					train.users().update(a.getStudent());
				}
			}
		}
	}

	public Absence updateAbsence(Absence absence) {

		// See if we can link up an event now
		Event linked = absence.getEvent();
		if (linked == null) {
			tryLink(absence);
		}

		// Do some validation
		Absence resolvedAbsence = validateAbsence(absence);
		if (resolvedAbsence == null) {
			// Null resolved absence means remove from database

			try {
				remove(absence);
			} catch (IllegalArgumentException e) {
				LOG.severe("Attempted to delete absence that wasn't associated.");
			}

			// Current absence has been invalidated somehow and removed from the
			// database, so return null to indicate that
			return null;
		} else {
			// And check for side-effects on the absence
			if (shouldBeAutoApproved(resolvedAbsence)) {
				resolvedAbsence.setStatus(Absence.Status.Approved);
			}

			// Then do actual store
			this.train.getDataStore().storeOrUpdate(resolvedAbsence);
			// this.train.getDataStore().store(resolvedAbsence);

			// Finally check for side-effects caused by absence
			train.users().update(resolvedAbsence.getStudent());

			// Success.
			return resolvedAbsence;
		}
	}

	/**
	 * This method ensures that conflicts with existing absences are resolved by
	 * removing the offending absence(s) and/or slightly modifing the return
	 * absence copy
	 * 
	 * This will return the updated Absence WITHOUT PUTTING IT IN THE DATABASE.
	 * Its internal calls to resolveConflict(), however, will potentially remove
	 * conflicts from the database.
	 * 
	 * @param absence
	 * @param student
	 * @return A copy of the absence, which may be slight different from the
	 *         passed in absence, if it was modified to resolve conflicts, or
	 *         null if the absence is not valid to be stored
	 */
	private Absence validateAbsence(Absence absence) {
		// Force a copy to work on
		// absence = ModelFactory.copyAbsence(absence);

		// If no linked event, its not possible to have conflicts
		Event linked = absence.getEvent();
		if (linked == null) {
			return absence;
		}

		User student = absence.getStudent();
		if (student == null) {
			LOG.severe("Can't validate Absence with null student");
			return null;
		}

		AbsenceManager ac = train.absences();
		List<Absence> conflicts = ac.getAll(linked, student);

		if (conflicts.size() > 2) {
			LOG.severe("Absence conflicting with more than two others, this indicates a possibly inconsistent database");
		}

		// this loop should remove any conflicting absences and leave us with
		// the correct absence to store once we're done
		for (Absence other : conflicts) {
			// If we are unable to resolve contacts, don't store
			if (!resolveConflict(absence, other))
				return null;
		}

		return absence;
	}
} public class DataTrain {

	private static class CacheKey implements Serializable {

		/**
		 * 
		 */
		private static final long serialVersionUID = -8879640511900898415L;

		@SuppressWarnings("unused")
		public String clazz;

		@SuppressWarnings("unused")
		public int id;

		public CacheKey(Class<?> clazz, int id) {
			this.clazz = clazz.getName();
			this.id = id;
		}
	}

	/**
	 * Transaction wrapper to keep in the train theme
	 * 
	 * For internal use in controllers when the outside code has already started
	 * a transaction when the controller tries to start one
	 * 
	 * Thus to deal with this nested transaction problem, we just linearize
	 * everything and let the outside code call the actual commit to commit
	 * everything.
	 * 
	 */
	private class InternalTrack extends Track {

		protected InternalTrack(Track t) {
			super(t.getTransaction());
		}

		/**
		 * Does nothing, this is a nested transaction
		 */
		@Override
		public void bendIronBack() {
			// Do nothing, this is a nested transaction
		}

		/**
		 * Does nothing, this is a nested transaction
		 */
		@Override
		public Future<Void> bendIronBackAsync() {
			// Do nothing, this is a nested transaction
			return null;
		}

	}

	/**
	 * Transaction wrapper to keep in the train theme
	 * 
	 */
	public class Track {

		private Transaction transaction;

		protected Track(Transaction t) {
			this.transaction = t;
		}

		/**
		 * Commits this transaction to the datastore
		 */
		public void bendIronBack() {
			this.transaction.commit();
		}

		/**
		 * Commits this transaction to the datastore asynchronously
		 */
		public Future<Void> bendIronBackAsync() {
			return this.transaction.commitAsync();
		}

		/**
		 * End this transaction without committing any of the changes made
		 */
		public void derail() {
			this.transaction.rollback();
		}

		/**
		 * End this transaction asynchronously without committing any of the
		 * changes made
		 */
		public Future<Void> derailAsync() {
			return this.transaction.rollbackAsync();
		}

		/**
		 * 
		 * @return The application id for the Transaction.
		 */
		public String getApp() {
			return this.transaction.getApp();
		}

		/**
		 * 
		 * @return The globally unique identifier for the Transaction.
		 */
		public String getId() {
			return this.transaction.getId();
		}

		protected Transaction getTransaction() {
			return this.transaction;
		}

		public boolean isActive() {
			return this.transaction.isActive();
		}

	}

	public static DataTrain depart() {
		DataTrain train = new DataTrain();
		return train;
	}

	private StandardObjectDatastore datastore = null;

	private Cache cache = null;

	/**
	 * Current transaction
	 */
	private Track track = null;

	public DataTrain() {
		datastore = ModelFactory.newObjectDatastore();
	}

	<T> RootFindCommand<T> find(Class<T> type) {
		return getDataStore().find().type(type);
	}

	public AbsenceManager absences() {
		return new AbsenceManager(this);
	}

	public AppDataManager appData() {
		return new AppDataManager(this);
	}

	public AuthManager auth() {
		return new AuthManager(this);
	}

	public DataManager data() {
		return new DataManager(this);
	}

	StandardObjectDatastore getDataStore() {
		return this.datastore;
	}

	public EventManager events() {
		return new EventManager(this);
	}

	public FormManager forms() {
		return new FormManager(this);
	}

	Cache getMemCache() {
		if (this.cache == null) {
			try {
				CacheFactory cacheFactory = CacheManager.getInstance()
						.getCacheFactory();
				this.cache = cacheFactory.createCache(Collections.emptyMap());
			} catch (CacheException e) {
				return null;
			}
		}

		return this.cache;
	}

	public MobileDataManager mobileData() {
		return new MobileDataManager(this);
	}

	// For implementing transaction support
	// Object getAncestor() {
	// return getVersionController().getCurrent();
	// }

	Key getTie(Class<?> type, long id) {
		return new KeyFactory.Builder(this.datastore.getConfiguration()
				.typeToKind(type), id).getKey();
	}

	Key getTie(Class<?> type, String id) {
		return new KeyFactory.Builder(this.datastore.getConfiguration()
				.typeToKind(type), id).getKey();
	}

	public UserManager users() {
		return new UserManager(this);
	}

	@SuppressWarnings("unchecked")
	<CachedType> CachedType loadFromCache(Class<CachedType> clazz, int id) {
		Cache cache = getMemCache();

		if (cache != null)
			return (CachedType) cache.get(new CacheKey(clazz, id));
		else
			return null;
	}

	/**
	 * Starts a new data store transaction
	 * 
	 * Note that only one transaction can be active at a time
	 */
	public Track switchTracks() {

		this.track = new Track(getDataStore().beginTransaction());
		return this.track;
	}

	/**
	 * Non-recursive begin transaction
	 * 
	 * Starts a new data store transaction if none is running, or simply reuses
	 * the existing one in a wrapper if it is active.
	 * 
	 * This is because only one transaction can be running at a time
	 * 
	 * @return Internal transaction for use in a controller. Must either be
	 *         committed to or canceled within the controller and never be
	 *         passed out of the controller
	 */
	Track switchTracksInternal() {
		if (this.track != null && this.track.isActive())
			return new InternalTrack(this.track);
		else
			return switchTracks();
	}

	<CachedType> boolean updateCache(int id, CachedType object) {

		if (object == null) {
			return false;
		}

		Class<?> clazz = object.getClass();

		Cache cache = getMemCache();

		if (cache != null) {
			cache.put(new CacheKey(clazz, id), object);

			return true;
		}

		return false;
	}
} public class EventManager extends AbstractManager {

	DataTrain train;

	private static final Logger log = Logger.getLogger(EventManager.class
			.getName());

	public EventManager(DataTrain dataTrain) {
		this.train = dataTrain;
	}

	public Event createOrUpdate(Type type, Interval interval) {
		DateTimeZone zone = this.train.appData().get().getTimeZone();

		Event event = null;

		List<Event> similarEvents = getExactlyAt(interval);
		// List<Event> overlapping = new ArrayList<Event>();
		for (Event e : similarEvents) {
			if (e.getType().equals(type)) {
				if (event != null) {
					// Already found an event for this time and type
					log.log(Level.WARNING, "Duplicate " + e.getType()
							+ "events found for time period " + interval);
				}
				event = e;
			}
		}

		if (event != null) {
			// this means that we found an event already in the datastore that
			// matches all fields of this one. We don't need to add the new one.
			return event;
		}

		// Safe to create a new event
		event = ModelFactory.newEvent(type, interval);

		// store it, and then we'll check for linking and refresh it in the
		// database before returning
		this.train.getDataStore().store(event);

		// we need get the subset of events that have any overlap with the new
		// event so we can send them into the battle royale to try and anchor
		// absences
		// List<Event> all = getAll();
		// for (Event e : all) {
		// if (overlaps(event, e)) {
		// overlapping.add(e);
		// }
		// }
		AbsenceManager ac = train.absences();
		List<Absence> unanchored = ac.getUnanchored();

		List<Event> all = getAll();
		for (Absence a : unanchored) {
			boolean foundOne = false;
			Event one = null;
			for (Event e : all) {
				// tardies/earlycheckouts are easy.
				// absences.... need to match start and end times.
				switch (a.getType()) {
				case Absence:
					if (a.getInterval(zone).equals(e.getInterval(zone))) {
						// match!
						if (foundOne) {
							// we can't link it, because there are multiple
							// choices
							one = null;
						} else {
							foundOne = true;
							one = e;
						}
					}
					break;
				case Tardy:
					if (e.getInterval(zone).contains(a.getCheckin(zone))) {
						// must be within the event
						if (foundOne) {
							one = null;
						} else {
							foundOne = true;
							one = e;
						}
					}
					break;
				case EarlyCheckOut:
					if (e.getInterval(zone).contains(a.getCheckout(zone))) {
						// must be within the event
						if (foundOne) {
							one = null;
						} else {
							foundOne = true;
							one = e;
						}
					}
					break;
				default:
					break;
				}
			}
			// we've compared this unanchored absence to all events now
			if (one != null) {
				a.setEvent(one);

				// LINK IT
				ac.updateAbsence(a);

				// TODO https://github.com/curtisullerich/attendance/issues/105
				// doing this every time is probably horrible for
				// performance. Another possible solution is keeping a list of
				// all students and updating each one before returning
				this.train.getDataStore().refresh(event);
				//this.train.users().update(a.getStudent());
			}
		}

		this.train.getDataStore().refresh(event);
		return event;
	}

	public void delete(Event event, boolean deleteLinkedAbsences) {
		DateTimeZone zone = this.train.appData().get().getTimeZone();
		ObjectDatastore od = this.train.getDataStore();
		AbsenceManager ac = this.train.absences();

		List<Absence> todie = ac.getAll(event);
		if (deleteLinkedAbsences) {
			// Just remove all of them
			ac.remove(todie);
		} else {
			// Still need to unlink them
			for (Absence absence : todie) {
				absence.setEvent(null);
				// ac.updateAbsence(absence);
			}
		}

		Interval eventInterval = event.getInterval(zone);

		od.delete(event);

		// Can't upDateTime until we actually delete the event otherwise
		// the absence will just link to the same event
		if (!deleteLinkedAbsences) {
			for (Absence absence : todie) {
				ac.updateAbsence(absence);
			}
		}

		/*
		 * If the deleted event had conflicted with another one before then all
		 * the absences for that date/time range would be unanchored. Now that
		 * one is deleted we can try to link them all up again
		 */
		ac.tryLinkUnanchoredInInterval(eventInterval);
		// od.delete() returns void, so we don't really have anything to return
		// here, either
	}

	public Event get(long id) {
		ObjectDatastore od = this.train.getDataStore();
		Event e = od.load(this.train.getTie(Event.class, id));
		return e;
	}

	public List<Event> getAll() {
		ObjectDatastore od = this.train.getDataStore();
		return od.find().type(Event.class).returnAll().now();
	}

	/**
	 * Gets a list of events that contain the specified time
	 */
	public List<Event> getContaining(DateTime instant, int limit) {
		List<Event> containingEvents = new ArrayList<Event>();
		DateTimeZone zone = this.train.appData().get().getTimeZone();

		RootFindCommand<Event> find = this.train.getDataStore().find()
				.type(Event.class);

		find.addFilter(Event.FIELD_START, FilterOperator.LESS_THAN_OR_EQUAL,
				instant.toDate());
		// TODO: Sort order and this whole method needs some looking at
		find.addSort(Event.FIELD_START, SortDirection.ASCENDING);

		QueryResultIterator<Event> qri = find.now();
		while (qri.hasNext() && containingEvents.size() < limit) {
			Event e = qri.next();

			if (e.getInterval(zone).contains(instant)) {
				containingEvents.add(e);
			}
		}

		return containingEvents;
	}

	public Integer getCount() {
		return this.train.getDataStore().find().type(Event.class).returnCount()
				.now();
	}

	/**
	 * Gets a list of events whose start and end coincide exactly with the given
	 * interval
	 */
	public List<Event> getExactlyAt(Interval interval) {
		RootFindCommand<Event> find = this.train.getDataStore().find()
				.type(Event.class);

		find.addFilter(Event.FIELD_START, FilterOperator.EQUAL, interval
				.getStart().toDate());
		find.addFilter(Event.FIELD_END, FilterOperator.EQUAL, interval.getEnd()
				.toDate());
		return find.returnAll().now();
	}

	public List<Event> readAll() {
		return this.train.getDataStore().find().type(Event.class).returnAll()
				.now();
	}

} public class UserManager extends AbstractManager {

	DataTrain datatrain;
	private static final Logger LOG = Logger.getLogger(UserManager.class
			.getName());

	// a user may not be later than this without 'missing' the whole rehearsal
	private static final int MAXIMUM_LATENESS_MINUTES = 30;

	public UserManager(DataTrain dataTrain) {
		this.datatrain = dataTrain;
	}

	public User createDirector(String schoolEmail, String loginEmail,
			String firstName, String lastName) throws IllegalArgumentException {

		// ValiDateTime email
		Email primaryEmail = Util.makeEmail(schoolEmail);
		Email secondaryEmail = Util.makeEmail(loginEmail);

		if (!ValidationUtil.isValidPrimaryEmail(primaryEmail, this.datatrain)) {
			throw new IllegalArgumentException("Invalid primary email");
		}

		if (!ValidationUtil.isValidSecondaryEmail(secondaryEmail,
				this.datatrain)) {
			throw new IllegalArgumentException("Invalid second email");
		}

		if (!ValidationUtil.isUniqueSecondaryEmail(secondaryEmail,
				primaryEmail, this.datatrain)) {
			throw new IllegalArgumentException("Non-unique secondary email");
		}

		// Check no duplicate users exist
		if (get(primaryEmail) != null)
			throw new IllegalArgumentException(
					"User already exists in the system");

		User user = ModelFactory.newUser(User.Type.Director, primaryEmail,
				"000000000");
		user.setFirstName(firstName);
		user.setLastName(lastName);
		if (secondaryEmail != null && secondaryEmail.getEmail() != "") {
			user.setSecondaryEmail(secondaryEmail);
		}

		validateUser(user);

		this.datatrain.getDataStore().store(user);

		return user;
	}

	public User createFakeStudent(String[] parts) {
		// first name, last name, primary email, secondary email, type,
		// section, uid, year, major, rank, minutesavailable
		String firstName = parts[0];
		String lastName = parts[1];
		Email primaryEmail = Util.makeEmail(parts[2]);
		Email secondaryEmail = Util.makeEmail(parts[3]);
		// String type = parts[4];// just in case we want it
		User.Section section = User.Section.valueOf(parts[5]);
		String univID = parts[6];
		int year = Integer.parseInt(parts[7]);
		String major = parts[8];

		String rank = parts[9];// just in case
		int minutesAvailable = Integer.parseInt(parts[10]);// just in case

		User user = ModelFactory.newUser(User.Type.Student, primaryEmail,
				univID);
		user.setFirstName(firstName);
		user.setLastName(lastName);
		user.setSecondaryEmail(secondaryEmail);
		user.setSection(section);
		user.setYear(year);
		user.setMajor(major);
		user.setRank(rank);
		user.setMinutesAvailable(minutesAvailable);
		user.setGrade(User.Grade.A);

		validateUser(user);

		this.datatrain.getDataStore().store(user);
		return user;
	}

	public User createStudent(com.google.appengine.api.users.User google_user,
			String univID, String firstName, String lastName, int year,
			String major, User.Section section, Email secondaryEmail)
			throws IllegalArgumentException {

		// Check google user
		if (google_user == null)
			throw new IllegalArgumentException("Must give a google user");

		// Extract email
		Email email = Util.makeEmail(google_user.getEmail());

		return createStudent(email, univID, firstName, lastName, year, major,
				section, secondaryEmail);
	}

	public User createStudent(Email primaryEmail, String univID,
			String firstName, String lastName, int year, String major,
			User.Section section, Email secondaryEmail)
			throws IllegalArgumentException {

		// Check google user
		if (primaryEmail == null)
			throw new IllegalArgumentException("Must give a google user");

		// Check no duplicate users exist
		if (get(primaryEmail) != null)
			throw new IllegalArgumentException(
					"User already exists in the system");

		User user = ModelFactory.newUser(User.Type.Student, primaryEmail,
				univID);
		user.setFirstName(firstName);
		user.setLastName(lastName);
		user.setYear(year);
		user.setMajor(major);
		user.setSection(section);
		user.setSecondaryEmail(secondaryEmail);

		// Just assume we start with an A
		user.setGrade(User.Grade.A);

		validateUser(user);

		this.datatrain.getDataStore().store(user);
		return user;
	}

	/*
	 * Full cleanup of user happens without affecting other things in the
	 * datastore
	 * 
	 * This includes:
	 * 
	 * Absences, Forms, mobile data uploads
	 */
	public void delete(User user) {
		ObjectDatastore od = this.datatrain.getDataStore();

		// Absences
		this.datatrain.absences().delete(user);

		// Forms
		this.datatrain.forms().delete(user);

		// Remove any Mobile Data uploads
		this.datatrain.mobileData().scrubUploader(user);

		od.delete(user);
	}

	/**
	 * 
	 * @param email
	 *            Primary email of a user
	 * @return
	 */
	public User get(Email primaryEmail) {

		User u;

		if (primaryEmail == null)
			return null;

		if (primaryEmail.getEmail() == null)
			return null;

		Iterator<User> users = this.datatrain
				.find(User.class)
				.addFilter(User.FIELD_PRIMARY_EMAIL, FilterOperator.EQUAL,
						primaryEmail.getEmail()).fetchMaximum(2).now();

		if (users.hasNext())
			u = users.next();
		else
			return null;

		if (users.hasNext())
			throw new IllegalStateException(
					"Found more than one user corresponding to primary email "
							+ primaryEmail);

		return u;
	}

	/**
	 * 
	 * @param netid
	 * @return User object for the netid, or null if there is no such netid
	 */
	public User get(String netid) {

		return this.datatrain.getDataStore().load(
				this.datatrain.getTie(User.class, netid));
	}

	public List<User> get(User.Type... types) {

		RootFindCommand<User> find = this.datatrain.find(User.class);
		find.addFilter(User.FIELD_TYPE, FilterOperator.IN, Arrays.asList(types));

		return find.returnAll().now();
	}

	public List<User> getAll() {
		return this.datatrain.find(User.class).returnAll().now();
	}

	/**
	 * 
	 * @param email
	 *            Secondary email of a user
	 * @return
	 */
	public User getSecondary(Email secondaryEmail) {

		User u;

		if (secondaryEmail == null)
			return null;

		if (secondaryEmail.getEmail() == null)
			return null;

		Iterator<User> users = this.datatrain
				.find(User.class)
				.addFilter(User.FIELD_SECONDARY_EMAIL, FilterOperator.EQUAL,
						secondaryEmail.getEmail()).fetchMaximum(2).now();

		if (users.hasNext())
			u = users.next();
		else
			return null;

		if (users.hasNext())
			throw new IllegalStateException(
					"Found more than one user corresponding to secondary email "
							+ secondaryEmail);

		return u;
	}

	private User.Grade intToGrade(int count, User student) {
		// users may miss up to 160 minutes for free (two rehearsals)
		int autoexcused = 160;
		count -= autoexcused;

		// give credit for the number of minutes worked
		count -= student.getMinutesAvailable();

		if (count <= 0) {
			return User.Grade.A;
		} else if (count <= 15) {
			return User.Grade.B;
		} else if (count <= 30) {
			return User.Grade.C;
		} else if (count <= 45) {
			return User.Grade.D;
		} else {
			return User.Grade.F;
		}
	}

	public boolean isUniqueId(String id, Email primary) {
		RootFindCommand<User> find = this.datatrain.find(User.class);
		find.addFilter(User.FIELD_UNIVERSITY_ID, FilterOperator.EQUAL, id);
		List<User> found = find.returnAll().now();
		// When we are registering we need this result to be zero because that
		// means
		// the person registering is the one who has it. But when we are just
		// validating stuff
		// this number can be 1, meaning the current person
		return found.size() == 0
				|| (found.size() == 1 && found.get(0).getPrimaryEmail()
						.equals(primary));
	}

	public boolean isUniqueSecondaryEmail(Email secondaryEmail, Email primary) {
		if (secondaryEmail == null || null == secondaryEmail.getEmail()
				|| "".equals(secondaryEmail.getEmail()))
			return true;

		RootFindCommand<User> find = this.datatrain.find(User.class);
		find.addFilter(User.FIELD_SECONDARY_EMAIL, FilterOperator.EQUAL,
				secondaryEmail);
		find.addFilter(User.FIELD_PRIMARY_EMAIL, FilterOperator.NOT_EQUAL,
				primary);

		int found = find.returnCount().now();
		return found == 0;
	}

	public void update(User u) {
		validateUser(u);
		updateUserGrade(u);
		this.datatrain.getDataStore().update(u);
	}

	/**
	 * Always calculated based on the attendance stored in the database. There
	 * should never be a reason for a user to override a grade manually.
	 * 
	 * Note that this DOES NOT currently refresh the student in the database.
	 */
	private void updateUserGrade(User student) {
		AbsenceManager ac = this.datatrain.absences();
		int minutes = 0;
		List<Absence> absences = ac.get(student);

		Map<Event, List<Absence>> map = new HashMap<Event, List<Absence>>(
				absences.size());
		for (Absence a : absences) {
			Event e = a.getEvent();
			if (e == null) {
				// it's unlinked!
			} else {
				if (map.containsKey(e)) {
					map.get(e).add(a);
				} else {
					List<Absence> list = new ArrayList<Absence>();
					list.add(a);
					map.put(e, list);
				}
			}
		}

		for (Event e : map.keySet()) {
			List<Absence> l = map.get(e);
			if (l.size() < 1) {
				throw new IllegalStateException(
						"Somehow the event had no associated absences");
			}
			if (l.size() == 1) {
				minutes += simpleGrade(e, l, student);
			} else {
				DateTimeZone zone = datatrain.appData().get().getTimeZone();
				String types = "";
				for (Absence a : l) {
					types += a.getType() + " ";
				}
				LOG.info("There were " + l.size() + " absences for "
						+ student.getId() + " during event "
						+ e.getInterval(zone).getStart() + " to "
						+ e.getInterval(zone).getEnd());
				LOG.info("absence types: " + types);
				minutes += generalGrade(e, l);
			}
		}
		student.setMinutesMissed(minutes);
		student.setGrade(intToGrade(minutes, student));
	}

	private int generalGrade(Event e, List<Absence> l) {
		// TODO this method does not properly handle tardies/ecos that have been
		// approved

		class PresenceInterval {

			private boolean present;
			private Interval interval;

			public PresenceInterval(Interval interval, boolean present) {
				this.setInterval(interval);
				this.setPresent(present);
			}

			public boolean isPresent() {
				return present;
			}

			public void setPresent(boolean present) {
				this.present = present;
			}

			public Interval getInterval() {
				return interval;
			}

			public void setInterval(Interval interval) {
				this.interval = interval;
			}

			@Override
			public String toString() {
				return (present ? "present " : "absent ") + interval.getStart()
						+ interval.getEnd();
			}
		}

		DateTimeZone zone = datatrain.appData().get().getTimeZone();

		for (Absence a : l) {
			if (a.getType().isAbsence()) {
				if (l.size() > 2) {
					LOG.info("There were " + l.size()
							+ " absences for a user during event "
							+ e.getInterval(zone));
				}
				return (int) e.getInterval(zone).toDuration()
						.getStandardMinutes();
			}
		}

		List<PresenceInterval> ints = new ArrayList<PresenceInterval>();
		ints.add(new PresenceInterval(e.getInterval(zone), true));
		for (Absence a : l) {
			// if (a.getType() == Absence.Type.Absence) {
			// throw new IllegalArgumentException(
			// "there were multiple absences of type Absence for a single event!");
			// }
			if (a.getStatus() == Absence.Status.Approved) {
				// skip it
				continue;
			}
			DateTime d = a.getType() == Absence.Type.Tardy ? a.getCheckin(zone)
					: a.getCheckout(zone);

			for (int i = 0; i < ints.size(); i++) {
				PresenceInterval p = ints.get(i);
				if (p.getInterval().contains(d)) {
					// found a match
					ints.remove(p);
					Interval first = new Interval(p.getInterval().getStart(), d);
					Interval second = new Interval(d, p.getInterval().getEnd());
					if (a.getType() == Absence.Type.Tardy) {
						// they were present AFTER the checkin
						ints.add(new PresenceInterval(first, false));
						ints.add(new PresenceInterval(second, true));
					} else {
						// they were present BEFORE the checkout
						ints.add(new PresenceInterval(first, true));
						ints.add(new PresenceInterval(second, false));
					}
					break;
					// move on to the next absence
				}
			}
		}
		int minutes = 0;
		for (PresenceInterval p : ints) {
			if (!p.isPresent()) {
				minutes += p.getInterval().toDuration().getStandardMinutes();
			}
		}

		if (minutes > MAXIMUM_LATENESS_MINUTES) {
			// too late!
			return (int) e.getInterval(zone).toDuration().getStandardMinutes();
		} else {
			return minutes;
		}
	}

	private int simpleGrade(Event e, List<Absence> l, User student) {
		if (l.size() != 1) {
			throw new IllegalArgumentException(
					"list of Absences must be of size 1");
		}
		int minutes = 0;
		DateTimeZone zone = datatrain.appData().get().getTimeZone();
		Absence a = l.get(0);
		if (a.getStatus() == Absence.Status.Approved) {
			// nothing to do, it's approved!
		} else if (a.getStatus() == Absence.Status.Pending
				|| a.getStatus() == Absence.Status.Denied) {

			// this means that absences with unanchored
			// events will have no grade penalty

			if (a.getEvent() != null) {
//				if (a.getType() == Absence.Type.Absence) {
//					minutes += a.getEvent().getInterval(zone).toDuration()
//							.getStandardMinutes();
//
//				} else {
					Duration d;
					if (a.getType() == Absence.Type.Tardy) {
						d = new Duration(a.getEvent().getInterval(zone)
								.getStart(), a.getCheckin(zone));
					} else if (a.getType() == Absence.Type.EarlyCheckOut) {
						d = new Duration(a.getCheckout(zone), a.getEvent()
								.getInterval(zone).getEnd());
					} else if (a.getType() == Absence.Type.Absence) {
						d = new Duration(a.getInterval(zone));
					}
					else {
						d = null;
						throw new IllegalArgumentException(
								"invalid absence type");
					}

					int minutesForTardyOrEco = 0;
					// in case there's an approved form, and they showed up
					// before or after the actual time by a bit
					List<Form> forms = datatrain.forms().get(student);
					for (Form f : forms) {
						if (f.getStatus() == Form.Status.Approved
								&& f.getType() == Form.Type.ClassConflict) {
							// Interval ei = e.getInterval(zone);
							// Interval fi = f.getInterval(zone);
							// f.getStartTime();
							// f.getEndTime();
							//
							// f.getDayOfWeek();
							// Interval ai;
							// // get interval of time absent
							// if (a.getType() == Type.Absence) {
							// ai = a.getInterval(zone);
							// } else if (a.getType() == Type.Tardy) {
							// ai = new Interval(e.getInterval(zone)
							// .getStartMillis(), a.getCheckin(zone)
							// .getMillis(), zone);
							// } else {
							// ai = new Interval(a.getCheckout(zone)
							// .getMillis(), e.getInterval(zone)
							// .getEndMillis(), zone);
							// }
							//
							// Interval lap = fi.overlap(ai);
							// if (lap != null) {
							// Duration overlap = lap.toDuration();
							// minutesForTardyOrEco -= overlap
							// .getStandardMinutes();
							// }

							Interval effectiveInterval = DataTrain.depart()
									.absences()
									.getEffectiveFormInterval(a, f, zone);

							Interval absence = DataTrain.depart().absences()
									.getAbsenceInterval(a, e, zone);

							Interval olap = effectiveInterval.overlap(absence);
							if (null != olap) {
								// there is some overlap, but apparently not
								// enough to just approve the absence

								// subtract the number of minutes that the form
								// DOES intersect with the absence (which we
								// consider to be approved)
								minutesForTardyOrEco -= olap.toDuration()
										.getStandardMinutes();
							}

						}
					}
					minutesForTardyOrEco += d.getStandardMinutes();

					if (minutesForTardyOrEco > MAXIMUM_LATENESS_MINUTES) {
						// too late!
						minutes += a.getEvent().getInterval(zone).toDuration()
								.getStandardMinutes();
					} else {
						minutes += minutesForTardyOrEco;
					}
				}
//			}
		}
		return minutes;
	}

	private void validateUser(User user) throws IllegalArgumentException {

		if (user == null)
			throw new IllegalArgumentException("Invalid user");

		// Check primary email
		if (!ValidationUtil.isValidPrimaryEmail(user.getPrimaryEmail(),
				this.datatrain))
			throw new IllegalArgumentException("Invalid primary email");

		// Check name
		if (!ValidationUtil.isValidName(user.getFirstName()))
			throw new IllegalArgumentException("Invalid first name");
		if (!ValidationUtil.isValidName(user.getLastName()))
			throw new IllegalArgumentException("Invalid last name");

		// Check id
		String uId = user.getUniversityID();
		if (!ValidationUtil.isValidUniversityID(uId)) {
			throw new IllegalArgumentException("Invalid university id");
		}

		if (user.getType() != User.Type.Director
				&& !ValidationUtil.isUniqueId(uId, user.getPrimaryEmail())) {
			throw new IllegalArgumentException("University id was not unique");
		}

		// Check secondary email
		if (!ValidationUtil.isValidSecondaryEmail(user.getSecondaryEmail(),
				this.datatrain)) {
			throw new IllegalArgumentException("Invalid secondary email");
		}
		if (!ValidationUtil.isUniqueSecondaryEmail(user.getSecondaryEmail(),
				user.getPrimaryEmail(), datatrain)) {
			throw new IllegalArgumentException("Non-unique secondary email");
		}

		// Check student specific things
		if (user.getType() == User.Type.Student) {
			String major = user.getMajor();
			String rank = user.getRank();
			User.Section section = user.getSection();
			int year = user.getYear();

			if (!ValidationUtil.isValidMajor(major)) {
				throw new IllegalArgumentException("Invalid major");
			}
			if (!ValidationUtil.isValidRank(rank)) {
				throw new IllegalArgumentException("Invalid rank");
			}
			if (!ValidationUtil.isValidSection(section)) {
				throw new IllegalArgumentException("Invalid section");
			}
			if (!ValidationUtil.isValidYear(year)) {
				throw new IllegalArgumentException("Invalid year");
			}
		}
	}
} @Entity(kind = "Absence", allocateIdsBy = 0)
public class Absence {

	public static enum Status {
		Pending, Approved, Denied;

		private String mDisplayString;

		private Status() {
			mDisplayString = this.toString();
		}

		public String getDisplayName() {
			return mDisplayString;
		}

		public String getValue() {
			return name();
		}

		public boolean isApproved() {
			return this == Approved;
		}

		public boolean isDenied() {
			return this == Denied;
		}

		public boolean isPending() {
			return this == Pending;
		}
	}

	public enum Type {
		Absence, Tardy, EarlyCheckOut;

		private String mDisplayString;

		private Type() {
			mDisplayString = this.toString();
		}

		private Type(String display_string) {
			mDisplayString = display_string;
		}

		public String getDisplayName() {
			return mDisplayString;
		}

		public String getValue() {
			return name();
		}

		public boolean isAbsence() {
			return Absence.equals(this);
		}

		public boolean isEarlyCheckOut() {
			return EarlyCheckOut.equals(this);
		}

		public boolean isTardy() {
			return Tardy.equals(this);
		}
	};

	public static final String FIELD_EVENT = "event";
	public static final String FIELD_STUDENT = "student";
	public static final String FIELD_TYPE = "type";

	@Id
	private long id;

	private Type type;

	private Status status;

	@Index
	@Activate
	private Event event;

	@Index
	@Activate
	private User student;

	private Date start;

	private Date end;

	@Store(false)
	private DateTime cachedCheckout;

	@Store(false)
	private DateTime cachedCheckin;
	@Store(false)
	private Interval cachedInterval;

	/**
	 * Create absence through AbsenceController (DataModel.absence().create(...)
	 */
	Absence() {
	}

	public static boolean isContainedIn(Absence a, ReadableInterval i) {
		if (a.getType() == null || i == null)
			return false;

		Chronology chron = i.getChronology();

		switch (a.getType()) {
		case Absence:
			return i.contains(a.getInterval(chron));
		case EarlyCheckOut:
			DateTime checkout = a.getCheckout(chron);
			return i.contains(checkout);
		case Tardy:
			DateTime checkin = a.getCheckin(chron);
			return i.contains(checkin);
		default:
			throw new UnsupportedOperationException();
		}
	}

	public DateTime getCheckin(DateTimeZone zone) {
		if (getType().isTardy())
			return new DateTime(this.start, zone);
		else
			throw new IllegalStateException("Intervals only valid for absences");
	}

	public DateTime getCheckin(Chronology chron) {
		if (getType().isTardy())
			return new DateTime(this.start, chron);
		else
			throw new IllegalStateException("Intervals only valid for absences");
	}

	public DateTime getCheckout(DateTimeZone zone) {
		if (getType().isEarlyCheckOut())
			return new DateTime(this.start, zone);
		else
			throw new IllegalStateException(
					"Checkouts only valid for early checkouts");
	}

	public DateTime getCheckout(Chronology chron) {
		if (getType().isEarlyCheckOut())
			return new DateTime(this.start, chron);
		else
			throw new IllegalStateException("Intervals only valid for absences");
	}

	@Deprecated
	public Date getDatetime() {
		return start;
	}

	@Deprecated
	public Date getEnd() {
		return end;
	}

	public Event getEvent() {
		return event;
	}

	public long getId() {
		return id;
	}

	/**
	 * Time-span of an absence
	 */
	public Interval getInterval(DateTimeZone zone) {
		if (getType().isAbsence()) {
			return new Interval(new DateTime(this.start, zone), new DateTime(
					this.end, zone));

		} else
			throw new IllegalStateException("Intervals only valid for absences");
	}

	/**
	 * Time-span of an absence
	 */
	public Interval getInterval(Chronology chron) {
		if (getType().isAbsence()) {
			return new Interval(new DateTime(this.start, chron), new DateTime(
					this.end, chron));

		} else
			throw new IllegalStateException("Intervals only valid for absences");
	}

	@Deprecated
	public Date getStart() {
		return start;
	}

	public Status getStatus() {
		return this.status;
	}

	public User getStudent() {
		return student;
	}

	public Type getType() {
		return this.type;
	}

	public void setCheckin(DateTime datetime) {
		if (getType().isTardy())
			this.start = datetime.toDate();
		else
			throw new IllegalStateException("Check-in times only valid for tardies");

	}

	public void setCheckout(DateTime datetime) {
		if (getType().isEarlyCheckOut())
			this.start = datetime.toDate();
		else
			throw new IllegalStateException("Checkout times only valid for early check outs");

	}

	public void setEvent(Event event) {
		this.event = event;
	}

	public void setInterval(Interval interval) {
		if (getType().isAbsence()) {
			this.end = interval.getEnd().toDate();
			this.start = interval.getStart().toDate();
		} else
			throw new IllegalStateException("Intervals only valid for absences");

	}

	public void setStatus(Status status) {
		this.status = status;
	}

	public void setStudent(User student) {
		this.student = student;
	}

	public void setType(Type type) {
		this.type = type;
	}

	@Override
	public String toString() {
		return student.toString() + " Start: " + start.toString() + " End: "
				+ end.toString();
	}

	public static long parseKey(String keyStr) {
		return Long.parseLong(keyStr);
	}
} public enum Type {
	Absence, Tardy, EarlyCheckOut;

	private String mDisplayString;

	private Type() {
		mDisplayString = this.toString();
	}

	private Type(String display_string) {
		mDisplayString = display_string;
	}

	public String getDisplayName() {
		return mDisplayString;
	}

	public String getValue() {
		return name();
	}

	public boolean isAbsence() {
		return Absence.equals(this);
	}

	public boolean isEarlyCheckOut() {
		return EarlyCheckOut.equals(this);
	}

	public boolean isTardy() {
		return Tardy.equals(this);
	}
}; @Entity(kind = "Event", allocateIdsBy = 0)
public class Event {

	public enum Type {
		Rehearsal, Performance;
		private String mDisplayString;

		private Type() {
			mDisplayString = this.toString();
		}

		private Type(String display_string) {
			mDisplayString = display_string;
		}

		public String getDisplayName() {
			return mDisplayString;
		}

		public String getValue() {
			return name();
		}

		public boolean isPerformance() {
			return Performance.equals(this);
		}

		public boolean isRehearsal() {
			return Rehearsal.equals(this);
		}

	}

	public static final String FIELD_START = "start";
	public static final String FIELD_END = "end";

	public static final String FIELD_TYPE = "type";

	@Id
	private long id;

	@Index
	private Date start;

	@Index
	private Date end;

	private Type type;

	/**
	 * No-args constructor for datastore
	 */
	Event() {

	}

	@Deprecated
	public Date getDate() {
		return start;
	}

	@Deprecated
	public Date getEnd() {
		return end;
	}

	public long getId() {
		return this.id;
	}

	public Interval getInterval(DateTimeZone zone) {
		return new Interval(new DateTime(start, zone), new DateTime(end, zone));
	}

	@Deprecated
	public Date getStart() {
		return start;
	}

	public Type getType() {
		return this.type;
	}

	public void setInterval(Interval interval) {
		this.start = interval.getStart().toDate();
		this.end = interval.getEnd().toDate();
	}

	public void setType(Event.Type type) {
		this.type = type;
	}

	@Override
	public String toString() {
		return "Start: " + start.toString() + " End: " + end.toString();
	}
} @Entity(kind = "User", allocateIdsBy = 0)
public class User implements Serializable {

	public enum Grade {
		A, B, C, D, F;
		private String mDisplayString;

		private Grade() {
			mDisplayString = this.toString();
		}

		private Grade(String display_string) {
			mDisplayString = display_string;
		}

		public String getDisplayName() {
			return mDisplayString;
		}

		public String getValue() {
			return name();
		}
	}

	public enum Section {
		Piccolo, Clarinet, AltoSax("Alto Sax"), TenorSax("Tenor Sax"), Trumpet, Trombone, Bass_Trombone(
				"Bass trombone"), Mellophone, Baritone, Sousaphone, Guard, DrumMajor(
				"Drum Major"), Staff, Drumline_Cymbals("Drumline: Cymbals"), Drumline_Tenors(
				"Drumline: Tenors"), Drumline_Snare("Drumline: Snare"), Drumline_Bass(
				"Drumline: Bass"), Twirler;

		private String mDisplayString;

		private Section() {
			mDisplayString = this.toString();
		}

		private Section(String display_string) {
			mDisplayString = display_string;
		}

		public String getDisplayName() {
			return mDisplayString;
		}

		public String getValue() {
			return name();
		}
	}

	public enum Type {
		Student, TA, Director;

		public boolean isDirector() {
			return this.equals(Director);
		}

		public boolean isStudent() {
			return this.equals(Student);
		}

		public boolean isTa() {
			return this.equals(TA);
		}
	}

	private static final long serialVersionUID = 1421557192976557704L;

	public static final String FIELD_TYPE = "type";

	public static final String FIELD_ID = "id";

	public static final String FIELD_PRIMARY_EMAIL = "email";

	public static final String FIELD_SECONDARY_EMAIL = "secondEmail";

	public static final String FIELD_UNIVERSITY_ID = "universityID";

	private Type type;

	private Grade grade;

	@Id
	private String id;

	private Email email;

	private Email secondEmail;

	private String universityID;

	private Section section;

	private String firstName;

	private String lastName;

	private int year;

	private String major;

	private String rank;

	private boolean showApproved;

	// number of minutes available from submitted TimeWorked forms
	private int minutesAvailable;

	// number of unexcused minutes absent so far this semester
	private int minutesMissed;

	/**
	 * Create users through UserController (DataModel.users().create(...)
	 */
	User() {

	}

	@Override
	public boolean equals(Object o) {
		if (o == null)
			return false;

		// emails are unique, so compare based on them
		if (o instanceof User) {
			User u = (User) o;

			if (this.email == null || this.email.getEmail() == null
					|| u.email == null || u.email.getEmail() == null) {
				if (this.email == null && u.email == null) {
					return true;
				} else if (this.email.getEmail() == null
						&& u.email.getEmail() == null) {
					return true;
				} else {
					return false;
				}
			}

			if (this.email.equals(u.email))
				return true;
		}
		return false;
	}

	public String getFirstName() {
		return firstName;
	}

	public Grade getGrade() {

		return this.grade;
	}

	public String getId() {
		return this.id;
	}

	public String getLastName() {
		return lastName;
	}

	public String getMajor() {
		return major;
	}

	public int getMinutesAvailable() {
		return minutesAvailable;
	}

	public String getName() {
		return this.getFirstName() + " " + this.getLastName();
	}

	/**
	 * Iowa State specific, instead use getId()
	 * 
	 * @return net id of the user
	 */
	@Deprecated
	public String getNetID() {
		return this.id;
	}

	public Email getPrimaryEmail() {
		return this.email;
	}

	public String getRank() {
		return this.rank;
	}

	public Email getSecondaryEmail() {
		return this.secondEmail;
	}

	public Section getSection() {
		return section;
	}

	public Type getType() {
		return this.type;
	}

	public String getUniversityID() {
		return universityID;
	}

	public int getYear() {
		return year;
	}

	@Override
	public int hashCode() {
		if (this.email == null || this.email.getEmail() == null)
			return 0;
		else
			return this.email.hashCode();
	}

	public boolean isShowApproved() {
		return this.showApproved;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public void setGrade(Grade grade) {
		this.grade = grade;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public void setMajor(String major) {
		this.major = major;
	}

	public void setMinutesAvailable(int minutesAvailable) {
		this.minutesAvailable = minutesAvailable;
	}

	public void setPrimaryEmail(Email email) {
		this.email = email;
		// TODO https://github.com/curtisullerich/attendance/issues/170
		// This shouldn't be in the model, it should happen in the controllers
		// somehow
		if (email.getEmail().indexOf('%') == -1) {
			this.id = email.getEmail().substring(0,
					email.getEmail().indexOf('@'));
		} else {
			this.id = email.getEmail().substring(0,
					email.getEmail().indexOf('%'));
		}
	}

	public void setRank(String rank) {
		this.rank = rank;
	}

	public void setSecondaryEmail(Email email) {
		this.secondEmail = email;
	}

	public void setSection(Section section) {
		this.section = section;
	}

	public void setShowApproved(boolean show) {
		this.showApproved = show;
	}

	public void setType(Type type) {
		this.type = type;
	}

	public void setUniversityID(String new_universityID) {
		this.universityID = new_universityID;
	}

	public void setYear(int year) {
		this.year = year;
	}

	public void setMinutesMissed(int minutes) {
		this.minutesMissed = minutes;
	}

	public int getMinutesMissed() {
		return this.minutesMissed;
	}

	@Override
	public String toString() {
		return getName();
	}

} @Ignore
public class AbstractDatastoreTest {

	private DataTrain datatrain = null;

	private LocalServiceTestHelper helper = null;
	
	static {
		App.CachingEnabled = false;
	}

	@Before
	public void setUp() {
		helper = new LocalServiceTestHelper(
				new LocalDatastoreServiceTestConfig(),
				new LocalUserServiceTestConfig());
		helper.setUp();
		datatrain = DataTrain.depart();
	}

	@After
	public void tearDown() {
		helper.tearDown();
		datatrain = null;
		helper = null;
	}

	protected final DataTrain getDataTrain() {
		if (datatrain == null) {
			throw new IllegalAccessError(
					"Access data train outside of a test case is not allowed.");
		} else {
			return datatrain;
		}
	}
} @Ignore
public class Users {

	public static final User createDefaultStudent(UserManager uc) {
		return createStudent(uc, "defaultstudent", "123456789", "John", "Doe", 2,
				"major", User.Section.AltoSax);
	}
	
	public static final User createDefaultTA(UserManager uc) {
		return createTA(uc, "defaultta", "987654321", "Johnny", "Doe", 2,
				"major", User.Section.AltoSax);
	}

	public static final User createStudent(UserManager uc,
			String email_firstpart, String univID, String firstName,
			String lastName, int year, String major, User.Section section) {

		com.google.appengine.api.users.User google_user = new com.google.appengine.api.users.User(
				email_firstpart + "@" + Config.getEmailDomain(),
				"gmail.com");

		return uc.createStudent(google_user, univID, firstName, lastName, year,
				major, section, Util.makeEmail(""));
	}

	public static final User createTA(UserManager uc, String email_firstpart,
			String univID, String firstName, String lastName, int year,
			String major, User.Section section) {

		com.google.appengine.api.users.User google_user = new com.google.appengine.api.users.User(
				email_firstpart + "@" + Config.getEmailDomain(),
				"gmail.com");

		User u = uc.createStudent(google_user, univID, firstName, lastName,
				year, major, section, Util.makeEmail(""));
		u.setType(User.Type.TA);
		uc.update(u);
		return u;
	}

	public static final User createDirector(UserManager uc,
			String email_firstpart, String firstName, String lastName) {
		return uc.createDirector(
				email_firstpart + "@" + Config.getEmailDomain(),
				email_firstpart + ".secondemail" + "@"
						+ Config.getEmailDomain(), firstName, lastName);
	}

	public static User createDefaultDirector(UserManager uc) {
		return Users.createDirector(uc, "director", "I am", "The Director");
	}
}
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.List;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.Interval;
import org.joda.time.LocalDateTime;
import org.joda.time.Period;
import org.junit.Test;
import edu.iastate.music.marching.attendance.model.interact.AbsenceManager;
import edu.iastate.music.marching.attendance.model.interact.DataTrain;
import edu.iastate.music.marching.attendance.model.interact.EventManager;
import edu.iastate.music.marching.attendance.model.interact.UserManager;
import edu.iastate.music.marching.attendance.model.store.Absence;
import edu.iastate.music.marching.attendance.model.store.Absence.Type;
import edu.iastate.music.marching.attendance.model.store.Event;
import edu.iastate.music.marching.attendance.model.store.User;
import edu.iastate.music.marching.attendance.testlib.AbstractDatastoreTest;
import edu.iastate.music.marching.attendance.testlib.Users;
package edu.iastate.music.marching.attendance.test.model.interact;





@SuppressWarnings("deprecation")
public class AbsenceManagerLinkingTest extends AbstractDatastoreTest {

	private static final LocalDateTime ADATETIME = new LocalDateTime(2012, 8,
			8, 17, 0, 0, 0);

	private static final LocalDateTime EDATETIME = new LocalDateTime(2012, 9,
			21, 6, 0, 0, 0);

	private enum OverlapLink {
		First, Second, Neither
	}

	@Test
	public void testOverlappingEventsWithTardyDuringEarlyEvent() {

		/*
		 * When we have 2 overlapping events adding a tardy during just the
		 * first event shouldn't cause any problems and it should be linked to
		 * the first event
		 */

		LocalDateTime startFirst = EDATETIME;
		LocalDateTime endFirst = EDATETIME.plusHours(1);

		LocalDateTime startSecond = EDATETIME.plusMinutes(45);
		LocalDateTime endSecond = EDATETIME.plusHours(1).plusMinutes(45);

		LocalDateTime tardyStart = EDATETIME.plusMinutes(15);

		overlappingEventsTestLinkingHelper(startFirst, endFirst, startSecond,
				endSecond, tardyStart, null, Absence.Type.Tardy,