package lu.tudor.santec.gecamed.core.utils;

import java.awt.Color;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Locale;
import java.util.regex.Pattern;

import lu.tudor.santec.gecamed.agenda.ejb.entity.beans.AgendaCalendar;
import lu.tudor.santec.gecamed.usermanagement.ejb.session.beans.LoginBean;
import lu.tudor.santec.gecamed.usermanagement.ejb.session.interfaces.LoginInterface;
import lu.tudor.santec.i18n.Translatrix;

import org.apache.log4j.Level;

/**
 * @author jens.ferring(at)tudor.lu
 * 
 * @version
 * <br>$Log: GECAMedUtils.java,v $
 * <br>Revision 1.7  2014-02-03 10:20:44  ferring
 * <br>Method adde to set a calendar's time to 00:00:00.000
 * <br>
 * <br>Revision 1.6  2014-01-23 15:00:15  ferring
 * <br>OS and Java checking moved from MainFrame (where only client components can access) to GECAMedUtils (where also server can access it)
 * <br>
 * <br>Revision 1.5  2014-01-09 16:44:33  ferring
 * <br>Error handling for SAML assertions changed
 * <br>
 * <br>Revision 1.4  2013-12-18 09:28:05  ferring
 * <br>Method stringValue and comments added
 * <br>
 * <br>Revision 1.3  2013-12-13 10:14:41  ferring
 * <br>isEmpty String check added
 * <br>
 * <br>Revision 1.2  2013-12-04 13:08:11  ferring
 * <br>ping url method added
 * <br>
 * <br>Revision 1.1  2013-11-28 10:38:19  ferring
 * <br>GECAMedUtils split into utils and GUI utils classes
 * <br>
 */

public class GECAMedUtils
{
	/* ======================================== */
	// CONSTANTS
	/* ======================================== */
	
	/* ---------------------------------------- */
	// DATE FORMAT STRINGS
	/* ---------------------------------------- */
	
	// chronicle descending formats
	public static final String DATE_FORMAT_INTERNATIONAL_DATE			= "yyyy-MM-dd";
	public static final String DATE_FORMAT_INTERNATIONAL_TIME			= "yyyy-MM-dd HH:mm:ss";
	public static final String DATE_FORMAT_INTERNATIONAL_TIME_MILLIS	= "yyyy-MM-dd HH:mm:ss.SSS";
	public static final String DATE_FORMAT_ONESTRING_DATE				= "yyyyMMdd";
	public static final String DATE_FORMAT_ONESTRING_TIME				= "yyyyMMddHHmmssSSS";
	public static final String DATE_FORMAT_FILE_TIME					= "yyyyMMdd_HHmmss_SSS";
	// locale formats
	public static final String DATE_FORMAT_GERMAN_DATE					= "dd.MM.yyyy";
	public static final String DATE_FORMAT_GERMAN_TIME					= "dd.MM.yyyy HH:mm";
	public static final String DATE_FORMAT_FRENCH_DATE					= "dd/MM/yyyy";
	public static final String DATE_FORMAT_FRENCH_TIME					= "dd/MM/yyyy HH:mm";
	public static final String DATE_FORMAT_ENGLISH_DATE					= "MM/dd/yyyy";
	public static final String DATE_FORMAT_ENGLISH_TIME					= "MM/dd/yyyy hh:mm a";
	
	private static final Pattern	TRUE_PATTERN	= Pattern.compile("\\s*(t(rue)?|y(es)?)\\s*", Pattern.CASE_INSENSITIVE);
	private static final Pattern	FALSE_PATTERN	= Pattern.compile("\\s*(f(alse)?|n(o)?)\\s*", Pattern.CASE_INSENSITIVE);
	
	
	public static final int OS_MAC			= 0;
	public static final int OS_LINUX		= 1;
	public static final int OS_WINDOWS		= 2;
	public static final int OS_UNDEFINED	= 3;
	
	private static Integer	currentOs;
	private static Boolean	isOs64Bit;
	private static Boolean	isJvm64Bit;
	
	
	private static final String PING_COUNT_PARAM	= getOS() == OS_WINDOWS ? "-n" : "-c";
	private static final String PING_TIMEOUT_PARAM	= getOS() == OS_WINDOWS ? "-w" : "-t";
	
	private static Boolean isDemo;
	
	
	
	/* ======================================== */
	// MEMBERS
	/* ======================================== */
	
	/** the logger Object for this class */
	private static org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(GECAMedUtils.class.getName());
	
	private static HashMap<String, DateFormat> dateFormatterCache = new HashMap<String, DateFormat>();
	
