package routines;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;

/*
 * user specification: the function's comment should contain keys as follows: 1. write about the function's comment.but
 * it must be before the "{talendTypes}" key.
 * 
 * 2. {talendTypes} 's value must be talend Type, it is required . its value should be one of: String, char | Character,
 * long | Long, int | Integer, boolean | Boolean, byte | Byte, Date, double | Double, float | Float, Object, short |
 * Short
 * 
 * 3. {Category} define a category for the Function. it is required. its value is user-defined .
 * 
 * 4. {param} 's format is: {param} <type>[(<default value or closed list values>)] <name>[ : <comment>]
 * 
 * <type> 's value should be one of: string, int, list, double, object, boolean, long, char, date. <name>'s value is the
 * Function's parameter name. the {param} is optional. so if you the Function without the parameters. the {param} don't
 * added. you can have many parameters for the Function.
 * 
 * 5. {example} gives a example for the Function. it is optional.
 */
public class LcagDateTime {
	private static LcagLogger log = LcagLogger.getLogger(LcagDateTime.class);

	public static String MDM_DATE_FORMAT_STR = "yyyy-MM-dd";
	public static String MDM_DATETIME_FORMAT_STR = "yyyy-MM-dd'T'HH:mm:ss";
	private static final String DATETIME_FORMAT_ISO8601_UTC_STR = "yyyy-MM-dd'T'HH:mm:ss'Z'";
	public static final String TALEND_INTERNAL_DATE_FORMAT_STR = "yyyy-MM-dd";

	// Used as file prefix for all relevant MDM job files
	public static final String MDM_FILE_TIMESTAMP_FORMAT_STR = "yyyy-MM-dd_HH-mm-ss-SSS'Z'";
	// Used as file prefix for all files sent to MDM consumers
	public static final String MDM_CONSUMER_FILE_TIMESTAMP_FORMAT_STR = "yyyy-MM-dd_HH-mm-ss-SSS";
	// Used in MDM messages sent to consumer queue.
	public static final String MDM_CONSUMER_TIMESTAMP_FORMAT_STR = DATETIME_FORMAT_ISO8601_UTC_STR;
	// Used in Talend UI XML trigger messages and as Default format for Date to
	// String Conversions
	public static final String TALEND_INTERNAL_DATETIME_FORMAT_STR = "yyyy-MM-dd HH:mm:ss";

	// Deprecated: Do not use these in Jobs or other code, use Standard MDM formats
	// defined in next paragraph
	public static SimpleDateFormat DATETIME_FORMAT_ISO8601_UTC = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
	public static SimpleDateFormat DATETIME_FORMAT_ISO8601_UTC_FILE = new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss'Z'");
	public static SimpleDateFormat DATE_FORMAT_ISO8601_UTC = new SimpleDateFormat("yyyy-MM-dd'Z'");
	public static SimpleDateFormat DATE_FORMAT_ISO8601 = new SimpleDateFormat("yyyy-MM-dd");

	public static final SimpleDateFormat MDM_FILE_TIMESTAMP_FORMAT = new SimpleDateFormat(
			MDM_FILE_TIMESTAMP_FORMAT_STR);
	// Used as file prefix for all files sent to MDM consumers
	public static final SimpleDateFormat MDM_CONSUMER_FILE_TIMESTAMP_FORMAT = new SimpleDateFormat(
			MDM_CONSUMER_FILE_TIMESTAMP_FORMAT_STR);
	// Used in MDM messages sent to consumer queue.
	public static final SimpleDateFormat MDM_CONSUMER_TIMESTAMP_FORMAT = new SimpleDateFormat(
			MDM_CONSUMER_TIMESTAMP_FORMAT_STR);
	// Used in Talend UI XML trigger messages and as Default format for Date to
	// String Conversions
	public static final SimpleDateFormat TALEND_INTERNAL_DATETIME_FORMAT = new SimpleDateFormat(
			TALEND_INTERNAL_DATETIME_FORMAT_STR);
	public static final SimpleDateFormat TALEND_INTERNAL_DATE_FORMAT = new SimpleDateFormat(
			TALEND_INTERNAL_DATE_FORMAT_STR);

	// Standard MDM Date format to use in all Jobs
	public static SimpleDateFormat MDM_DATE_FORMAT = new SimpleDateFormat(MDM_DATE_FORMAT_STR);
	// Standard MDM Timestamp format to use in all Jobs for LastUpdateTimestamp
	public static SimpleDateFormat MDM_DATETIME_FORMAT = new SimpleDateFormat(MDM_DATETIME_FORMAT_STR);

