package routines;

import java.lang.Math;
import java.text.ParseException;
import java.text.DateFormat;
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.Calendar;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
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.replaceFirst("+", "");
    	} else if (aTime.startsWith("-")) {
    		inFuture = false;
    		aTime = aTime.replaceFirst("-", "");
    	}
    	
    	String values[] = aTime.split(":");
    	
    	if (values.length == 2) {
    		hours = Integer.parseInt(values[0]);
    		minutes = Integer.parseInt(values[1]);
    		
    		result = String.format("%sPT%02H%02M", 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);
    		
    		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", "JO", "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;
    }
}
