package routines;

import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.ParseException;
import java.text.SimpleDateFormat;

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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.regex.MatchResult;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;

import com.jayway.jsonpath.JsonPath;

import de.lcag.common.CsvTable;

/*
 * 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 {
	public enum StringType {
		CSV("CSV"), HTML("HTML"), JSON("JSON"), XML("XML");

		String typeName;

		StringType(String pType) {
			this.typeName = pType;
		}

		@Override
		public String toString() {
			return this.typeName;
		}
	}

	// Use this to format output of "," seperated lists (split shall use first
	// character and then ignore the rest)
	public static final String COMMA_DELIMITER_FORMATTER = ", ";
	// Use this to format output of ";" seperated lists (split shall use first
	// character and then ignore the rest)
	public static final String SEMI_COLON_DELIMITER_FORMATTER = ", ";
	public static final String EMPTY_STRING = "";

	private static LcagLogger log = LcagLogger.getLogger(LcagStringTools.class);
	private static final Map<Pattern, StringType> CONTENT_DETECTOR_PATTERN_MAP = new LinkedHashMap<>();

	static {
		// Order items by most specific first! ALL for HTML also match XML!
		CONTENT_DETECTOR_PATTERN_MAP.put(
				Pattern.compile("<(b|br|h\\d|i|ul|li|p|pre|td|th)?>", Pattern.DOTALL | Pattern.MULTILINE),
				StringType.HTML);
		CONTENT_DETECTOR_PATTERN_MAP.put(Pattern.compile("</.*?>", Pattern.DOTALL | Pattern.MULTILINE), StringType.XML);
	}

	/**
	 * 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 = EMPTY_STRING.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 ? EMPTY_STRING : 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<>();

		if (csv != null) {
			List<String> aList = Arrays.asList(csv.split(regExSeparator));
			result.addAll(aList);
		}

		return result;
	}

	public static String asHtml(String tagName, String text) {
		return String.format("<%s>%s</%s>", tagName, StringEscapeUtils.escapeHtml4(text), tagName);
	}

	/**
	 * 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 EMPTY_STRING;
		}

		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;
	}

	public static String getString(String text) {
		String result = text;
		// Check if the argument is a valid path to a file:
		try {
			Path path = Paths.get(text);

			if (Files.isReadable(path)) {
				// Yes, it is a file, use its content instead:
				result = new String(Files.readAllBytes(path));
			}
		} catch (Exception e) {
		}

		return result;
	}

	public static StringType getStringType(String text) {
		StringType result = null;

		text = text.strip();

		int n = text.indexOf("\n");
		String firstLine = n < 0 ? text : text.substring(0, n).trim();

		if (firstLine.startsWith("<?xml")) {
			result = StringType.XML;
		} else if (text.startsWith("{") && text.endsWith("}")) {
			result = StringType.JSON;
		} else if (StringUtils.countMatches(firstLine, ';') > 0 && StringUtils.countMatches(text, '\n') > 1) {
			result = StringType.CSV;
		} else if (firstLine.startsWith("<!DOCTYPE html>") || firstLine.startsWith("<html>")) {
			result = StringType.HTML;
		}

		if (result == null) {
			// Count tags in the text to find out the correct type
			final int MIN_MATCHES_TO_DETECT = 2;
			// For performance only count in 1K of text
			final int MAX_TEXT_SCAN_SIZE = 1024;

			int textLength = text.length();
			if (textLength > MAX_TEXT_SCAN_SIZE)
				text = text.substring(0, MAX_TEXT_SCAN_SIZE);

			for (Pattern detector : CONTENT_DETECTOR_PATTERN_MAP.keySet()) {
				Stream<MatchResult> results = detector.matcher(text).results();
				long noMatches = results.count();

				if (noMatches >= MIN_MATCHES_TO_DETECT) {
					result = CONTENT_DETECTOR_PATTERN_MAP.get(detector);

					log.debug("getStringType: text '%s...' detected as '%s' by pattern analysis", firstLine, result);
					break;
				}
			}
		} else {
			log.debug("getStringType: text '%s...' detected as '%s' by start tag analysis", firstLine, 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 = EMPTY_STRING;

		if (txt == null || map == null)
			return txt;

		// handle all allowed bracket pairs that may enclose a variabe name:
		for (String bracket : new String[] { "{}", "()" }) {
			char openBracket = bracket.charAt(0);
			char closeBracket = bracket.charAt(1);

			// handle all variables found:
			result = _tryResolvingUsingBracketType(txt, map, openBracket, closeBracket);

			// Only support one bracket type:
			if (!EMPTY_STRING.equals(result))
				break;

		} // end of bracket type handling

		if (EMPTY_STRING.equals(result))
			result = txt;

		return result;

	}

	private static String _tryResolvingUsingBracketType(String pText,
			Map<? extends Object, ? extends Object> pVariableMap, char pOpenBracket, char pCloseBracket) {

		String result = EMPTY_STRING;
		String[] varList = pText.split("\\$\\" + pOpenBracket); // split txt by ${ or $(

		if (varList.length == 1) {
			// no bracket found, return incomming text

			return result;
		}

		for (String s : varList) {
			Object value = null;

			if (EMPTY_STRING.equals(s))
				continue;

			int n = s.indexOf(pCloseBracket);

			if (n > 0) {
				// Var found to replace
				String key = s.substring(0, n);
				String format = null;
				String path = null; // for Json or xpath: /-separated list of attributes

				int formatIndex = key.indexOf("#");
				int pathIndex = 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);
				}

				if (pathIndex > 0) {
					path = key.substring(pathIndex + 1);
					key = key.substring(0, pathIndex);
				}

				value = pVariableMap.get(key);

				// handle reserved keys:
				if ("now".equals(key)) { // current date/time
					value = new Date();
				} else if ("today".equals(key)) {
					value = new Date();
					format = LcagDateTime.MDM_DATE_FORMAT_STR;
				} else if ("tomorrow".equals(key)) {
					value = LcagDateTime.addDays(new Date(), 1);
					format = LcagDateTime.MDM_DATE_FORMAT_STR;
				} else if ("yesterday".equals(key)) {
					value = LcagDateTime.addDays(new Date(), -1);
					format = LcagDateTime.MDM_DATE_FORMAT_STR;
				}

				if (value != null) {
					if (value instanceof Date) {
						if (format == null) {
							value = LcagDateTime.DATETIME_FORMAT_ISO8601_UTC_FILE.format((Date) value);
						} else if ("milliseconds".equals(format)) {
							Long v = ((Date) value).getTime();

							value = v;
						} else {
							SimpleDateFormat f = new SimpleDateFormat(format);

							value = f.format((Date) value);
						}
					} else if (value instanceof String) {
						String stringValue = (String) value;
						String text = getString(stringValue);
						StringType textType = getStringType(text);

						if (textType == StringType.JSON && pathIndex >= 0) {
							String jsonPath = "." + path.replace('/', '.');

							List<Object> itemsFound = JsonPath.parse((String) value).read(jsonPath);
							// List<> uniqueResultList = new ArrayList<>();

							if (itemsFound != null) {
								Set<Object> uniqueValues = new LinkedHashSet<Object>(itemsFound);

								text = joinIgnoreNull(COMMA_DELIMITER_FORMATTER, uniqueValues);
							} else {
								text = EMPTY_STRING;
							}
						}

						if (StringType.HTML.toString().equals(format)) {
							String htmlText = EMPTY_STRING;

							if (textType == StringType.CSV) {
								CsvTable table = new CsvTable();

								table.read(text);
								htmlText = table.asHTML();
							} else if (textType == StringType.HTML) {
								htmlText = String.format(
										"<b>ERROR: Converting %s tables into HTML not yet supported !!</b>", textType);
							} else {
								htmlText = StringEscapeUtils.escapeHtml4(text);
							}
							text = htmlText;
						}

						value = text;

					}

					// Replace env var by value
					s = value.toString() + s.substring(n + 1);
				} else { // value == null
					s = "$" + pOpenBracket + s;
				}
			} // end of n > 0 (variable handling)

			result += s;
		} // end of bracket pair handling

		return result;
	}

	/**
	 * resolveVariables: Replaces all standard variables in the txt argument by the
	 * values.
	 * 
	 * Supports the following standard variables: ${now}, ${today}, ${tomorrow},
	 * ${yesterday}
	 * 
	 * Examples: (if now is June, 23rd 2023 at 11:45:00 UTC): resolveVariables("This
	 * is ${today}") = "This is 2023-06-23" resolveVariables("This is ${tomorrow}")
	 * = "This is 2023-06-24" resolveVariables("This is ${now}") = "This is
	 * 2023-06-23T11-45-00Z" resolveVariables("This is ${now#HH:mm}") = "This is
	 * 11:45"
	 * 
	 * {talendTypes} String
	 * 
	 * {Category} LCAG-String
	 * 
	 * {param} String("txt") input: string to substitutes
	 */
	public static String resolveVariables(String txt) {
		return resolveVariables(txt, new HashMap<String, String>());
	}

	/**
	 * 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 = EMPTY_STRING;
		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 = EMPTY_STRING;
		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;
	}

	public static List<String> splitByCamelCase(String s) {
		List<String> splitted = new ArrayList<String>(Arrays.asList(StringUtils.splitByCharacterTypeCamelCase(s)));

		splitted.removeAll(Collections.singleton("_"));

		return splitted;
	}

	public static <T> T convertToObject(String pType, String pStringValue, String pDateFormat) throws ParseException {
		T result = null;
		log.debug("LcagStringTools.convertToObject(type %s, value %s, format %s) starts", pType, pStringValue,
				pDateFormat);

		if (pStringValue == null || pStringValue.trim().equals(EMPTY_STRING)) {
			result = null;
		} else {
			if ("String".equals(pType)) {
				result = (T) pStringValue;
			} else if ("Date".equals(pType)) {
				if (pDateFormat != null) {
					result = (T) LcagDateTime.toDate(pStringValue, pDateFormat);
				} else {
					result = (T) LcagDateTime.MDM_DATE_FORMAT.parse(pStringValue);
				}
			} else if ("Timestamp".equals(pType)) {
				result = (T) LcagDateTime.MDM_DATETIME_FORMAT.parse(pStringValue);
			} else if ("Integer".equals(pType)) {
				Integer v = Integer.parseInt(pStringValue);
				result = (T) v;
			} else if ("Decimal".equals(pType)) {
				pStringValue = pStringValue.replace(',', '.');
				Double v = Double.parseDouble(pStringValue);
				result = (T) BigDecimal.valueOf(v);
			}
		}

		return result;
	}

	public static Map<String, Object> asMap(String fieldMapping, String pPairSeperator, String pKeyValueSeperator) {
		// Create the HashMap to store the result
		Map<String, Object> map = new HashMap<>();

		// Split the string by ';' to get each key-value pair
		String[] pairs = fieldMapping.split(pPairSeperator);

		for (String pair : pairs) {
			// Split each pair
			String[] keyValue = pair.split(pKeyValueSeperator);
			if (keyValue.length == 2) {
				// Trim to remove any whitespace
				map.put(keyValue[0].trim(), keyValue[1].trim());
			}
		}

		return map;
	}
}