	public final static class PeriodCompareResult {
		// Set default values for result when comparing to null dates:
		public boolean EXTENDS_BEFORE = true; /* A extends B directly before B */
		public boolean EXTENDS_AFTER = true; /* extends B directly after B */
		public boolean EXTENDS = true; /* extends before or after directly adjactent */
		public boolean OVERLAP = false; /* fully or partially overlap with each other */
		public boolean OVERLAPS_WITH_START = false; /* A overlaps with start of B */
		public boolean OVERLAPS_WITH_END = false; /* A overlaps with end of B */
		public boolean CONTAINS = false; /* A fully contains B */
		public boolean CONTAINS_AT_START = false; /* A fully contains B starting at start of A */
		public boolean CONTAINS_AT_END = false; /* A fully contains B at end at A */
		public boolean IS_CONTAINED = false; /* B fully contains A */
		public boolean HAS_GAP_BEFORE = false;
		public boolean HAS_GAP_AFTER = false;
		public boolean HAVE_GAP = false; /* There is a gap between A and B */
		public boolean EQUAL = false;
		public boolean EARLIER = false; /* period completely earlier */
		public boolean LATER = false; /* period completely later */
		public long NO_DAYS = 0; /* amount of days overlapping or in-between a gap */

		public PeriodCompareResult() {
		}

		{
		}

		public String toString() {
			List<String> result = new ArrayList<String>();

			if (this.EQUAL) {
				result.add("equals");
			}
			if (this.EXTENDS_BEFORE) {
				result.add("extends before");
			}
			if (this.EXTENDS_AFTER) {
				result.add("extends after");
			}
			if (this.EXTENDS) {
				result.add("extends");
			}
			if (this.OVERLAPS_WITH_START) {
				result.add("overlaps with start of");
			}
			if (this.OVERLAPS_WITH_END) {
				result.add("overlaps with end of");
			}
			if (this.OVERLAP) {
				result.add("overlaps");
			}
			if (this.CONTAINS) {
				result.add("contains");
			}
			if (this.CONTAINS_AT_START) {
				result.add("contains at start of");
			}
			if (this.CONTAINS_AT_END) {
				result.add("contains at end of");
			}
			if (this.IS_CONTAINED) {
				result.add("is contained in");
			}
			if (this.HAVE_GAP) {
				result.add("has a gap with");
			}
			if (this.HAS_GAP_BEFORE) {
				result.add("has gap before");
			}
			if (this.HAS_GAP_AFTER) {
				result.add("has gap after");
			}
			if (this.EARLIER) {
				result.add("is earlier than");
			}
			if (this.LATER) {
				result.add("is later than");
			}

			return result.toString();
		}
	}

	/**
	 * addDays: Add a number of days to a given date
	 * 
	 * 
	 * {talendTypes} Date
	 * 
	 * {Category} LCAG-DateTime
	 * 
	 * {param} Date("date") input: date to increase {param} int("days") input:
	 * number of days to add (can be negative
	 * 
	 * {example} asDurationString("+12:04") = "PT12H04M".
	 */
	public static Date addDays(Date date, int days) {
		Calendar cal = Calendar.getInstance();
		cal.setTime(date);
		cal.add(Calendar.DATE, days); // minus number would decrement the days
		return cal.getTime();
	}

	/**
	 * asDurationString: returns a XML duration format string.
	 * 
	 * 
	 * {talendTypes} String
	 * 
	 * {Category} LCAG-DateTime
	 * 
	 * {param} string("aTime") input: The time duration string in MDS format
	 * (+/-HH:MM) to be converted into a XML duration format string.
	 * 
	 * {example} asDurationString("+12:04") = "PT12H04M".
	 */
	public static String asDurationString(String aTime) {
		boolean inFuture = true;
		String result = null;
		int hours = 0, minutes = 0;

		if (aTime.startsWith("+")) {
			aTime = aTime.substring(1);
		} else if (aTime.startsWith("-")) {
			inFuture = false;
			aTime = aTime.substring(1);
		}

		String values[] = aTime.split(":");

		if (values.length == 2) {
			hours = Integer.parseInt(values[0]);
			minutes = Integer.parseInt(values[1]);

			result = String.format("%sPT%02dH%02dM", inFuture ? "" : "-", hours, minutes);
		}

		return result;
	}

	/**
	 * asDuration: Return a Java Duration object from a MDS formatted String
	 * 
	 * 
	 * {talendTypes} String
	 * 
	 * {Category} LCAG-DateTime
	 * 
	 * {param} string("aTime") input: The time duration string in MDS format
	 * (+/-HH:MM) to be converted into a XML duration format string.
	 * 
	 * {example} asDuration("+12:04") = "PT12H04M".
	 */
	public static Duration asDuration(String aTime) {

		String durationString = asDurationString(aTime);
		Duration result = Duration.parse(durationString);

		return result;
	}

	/**
	 * 
	 * utcOffsetToDouble: convert UTC offset String to floating point number
	 * 
	 * {talendTypes} Double
	 * 
	 * {Category} LCAG-DateTime
	 * 
	 * {param} String("offset") input: UTC offset string +04:30
	 * 
	 */
	public static Double utcOffset2Double(String offset) {
		Double result = null;

		if (offset != null) {
			String sign = offset.substring(0, 1);
			String hours = offset.substring(1, 3);
			String mins = offset.substring(4);

			result = new Double(
					(sign.equals("-") ? -1.0 : 1.0) * (Integer.parseInt(hours) + Integer.parseInt(mins) / 60.0));
		}

		return result;
	}

