package eu.dnetlib.functionality.index.model;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import java.util.regex.Pattern;

/**
 * helper class for formatting and parsing Values. all methods synchronize on the used local formatter object, so you
 * can use the shared instance. Using multiple instances may improve performance, though, because of less
 * synchronization.
 */
public class ValueFormatHelper {
	/** shared global helper instance. */
	public static final ValueFormatHelper INSTANCE = new ValueFormatHelper();

	/** The max. length of strings to be parsed as date. */
	private static final int DATE_LENGTH = 10;

	/** The length of strings to be parsed as date time for (default) pattern 1. */
	private static final int DATE_TIME_LENGTH_PATTERN_DEFAULT = 28;

	/** The length of strings to be parsed as date time for pattern 2 and 3. */
	private static final int DATE_TIME_LENGTH_PATTERN_2_AND_3 = 24;

	/** The length of strings to be parsed as date time for pattern 4. */
	private static final int DATE_TIME_LENGTH_PATTERN_4 = 20;

	/** formatter to create and parse standard string representations of Date values: "yyyy-MM-dd". */
	private final DateFormat _formatDate = new SimpleDateFormat("yyyy-MM-dd");

	/** valid Datetime value pattern with milliseconds and time zone (default for printing). */
	private final DateFormat _formatDateTimePatternDefault = getDefaultDateTimeFormat();

	/** valid Datetime value pattern with time zone. */
	private final DateFormat _formatDateTimePattern2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");

	/** valid Datetime value pattern with milliseconds and time zone 'Z' (UTC). */
	private final DateFormat _formatDateTimePattern3 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");

	/** valid Datetime value pattern without milliseconds and time zone 'Z' (UTC). */
	private final DateFormat _formatDateTimePattern4 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

	/** pattern for checking if the string might be a date. */
	private final Pattern _formatPatternDateDefault = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");

