package industries.doublehelix.conditionalfilter.filtertypes.flat;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.AbstractMap;
import java.util.Map;

/**
 * A filter for a date type
 * @author klaue
 *
 */
public class DateFilter extends FlatFilter {
	private static final long serialVersionUID = 1L;
	public enum DateFilterMode {
		IS("is"), ISNOT("is not"), BEFORE("is before"), AFTER("is after"), BETWEEN("is between"), NOTBETWEEN("is not between");
		private String name;
		private DateFilterMode(String name) {
			this.name = name;
		}
		@Override
		public String toString() {
			return name;
		}
		public static DateFilterMode fromString(String name) {
			for (DateFilterMode dfmode : DateFilterMode.values()) {
				if (dfmode.toString().equals(name)) return dfmode;
			}
			return null;
		}
	}
	
	private DateFilterMode filterMode = null;
	private LocalDate nowDateAtInstantiation = LocalDate.now();
	private LocalDate firstFilterDate = nowDateAtInstantiation;
	private LocalDate secondFilterDate = null; // may not be set
	
	/**
	 * A filter for a date field
	 * @param filterMode the mode
	 * @param filterName the name of the filtered attribute 
	 * @param filterRawName the name of the filter for the generated SQL
	 */
	public DateFilter(DateFilterMode filterMode, String filterName, String filterRawName) {
		super(filterName, filterRawName);
		setFilterMode(filterMode);
	}
	
	/**
	 * A template for a filter for a date field
	 * @param filterName the name of the filtered attribute 
	 * @param filterRawName the name of the filter for the generated SQL
	 */
	public DateFilter(String filterName, String filterRawName) {
		super(filterName, filterRawName);
		setFilterMode(DateFilterMode.IS);
	}
	
	/**
	 * A template for a filter for a date field
	 * @param filterName the name of the filtered attribute
	 */
	public DateFilter(String filterName) {
		super(filterName);
		setFilterMode(DateFilterMode.IS);
	}
	
	/**
	 * Returns the (first) filtered date. Short-hand for getFirstDate()
	 * If it's a between-filter, it's inclusive. if it's before/after, it's exclusive
	 * @return the number
	 */
	public LocalDate getDate() {
		return getFirstDate();
	}

	/**
	 * returns the first filtered date. If it's a between-filter, it's inclusive. if it's before/after, it's exclusive
	 * @return the date
	 */
	public LocalDate getFirstDate() {
		return firstFilterDate;
	}

	/**
	 * returns the second filtered date, may be null if filter is not of a "between" type. If it's a between-filter, it's inclusive
	 * @return the date or null
	 */
	public LocalDate getSecondDate() {
		return secondFilterDate;
	}

	/**
	 * sets the first filtered date. If it's a between-filter, it's inclusive. if it's before/after, it's exclusive
	 * @param number
	 */
	public void setFirstDate(LocalDate date) {
		this.firstFilterDate = date;
	}

	/**
	 * sets the second filtered date. If type of this filter is none that allows multiple dates, this will be disregarded. If it's a between-filter, it's inclusive
	 * @param number
	 */
	public void setSecondDate(LocalDate date) {
		this.secondFilterDate = date;
	}

	/**
	 * @return The mode of this filter
	 */
	public DateFilterMode getFilterMode() {
		return filterMode;
	}

	/**
	 * Sets the new mode of this filter.
	 * If it was a single-date filter and is now a two date one, the second date will be initialized to now
	 * If it was a two date filter and is now a single date one, the second number will be reset to null
	 * @param filterMode
	 */
	public void setFilterMode(DateFilterMode filterMode) {
		boolean wasTwoDateFilter = this.filterMode != null && (this.filterMode == DateFilterMode.BETWEEN || this.filterMode == DateFilterMode.NOTBETWEEN);
		boolean isTwoDateFilter = filterMode == DateFilterMode.BETWEEN || filterMode == DateFilterMode.NOTBETWEEN;
		
		this.filterMode = filterMode;
		if (!isTwoDateFilter) {
			this.secondFilterDate = null;
		} else {
			if (!wasTwoDateFilter) {
				this.secondFilterDate = nowDateAtInstantiation;
			}
		}
	}
	
	/**
	 * set the conditions for this filter
	 * @param filterMode
	 * @param firstDate
	 * @param secondDate is disregarded if mode is not (not)between. if mode is (not)between, may not be null
	 */
	public void setFilterConditions(DateFilterMode filterMode, LocalDate firstDate, LocalDate secondDate) {
		boolean twoValuesFilter = filterMode == DateFilterMode.BETWEEN || filterMode == DateFilterMode.NOTBETWEEN;
		if (twoValuesFilter && secondDate == null) {
			throw new NullPointerException("Second date cannot be null if filtermode is (not)between");
		}
		
		setFilterMode(filterMode);
		setFirstDate(firstDate);
		// important, after setting filter mode which may reset second date
		if (twoValuesFilter) setSecondDate(secondDate);
	}
	