	/**
	 * toDate: Returns a Date object from a "YYYY-MM-DD" formated String 'obj' or
	 * obj itself if it is already a Date object
	 * 
	 * 
	 * {talendTypes} Date
	 * 
	 * {Category} LCAG-DateTime
	 * 
	 * {param} Object("obj") input: The obj to convert to Date
	 * 
	 * {example} toDate("2017-01-31"), toDate(new Date())
	 */
	public static Date toDate(Object obj) throws ParseException {
		Date result = null;

		if (obj != null) {
			SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
			String dateString = null;

			if (obj instanceof Date) {
				result = (Date) obj;
				dateString = df.format(result); // to strip the time part from date at next parse
			} else {
				dateString = obj.toString();
			}

			if (!StringUtil.isEmpty(dateString)) {
				result = df.parse(dateString);
				log.trace(String.format("toDate(%s) = %s\n", dateString, df.format(result)));
			} else {
				log.debug("LcagDateTime.toDate called with empty (%s) string argument", dateString);
			}
		}

		return result;
	}

	/**
	 * toDate: Converts a Date string to a Date object if any String representation
	 * in the formatList is applicable
	 * 
	 * @param aDate      Date string to convert
	 * @param formatList list of String formats to try for conversion
	 * @return Date object or null if no conversion is applicable
	 * 
	 *         {talendTypes} Object
	 * 
	 *         {Category} LCAG-DateTime
	 * 
	 *         {param} String("aDate") aDate: Date string to convert {param}
	 *         Object("formatList") formatList...: variable number of String formats
	 *         to try for conversion
	 * 
	 *         {examples}
	 */
	public static Date toDate(String aDate, String... formatList) {
		Date result = null;

		if (aDate == null)
			return null;

		// try all formats until first one succeeds:
		for (String fmt : formatList) {
			SimpleDateFormat df = new SimpleDateFormat(fmt, Locale.ENGLISH);

			try {
				result = df.parse(aDate);
				break;
			} catch (Exception e) {
			}
		}

		return result;
	}

	/**
	 * countDays: Returns the number of days with the Date period [d1 - d2]. If
	 * given by "YYYY-MM-DD" string it is converted first.
	 * 
	 * 
	 * {talendTypes} Long
	 * 
	 * {Category} LCAG-DateTime
	 * 
	 * {param} Object("d1") input: The first date to use {param} Object("d2") input:
	 * The second date to use
	 * 
	 * {example} countDays("2017-01-01", "2017-01-03") = 3
	 */
	public static long countDays(Object d1, Object d2) throws ParseException {
		long result = 0;

		if (d1 != null && d2 != null) {
			Date date1 = toDate(d1);
			Date date2 = toDate(d2);

			result = Math.round((date2.getTime() - date1.getTime()) / (60 * 60 * 24 * 1000.0));
			result += 1;
		}

		return result;
	}

	/**
	 * getStartOfToday: Converts a Date string to a Date object if any String
	 * representation in the formatList is applicable
	 * 
	 * @return Date object for today, 00:00:00
	 * 
	 *         {talendTypes} Date
	 * 
	 *         {Category} LCAG-DateTime
	 * 
	 *         {examples}
	 */
	public static Date getStartOfToday() {
		Date result = new Date();

		String s = DATE_FORMAT_ISO8601_UTC.format(new Date());
		String resultDate = s.replace("Z", "T00:00:00Z");

		try {
			result = DATETIME_FORMAT_ISO8601_UTC.parse(resultDate);
		} catch (Exception e) {
			result = null;
		}

		return result;
	}

	/**
	 * isEarlierToday: return true if the date passed is at a earlier time today
	 * 
	 * @return boolean true if the date passed is at a earlier time today
	 * 
	 *         {talendTypes} boolean
	 * 
	 *         {Category} LCAG-DateTime
	 * 
	 *         {examples}
	 */
	public static boolean isEarlierToday(Date aDate) {
		Date now = new Date();
		Date todayStart = getStartOfToday();

		boolean result = (aDate.before(now) && aDate.after(todayStart));

		return result;
	}

	/**
	 * isEarlierToday: return true if the date passed in seconds since 1970 is at a
	 * earlier time today
	 * 
	 * @return boolean true if the date passed is at a earlier time today
	 * 
	 *         {talendTypes} boolean
	 * 
	 *         {Category} LCAG-DateTime
	 * 
	 *         {examples}
	 */
	public static boolean isEarlierToday(long aDateInSecs) {
		Date aDate = new Date();

		aDate.setTime(aDateInSecs);

		boolean result = isEarlierToday(aDate);

		return result;
	}

