package industries.doublehelix.conditionalfilter.filtertypes.flat;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

import industries.doublehelix.conditionalfilter.filtertypes.flat.DateFilter.DateFilterMode;

/**
 * A filter for a date and time type
 * @author klaue
 *
 */
public class DateTimeFilter extends FlatFilter {
	private static final long serialVersionUID = 1L;
	protected DateFilterMode filterMode = null;
	private LocalDateTime nowDateAtInstantiation = LocalDateTime.now();
	private LocalDateTime firstFilterDate = nowDateAtInstantiation;
	private LocalDateTime secondFilterDate = null; // may not be set
	private ZoneId zoneId = null;
	
	/**
	 * 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 DateTimeFilter(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 DateTimeFilter(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 DateTimeFilter(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 LocalDateTime getDateTime() {
		return getFirstDateTime();
	}

	/**
	 * 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 LocalDateTime getFirstDateTime() {
		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 LocalDateTime getSecondDateTime() {
		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 setFirstDateTime(LocalDateTime 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 setSecondDateTime(LocalDateTime 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, LocalDateTime firstDate, LocalDateTime 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);
		setFirstDateTime(firstDate);
		// important, after setting filter mode which may reset second date
		if (twoValuesFilter) setSecondDateTime(secondDate);
	}

	/**
	 * set the time zone that will be used for the toString() and addPreparedStatementValues()
	 * The date in this filter will be converted to the given zone assuming it was in the local time zone
	 * @param zoneId the time zone or null to use no conversion
	 */
	public void setTimezoneToUseForSql(ZoneId zoneId) {
		this.zoneId = zoneId;
	}
	
	/**
	 * Get the time zone that will be used for the toString() and addPreparedStatementValues()
	 * The date in this filter will be converted to the given zone assuming it was in the local time zone
	 * @return the time zone used (null if none/no conversion)
	 */
	public ZoneId getTimezoneToUseForSql() {
		return this.zoneId;
	}
	
	/**
	 * Convert the first date to the given zone assuming it was in local time zone
	 * @param zoneToUse
	 * @return The first date converted from assumed local time zone to given time zone
	 */
	public LocalDateTime getFirstDateAfterTimezoneConversion(ZoneId zoneToUse) {
		return convertLocalDateBetweenTimezones(this.getFirstDateTime(), ZoneId.systemDefault(), zoneToUse);
	}
	
	/**
	 * Convert the second date to the given zone assuming it was in local time zone
	 * @param zoneToUse
	 * @return The second date converted from assumed local time zone to given time zone or null if the filter is for one date only
	 */
	public LocalDateTime getSecondDateAfterTimezoneConversion(ZoneId zoneToUse) {
		return convertLocalDateBetweenTimezones(this.getSecondDateTime(), ZoneId.systemDefault(), zoneToUse);
	}
	
	/**
	 * Convert the given local date, which is timezone less by design, between the given zones
	 * @param from the zone the LocalDateTime is assumed to be in
	 * @param to the zone to convert it to
	 * @return The date converted between the time zones
	 */
	public LocalDateTime convertLocalDateBetweenTimezones(LocalDateTime date, ZoneId from, ZoneId to) {
		if (date == null) return null;
		if (from == null || to == null) return date;
		
		// convert the date to a zoned one
	    ZonedDateTime zonedDate = date.atZone(from);
	    // convert the zoned date to the second time zone
	    zonedDate = zonedDate.withZoneSameInstant(to);
	    // get the localDateTime object again
		return zonedDate.toLocalDateTime();
	}
	
	@Override
	public String toString() {
		String field = getRealFilterField(false);
		
		StringBuffer buf = new StringBuffer();
		buf.append(field).append(" ").append(this.filterMode.toString()).append(" ");
		buf.append(this.getFirstDateTime());
		if (this.filterMode == DateFilterMode.BETWEEN || this.filterMode == DateFilterMode.NOTBETWEEN) {
			buf.append(" and ").append(this.getSecondDateTime());
		}
		return buf.toString();
	}
	
	@Override
	public String getSqlWhere() {
		String field = getRealFilterField(true);
		
		StringBuffer buf = new StringBuffer();
		buf.append(field);
		
		DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
		String firstDate = getFirstDateAfterTimezoneConversion(this.zoneId).format(dtf);
		String secondDate = null;
		if (this.getSecondDateTime() != null) {
			secondDate = getSecondDateAfterTimezoneConversion(this.zoneId).format(dtf);
		}
		
		switch(this.filterMode) {
			case NOTBETWEEN:
				buf.append(" not "); // fall through
			case BETWEEN:
				buf.append(" between '");
				buf.append(firstDate).append("' AND '");
				buf.append(secondDate).append("'");
				break;
			case IS:
				buf.append(" = '").append(firstDate).append("'");
				break;
			case ISNOT:
				buf.append(" != '").append(firstDate).append("'");
				break;
			case BEFORE:
				buf.append(" < '").append(firstDate).append("'");
				break;
			case AFTER:
				buf.append(" > '").append(firstDate).append("'");
				break;
		}
		
		return buf.toString();
	}

	@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 NOTBETWEEN:
				buf.append(" not "); // fall through
			case BETWEEN:
				buf.append(" between ? AND ?");
				break;
			case IS:
				buf.append(" = ?");
				break;
			case ISNOT:
				buf.append(" != ?");
				break;
			case BEFORE:
				buf.append(" < ?");
				break;
			case AFTER:
				buf.append(" > ?");
				break;
		}
		
		return buf.toString();
	}

	@Override
	public int addPreparedStatementValues(PreparedStatement ps, int currentIndex) throws SQLException {
		LocalDateTime convertedFirstDate = this.getFirstDateAfterTimezoneConversion(zoneId);
		LocalDateTime convertedSecondDate = this.getSecondDateAfterTimezoneConversion(zoneId);
		
		// JDBC 4.2 or newer should be able to handle this
		ps.setObject(currentIndex, convertedFirstDate);
		
		switch(this.filterMode) {
			case NOTBETWEEN: // fall through
			case BETWEEN:
				ps.setObject(currentIndex+1, convertedSecondDate);
				return currentIndex += 2;
			default:
				return ++currentIndex;
		}
	}

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