	/** pattern for checking if the string might be a date time for default pattern. */
	private final Pattern _formatPatternTimeDefault = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}[+-]{1}\\d{4}");

	/** pattern for checking if the string might be a date time for pattern 2. */
	private final Pattern _formatPatternTime2 = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]{1}\\d{4}");

	/** pattern for checking if the string might be a date time for pattern 3. */
	private final Pattern _formatPatternTime3 = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z");

	/** pattern for checking if the string might be a date time for pattern 4. */
	private final Pattern _formatPatternTime4 = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z");

	/**
	 * create local instance.
	 */
	public ValueFormatHelper() {
		_formatDate.setLenient(false);
		_formatDateTimePatternDefault.setLenient(false);
		_formatDateTimePattern2.setLenient(false);
		_formatDateTimePattern3.setLenient(false);
		_formatDateTimePattern3.setTimeZone(TimeZone.getTimeZone("UTC"));
		_formatDateTimePattern4.setLenient(false);
		_formatDateTimePattern4.setTimeZone(TimeZone.getTimeZone("UTC"));
	}

	/**
	 * @return the default format for datetime values.
	 */
	public static SimpleDateFormat getDefaultDateTimeFormat() {
		final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
		sdf.setLenient(false);
		return sdf;
	}

	/**
	 * format value as Date string.
	 * 
	 * @param value
	 *            a date value
	 * @return formatted date.
	 */
	public String formatDate(final Date value) {
		synchronized (_formatDate) {
			return _formatDate.format(value);
		}
	}

	/**
	 * format value as DateTime string.
	 * 
	 * @param value
	 *            a datetime value
	 * @return formatted datetime string
	 */
	public String formatDateTime(final Date value) {
		synchronized (_formatDateTimePatternDefault) {
			return _formatDateTimePatternDefault.format(value);
		}
	}

	/**
	 * parse a date string.
	 * 
	 * @param dateString
	 *            a date string
	 * @return parsed Date
	 * @throws ParseException
	 *             string has wrong format
	 */
	public Date parseDate(final String dateString) throws ParseException {
		if (dateString.length() == DATE_LENGTH && _formatPatternDateDefault.matcher(dateString).matches()) {
			synchronized (_formatDate) {
				return _formatDate.parse(dateString);
			}
		} else {
			throw new ParseException("Length of date string '" + dateString + "' exceeds maximum date length of " + DATE_LENGTH, DATE_LENGTH);
		}
	}

	/**
	 * parse a date string.
	 * 
	 * @param dateString
	 *            a date string
	 * @return parsed Date if correct format, else null.
	 */
	public Date tryParseDate(final String dateString) {
		if (dateString.length() == DATE_LENGTH && _formatPatternDateDefault.matcher(dateString).matches()) {
			synchronized (_formatDate) {
				try {
					return _formatDate.parse(dateString);
				} catch (final ParseException ex) {
					; // not a date ... fine.
				}
			}
		}
		return null;
	}

	/**
	 * parse datetime string.
	 * 
	 * @param dateTimeString
	 *            a datetime string
	 * @return parsed Date
	 * @throws ParseException
	 *             string has wrong format
	 */
	public Date parseDateTime(final String dateTimeString) throws ParseException {
		final Date result;
		final int dateLen = dateTimeString.length();
		if (dateLen == DATE_TIME_LENGTH_PATTERN_DEFAULT && _formatPatternTimeDefault.matcher(dateTimeString).matches()) {
			synchronized (_formatDateTimePatternDefault) {
				result = _formatDateTimePatternDefault.parse(dateTimeString);
			}
		} else if (dateLen == DATE_TIME_LENGTH_PATTERN_2_AND_3 && _formatPatternTime2.matcher(dateTimeString).matches()) {
			synchronized (_formatDateTimePattern2) {
				result = _formatDateTimePattern2.parse(dateTimeString);
			}
		} else if (dateLen == DATE_TIME_LENGTH_PATTERN_2_AND_3 && _formatPatternTime3.matcher(dateTimeString).matches()) {
			synchronized (_formatDateTimePattern3) {
				result = _formatDateTimePattern3.parse(dateTimeString);
			}
		} else if (dateLen == DATE_TIME_LENGTH_PATTERN_4 && _formatPatternTime4.matcher(dateTimeString).matches()) {
			synchronized (_formatDateTimePattern4) {
				result = _formatDateTimePattern4.parse(dateTimeString);
			}
		} else {
			throw new ParseException("Length '" + dateTimeString.length() + "' of datetime string '" + dateTimeString
					+ "' doesn't match expected pattern length", dateTimeString.length());
		}
		return result;
	}

	/**
	 * parse datetime string.
	 * 
	 * @param dateTimeString
	 *            a datetime string
	 * @return parsed Date if correct format, else null;
	 */
	public Date tryParseDateTime(final String dateTimeString) {
		Date result = null;
		try {
			switch (dateTimeString.length()) {
			case DATE_TIME_LENGTH_PATTERN_DEFAULT:
				if (_formatPatternTimeDefault.matcher(dateTimeString).matches()) {
					synchronized (_formatDateTimePatternDefault) {
						result = _formatDateTimePatternDefault.parse(dateTimeString);
					}
				}
				break;
			case DATE_TIME_LENGTH_PATTERN_2_AND_3:
				if (_formatPatternTime2.matcher(dateTimeString).matches()) {
					synchronized (_formatDateTimePattern2) {
						result = _formatDateTimePattern2.parse(dateTimeString);
					}
				} else if (_formatPatternTime3.matcher(dateTimeString).matches()) {
					synchronized (_formatDateTimePattern3) {
						result = _formatDateTimePattern3.parse(dateTimeString);
					}
				}
				break;
			case DATE_TIME_LENGTH_PATTERN_4:
				if (_formatPatternTime4.matcher(dateTimeString).matches()) {
					synchronized (_formatDateTimePattern4) {
						result = _formatDateTimePattern4.parse(dateTimeString);
					}
				}
				break;
			default:
				break;
			}
		} catch (final ParseException ex) {
			; // not a datetime ... fine.
		}
		return result;
	}
}