	/**
	 * compareDatePeriods: Compares two date ranges and returns a
	 * PeriodCompareResult object that lists the results: .EQUAL, if Period A is the
	 * same as B .EXTENDS, A extends B if B extends the range of A without
	 * overlapping .CONTAINS, A contains B completely (e.g. B1, B2) .OVERLAP, A
	 * overlaps B if range A overlaps B somehow, i.e. A intersects with B .EARLIER,
	 * A is completely earlier than B, i.e. both the “from date” and the “to date of
	 * A are before or equal to the start of B .LATER, A is completely later than B
	 * 
	 * {talendTypes} PeriodCompareResult
	 * 
	 * {Category} LCAG-DateTime
	 * 
	 * {param} Date("fromTimeA") input: Start date of period A {param}
	 * Date("toTimeA") input: End date of period A {param} Date("fromTimeB") input:
	 * Start date of period B {param} Date("toTimeA") input: Start date of period B
	 * 
	 * {example} compareDatePeriods(fromDateA, toDateA, fromDateB, toDateB).EXTENDS
	 * returns true if A extends B, i.e. if A is adjacent to B
	 * 
	 * @throws ParseException
	 */

	public static LcagDateTime.PeriodCompareResult compareDatePeriods(Object fromA, Object toA, Object fromB,
			Object toB) throws ParseException {
		Date fromDateA = toDate(fromA);
		Date toDateA = toDate(toA);
		Date fromDateB;
		Date toDateB;

		PeriodCompareResult result = new LcagDateTime.PeriodCompareResult();

		if (fromA == null || toA == null || fromB == null || toB == null)
			return result;

		fromDateB = toDate(fromB);
		toDateB = toDate(toB);

		Date dayBeforeA = addDays(fromDateA, -1);
		Date dayAfterA = addDays(toDateA, 1);

		result.EQUAL = (fromDateA.compareTo(fromDateB) == 0 && toDateA.compareTo(toDateB) == 0);

		result.EXTENDS_AFTER = (dayBeforeA.compareTo(toDateB) == 0 && fromDateB.compareTo(fromDateA) < 0); /* B4 */
		result.EXTENDS_BEFORE = (dayAfterA.compareTo(fromDateB) == 0 && toDateB.compareTo(toDateA) > 0); /* B5 */
		result.EXTENDS = result.EXTENDS_BEFORE || result.EXTENDS_AFTER;

		result.CONTAINS = (fromDateB.compareTo(fromDateA) >= 0 && toDateB.compareTo(toDateA) <= 0); /* fully B1,B2 */
		result.CONTAINS_AT_START = (fromDateB.compareTo(fromDateA) == 0 && toDateB.compareTo(toDateA) <= 0);
		result.CONTAINS_AT_END = (fromDateB.compareTo(fromDateA) >= 0 && toDateB.compareTo(toDateA) == 0);
		result.IS_CONTAINED = (fromDateA.compareTo(fromDateB) >= 0) && (toDateA.compareTo(toDateB) <= 0); /* B2, B3 */

		result.EARLIER = /* B5, B9 */
				(dayAfterA.compareTo(fromDateB) <= 0);
		result.LATER = /* B4, B8 */
				(dayBeforeA.compareTo(toDateB) >= 0);

		result.OVERLAPS_WITH_END = (fromDateA.compareTo(toDateB) <= 0)
				/* B1,B2,B3,B5,B6,B7,B9 */ && (toDateA.compareTo(toDateB) > 0 /* B1,B4,B6,B8 */);
		/* => Result: B1, B6 */

		result.OVERLAPS_WITH_START = (fromDateA.compareTo(fromDateB) < 0)
				/* B1,B5,B7,B9 */ && (toDateA.compareTo(fromDateB) >= 0 /* B1,B2,B3,B4,B6,B7,B8 */);
		/* => Result: B1, B7 */

		// Lastly, also consider partial overlapping between A and B:
		result.OVERLAP = result.CONTAINS || result.IS_CONTAINED || result.OVERLAPS_WITH_START
				|| result.OVERLAPS_WITH_END; /* B1,B2,B3, B6, B7 */

		result.HAS_GAP_BEFORE = toDateB.before(dayBeforeA);
		result.HAS_GAP_AFTER = fromDateB.after(dayAfterA);

		result.HAVE_GAP = result.HAS_GAP_BEFORE || result.HAS_GAP_AFTER;

		if (result.CONTAINS) {
			result.NO_DAYS = countDays(fromDateB, toDateB);
		} else if (result.IS_CONTAINED) {
			result.NO_DAYS = countDays(fromDateA, toDateA);
		} else if (result.OVERLAPS_WITH_START) {
			result.NO_DAYS = countDays(fromDateB, toDateA);
		} else if (result.OVERLAPS_WITH_END) {
			result.NO_DAYS = countDays(fromDateA, toDateB);
		} else if (result.HAS_GAP_BEFORE) {
			result.NO_DAYS = countDays(toDateB, fromDateA) - 2;
		} else if (result.HAS_GAP_AFTER) {
			result.NO_DAYS = countDays(toDateA, fromDateB) - 2;
		}

		SimpleDateFormat df = new SimpleDateFormat("dd. MM YYYY");
		String logText = String.format(
				"[%1$td.%1$tm.%1$tY - %2$td.%2$tm.%2$tY] %3$s [%4$td.%4$tm.%4$tY - %5$td.%5$tm.%5$tY]", fromDateA,
				toDateA, result.toString(), fromDateB, toDateB);
		log.info(logText);
		return result;
	}