	private static HashMap<Class<?>, Comparator<?>> comparatorCache = new HashMap<Class<?>, Comparator<?>>();
	
	private static String localDateFormat;
	
	private static String localTimeFormat;
	private static String installationName;
	private static Color installationColor;
	
	
	
	/* ======================================== */
	// CLASS BODY
	/* ======================================== */
	
	public static <T> void insertIntoArray (T[] array, T value, int position)
	{
		if (position >= 0 && position < array.length)
			array[position] = value;
	}
	
	
	public static String reformatDateString (String dateString, String toDateFormat, String toStringFormat)
	{
		DateFormat	toDateFormatter		= getDateFormatter(toDateFormat);
		DateFormat	toStringFormatter	= getDateFormatter(toStringFormat);
		Date		date;
		
		
		try
		{
			date	= toDateFormatter.parse(dateString);
		}
		catch (Exception e)
		{
			logger.warn("Couldn't parse date string \""+dateString+"\" to a date, using the format \""+toDateFormat+"\"");
			return null;
		}
			
		return toStringFormatter.format(date);
	}
	
	
	/**
	 * @param dateFormatString The date format string that how the date string should look like
	 * @return A date formatted as String. 
	 */
	public static DateFormat getDateFormatter (String dateFormatString)
	{
		DateFormat formatter = dateFormatterCache.get(dateFormatString);
		
		if (formatter == null)
		{
			formatter = new SimpleDateFormat(dateFormatString);
			dateFormatterCache.put(dateFormatString, formatter);
		}
		
		return formatter;
	}
	
	
	/**
	 * @return Returns a DateFormat without time depending on the default locale
	 */
	public static DateFormat getLocaleDateFormatter ()
	{
		if (localDateFormat == null)
		{
			String language = Translatrix.getLocale().getLanguage();
			
			if (Locale.GERMAN.getLanguage().equals(language))
				localDateFormat = DATE_FORMAT_GERMAN_DATE;
			else if (Locale.FRENCH.getLanguage().equals(language))
				localDateFormat = DATE_FORMAT_FRENCH_DATE;
			else if (Locale.ENGLISH.getLanguage().equals(language))
				localDateFormat = DATE_FORMAT_ENGLISH_DATE;
			else
				localDateFormat = DATE_FORMAT_INTERNATIONAL_DATE;
		}
		
		return getDateFormatter(localDateFormat);
	}
	
	
	/**
	 * @return Returns a DateFormat with time depending on the default locale
	 */
	public static DateFormat getLocaleTimeFormatter ()
	{
		if (localTimeFormat == null)
		{
			String language = Translatrix.getLocale().getLanguage();
			
			if (Locale.GERMAN.getLanguage().equals(language))
				localTimeFormat = DATE_FORMAT_GERMAN_TIME;
			else if (Locale.FRENCH.getLanguage().equals(language))
				localTimeFormat = DATE_FORMAT_FRENCH_TIME;
			else if (Locale.ENGLISH.getLanguage().equals(language))
				localTimeFormat = DATE_FORMAT_ENGLISH_TIME;
			else
				localTimeFormat = DATE_FORMAT_INTERNATIONAL_TIME;
		}
		
		return getDateFormatter(localTimeFormat);
	}
	
	
	/**
	 * The stripTime method neutralizes time data contained in specified date.
	 * Neutralization is done by setting hour, minute, second and milliseconds
	 * to zero.
	 * @param originalDate specifies original date to strip time data from.
	 * @return A date object reflecting the date specified originalDate but
	 * being devoid of any time data.
	 */
	public static Date stripTime (Date originalDate)
	{
		return stripDate(originalDate, false, false, false, true, true, true, true);
	}
	
	
	/**
	 * The stripDate method sets the specified parts of a date to zero.
	 * 
	 * @param originalDate specifies original date to strip time data from.
	 * @param year If <code>true</code>, sets the year to zero
	 * @param month If <code>true</code>, sets the month to zero
	 * @param days If <code>true</code>, sets the day of the year to zero
	 * @param hours If <code>true</code>, sets the hour of the day to zero
	 * @param minutes If <code>true</code>, sets the minutes to zero
	 * @param seconds If <code>true</code>, sets the seconds to zero
	 * @param millis If <code>true</code>, sets the milli seconds to zero
	 * @return A date object reflecting the date specified originalDate but
	 * being devoid of any time data.
	 */
	public static Date stripDate (Date originalDate, boolean year, boolean month, boolean days, 
			boolean hours, boolean minutes, boolean seconds, boolean millis)
	{
		Calendar cal = new GregorianCalendar();
		cal.setTime(originalDate);
		
		if (year)
			cal.set(Calendar.YEAR,			0);
		if (month)
			cal.set(Calendar.MONTH,			0);
		if (days)
			cal.set(Calendar.DAY_OF_YEAR,	0);
		if (hours)
			cal.set(Calendar.HOUR_OF_DAY,	0);
		if (minutes)
			cal.set(Calendar.MINUTE,		0);
		if (seconds)
			cal.set(Calendar.SECOND,		0);
		if (millis)
			cal.set(Calendar.MILLISECOND,	0);
		
		return cal.getTime();
	}
	
	
	public static int indexOf (Collection<?> items, Object item)
	{
		int index = 0;
		
		if (item == null)
			return -1;
		
		for (Object t : items)
		{
			if (t.equals(item))
				return index;
			index++;
		}
		
		return -1;
	}
	
	
	/**
	 * @param ipOrHostname The IP or hostname to ping
	 * @return <code>true</code> if the server was pinged successfully, else <code>false</code>
	 */
	public static boolean ping (String ipOrHostname)
	{
		try
		{
			Process process = new ProcessBuilder("ping", 
					PING_COUNT_PARAM, "2", 
					PING_TIMEOUT_PARAM, "1000", 
					ipOrHostname).start();
//			BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
//		    BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream()));
		    
		    return process.waitFor() == 0;
		}
		catch (Exception e)
		{
			logger.error("Error while trying to ping address " + ipOrHostname, e);
			return false;
		}
	}
	
	
	/**
	 * @param s The value to check
	 * @return <code>true</code> if s is <code>null</code> or the length of the trimmed string is 0, else <code>false</code>
	 */
	public static boolean isEmpty (String s)
	{
		return s == null || s.trim().length() == 0;
	}
	
	
	/**
	 * @param o The Object to transform into a String
	 * @param emptyValue The value returned, if o is null or its string value is empty.
	 * @return The string value of o or emptyValue, if o is null or its string value is empty
	 */
	public static String stringValue (Object o, String emptyValue)
	{
		if (o == null)
			return emptyValue;
		
		String s = String.valueOf(o);
		if (GECAMedUtils.isEmpty(s))
			return emptyValue;
		else
			return s;
	}
	
	
	public static <I> boolean contains (I[] array, I item)
	{
		if (item == null)
			return false;
		
		for (I i : array)
			if (item.equals(i))
				return true;
		
		return false;
	}
	
	
	public static boolean contains (int[] array, int item)
	{
		for (int i : array)
			if (item == i)
				return true;
		
		return false;
	}
	
	
	/**
	 * @return An integer specifying the OS.<br>
	 * Compare the result with
	 * <ul>
		 * <li>GECAMedUtils.OS_LINUX</li>
		 * <li>GECAMedUtils.OS_WINDOWS</li>
		 * <li>GECAMedUtils.OS_MAC</li>
		 * <li>GECAMedUtils.OS_UNDEFINED</li>
	 * </ul>
	 */
	public static int getOS ()
	{
		if (currentOs == null)
		{
			int		os;
			String	osName = System.getProperty("os.name").toLowerCase();
			
			if (osName.matches(".*linux.*"))
				os = OS_LINUX;
			else if (osName.matches(".*mac os.*"))
				os = OS_MAC;
			else if (osName.matches(".*windows.*"))
				os = OS_WINDOWS;
			else 
				os = OS_UNDEFINED;
			
			currentOs	= Integer.valueOf(os);
		}
		
		return currentOs.intValue();
	}
	
	
	public static boolean isJava64Bit ()
	{
		if (isJvm64Bit == null)
		{
			try
			{
				int		javaArch			= Integer.parseInt(System.getProperty("sun.arch.data.model"));
				if (javaArch == 64)
				{
					isJvm64Bit	= Boolean.TRUE;
				}
				else if (javaArch == 32)
				{
					isJvm64Bit	= Boolean.FALSE;
				}
				else 
				{
					logger.error("Coudln't identify Java VM Bit architecture: "+javaArch+". Telling GECAMed that it is 32 Bit.");
					isJvm64Bit	= Boolean.FALSE;
				}
			}
			catch (NumberFormatException e)
			{
				logger.error("Coudln't identify Java VM Bit architecture: "+System.getProperty("sun.arch.data.model")+". Telling GECAMed that it is 32 Bit.");
				isJvm64Bit	= Boolean.FALSE;
			}
		}
		
		return isJvm64Bit.booleanValue();
	}
	
	
	public static boolean isOs64Bit ()
	{
		if (isOs64Bit == null)
		{
			isOs64Bit	= Boolean.FALSE;
			String osArch = System.getProperty("os.arch");
			
			if (osArch != null)
			{
				if (!"x86".equals(osArch) && osArch.contains("64"))
					isOs64Bit	= Boolean.TRUE;
			}
			else
			{
				logger.error("Couldn't identify OS Bit architecture: "+osArch+". Telling GECAMed that it is 32Bit.");
			}
		}
		
		return isOs64Bit.booleanValue();
	}