	@Override
	public String toString() {
		String field = getRealFilterField(false);
		
		StringBuffer buf = new StringBuffer();
		buf.append(field).append(" ").append(this.filterMode.toString()).append(" ");
		buf.append(this.getFirstDate());
		if (this.filterMode == DateFilterMode.BETWEEN || this.filterMode == DateFilterMode.NOTBETWEEN) {
			buf.append(" and ").append(this.getSecondDate());
		}
		return buf.toString();
	}
	
	@Override
	public String getSqlWhere() {
		String field = getRealFilterField(true);
		
		StringBuffer buf = new StringBuffer();
		buf.append(field);
		
		// sql date column have no date-only, just date and time
		// to fix this, is/isnot  modes become a (not) between, from start of day to end of day
		Map.Entry<String, String> dates = getDateAreaForSqlAsString();
		String firstDate = dates.getKey();
		String secondDate = dates.getValue();
		switch(this.filterMode) {
			case NOTBETWEEN: // fallthrough
			case ISNOT:
				buf.append(" not "); // fall through
			case BETWEEN: // fallthrough
			case IS:
				buf.append(" between '");
				buf.append(firstDate).append("' AND '");
				buf.append(secondDate).append("'");
				break;
			case BEFORE:
				buf.append(" < '").append(firstDate).append("'"); // startofday
				break;
			case AFTER:
				buf.append(" > '").append(secondDate).append("'"); // endofday
				break;
		}
		
		return buf.toString();
	}
	
	/**
	 * As SQL has no date-without-time column, dates without times have to count as startofday to endofday
	 * this method will calculate proper pairs for the dates of this filter
	 * @return the dates formatted as string
	 */
	public Map.Entry<String, String> getDateAreaForSqlAsString() {
		Map.Entry<LocalDateTime, LocalDateTime> dates = getDateAreaForSqlAsLocalDateTime();
		DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
		String firstDate = dates.getKey().format(dtf);
		String secondDate = dates.getValue().format(dtf);
		return new AbstractMap.SimpleEntry<>(firstDate, secondDate);
	}
	
	/**
	 * As SQL has no date-without-time column, dates without times have to count as startofday to endofday
	 * this method will calculate proper pairs for the dates of this filter
	 * @return the dates formatted as string
	 */
	public Map.Entry<LocalDateTime, LocalDateTime> getDateAreaForSqlAsLocalDateTime() {
		LocalDate fromDate = this.getFirstDate();
		LocalDate toDate = this.getSecondDate();
		
		if (this.filterMode != DateFilterMode.BETWEEN && this.filterMode != DateFilterMode.NOTBETWEEN) {
			toDate = fromDate;
		}
		
		if (fromDate.isAfter(toDate)) {
			LocalDate temp = fromDate;
			fromDate = toDate;
			toDate = temp;
		}
		return new AbstractMap.SimpleEntry<>(fromDate.atStartOfDay(), toDate.atTime(23, 59, 59));
	}

	@Override
	public String getPreparedStatementWhereString() {
		if (this.filterRawName == null && this.filterName == null) throw new IllegalArgumentException("filtername not set");
		String field = this.filterRawName == null ? this.filterName : this.filterRawName;
		
		StringBuffer buf = new StringBuffer();
		
		buf.append(field);
		switch(this.filterMode) {
			case ISNOT: // fall through
			case NOTBETWEEN:
				buf.append(" not "); // fall through
			case IS: // fall through
			case BETWEEN:
				buf.append(" between ? AND ?");
				break;
			case BEFORE:
				buf.append(" < ?");
				break;
			case AFTER:
				buf.append(" > ?");
				break;
		}
		
		return buf.toString();
	}

	@Override
	public int addPreparedStatementValues(PreparedStatement ps, int currentIndex) throws SQLException {
		// sql date column have no date-only, just date and time
		// to fix this, is/isnot  modes become a (not) between, from start of day to end of day
		Map.Entry<LocalDateTime, LocalDateTime> dates = getDateAreaForSqlAsLocalDateTime();
		LocalDateTime firstDate = dates.getKey();
		LocalDateTime secondDate = dates.getValue();
		
		switch(this.filterMode) {
			case NOTBETWEEN: // fall through
			case BETWEEN: // fall through
			case ISNOT: // fall through
			case IS: // fall through
				// JDBC 4.2 or newer should be able to handle this
				ps.setObject(currentIndex, firstDate);
				ps.setObject(currentIndex+1, secondDate);
				return currentIndex += 2;
			case BEFORE:
				// JDBC 4.2 or newer should be able to handle this
				ps.setObject(currentIndex, firstDate); // startofday
				return ++currentIndex;
			case AFTER:
				// JDBC 4.2 or newer should be able to handle this
				ps.setObject(currentIndex, secondDate); // endofday
				return ++currentIndex;
		}
		return currentIndex; // should never happen
	}

	@Override
	public boolean isEmpty() {
		return this.firstFilterDate.equals(nowDateAtInstantiation)
				&& (this.secondFilterDate == null || this.secondFilterDate.equals(nowDateAtInstantiation));
	}
	
}