	/**
	 * dateInbetween: returns a comparison of three datetime objects, i.e. whether
	 * one date located between the other two dates
	 * 
	 * 
	 * {talendTypes} Boolean
	 * 
	 * {Category} LCAG-DateTime
	 * 
	 * {param} Object("from") input: The first date to use {param}
	 * Object("inbetween") input: The date that is used for comparison {param}
	 * Object("to") input: The last date to use
	 * 
	 */
	public static boolean dateInbetween(Date from, Date inbetween, Date to) {
		boolean result = false;

		if (from != null && inbetween != null && to != null) {
			result = (from.equals(inbetween) || from.before(inbetween))
					&& (to.equals(inbetween) || to.after(inbetween));
		}

		return result;
	}

	/**
	 * secureParseDate: returns a Date object parsed from String daterepresentation
	 * and, hopefully, does return
	 * 
	 * 
	 * {talendTypes} Date
	 * 
	 * {Category} LCAG-DateTime
	 * 
	 * {param} Object("pattern") input: the pattern describing how to encode a
	 * datetime value YYYY-MM-dd HH:mm:ss {param} Object("daterepresentation")
	 * input: the date that is parsed
	 * 
	 */
	public static Date secureParseDate(String pattern, String daterepresentation) {
		Date result = null;

		if (daterepresentation != null)
			if (!daterepresentation.equals("")) {
				try {
					result = TalendDate.parseDate(pattern, daterepresentation);
				} catch (Exception e) {
				}
			}

		return result;
	}

	/**
	 * Converts a double value to a UTC time offset String (Used in MAD timezone
	 * conversion)
	 * 
	 * @param d output: Talend or Dom4J or String xml document to be changed
	 * @return Time offset as String [+-]HH:MM
	 *
	 *         {talendTypes} String {Category} LCAG-DateTime
	 * 
	 *         {param} Double(d) d: input: xpath lookup string
	 * 
	 *         {example} double2UtcOffset(6) = "+06:00", double2UtcOffset(6.5) =
	 *         "+06:30"
	 */

	public static String double2UtcOffset(Double d) {
		String result = null;

		if (d == null)
			return result;

		if (d <= -24.0 || d >= 24.0)
			throw new IllegalArgumentException(String.format("Argument must be > -24 and < 24, %f given", d));

		if (d != null) {
			String sign = "+";

			if (d < 0) {
				sign = "-";
				d = -d;
			}

			int hours = (int) Math.floor(d);
			int minutes = (int) Math.round((d - hours) * 60.0);

			if (minutes % 15 > 0) {
				log.warn("double2UtcOffset: %.2f is an UNUSUAL offset (not a a multiple of 15 mins) !", d);
			}
			result = sign + String.format("%02d:%02d", hours, minutes);
		}

		return result;
	}

	/**
	 * Checks if the first 2 letters of a string represent a IATA Country Code of a
	 * Country that has more than one time zone.
	 * 
	 * Note: List of Countries is hard-coded
	 * 
	 * @param aCode input: Code to check
	 * @return true if Code is valid as Multi-Timezone Country Code
	 *
	 *         {talendTypes} String {Category} LCAG-DateTime
	 * 
	 *         {param} String(aCode) aCode: input: Code to check
	 * 
	 *         {example} isMultiTimezoneCountry("DE:01") = false,
	 *         isMultiTimezoneCountry("DE0") = false isMultiTimezoneCountry("AU2A")
	 *         = true isMultiTimezoneCountry("US:02") = true
	 */
	public static boolean isMultiTimezoneCountry(String aCode) {
		// List of Countries that have multiple Timezones according to IATA Airline
		// Coding Directory (ACD):
		final List<String> multiTzCountries = java.util.Arrays.asList("AU", "BR", "CA", "CD", "CL", "EC", "ES", "FM",
				"GL", "ID", "KI", "KZ", "MX", "NZ", "PF", "PG", "PT", "RU", "UM", "US", "ZZ");

		if (aCode == null || aCode.length() < 2) {
			throw new IllegalArgumentException(
					String.format("Illegal Code length of '%s': Must start with 2 letter Country Code !", aCode));
		}

		String countryCode = aCode.substring(0, 2);

		boolean result = multiTzCountries.contains(countryCode);

		return result;
	}