	public static Calendar setTime2Zero (Calendar cal)
	{
		cal.set(Calendar.HOUR_OF_DAY, 0);
		cal.set(Calendar.MINUTE, 0);
		cal.set(Calendar.SECOND, 0);
		cal.set(Calendar.MILLISECOND, 0);
		return cal;
	}
	
	
	public static Comparator<?> getComparator (Class<?> clazz)
	{
		if (clazz == null)
			return null;
		
		Comparator<?> c = comparatorCache.get(clazz);
		
		if (c == null)
		{
			if (clazz.equals(AgendaCalendar.class))
			{
				c = new Comparator<AgendaCalendar> ()
				{
					public int compare (AgendaCalendar o1, AgendaCalendar o2)
					{
						String	title1	= o1.getTitle();
						String	title2	= o2.getTitle();
						int		i		= 0;
						
						if (title1 != null && title2 != null)
							i = title1.compareTo(title2);
						
						if (i == 0)
							return o1.compareTo(o2);
						else
							return i;
					}
				};
				comparatorCache.put(clazz, c);
			}
		}
		
		return c;
	}
	
	
	/**
	 * Calls the compareTo of both objects method, but checks both objects on <code>null</code> first. 
	 * Null objects are rated higher (positive integer).<br>
	 * If both parameter are strings, the compareToIgnoreCase method is performed.
	 * 
	 * @param c1 The first object to compared.
	 * @param c2 The second object to be compared with the first.
	 * @return An integer specifying the order of the two objects.
	 */
	public static <T> int compareTo (Comparable<T> c1, T c2)
	{
		if (c1 == null)
		{
			if (c2 == null)
				return 0;
			else
				return 1;
		}
		else if (c2 == null)
		{
			return -1;
		}
		else if (c1 instanceof String && c2 instanceof String)
		{
			return ((String)c1).compareToIgnoreCase((String) c2);
		}
		else
		{
			return c1.compareTo(c2);
		}
	}


