package routines;

/* LCAG String helper functions 
 * 
 * Change History:
 * V3.5.0: Added new function isEmpty
 */

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.HashMap;
import java.util.Properties;
import java.util.Collection;
import java.text.SimpleDateFormat;
import java.util.Date;



/*
 * 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 LcagStringTools
{

    
	private static LcagLogger log = LcagLogger.getLogger(LcagStringTools.class);
	
	/**
     * isEmpty: Checks if a string representation of an object is null or some empty string
     * 
     * {talendTypes} boolean
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} Object("s") input: object to check
     */    
    public static boolean isEmpty(Object s) 
    {
    	boolean result = false;
    	
    	if (s == null) {	
    		return true;
    	} else {
    		String asString = s.toString().trim();
    		
    		result = "".equals(asString) || "(null)".equals(asString) || "null".equals(asString);
    	}
    	
    	return result;
    }	

	/**
     * isEqualTo: Compares a String to another object
     * 
     * {talendTypes} boolean
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} String("s") input: string to compare with
     * {param} int("obj")  input: Object to compare to
     * 
     * 
     */    
    public static boolean isEqualTo(String s, Object obj)
    {
    	boolean result = false;
    	
    	if (s == null && obj == null) {	
    		return true;
    	} else if (s == null || obj == null) {
    		return false;
    	} else if (obj instanceof String || obj instanceof Integer || obj instanceof Long || obj instanceof Boolean){
    		String s2 = obj.toString();
    		
    		result = s.equals(s2);
    	} else {
    		// Float, Double cause rounding problems, Date format problems when using toString()
    		throw new IllegalArgumentException(String.format("Comparison type '%s'not yet supported", obj.getClass().getName()));
    	}

    	return result;
    }	

	/**
     * isEqualToIgnoreCase: Compares a String to another object
     * 
     * {talendTypes} boolean
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} String("s") input: string to compare with
     * {param} int("obj")  input: Object to compare to
     * 
     * 
     */    
    public static boolean isEqualToIgnoreCase(String s, Object obj)
    {
    	boolean result = false;
    	
    	if (s == null && obj == null) {	
    		return true;
    	} else if (s == null || obj == null) {
    		return false;
    	} else if (obj instanceof String || obj instanceof Integer || obj instanceof Long || obj instanceof Boolean){
    		String s2 = obj.toString();
    		
    		result = s.equalsIgnoreCase(s2);
    	} else {
    		// Float, Double cause rounding problems, Date format problems when using toString()
    		throw new IllegalArgumentException(String.format("Comparison type '%s'not yet supported", obj.getClass().getName()));
    	}

    	return result;
    }	

	/**
     * Convert an object to a non null String  
     *  
     * {talendTypes} String
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} Object anObj : 			input: Object to turn into String
     * {returns} String representation using toString or empty string
     * {example} toString(null) = "", toString(new Double(9.9)) = "9.9" 
     */
	public static String toString(Object anObj) {
		String result = anObj == null ? "" : anObj.toString();
		return result;
	}

	/**
     * Convert a CSV string into a Java String List 
     *  
     * {talendTypes} List<String>
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} String csv : 			input: text to split into a list
     * {param} String regExSeparator : 	input: Character or regular exression string to use for splitting
     * {returns} List of strings
     * {example} assembleMDS_DateTime(aDate, "23:59") returns next day 00:00 of aDate
     */
	public static List<String> asList(String csv, String regExSeparator) {
		List<String> result = new ArrayList<>();

		result = Arrays.asList( csv.split(regExSeparator) );
		return result;
	}
	
	/**
     * Joins an object as a result String. Usually the parameter is a List<>. The functions joins multiple elements
     * using the delimiter passed. It replaces null values by the nullString argument unless it is null itself.
     * If nullString argument is null the function skips all null values from the stringList argument 
	 * 
	 * @param delimiter		- separstor of strings to use
	 * @param nullString	- replacement of null values (if null skips null values)
	 * @param stringList	- any object or List<>. Elements are converted to String using toString() 
	 * @return				- String joined by delimiter from all arguments. 
	 */
	private static String join(String delimiter, String nullString, Object stringList) {
		List<Object> objList = new ArrayList<Object>();
		List<String> txtList = new ArrayList<String>();

		if (stringList instanceof Collection) {
	        objList = new java.util.ArrayList<>((Collection<?>)stringList);
	    } else {
	    	objList.add(stringList);
	    }
		
		for (Object o : objList) {
			if ( o != null ) {
				txtList.add(o.toString());
			} else if (nullString != null) {
				txtList.add(nullString);
			}
		}

		String result = String.join(delimiter, txtList);
		return result;
	}

	/**
     * Joins an object as a result String. Usually the parameter is a List<>. The functions joins multiple elements
     * using the delimiter passed. It skips null values!
     *     
     * {talendTypes} String
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} String stringList : 		input: List of Objects to join
     * {returns} All elements joined by delimiter. 
     * {example} join( "/", { "a", null, "b" } ) = "a/b"  
     */
	public static String joinIgnoreNull(String delimiter, Object stringList) {
		return join(delimiter, null, stringList);
	}

	/**
     * Joins an object as a result String. Usually the parameter is a List<>. The functions joins multiple elements
     * using the delimiter passed. Uses nullString argument to represent null values to keep same number of elements.
     *     
     * {talendTypes} String
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} String stringList : 		input: List of Objects to join
     * {returns} All elements joined by delimiter. 
     * {example} join( "; ", "n/a", { "a", null, "b" } ) = "a; n/a; b"  
     */
	public static String joinAll(String delimiter, String nullString, Object stringList) {
		return join(delimiter, nullString, stringList);
	}

	/**
     * Joins all arguments as a result string using the delimiter to seperate the arguments.
     * Ignores null values in the list.
     *     
     * {talendTypes} String
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} String stringList : 		input: List of Objects to join
     * {returns} All elements joined by delimiter. 
     * {example} join(", ", "a", "b", "c") = "a, b, c
     * 			 join( "; ", "a", 1, 1.2, null, true ) = "a; 1; 1.2; true"   
     */
	public static String joinIgnoreNull(String delimiter, Object... stringList) {
		if (stringList == null) {
			return "";
		} 
		
		List<Object> aList = Arrays.asList(stringList);
		
		String result = join(delimiter, null, aList);

		return result;
	}
	
	/**
     * Joins all arguments as a result string using the delimiter to separate the arguments.
     * Uses nullString argument to represent null values to keep same number of elements.
     *     
     * {talendTypes} String
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} String stringList : 		input: List of Objects to join
     * {returns} All elements joined by delimiter. 
     * {example} join(", ", "n/a", "a", "b", "c") = "a, b, c
     * 			 join( "; ", "n/a", "a", 1, 1.2, null, true ) = "a; 1; 1.2; n/a; true"   
     */
	public static String joinAll(String delimiter, String nullString, Object... stringList) {
		if (stringList == null) {
			return nullString;
		} 
		
		List<Object> aList = Arrays.asList(stringList);
		
		String result = join(delimiter, nullString, aList);

		return result;
	}

	/**
     * fillBlanksUpcase: add blanks to given Strings for a fixed length
     * 
     * 
     * {talendTypes} String
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} String("s") input: string to fill with blanks
     * {param} int("n")  input: max number of blanks to add
     * 
     * 
     */    
    public static String fillBlanksUpcase(String s, int n)
    {
    	if (s == null)	return null;
    	else			return String.format("%1$-" + n + "s", s.toUpperCase());  
    }	
	
    /**
     * splitString: add blanks to given Strings for a fixed length
     * 
     * {talendTypes} String
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} String("s") input: string to tokenize
     * {param} String("delim") input: delimiter string
     * {param} int("n")  input: token indicator/counter starting with 0
     * 
     * 
     */    
    public static String splitString(String s, String delim, int n)
    {
    	if (s == null)	return null;
    	else			
    	{
    		/* s != null */
    		String result = null;
    		String[] tokens = s.split(delim);
    		if (tokens != null  &&  tokens.length > n)	result = tokens[n];
    		return result;
    	}
    }	
	
    /**
     * selectCaseSensitiveIfEqual: In case both String parameters are equal ignoring case return the one
     * that is case-sensitive.  In all other cases always return the first parameter that is not null or empty.
     * 
     * {talendTypes} String
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} String("s1") input: first string to evaluate
     * {param} String("s2") input: second string to evaluate
     * 
     */    
    public static String selectCaseSensitiveIfEqual(String s1, String s2)
    {    	
    	if (s1 == null)	return s2;
    	if (s2 == null)	return s1;
    	
    	s1 = s1.trim();
    	s2 = s2.trim();
    	
    	String result = s1;

    	int n1 = s1.length();
    	int n2 = s2.length();
    	
    	if (
    		n1 == n2 && n1 > 1 && s1.equalsIgnoreCase(s2) && 
    		Character.isUpperCase(s1.charAt(1)) && Character.isLowerCase(s2.charAt(1)) ) {

    		// both are equal ignoring case but s2 is likely case-sensitive whereas as s1 is not. So use s2:
    		result = s2;
    	}

    	return result;
    }	

    /**
     * selectBestOf: Selects the String with the most available information according to the rules:
     *   1) if first parameter is null then return the second.
     *   2) if one string start with the other return the longer string (after trimming)
     *   3) if both are of same length but differ in content ignoring case, return the first one
     *   4) if both are equal when ignoring case, return the one that also contains lower-case letters (tested by 2nd char)
     * 
     * {talendTypes} String
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} String("s1") input: first string to evaluate
     * {param} String("s2") input: second string to evaluate
     * 
     */    
    public static String selectBestOf(String s1, String s2)
    {    	
    	if (s1 == null)	return s2;
    	if (s2 == null)	return s1;
    	
    	s1 = s1.trim();
    	s2 = s2.trim();
    	
    	String result = s1;
    	String s1CaseIgnored = s1.toLowerCase();
    	String s2CaseIgnored = s2.toLowerCase();

    	int n1 = s1.length();
    	int n2 = s2.length();
    	
    	if (n2 > n1 && s2CaseIgnored.startsWith(s1CaseIgnored)) {
    		result = s2;
    	} else if (
    		n1 == n2 && n1 > 1 && s1.equalsIgnoreCase(s2) && 
    		Character.isUpperCase(s1.charAt(1)) && Character.isLowerCase(s2.charAt(1)) ) {

    		// both are equal ignoring case but s2 is likely case-sensitive whereas as s1 is not. So use s2:
    		result = s2;
    	}

    	return result;
    }	

    /**
     * applyChangeRule: applies the given change rule to the first two parameters and returs the corresponding result.
     * Rules:
     *   *) in general: if the first string s1 is null then apply the rule to the second string s2
     *   1) "to-upper-case": 
     *   2) "to-lower-case": 
     *   3) "prefer case-sensitive": if both string are equal except case, use the one that is case-sensitive
     *   4) "select best-of"; if one string start with the other return the longer string (after trimming)
     *   5) null: 
     * 
     * {talendTypes} String
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} String("s1") input: first string to evaluate
     * {param} String("s2") input: second string to evaluate
     * {param} String("changeRule") input: rule used to change
     * 
     */    
    public static String applyChangeRule(String s1, String s2, String changeRule) {
    	String result;
    	
    	if (s1 == null) {
    		s1 = s2;
    	}
    	
    	result = s1;
    	
    	if (changeRule == null) {
    		
    	} else if ("prefer case-sensitive".equals(changeRule)) {
    		result = selectCaseSensitiveIfEqual( s1, s2); 
    	} else if ("to-upper-case".equals(changeRule)) {
    		result = s1.toUpperCase();
    	} else if ("to-lower-case".equals(changeRule)) {
    		result = s1.toLowerCase();
    	} else if ("select best-of".equals(changeRule)) {
    		result = selectBestOf(s1,s2);
    	}
    	
        log.debug(String.format("applyChangeRule(%s, %s, %s) = %s", s1, s2, changeRule, result));
    	return result;
    }

    /**
     * resolveVariables: Replaces all variables ${VAR} in the txt argument by the value defined in the map.
     * If the variable is not defined the function will not change this placeholder.
     * 
     * Example:
     *   resolveVariables("This is ${MDM_Cargo_MDM_DataContainer}", globalMap) = "This is MDM_4_DEV_4_TST1_REG"
     * 
     * {talendTypes} String
     * 
     * {Category} LCAG-String
     * 
     * {param} String("txt") input: string to substitutes 
     * {param} Map("map") 	 input: Map of strings to replace 
     */    
	public static String resolveVariables(String txt, Map<? extends Object, ? extends Object> map) {
		String result = "";
		if (txt == null || map == null) return txt;

		String[]	varList = txt.split("\\$\\{");	// split txt by ${ 
		
		
		for (String s: varList) {
			int n = s.indexOf("}");
			
			if (n > 0) {
				// Var found to replace
				String key = s.substring(0, n);
				String format = null;
				
				int formatIndex = key.indexOf("#");
				
				if (formatIndex > 0) {
					// There is a formatting instruction after the variable name => get name and format:
					format = key.substring(formatIndex + 1);
					key = key.substring(0, formatIndex);
				}
				
				Object value = map.get(key);

				// handle reserved keys: 
				if ("now".equals(key)) {	// current date/time
					value = new Date();
				}

				if (value != null) {
					if (value instanceof Date && format != null) {
						if ("milliseconds".equals(format)) {
							Long v = ((Date)value).getTime();
							
							value = v;
						} else {
							SimpleDateFormat	f = new SimpleDateFormat(format);
							
							value	= f.format( (Date)value );
						}
					}
					// Replace env var by value
					s = value.toString() + s.substring(n+1);
				} else {
					s = "${" + s;
				}
			}
			result += s;
		}
		
		return result;
	}

    /**
     * resolveVariables: Replaces all variables ${VAR} in the txt argument by the value defined in the map.
     * If the variable is not defined the function will not change this placeholder.
     * 
     * Example:
     *   resolveVariables("This is ${MDM_Cargo_MDM_DataContainer}", context) = "This is MDM_4_DEV_4_TST1_REG"
     * 
     * {talendTypes} String
     * 
     * {Category} LCAG-String
     * 
     * {param} String("txt") 			input: string to substitutes 
     * {param} HashTable("context") 	input: Talend Context in a job 
     */    
	public static String resolveVariables(String txt, Properties context) {
		Map<Object, Object> aMap = new HashMap<Object, Object>();
		
		String result = "";
		if (txt == null || context == null) return txt;

		for (Entry<Object, Object> e: context.entrySet()) {
			aMap.put(e.getKey(), e.getValue());
		}
		
		result = resolveVariables(txt, aMap);
		
		return result;
	}

	/**
     * resolveEnvironmentVariables: Replaces all environment variables ${VAR} by its value.
     * If the variable is not defined the function will not change this placeholder.
     * 
     * Example:
     *   resolveEnvironmentVariables("This is ${OS}") = "This is Windows_NT"
     * 
     * {talendTypes} String
     * 
     * {Category} LCAG-StringTools
     * 
     * {param} String("txt") input: string to substitutes 
     */    
	public static String resolveEnvironmentVariables(String txt) {
		String result = "";
		String[]	varList = txt.split("\\$\\{");	// split txt by ${ 
		
		for (String s: varList) {
			int n = s.indexOf("}");
			
			if (n > 0) {
				// Env var found to replace
				String key = s.substring(0, n);
				String value = System.getenv(key);
				
				if (value != null) {
					// Replace env var by value
					s = value + s.substring(n+1);
				} else {
					s = "${" + s;
				}
			}
			result += s;
		}
		
		return result;
	}
}