	/**
	 * Converts a Timezone Code in MDS format (should by IATA) into MAD format.
	 * Note: 1. Does not transform special values like for countries MX, BR,... 2.
	 * Includes a hard coded list of Country Codes having multiple Timezone Codes
	 * (Created for jobs updating Timezone)
	 * 
	 * @param aCode input: MDS Timezone Code
	 * @return MAD Timezone Code format
	 *
	 *         {talendTypes} String {Category} LCAG-DateTime
	 * 
	 *         {param} String(aCode) aCode: input: MDS Timezone Code
	 * 
	 *         {example} convertMDS_TimezoneCodeToMAD("DE:01") = "DE0",
	 *         convertMDS_TimezoneCodeToMAD("PF:01") = "PF1" (PF is multi-tz
	 *         country) convertMDS_TimezoneCodeToMAD("MX:05") = "MX5" (but MAD uses
	 *         MX1B)
	 */
	public static String convertMDS_TimezoneCodeToMAD(String aCode) {
		if (aCode == null || aCode.length() < 5) {
			throw new IllegalArgumentException(
					String.format("Illegal MDS Code '%s': Must have at least 5 characters!", aCode));
		}

		String countryCode = aCode.substring(0, 2);
		String tzNo = aCode.substring(3);
		String result = countryCode;

		if (tzNo.startsWith("0")) {
			if ("01".equals(tzNo) && !isMultiTimezoneCountry(aCode)) {
				// In MAD countries that have just one TZ always use "0" as last character
				result += "0";
			} else {
				result += tzNo.substring(1); // drop leading "0"
			}
		} else {
			result += tzNo;
		}

		return result;
	}

	/**
	 * Converts a Timezone Code in MAD format (should by IATA) into MDS format.
	 * Note: Does not transform special values like for countries MX, BR,...
	 * 
	 * @param aCode input: MAD Timezone Code
	 * @return MDS Timezone Code format
	 *
	 *         {talendTypes} String {Category} LCAG-DateTime
	 * 
	 *         {param} String(aCode) aCode: input: MAD Timezone Code
	 * 
	 *         {example} convertMAD_TimezoneCodeToMDS("DE0") = "DE:01",
	 *         convertMAD_TimezoneCodeToMDS("PF1") = "PF:01"
	 *         convertMAD_TimezoneCodeToMDS("MX1A") = "MX:1A" (but MDS uses MX:05)
	 */
	public static String convertMAD_TimezoneCodeToMDS(String aCode) {

		if (aCode == null || aCode.length() < 3) {
			throw new IllegalArgumentException(
					String.format("Illegal MAD Code '%s': Must have at least 3 characters!", aCode));
		}

		String countryCode = aCode.substring(0, 2);
		String tzNo = aCode.substring(2);
		String result = countryCode + ":";

		if ("0".equals(tzNo)) {
			// Code of a Country that has only one Timezone
			result += "01";
		} else if (tzNo.length() == 1) {
			result += "0" + tzNo;
		} else {
			result += tzNo;
		}

		return result;
	}

	/**
	 * Convert a Local Date with Time to UTC using the local time offset to UTC
	 * 
	 * {talendTypes} OffsetDateTime
	 * 
	 * {Category} LCAG-DateTime
	 *
	 * {param} LocalDateTime localDateTime : input: Date with time to convert
	 * {param} String("+04:30") offsetTime : input: Time offset to use, e.g. -04:00,
	 * +04:00, 04:00
	 * 
	 * {example} convertLocalTimeToUTC(aDate, "-03:00")
	 */
	public static OffsetDateTime convertLocalTimeToUTC(LocalDateTime localDateTime, String offsetTime) {
		String[] hoursMins = offsetTime.split(":");

		if (hoursMins.length != 2) {
			throw new IllegalArgumentException(
					String.format("Unsupport Offset Time ('%s'), required format is [+-]HH:MM", offsetTime));
		}

		if (localDateTime == null) {
			localDateTime = LocalDateTime.now();
		}

		if (!offsetTime.startsWith("-") && !offsetTime.startsWith("+"))
			offsetTime = "+" + offsetTime;

		DateTimeFormatter df = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm");
		String dateString = df.format(localDateTime) + ":00:000" + offsetTime;

		DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss:SSSXXXXX");
		OffsetDateTime localOffsetDateTime = OffsetDateTime.parse(dateString, DATE_TIME_FORMATTER);
		OffsetDateTime utcOffsetDateTime = localOffsetDateTime.withOffsetSameInstant(ZoneOffset.UTC);

		return utcOffsetDateTime;
	}

	/**
	 * Convert a Date with Time and Time Zone Offset to UTC.
	 * 
	 * {talendTypes} OffsetDateTime
	 * 
	 * {Category} LCAG-DateTime
	 *
	 * {param} OffsetDateTime dateTime : input: Date with time to convert
	 */
	public static OffsetDateTime convertToUTC(OffsetDateTime dateTime) {

		if (dateTime == null) {
			dateTime = OffsetDateTime.now();
		}

		OffsetDateTime utcOffsetDateTime = dateTime.withOffsetSameInstant(ZoneOffset.UTC);

		return utcOffsetDateTime;
	}