	public static String print (Iterable<?> c, String seperator, boolean printNullValues)
	{
		StringBuilder	b = new StringBuilder();
		
		
		for (Object o : c)
		{
			if (o == null && !printNullValues)
				continue;
			
			if (b.length() > 0)
				b.append(seperator);
			b.append(String.valueOf(o));
		}
		
		return b.toString();
	}


	public static String print (Object[] c, String seperator, boolean printNullValues)
	{
		StringBuilder	b = new StringBuilder();
		
		
		for (Object o : c)
		{
			if (o == null && !printNullValues)
				continue;
			
			if (b.length() > 0)
				b.append(seperator);
			b.append(String.valueOf(o));
		}
		
		return b.toString();
	}
	
	
	public static boolean isTrue (String value, boolean defaultValue)
	{
		if (value == null)
			return defaultValue;
		else if (TRUE_PATTERN.matcher(value).matches())
			return true;
		else if (FALSE_PATTERN.matcher(value).matches())
			return false;
		else
			return defaultValue;
	}
	
	
	public static boolean isDemo ()
	{
		if (isDemo == null)
		{
			// set default value
			isDemo	= Boolean.FALSE;
			
			LoginInterface login	= (LoginInterface) ManagerFactory.getRemote(LoginBean.class);
			try {
				String demo = login.getInfo("DEMO").getValue();
				if (demo.toLowerCase().equals("true"))
					isDemo = Boolean.TRUE;
			} catch (Exception e) {
				logger.log(Level.INFO, "unable to retrieve DEMO property, setting demo to FALSE");
			}
		}
		
		return isDemo.booleanValue();
	}
	
	
	public static String getInstallationName()
	{
		if (installationName == null)
		{
			// set default value
			isDemo	= Boolean.FALSE;
			
			LoginInterface login	= (LoginInterface) ManagerFactory.getRemote(LoginBean.class);
			try {
				installationName = login.getInfo("NAME").getValue();
				if (installationName == null) {
					installationName = "";
				}
			} catch (Exception e) {
				installationName = "";
				logger.log(Level.INFO, "unable to retrieve NAME DB property, setting name to \"\"");
			}
		}
		return installationName;
	}
	
