package org.fluxtream.core.mvc.models;
import java.util.TimeZone;
import org.fluxtream.core.Configuration;
import org.fluxtream.core.TimeUnit;
import org.fluxtream.core.services.MetadataService;
import org.fluxtream.core.utils.TimeUtils;
import org.fluxtream.core.utils.Utils;
import net.sf.json.JSONObject;
import org.joda.time.DateTimeConstants;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
public class CalendarModel {
private static final DateTimeFormatter currentDateFormatter = DateTimeFormat
.forPattern("EEE, MMM d, yyyy");
private static final DateTimeFormatter shortDayFormatter = DateTimeFormat
.forPattern("MMM d");
private static final DateTimeFormatter currentMonthFormatter = DateTimeFormat
.forPattern("MMMMMMMMMMMMM yyyy");
private static final DateTimeFormatter currentYearFormatter = DateTimeFormat
.forPattern("yyyy");
private final long guestId;
private final MetadataService metadataService;
private TimeUnit timeUnit = TimeUnit.DAY;
private LocalDate fromDate;
public CalendarModel(final long guestId, final MetadataService metadataService) {
this.guestId = guestId;
this.metadataService = metadataService;
setToToday();
}
public static CalendarModel fromState(final long guestId,
final MetadataService metadataService,
final String state) {
CalendarModel calendarModel = new CalendarModel(guestId, metadataService);
calendarModel.replaceState(state);
return calendarModel;
}
public void setYear(final int year) {
timeUnit = TimeUnit.YEAR;
fromDate = new LocalDate(year, 1, 1);
}
public void setDate(final String formattedDate) {
this.timeUnit = TimeUnit.DAY;
// TODO: If we were using JodaTime 2.0 or later, we could simply write
// fromDate = LocalDate.parse(formattedDate, jsDateFormatter)
fromDate = TimeUtils.dateFormatter.parseDateTime(formattedDate).toLocalDate();
}
public void setWeek(final int year, final int week) {
this.timeUnit = TimeUnit.WEEK;
fromDate = TimeUtils.getBeginningOfWeek(year, week);
}
public int getWeekYear() {
if (timeUnit != TimeUnit.WEEK)
throw new IllegalStateException("Unexpected check for week year when not using week time unit");
// Off by 1 because getBeginningOfWeek(year, week), and by extension
// setWeek(year, week) goes back by 1 week
return fromDate.plusWeeks(1).getWeekyear();
}
public int getWeek() {
if (timeUnit != TimeUnit.WEEK)
throw new IllegalStateException("Unexpected check for week when not using week time unit");
// Off by 1 because getBeginningOfWeek(year, week), and by extension
// setWeek(year, week) goes back by 1 week
return fromDate.plusWeeks(1).getWeekOfWeekyear();
}
public void setMonth(final int year, final int month) {
timeUnit = timeUnit.MONTH;
fromDate = new LocalDate(year, 1, 1).withMonthOfYear(month);
}
/**
* Returns one past the end date.
*
* This is similar to the behavior of Python's range function or Java's String.substring method,
* and it has the desirable property that the number of milliseconds between midnight on
* getToDate() and midnight on fromDate is equal to the number of milliseconds in a day times
* the number of days in the range described by TimeUnit (assuming there is no time zone change,
* leap second, or daylight savings time start/end in the middle).
*/
private LocalDate getToDate() {
switch (timeUnit) {
case DAY:
return fromDate.plusDays(1);
case WEEK:
return fromDate.plusWeeks(1);
case MONTH:
return fromDate.plusMonths(1);
case YEAR:
return fromDate.plusYears(1);
}
throw new UnsupportedOperationException("Unexpected TimeUnit value");
}
private TimeZone getTimeZone(final LocalDate date) {
// The format used for dateString is the same format used in
// MetadataServiceImpl's formatter
final String dateString = date.toString("yyyy-MM-dd");
return metadataService.getTimeZone(guestId, dateString);
}
private DateTimeZone getBrowserDateTimeZone() {
// TODO: This doesn't match the implementation, even though it should be right
final TimeZone tz = metadataService.getCurrentTimeZone(guestId);
return DateTimeZone.forTimeZone(tz);
}
private long getMillis(final LocalDate date) {
return date.toDateTimeAtStartOfDay(DateTimeZone.forTimeZone(getTimeZone(date)))
.getMillis();
}
private long getMillisAtTrailingMidnight(final LocalDate date){
return getMillis(date) + DateTimeConstants.MILLIS_PER_DAY;
}
public long getStart() {
return getMillis(fromDate);
}
public long getEnd() {
return getMillisAtTrailingMidnight(getToDate().minusDays(1));
}
public String toJSONString(final Configuration env) {
// TODO: Include information in JSON that tells which calendar cells should
// be lit up - don't want to use UTC timestamps to determine that info
final JSONObject json = new JSONObject();
json.put("timeUnit", timeUnit.toString());
json.put("currentTimespanLabel", timespanLabel());
json.put("isToday", isToday());
json.put("state", getState());
json.put("start", getStart());
json.put("end", getEnd());
return json.toString();
}
/**
* Returns a hash that serves as a client-side caching key; it is
* release-based
*/
private String getTimeHash(final Configuration env, final String configKey) {
final String toHash = env.get("release") + getStart() + getEnd() + configKey;
return Utils.hash(toHash);
}
private String getState() {
switch (timeUnit) {
case DAY:
return "date/" + TimeUtils.dateFormatter.print(fromDate);
case WEEK:
return String.format("week/%d/%d", getWeekYear(), getWeek());
case MONTH:
return String.format("month/%d/%d", fromDate.getYear(), fromDate.getMonthOfYear());
case YEAR:
return String.format("year/%d", fromDate.getYear());
}
throw new UnsupportedOperationException("Unexpected TimeUnit value");
}
public void setToToday() {
timeUnit = TimeUnit.DAY;
fromDate = new LocalDate(getBrowserDateTimeZone());
}
private boolean isToday() {
final LocalDate today = new LocalDate(getBrowserDateTimeZone());
return (timeUnit == TimeUnit.DAY)
&& (fromDate.getYear() == today.getYear())
&& (fromDate.getMonthOfYear() == today.getMonthOfYear())
&& (fromDate.getDayOfMonth() == today.getDayOfMonth());
}
private String timespanLabel() {
switch (timeUnit) {
case DAY:
return currentDateFormatter.print(fromDate);
case WEEK:
final String from = shortDayFormatter.print(fromDate);
final String to = shortDayFormatter.print(getToDate().minusDays(1));
// TODO: Way to handle from and to in different years?
final String year = currentYearFormatter.print(fromDate);
final String toYear = currentYearFormatter.print(getToDate().minusDays(1));
if (toYear.equals(year))
return from + " - " + to + " " + year;
else
return String.format("%s %s - %s %s", from, year, to, toYear);
case MONTH:
return currentMonthFormatter.print(fromDate);
case YEAR:
return currentYearFormatter.print(fromDate);
}
throw new UnsupportedOperationException("Unexpected TimeUnit value");
}
public void incrementTimespan() {
switch (timeUnit) {
case DAY:
fromDate = fromDate.plusDays(1);
break;
case WEEK:
fromDate = fromDate.plusWeeks(1);
break;
case MONTH:
fromDate = fromDate.plusMonths(1);
break;
case YEAR:
fromDate = fromDate.plusYears(1);
break;
}
}
public void decrementTimespan() {
switch (timeUnit) {
case DAY:
fromDate = fromDate.minusDays(1);
break;
case WEEK:
fromDate = fromDate.minusWeeks(1);
break;
case MONTH:
fromDate = fromDate.minusMonths(1);
break;
case YEAR:
fromDate = fromDate.minusYears(1);
break;
}
}
public void replaceState(final String state) {
final String[] stateParts = state.split("/");
final TimeUnit timeUnit = TimeUnit.fromValue(stateParts[0].equals("date") ? "day" : stateParts[0]);
final int year, month, week;
switch (timeUnit) {
case DAY:
setDate(stateParts[1]);
break;
case WEEK:
year = Integer.valueOf(stateParts[1]);
week = Integer.valueOf(stateParts[2]);
setWeek(year, week);
break;
case MONTH:
year = Integer.valueOf(stateParts[1]);
month = Integer.valueOf(stateParts[2]);
setMonth(year, month);
break;
case YEAR:
year = Integer.valueOf(stateParts[1]);
setYear(year);
break;
}
}
public void setYearTimeUnit() {
timeUnit = TimeUnit.YEAR;
fromDate = new LocalDate(fromDate.getYear(), 1, 1);
}
public void setMonthTimeUnit() {
timeUnit = TimeUnit.MONTH;
fromDate = new LocalDate(fromDate.getYear(), fromDate.getMonthOfYear(), 1);
}
public void setDayTimeUnit() {
timeUnit = TimeUnit.DAY;
fromDate = new LocalDate(fromDate.getYear(), fromDate.getMonthOfYear(), fromDate.getDayOfMonth());
}
public void setWeekTimeUnit() {
LocalDate origFromDate = fromDate;
timeUnit = TimeUnit.WEEK;
fromDate = new LocalDate(fromDate.getYear(), fromDate.getMonthOfYear(), fromDate.getDayOfMonth())
.withDayOfWeek(TimeUtils.FIRST_DAY_OF_WEEK);
// Unfortunately, the above code returns the following week instead of the containing week for
// every day other than Sunday. Check for this and decrement the week if needed.
if(fromDate.isAfter(origFromDate)) {
fromDate = fromDate.minusWeeks(1);
}
}
}