	/**
	 * Convert a UTC Time to another timzone given by its offset to UTC
	 * 
	 * {talendTypes} OffsetDateTime
	 * 
	 * {Category} LCAG-DateTime
	 *
	 * {param} Date localDateTime : input: Date with time to convert {param}
	 * String("-HH:mm") offsetTime : input: Time offset to use, e.g. -04:00, +04:00,
	 * 04:00
	 * 
	 * {example} convertUTCTimeToLocal(aDate, "+03:30")
	 */
	public static OffsetDateTime convertUTCTimeToLocal(Date localDateTime, String offsetTime) {
		String[] hoursMins = offsetTime.split(":");

		if (hoursMins.length != 2) {
			throw new IllegalArgumentException(
					String.format("Unsupport Offset Time ('%s'), required format is [+-]HH:MM", offsetTime));
		}

		if (localDateTime == null) {
			localDateTime = new Date();
		}

		int hours = Integer.parseInt(hoursMins[0]);
		int minutes = Integer.parseInt(hoursMins[1]);

		if (hours < 0)
			minutes = -minutes;

		if (!offsetTime.startsWith("-") && !offsetTime.startsWith("+"))
			offsetTime = "+" + offsetTime;

		SimpleDateFormat df = new SimpleDateFormat("dd/MM/yyyy'T'HH:mm");
		String dateString = df.format(localDateTime) + ":00:000+00:00";

		DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/uuuu'T'HH:mm:ss:SSSXXXXX");
		OffsetDateTime utcOffsetDateTime = OffsetDateTime.parse(dateString, DATE_TIME_FORMATTER);
		OffsetDateTime localOffsetDateTime = utcOffsetDateTime
				.withOffsetSameInstant(ZoneOffset.ofHoursMinutes(hours, minutes));

		return localOffsetDateTime;
	}

	/**
	 * Adjusts a given date in MDS notation to a regular date. Interprets MDS
	 * special time 23:59 as 00:00 same day and 00:01 as 00:00 same day. MDS uses
	 * 00:01 for DST start time at the same day and 23:59 for DST end time at the
	 * same day. Example: the DST in Iran (IR:01) ends on Sep, 22th 2021 00:00 UTC,
	 * but MDS uses Sep, 22th, 23:59!
	 * 
	 * {talendTypes} OffsetDateTime
	 * 
	 * {Category} LCAG-DateTime
	 * 
	 * {param} OffsetDateTime aDate : input: Date to use adjust {returns} Date
	 * assembled from input parameters {example} adjustMDS_Date(aDate whith value
	 * 2021-02-28 23:59) returns a Date for 2021-03-01 00:00:00.000)
	 */
	public static OffsetDateTime adjustMDS_Date(OffsetDateTime aDate) {
		int hours = aDate.getHour();
		int minutes = aDate.getMinute();
		OffsetDateTime result = aDate.withSecond(0).withNano(0);

		if (hours == 0 && minutes == 1) {
			result = result.withMinute(0);
		} else if (hours == 23 && minutes == 59) {
			result = result.plusMinutes(1);
		}

		return result;
	}

	/**
	 * Assembles a DateTime object from a local Date and a local time string HH:mm
	 * that uses MDS special times 23:59 (00:00 next day) and 00:01 (00:00 same
	 * day).
	 * 
	 * {talendTypes} Date
	 * 
	 * {Category} LCAG-DateTime
	 * 
	 * {param} LocalDate localDate : input: Date part to use {param} LocalTime
	 * localTime : input: Local Time to append to the date part {param} String
	 * offsetTime : input: Used to define the time zone of the LocalDate and
	 * LocalTime {returns} Date assembled from input parameters {example}
	 * assembleMDS_DateTime(aDate, "23:59", "+00:00") returns the same day at 00:00
	 * as aDate
	 * 
	 * @throws ParseException
	 */
	public static OffsetDateTime assembleMDS_DateTime(LocalDate localDate, LocalTime localTime, String offsetTime)
			throws ParseException {

		if (!offsetTime.startsWith("-") && !offsetTime.startsWith("+"))
			offsetTime = "+" + offsetTime;

		OffsetDateTime dateTime = OffsetDateTime.of(localDate, localTime, ZoneOffset.of(offsetTime));
		OffsetDateTime result = adjustMDS_Date(dateTime);

		return result;
	}

	/**
	 * Created for DST enrichment: Calculates the UTC time from a given local MDS
	 * DST start/end time using the offsetTime given
	 * 
	 * {talendTypes} String
	 * 
	 * {Category} LCAG-DateTime
	 *
	 * {param} Object resultDateFormat : input: Date format to return, either a
	 * format string or a SimpleDateFormat. {param} LocalDateTime localDateTime:
	 * input: Date with time to convert {param} String("+HH:mm") offsetTime : input:
	 * Time offset to use, e.g. -04:00, +04:00, 04:00
	 * 
	 * @throws ParseException
	 */
	public static String convertLocalTimeToUTC(Object resultDateFormat, LocalDateTime localDateTime, String offsetTime)
			throws ParseException {
		String result = null;

		OffsetDateTime resultDate = convertLocalTimeToUTC(localDateTime, offsetTime);

		result = formatDate(resultDateFormat, resultDate).toUpperCase();
		return result;
	}