	public static Color getInstallationColor()
	{
		if (installationColor == null)
		{
			// set default value
			isDemo	= Boolean.FALSE;
			
			LoginInterface login	= (LoginInterface) ManagerFactory.getRemote(LoginBean.class);
			try {
				String[] colorStrings = login.getInfo("COLOR").getValue().split(",");
				installationColor = new Color(Integer.parseInt(colorStrings[0]),Integer.parseInt(colorStrings[1]),Integer.parseInt(colorStrings[2]));
			} catch (Exception e) {
				installationColor = Color.WHITE;
				logger.log(Level.INFO, "unable to retrieve or parse COLOR DB property, setting name to \"\"");
			}
		}
		return installationColor;
	}
	
	
	public static <T> Collection<T> addAll (Collection<T> target, Collection<? extends T> ... sources)
	{
		for (Collection<? extends T> s : sources)
			target.addAll(s);
		
		return target;
	}
	
	
	public static <T> Collection<T> add (Collection<T> target, T ... sources)
	{
		for (T s : sources)
			target.add(s);
		
		return target;
	}
	
	
	/**
	 * Calculates the number of years between these two calendars.
	 * 
	 * @param firstDate The first Calendar to compare
	 * @param secondDate The second Calendar to compare
	 * @param returnAmount If <code>true</code>, the result is always positive, 
	 * 		else a negative value will be returned, if firstDate is after secondDate.
	 * @return The difference between the two dates
	 */
	public static int getDifferenceInYears (Calendar firstDate, Calendar secondDate, boolean returnAmount)
	{
		return getDifference(firstDate, secondDate, Calendar.YEAR, returnAmount);
	}
	
	
	/**
	 * Calculates the number of days between these two calendars.<br>
	 * The hours of a day are not taken into consideration. Therefore there is one day 
	 * between the dates 2015-01-01 23:59 and 2015-01-02 00:00.
	 * 
	 * @param firstDate The first Calendar to compare
	 * @param secondDate The second Calendar to compare
	 * @param returnAmount If <code>true</code>, the result is always positive, 
	 * 		else a negative value will be returned, if firstDate is after secondDate.
	 * @return The difference between the two dates
	 */
	public static int getDifferenceInDays (Calendar firstDate, Calendar secondDate, boolean returnAmount)
	{
		return getDifference(firstDate, secondDate, Calendar.DAY_OF_YEAR, returnAmount);
	}
	
	
	/**
	 * @param firstDate The first Calendar to compare
	 * @param secondDate The second Calendar to compare
	 * @param field The Calendar field to find the difference of
	 * @param returnAmount If <code>true</code>, the result is always positive, 
	 * 		else a negative value will be returned, if firstDate is after secondDate.
	 * @return The difference between the two dates
	 */
	private static int getDifference (Calendar firstDate, Calendar secondDate, int field, boolean returnAmount)
	{
		return getDifference(firstDate.getTime(), secondDate.getTime(), field, returnAmount);
	}
	
	
	/**
	 * @param firstDate The first date to compare
	 * @param secondDate The second date to compare
	 * @param field The Calendar field to find the difference of
	 * @param returnAmount If <code>true</code>, the result is always positive, 
	 * 		else a negative value will be returned, if firstDate is after secondDate.
	 * @return The difference between the two dates
	 */
	private static int getDifference (Date firstDate, Date secondDate, int field, boolean returnAmount)
	{
		int difference;
		Calendar	_1st	= new GregorianCalendar();
		Calendar	_2nd	= new GregorianCalendar();
		
		
		if (firstDate.after(secondDate))
		{
			// switch the calendars, so the first date is the earliest
			_1st.setTime(secondDate);
			_2nd.setTime(firstDate);
		}
		else
		{
			_1st.setTime(firstDate);
			_2nd.setTime(secondDate);
		}
		
		difference	= _2nd.get(field) - _1st.get(field);
		
		if (difference < 0)
		{
			difference	= _2nd.get(field) - (_1st.get(field) - _1st.getActualMaximum(field));
			_1st.add(field, _1st.getActualMaximum(field));
			// there is a switch the next higher field type
			while (_1st.before(_2nd))
			{
				difference += _1st.getActualMaximum(field);
				_1st.add(field, _1st.getActualMaximum(field));
			}
		}
		
		if (!returnAmount && firstDate.after(secondDate))
			return difference * (-1);
		else
			return difference;
	}
	
	
	public static void main (String[] args)
	{
		System.out.println(getDifferenceInDays(
				new GregorianCalendar(2015,11,31),
				new GregorianCalendar(2015,10,1),
				false));
	}
}
