public class XCalendarConfig {
	//
	protected XCalendarLayout layout;
	protected XCalendarRender render;
	
	/**
	 * 
	 */
	public XCalendarConfig() {
		this.layout = new XCalendarDefaultLayout();
		this.render = new XCalendarDefaultRender();
	}

	/**
	 * 
	 */
	public final XCalendarLayout getLayout() {
		return layout;
	}

	public final void setLayout(XCalendarLayout layout) {
		this.layout = layout;
	}

	public final XCalendarRender getRender() {
		return render;
	}

	public final void setRender(XCalendarRender render) {
		this.render = render;
	}
} public abstract class XCalendarTheme {
	
	private static final String[][] HOUR = { { "00", "01", "02", "03" }, 
			                                 { "04", "05", "06", "07" }, 
			                                 { "08", "09", "10", "11" }, 
			                                 { "12", "13", "14", "15" }, 
			                                 { "16", "17", "18", "19" }, 
			                                 { "20", "21", "22", "23" } };

	private static final String[][] MINUTE = { { "00", "05", "10", "15" }, 
                                               { "20", "25", "30", "35" }, 
                                               { "40", "45", "50", "55" } };

	private static final String[][] SECOND = { { "00", "05", "10", "15" }, 
                                               { "20", "25", "30", "35" }, 
                                               { "40", "45", "50", "55" } };
	
	//
	protected int arc = 8;
	protected int margin = 5;
	protected int width = 220;
	protected Font font = Fonts.getSystemFont();
	protected Font bold = Fonts.getFont(XCalendarTheme.class, "bold", () -> Fonts.bold(this.font));
	
	protected Color[] grid = new Color[] { getColor(0xc8, 0xc8, 0xc8), getColor(0x8f, 0xbc, 0xee) };
	protected Color[] toolbar = new Color[] { getColor(0x00, 0x00, 0x00), getColor(0x40, 0x87, 0xcc) };
	protected Color[] foreground = new Color[] { getColor(0x00, 0x00, 0x00), getColor(0x9f, 0x9f, 0x9f), getColor(0xff, 0xff, 0xff) };
	protected Color[] background = new Color[] { getColor(0xff, 0xff, 0xff), getColor(0xe0, 0xe0, 0xe0), getColor(0x00, 0x69, 0xcf), getColor(0xfe, 0xde, 0x00) };
	
	/**
	 * 
	 */
	public abstract String[][] getMonthTheme();
	public abstract String[] getDayOfWeekTheme();
	public abstract String[] getMonthOfYearTheme();
	public abstract String header(int month, int year);
	
	public String[][] getHourTheme(){ return HOUR; }
	public String[][] getMinuteTheme(){ return MINUTE; }
	public String[][] getSecondTheme(){ return SECOND; }

	/**
	 * 
	 */
	public int getArc() {
		return arc;
	}
	
	public Font getBold() {
		return bold;
	}
	
	public Font getFont() {
		return font;
	}

	public int getWidth() {
		return width;
	}
	
	public int getMargin() {
		return margin;
	}
	
	public Color[] getToolbar() {
		return toolbar;
	}
	
	public Color[] getForeground() {
		return foreground;
	}
	
	public Color[] getBackground() {
		return background;
	}
	
	/**
	 * 
	 */
	public void setArc(int arc) {
		this.arc = arc;
	}
	
	public void setBold(Font bold) {
		this.bold = bold;
	}
	
	public void setFont(Font font) {
		this.font = font;
	}
	
	public void setWidth(int width) {
		this.width = width;
	}
	
	public void setMargin(int margin) {
		this.margin = margin;
	}

	public void setToolbar(Color[] toolbar) {
		this.toolbar = toolbar;
	}
	
	public final void setGrid(Color[] colors) {
		this.grid = colors;
	}
	
	public void setForeground(Color[] foreground) {
		this.foreground = foreground;
	}

	public void setBackground(Color[] background) {
		this.background = background;
	}

	/**
	 * 
	 */
	public Color getGrid(boolean focused) {
		return focused ? grid[1] : grid[0];
	}
	
	public Color getToolBarBackground(boolean hovered){
		return hovered ? toolbar[0] : toolbar[1];
	}
	
	public Color getForeground(boolean enabled, boolean selected, boolean same) {
		return !enabled? foreground[1] : selected ? foreground[2] : same ? foreground[0] : foreground[1];
	}
	
	public Color getBackground(boolean enabled, boolean selected, boolean same, boolean hovered) {
		return !enabled ? background[0] : selected ? background[2] : same ? background[3] : hovered? background[1] : background[0];
	}
} public class XCalendarThemeFactory {
	//
	private Map<Locale, XCalendarTheme> map = new HashMap<>();
	
	/**
	 * 
	 */
	public XCalendarThemeFactory() {
		register(Locale.ENGLISH, new XCalendarThemeEn());
		register(Locale.JAPANESE, new XCalendarThemeJp());
	}
	
	/**
	 * 
	 */
	public XCalendarTheme create(Locale locale) {
		return map.getOrDefault(locale, new XCalendarThemeEn());
	}
	
	/**
	 * 
	 */
	protected void register(Locale locale, XCalendarTheme theme) {
		map.put(locale, theme);
	}
} public final class XCalendarUtils {
	//
	public static final Integer[] DECADE_OFFSET = new Integer[] { 
			0, 12, 24, 36, 48, 60, 72, 84,  96, null, null, null };

	/**
	 * 
	 */
	public static ZonedDateTime now(ZoneId z) {
		return ZonedDateTime.now(z);
	}
	
	/**
	 * 
	 */
	public static Date som(YearMonth ym, ZoneId zid) {
		return toDate(ym.atDay(1).atStartOfDay(zid));
	}
	
	public static Date eom(YearMonth ym, ZoneId zid) {
		return toDate(ym.atEndOfMonth().atStartOfDay(zid));
	}
	
	public static Date som(int y, int m, ZoneId zid) {
		return toDate(YearMonth.of(y, m).atDay(1).atStartOfDay(zid));
	}
	
	public static Date eom(int y, int m, ZoneId zid) {
		return toDate(YearMonth.of(y, m).atEndOfMonth().atStartOfDay(zid));
	}
	
	public static Date soy(final int y, final ZoneId zid) {
		return toDate(YearMonth.of(y, 1).atDay(1).atStartOfDay(zid));
	}
	
	public static Date eoy(final int y, final ZoneId zid) {
		return toDate(YearMonth.of(y, 12).atEndOfMonth().atStartOfDay(zid));
	}
	
	/**
	 * 
	 */
	public static Instant truncatedTo(Date d, TemporalUnit unit) {
		return d.toInstant().truncatedTo(unit);
	}
	
	public static Instant truncatedTo(Instant d, TemporalUnit unit) {
		return d.truncatedTo(unit);
	}
	
	public static Instant truncatedTo(ZonedDateTime d, TemporalUnit unit) {
		return d.truncatedTo(unit).toInstant();
	}
	
	/**
	 * 
	 */
	public static boolean isSameYear(int y1, int y2) {
		return y1 == y2;
	}
	
	public static boolean isSameYear(Date d1, Date d2) {
		return isSameYear(from(d1.toInstant()), from(d2.toInstant()));
	}
	
	public static boolean isSameYear(ZonedDateTime zdt1, ZonedDateTime zdt2) {
		return zdt1.getYear() == zdt2.getYear();
	}
	
	public static boolean isSameYear(LocalDateTime ldt1, LocalDateTime ldt2) {
		return ldt1.getYear() == ldt2.getYear();
	}
	
	/**
	 * 
	 */
	public static boolean isSameYearMonth(Date d1, Date d2) {
		return isSameYearMonth(from(d1.toInstant()), from(d2.toInstant()));
	}
	
	public static boolean isSameYearMonth(ZonedDateTime d1, ZonedDateTime d2) {
		return d1.getYear() == d2.getYear() && d1.getMonthValue() == d2.getMonthValue();
	}
	
	public static boolean isSameYearMonth(LocalDateTime d1, LocalDateTime d2) {
		return d1.getYear() == d2.getYear() && d1.getMonthValue() == d2.getMonthValue();
	}
	
	/**
	 * 
	 */
	public static boolean isSameDate(Date d1, Date d2) {
		return isSameDate(from(d1.toInstant()), from(d2.toInstant()));
	}

	public static boolean isSameDate(ZonedDateTime d1, ZonedDateTime d2) {
		return d1.truncatedTo(ChronoUnit.DAYS).compareTo(d2.truncatedTo(ChronoUnit.DAYS)) == 0;
	}
	
	public static boolean isSameDate(LocalDateTime d1, LocalDateTime d2) {
		return d1.truncatedTo(ChronoUnit.DAYS).compareTo(d2.truncatedTo(ChronoUnit.DAYS)) == 0;
	}
	
	/**
	 * 
	 */
	public static boolean isValidDate(Date d, Predicate<Date> test) {
		return test.test(d);
	}
	
	public static boolean isValidYear(final int y, final ZoneId zid, Predicate<Date> test) {
		LocalDate soy = toDateTime(soy(y, zid), zid).toLocalDate();
		LocalDate eoy = toDateTime(eoy(y, zid), zid).toLocalDate();
		return isValid(soy, eoy, zid, test);
	}

	public static boolean isValidYearMonth(int y, int m, ZoneId zid, Predicate<Date> test) {
		LocalDate som = toDateTime(som(y, m, zid), zid).toLocalDate();
		LocalDate eom = toDateTime(eom(y, m, zid), zid).toLocalDate();
		return isValid(som, eom, zid, test);
	}
	
	public static boolean isValid(LocalDate ld1, LocalDate ld2, ZoneId zid, Predicate<Date> test) {
		while (ld1.compareTo(ld2) != 0) {
			if (test.test(toDate(ld1.atStartOfDay(zid)))) return true; else ld1 = ld1.plusDays(1);
		}
		return false;
	}
	
	/**
	 * 
	 */
	public static Date toDate(Instant v) {
		return Date.from(v);
	}

	public static Date toDate(ZonedDateTime v) {
		return Date.from(v.toInstant());
	}
	
	public static ZonedDateTime toDateTime(Date v, ZoneId z) {
		return toDateTime(v.toInstant(), z);
	}

	public static ZonedDateTime toDateTime(Instant v, ZoneId z) {
		return ZonedDateTime.ofInstant(v, z);
	}
	
	public static Date toDate(int y, int m, int d, int hh, int mm, int ss, int nano, ZoneId z) {
		return toDate(ZonedDateTime.of(y, m, d, hh, mm, ss, nano, z));
	}

	public static ZonedDateTime toDateTime(int y, int m, int d, int hh, int mm, int ss, int nano, ZoneId z) {
		return ZonedDateTime.of(y, m, d, hh, mm, ss, nano, z);
	}
} public class XVirtualCalendar {
	//
	private int decade;
	private int century;
	private Date prevDate;
	private int depth = 400;
	private final boolean time;
	private final ZoneId zoneId;
	private ZonedDateTime travel;
	private ZonedDateTime calendar;
	private Predicate<Date> predicate;
	
	/**
	 * 
	 */
	public XVirtualCalendar(ZoneId zoneId, boolean time) {
		this.zoneId = zoneId; this.time = time; this.predicate = (v) -> true; 
		adjust(toDate(truncatedTo(now(zoneId), DAYS))); this.prevDate = null;
	}
	
	/**
	 * 
	 */
	private void update() { updateDecade(); updateCentury(); }
	private void updateDecade() { decade = travel.getYear() / 10 * 10; }
	private void updateCentury() { this.century = this.decade / 100 * 100; }
	
	/**
	 * 
	 */
	public void prevCentury() { century -= 100; }
	public void prevDecade() { decade -= 10; updateCentury(); }
	public void prevYear() { travel = travel.minusYears(1); update(); }
	public void prevMonth() { travel = travel.minusMonths(1); update(); }
	public void prevHour() { this.calendar = this.calendar.minusHours(1); }
	public void prevMinute() { this.calendar = this.calendar.minusMinutes(1); }
	public void prevSecond() { this.calendar = this.calendar.minusSeconds(1); }
	
	/**
	 * 
	 */
	public void nextCentury() { century += 100; }
	public void nextDecade() { decade += 10; updateCentury(); }
	public void nextYear() { travel = travel.plusYears(1); update(); }
	public void nextMonth() { travel = travel.plusMonths(1); update(); }
	public void nextHour() { this.calendar = this.calendar.plusHours(1); }
	public void nextMinute() { this.calendar = this.calendar.plusMinutes(1); }
	public void nextSecond() { this.calendar = this.calendar.plusSeconds(1); }
	
	/**
	 * 
	 */
	public Date getDate() { return toDate(calendar); }
	public Date getTravel() { return toDate(travel); }
	public ZonedDateTime getTravelDateTime() { return travel; }
	public ZonedDateTime getCalendarDateTime() { return calendar; }
	public boolean isValid(Date date) { return predicate.test(date); }
	public void setFilter(Predicate<Date> filter) { this.predicate = filter; }
	
	/**
	 * 
	 */
	public int getYear() { return travel.getYear(); }
	public int getMonth() { return travel.getMonthValue(); }
	public int getHour() { return this.calendar.getHour(); }
	public int getMinute() { return this.calendar.getMinute(); }
	public int getSecond() { return this.calendar.getSecond(); }
	public Range<Integer> getDecade() { return closed(decade, decade + 11); }
	public Range<Integer> getCentury() { return closed(century, century + 107); }
	
	/**
	 * 
	 */
	public void setCentury(int century) { this.century = century; }
	public void setDecade(int decade) { this.decade = decade; updateCentury(); }
	public void setYear(int year) { travel = travel.withYear(year); update(); }
	public void setMonth(int month) { travel = travel.withMonth(month); update(); }
	public void setHour(int hour ) { this.calendar = this.calendar.withHour(hour); }
	public void setMinute(int minute) { this.calendar = this.calendar.withMinute(minute); }
	public void setSecond(int second) { this.calendar = this.calendar.withSecond(second); }
	
	/**
	 * 
	 */
	public Date adjust(Date date) { 
		return adjust(date, false); 
	}
	
	public Date adjust(Date date, boolean clear) {
		//
		Date currDate = null;
		if (!this.time || clear) this.prevDate = null;
		this.travel = toDateTime(truncatedTo(date, ChronoUnit.SECONDS), zoneId);
		this.calendar = toDateTime(truncatedTo(date, ChronoUnit.SECONDS), zoneId);
		
		// +1 direction
		ZonedDateTime next = this.travel;
		for (int i = 0; i < depth && !isValid(currDate = toDate(next)); i++) 
			next = next.plusDays(prevDate == null ? 1 : !prevDate.after(currDate) ? 1 : -1);
		if (isValid(toDate(next))) {
			this.travel = next; this.calendar = next; prevDate = currDate; update(); return toDate(calendar);
		}
		
		// -1 direction
		ZonedDateTime prev = this.travel;
		for (int i = 0; i < depth && !isValid(currDate = toDate(prev)); i++) 
			prev = prev.plusDays(prevDate == null ? -1 : !prevDate.after(currDate) ? 1 : -1);
		if (isValid(toDate(prev))) {
			this.travel = prev; this.calendar = prev; prevDate = currDate; update(); return toDate(calendar);
		}
		return adjust(prevDate);
	}
} public class XCalendarStateMachine {
	//
	private XCalendarState curr, prev;
	
	/**
	 * 
	 */
	public XCalendarStateMachine(XCalendarState curr) {
		this(curr, null);
	}
	
	public XCalendarStateMachine(XCalendarState curr, @Nullable XCalendarState prev) {
		this.curr = curr; this.prev = prev;
	}
	
	/**
	 * 
	 */
	public XCalendarState current() {
		return curr; 
	}
	
	public void reset() {
		if(isTime()) this.curr = prev; this.prev = TIME;
	}
	
	public boolean isTime() {
		if(this.curr == TIME) return true;
		return curr == HOUR || curr == MINUTE || curr == SECOND;
	}
	
	/**
	 * 
	 */
	public void mutate(XCalendarSelectTimeWidget widget) {
		XCalendarState temp = this.curr;
		this.curr = this.prev; this.prev = temp;
	}

	public void mutate(final XCalendarTimeWidget widget) {
		switch (widget.getType()) {
		case HOUR: this.curr = XCalendarState.HOUR; break;
		case MINUTE: this.curr = XCalendarState.MINUTE; break;
		case SECOND: this.curr = XCalendarState.SECOND; break;
		default: throw new AssertionError("invalid widget: " + widget);
		}
	}
	
	public void mutate(final XCalendarSelectWidget widget) {
		switch (this.curr) {
		case DATE: this.curr = XCalendarState.MONTH; break;
		case MONTH: this.curr = XCalendarState.YEAR; break;
		case YEAR: this.curr = XCalendarState.DECADE; break;
		case DECADE: break;
		default: throw new AssertionError("invalid widget: " + widget);
		}
	}
	
	/**
	 * 
	 */
	public void mutate(XCalendarDecadeWidget widget) { this.curr = XCalendarState.YEAR; }
	public void mutate(XCalendarYearWidget widget)  { this.curr = XCalendarState.MONTH; }
	public void mutate(XCalendarMonthWidget widget)  { this.curr = XCalendarState.DATE; }
	public void mutate(XCalendarHourWidget widget)   { this.curr = XCalendarState.TIME; }
	public void mutate(XCalendarMinuteWidget widget) { this.curr = XCalendarState.TIME; }
	public void mutate(XCalendarSecondWidget widget) { this.curr = XCalendarState.TIME; }
} public enum XCalendarState {
	//
	DATE, MONTH, YEAR, DECADE, TIME, HOUR, MINUTE, SECOND;
	
	/**
	 * 
	 */
	public void prev(XVirtualCalendar calendar) {
		switch(this){
		case DATE: calendar.prevMonth(); break;
		case MONTH: calendar.prevYear(); break;
		case YEAR: calendar.prevDecade(); break;
		case DECADE: calendar.prevCentury(); break;
		default: throw new SWTException("invalid calendar state: " + this);
		}
	}
	
	public void next(XVirtualCalendar calendar) {
		switch(this) {
		case DATE: calendar.nextMonth(); break;
		case MONTH: calendar.nextYear(); break;
		case YEAR: calendar.nextDecade(); break;
		case DECADE: calendar.nextCentury(); break;
		default: throw new SWTException("invalid calendar state: " + this);
		}
	}

	/**
	 * 
	 */
	public String text(XCalendarTheme theme, XVirtualCalendar calendar) {
		switch (this) {
		case MONTH:
			return Strings.toString(calendar.getYear());
		case YEAR:
			Range<Integer> decade = calendar.getDecade();
			return decade.getValue1() + "-" + decade.getValue2();
		case DECADE:
			Range<Integer> century = calendar.getCentury();
			return century.getValue1() + "-" + century.getValue2();
		case DATE:
			return theme.header(calendar.getMonth(), calendar.getYear());
		default: throw new SWTException("invalid calendar state: " + this);
		}
	}
} public static Date toDate(Instant v) {
	return Date.from(v);
} public static Instant truncatedTo(Date d, TemporalUnit unit) {
	return d.toInstant().truncatedTo(unit);
} public enum XCalendarState {
	//
	DATE, MONTH, YEAR, DECADE, TIME, HOUR, MINUTE, SECOND;
	
	/**
	 * 
	 */
	public void prev(XVirtualCalendar calendar) {
		switch(this){
		case DATE: calendar.prevMonth(); break;
		case MONTH: calendar.prevYear(); break;
		case YEAR: calendar.prevDecade(); break;
		case DECADE: calendar.prevCentury(); break;
		default: throw new SWTException("invalid calendar state: " + this);
		}
	}
	
	public void next(XVirtualCalendar calendar) {
		switch(this) {
		case DATE: calendar.nextMonth(); break;
		case MONTH: calendar.nextYear(); break;
		case YEAR: calendar.nextDecade(); break;
		case DECADE: calendar.nextCentury(); break;
		default: throw new SWTException("invalid calendar state: " + this);
		}
	}

	/**
	 * 
	 */
	public String text(XCalendarTheme theme, XVirtualCalendar calendar) {
		switch (this) {
		case MONTH:
			return Strings.toString(calendar.getYear());
		case YEAR:
			Range<Integer> decade = calendar.getDecade();
			return decade.getValue1() + "-" + decade.getValue2();
		case DECADE:
			Range<Integer> century = calendar.getCentury();
			return century.getValue1() + "-" + century.getValue2();
		case DATE:
			return theme.header(calendar.getMonth(), calendar.getYear());
		default: throw new SWTException("invalid calendar state: " + this);
		}
	}
}
import cn.nextop.thorin.rcp.support.swt.widget.xcalendar.model.config.XCalendarConfig;
import cn.nextop.thorin.rcp.support.swt.widget.xcalendar.model.theme.XCalendarTheme;
import cn.nextop.thorin.rcp.support.swt.widget.xcalendar.model.theme.XCalendarThemeFactory;
import cn.nextop.thorin.rcp.support.swt.widget.xcalendar.support.XCalendarUtils;
import cn.nextop.thorin.rcp.support.swt.widget.xcalendar.support.XVirtualCalendar;
import cn.nextop.thorin.rcp.support.swt.widget.xcalendar.support.fsm.XCalendarStateMachine;
import cn.nextop.thorin.rcp.support.swt.widget.xcalendar.support.glossary.XCalendarState;
import org.eclipse.swt.SWT;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Locale;
import static cn.nextop.thorin.rcp.support.swt.widget.xcalendar.support.XCalendarUtils.toDate;
import static cn.nextop.thorin.rcp.support.swt.widget.xcalendar.support.XCalendarUtils.truncatedTo;
import static cn.nextop.thorin.rcp.support.swt.widget.xcalendar.support.glossary.XCalendarState.*;
package cn.nextop.thorin.rcp.support.swt.widget.xcalendar.model;




/**
 * 
 * @author Baoyi Chen
 */
public class XCalendarModel {
	//
	protected int style;
	protected Date date;
	protected ZoneId zoneId;
	protected boolean nullable;
	protected XCalendarTheme theme;
	protected XCalendarConfig config;