	/**
	 * Created for CABS timezone export: Calculates the UTC time from a given local
	 * MDS DST start/end time using the offsetTime given
	 * 
	 * {talendTypes} String
	 * 
	 * {Category} LCAG-DateTime
	 *
	 * {param} Object resultDateFormat : input: Date format to return, either a
	 * format string or a SimpleDateFormat. {param} Date localDate : input: Date to
	 * convert {param} Object("HH:mm") localTime : input: Local Time to convert as
	 * String, e.g. 02:00 or a Date object to extract this info from {param}
	 * String("+HH:mm") offsetTime : input: Time offset to use, e.g. -04:00, +04:00,
	 * 04:00
	 * 
	 * {example} calculate_UTC_TimeFromLocal("ddMMMyy_HHmm", aDate, "23:59",
	 * "+04:30")
	 * 
	 * @throws ParseException
	 */
	public static String convertLocalDateTimeToUTC(Object resultDateFormat, Date localDate, Object localTime,
			String offsetTime) throws ParseException {
		String result = null;
		String localTimeString = null;
		String[] hoursMins = offsetTime.split(":");

		if (hoursMins.length != 2) {
			throw new IllegalArgumentException(
					String.format("Unsupport Local Time ('%s'), required format is HH:mm", localTimeString));
		}

		LocalDate ld = localDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
		LocalTime lt = null;

		if (localTime instanceof Date) {
			lt = ((Date) localTime).toInstant().atZone(ZoneId.systemDefault()).toLocalTime();
		} else if (localTime instanceof String) {
			localTimeString = (String) localTime + ":00";

			lt = LocalTime.parse(localTimeString);
		} else {
			throw new IllegalArgumentException("Local time argument type not supported");
		}

		OffsetDateTime localDateTime = assembleMDS_DateTime(ld, lt, offsetTime);
		OffsetDateTime odt = convertToUTC(localDateTime);

		result = formatDate(resultDateFormat, odt).toUpperCase();
		return result;
	}

	/**
	 * Formats a Date into a date/time string.
	 *
	 * @param aFormatter the pattern to format.
	 * @param aDate      the time value to be formatted into a time string.
	 * @return the formatted time string.
	 *
	 *         {talendTypes} String
	 *
	 *         {Category} LCAG-DateTime
	 *
	 *         {param} Object("yyyy-MM-dd HH:mm:ss") aFormatter : the pattern to
	 *         format. May be a formatting String, a java DateFormat or a
	 *         DateTimeFormatter
	 *
	 *         {param} Object(aDate) aDate : the date und time value to be formatted
	 *         into a date time string. May be of Type Date or OffsetDateTime
	 *
	 *         {example} formatDate("yyyy-MM-dd", new Date())
	 */

	public synchronized static String formatDate(Object aFormatter, Object aDate) {
		String result = null;
		DateFormat df = null;
		DateTimeFormatter formatter = null;

		if (aFormatter instanceof String) {
			String fmt = (String) aFormatter;

			// Use the best suited formatter next! Otherwise you may get problems in time
			// zone handling!!
			if (aDate instanceof Date) {
				df = new SimpleDateFormat(fmt.replaceAll("u", "y"), Locale.ENGLISH);
			}
			if (aDate instanceof OffsetDateTime) {
				formatter = DateTimeFormatter.ofPattern(fmt.replaceAll("y", "u"), Locale.ENGLISH);
			}
		} else if (aFormatter instanceof DateFormat) {
			df = (DateFormat) aFormatter;
		} else if (aFormatter instanceof DateTimeFormatter) {
			formatter = (DateTimeFormatter) aFormatter;
		} else {
			throw new IllegalArgumentException("Formatter argument type not supported");
		}

		if (aDate instanceof Date) {
			Date d = (Date) aDate;

			if (df != null) {
				result = df.format(d);
			} else if (formatter != null) {
				result = formatter.format(d.toInstant());
			}
		} else if (aDate instanceof Long) {
			Date d = new Date((Long) aDate);

			if (df != null) {
				result = df.format(d);
			} else if (df != null) {
				result = formatter.format(d.toInstant());
			}
		} else if (aDate instanceof OffsetDateTime) {
			OffsetDateTime d = (OffsetDateTime) aDate;

			if (formatter != null) {
				result = formatter.format(d);
			} else if (df != null) {
				result = df.format(Date.from(d.toInstant()));
			}
		} else {
			throw new IllegalArgumentException("Date argument type not supported");
		}

		return result;
	}
}
