/*
 *    XSLTUtil.java
 *    Copyright (C) 2008 New Zealand Digital Library, http://www.nzdl.org
 *
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/**
 * Note (cstephen):
 * Use of org.apache.commons.lang3.StringUtils can likely be removed when compiling
 * for Java 1.13+, due to performance improvements in the standard library.
 * https://stackoverflow.com/questions/16228992/commons-lang-stringutils-replace-performance-vs-string-replace
 */

package org.greenstone.gsdl3.util;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Locale;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import net.tanesha.recaptcha.ReCaptcha;
import net.tanesha.recaptcha.ReCaptchaFactory;

import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.greenstone.util.GlobalProperties;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
import org.w3c.dom.Document;

/**
 * a class to contain various static methods that are used by the xslt
 * stylesheets
 */
public class XSLTUtil
{
	protected static HashMap<String, ArrayList<String>> _foundTableValues = new HashMap<String, ArrayList<String>>();
	static Logger logger = Logger.getLogger(org.greenstone.gsdl3.util.XSLTUtil.class.getName());

    // just a place to hold some variables, eg prevMonth in classifiertools.xsl
	protected static HashMap<String, String> _stringVariables = new HashMap<String, String>();

	public static void storeString(String name, String value)
	{
		_stringVariables.put(name, value);
	}

	public static String getString(String name)
	{
		return _stringVariables.get(name);
	}

	/* some tests */
	public static boolean equals(String s1, String s2)
	{
		return s1.equals(s2);
	}

	public static boolean notEquals(String s1, String s2)
	{
		return !s1.equals(s2);
	}

	public static boolean exists(String s1, String s2)
	{
		return !s1.equals("");
	}

	public static boolean contains(String s1, String s2)
	{
		return (s1.indexOf(s2) != -1);
	}

	public static boolean startsWith(String s1, String s2)
	{
		return s1.startsWith(s2);
	}

	public static boolean endsWith(String s1, String s2)
	{
		return s1.endsWith(s2);
	}

	public static boolean lessThan(String s1, String s2)
	{
		return (s1.compareTo(s2) < 0);
	}

	public static boolean lessThanOrEquals(String s1, String s2)
	{
		return (s1.compareTo(s2) <= 0);
	}

	public static boolean greaterThan(String s1, String s2)
	{
		return (s1.compareTo(s2) > 0);
	}

	public static boolean greaterThanOrEquals(String s1, String s2)
	{
		return (s1.compareTo(s2) >= 0);
	}

  public static boolean csvContains(String csv_string, String this_item)
  {
    String[] csv_array = csv_string.split(",");
    for (int i=0; i<csv_array.length; i++) {
      if (csv_array[i].trim().equals(this_item)) {
	return true;
      }
    }
    return false;

  }
	public static boolean oidIsMatchOrParent(String first, String second)
	{
		if (first.equals(second))
		{
			return true;
		}

		String[] firstParts = first.split(".");
		String[] secondParts = second.split(".");

		if (firstParts.length >= secondParts.length)
		{
			return false;
		}

		for (int i = 0; i < firstParts.length; i++)
		{
			if (!firstParts[i].equals(secondParts[i]))
			{
				return false;
			}
		}

		return true;
	}

    public static boolean oidIsTopChild(String oid) {
	String[] oidParts = oid.split("\\.");
	return oidParts.length ==2;

    }
	public static String oidDocumentRoot(String oid)
	{
		String[] oidParts = oid.split("\\.");

		return oidParts[0];
	}

	public static String replace(String orig, String match, String replacement)
	{
		return orig.replace(match, replacement);
	}

	public static String getNumberedItem(String list, int number)
	{
	    String[] items = list.split(",", -1); //String[] items = StringUtils.split(list, ",", -1); 
	    // Using StringUtils.split() causes an off-by-one error for the boolean operators (fqk)
	    // where boolean operators combining rows in multiforms are shifted up by 1 row.

		if (items.length > number)
		{
			return items[number];
		}
		return ""; // index out of bounds
	}

  public static String getFormattedCCSSelection(String collections, String groups) {

    String result = ",";
    if (collections != null && !collections.equals("")) {
      result += collections+",";
    }
    if (groups != null && !groups.equals("")) {
      String[] gps = groups.split(",");
      for (int i=0; i<gps.length; i++) {
	result += "group."+gps[i].replace('/','.') +",";
      }
    }
    return result;
  }
	/**
	 * Generates links to equivalent documents for a document with a default
	 * document icon/type. Links are generated from the parameters: a list of
	 * document icons which are each in turn embedded in the matching starting
	 * link tag in the list of docStartLinks (these starting links link to the
	 * equivalent documents in another format). Each link's start tag is closed
	 * with the corresponding closing tag in the docEndLinks list. Parameter
	 * token is the list separator. Parameter divider is the string that should
	 * separate each final link generated from the next. Returns a string that
	 * represents a sequence of links to equivalent documents, where the anchor
	 * is a document icon.
	 */
	public static String getEquivDocLinks(String token, String docIconsString, String docStartLinksString, String docEndLinksString, String divider)
	{
		String[] docIcons = StringUtils.split(docIconsString, token, -1);
		String[] startLinks = StringUtils.split(docStartLinksString, token, -1);
		String[] endLinks = StringUtils.split(docEndLinksString, token, -1);

		StringBuffer buffer = new StringBuffer();
		for (int i = 0; i < docIcons.length; i++)
		{
			if (i > 0)
			{
				buffer.append(divider);
			}
			buffer.append(startLinks[i] + docIcons[i] + endLinks[i]);
		}

		return buffer.toString();
	}

	public static String getInterfaceText(String interface_name, String lang, String key)
	{
		return getInterfaceTextWithArgs(interface_name, lang, key, null);
	}

	public static String getInterfaceText(String interface_name, String lang, String key, String args_str)
	{
		String[] args = null;
		if (args_str != null && !args_str.equals(""))
		{
			args = StringUtils.split(args_str, ";");
		}
		return getInterfaceTextWithArgs(interface_name, lang, key, args);
	}
  
	public static String getInterfaceText(String interface_name, String dictionary_name, String lang, String key, String args_str)
	{
	    String i_name = interface_name;
	    if (i_name.startsWith("aux_")) {
              //i_name = i_name.substring(0, i_name.length()-1);
              i_name = i_name.substring(4);
            }
	    // now we allow looking for files in the interface's resources folder
          CustomClassLoader my_loader = new CustomClassLoader(XSLTUtil.class.getClassLoader(), GSFile.interfaceResourceDir(GlobalProperties.getGSDL3Home(), i_name));

	  String[] args = null;
		if (args_str != null && !args_str.equals(""))
		{
			args = StringUtils.split(args_str, ";");
		}

		// try the specified dictionary first
                String result = Dictionary.createDictionaryAndGetString(dictionary_name, my_loader, key, lang, args);
	 	if (result == null) {

		  // fall back to a general interface text search
		  return getInterfaceTextWithArgs(interface_name, lang, key, args);
		}
		result = result.replaceAll("__INTERFACE_NAME__", i_name); // do we need to do this here?
		return result;
	}
  
	public static String getInterfaceTextWithDOM(String interface_name, String lang, String key, Node arg_node)
	{
	  return getInterfaceTextWithDOMMulti(interface_name, lang, key, arg_node);
	}

	public static String getInterfaceTextWithDOM(String interface_name, String lang, String key, Node arg1_node, Node arg2_node)
	{
	  return getInterfaceTextWithDOMMulti(interface_name, lang, key, arg1_node, arg2_node);
	}

  public static String getInterfaceTextWithDOMMulti(String interface_name, String lang, String key, Node... nodes)
  {
    int num_nodes = nodes.length;
    String[] args = null;
    if (num_nodes != 0)
      {
	args = new String[num_nodes];
	
	for (int i = 0; i < num_nodes; i++)
	  {
	    String node_str = XMLConverter.getString(nodes[i]);
	    args[i] = node_str;
	  }
      }
    return getInterfaceTextWithArgs(interface_name, lang, key, args);

  }

  /* This is the base method that actually does the work of looking up the various chain of dictionaries */
  public static String getInterfaceTextWithArgs(String interface_name, String lang, String key, String[] args)
  {
    // now we allow looking for files in the interface's resources folder
      String i_name = interface_name;
//      if (i_name.endsWith("2")) {
//	  i_name = i_name.substring(0, i_name.length()-1);
      //    }

      if (i_name.startsWith("aux_")) {
        i_name = i_name.substring(4);
      }

      CustomClassLoader my_loader = new CustomClassLoader(XSLTUtil.class.getClassLoader(), GSFile.interfaceResourceDir(GlobalProperties.getGSDL3Home(), i_name));
    String result = Dictionary.createDictionaryAndGetString("interface_"+interface_name, my_loader, key, lang, args);

// TODO get rid of this. move flax interface properties into interfaces/flax/resources/
    if (result == null)
    {
	//if not found, search a separate subdirectory named by the interface name. this is used for eg the flax interface. this could be replaced by new class loader option?
	String sep_interface_dir = interface_name + File.separatorChar + lang + File.separatorChar + "interface";
        result = Dictionary.createDictionaryAndGetString(sep_interface_dir, my_loader, key, lang, args);
	if (result != null)
	  {
	    return result;
	  }
    }

    //if (result == null && !interface_name.startsWith("default")) {
    if (result == null && !i_name.equals("default")) {
    // not found, try the default interface - TODO, this should use inheritance chain
      String dictionary_name;
      if (interface_name.startsWith("aux_")) { // hack for interface_aux_xxx.properties
	dictionary_name = "interface_aux_default";
      } else {
	dictionary_name = "interface_default";
      }
      result = Dictionary.createDictionaryAndGetString(dictionary_name, my_loader, key, lang, args);
    }
		
    if (result == null)
      { // not found
	return "?" + key + "?";
      }
    result = result.replaceAll("__INTERFACE_NAME__", i_name);

    return result;



  }
    	public static String getInterfaceTextSubstituteArgs(String value, String args_str)
	{
	    String[] args = null;
	    if (args_str != null && !args_str.equals("")) {
		args = StringUtils.split(args_str, ";");
	    }

	    return Dictionary.processArgs(value,args);
	}
	    
	public static Node getCollectionText(String collection, String site_name, String lang, String key)
	{
	    return getCollectionTextWithArgs(collection, site_name, "interface_custom", lang, key, null);
	}

	public static Node getCollectionText(String collection, String site_name, String lang, String key, String args_str)
	{
	    return getCollectionText(collection, site_name, "interface_custom", lang, key, args_str);
	}
    public static Node getCollectionText(String collection, String site_name, String dictionary_name, String lang, String key, String args_str) {
	String[] args = null;
	if (args_str != null && !args_str.equals(""))
	    {
		args = StringUtils.split(args_str, ";");
	    }
	
	return getCollectionTextWithArgs(collection, site_name, dictionary_name, lang, key, args);

    }
    
	// xslt didn't like calling the function with Node varargs, so have this hack for now
	public static Node getCollectionTextWithDOM(String collection, String site_name, String lang, String key, Node n1)
	{
		return getCollectionTextWithDOMMulti(collection, site_name, lang, key, n1);
	}

	public static Node getCollectionTextWithDOM(String collection, String site_name, String lang, String key, Node n1, Node n2)
	{
		return getCollectionTextWithDOMMulti(collection, site_name, lang, key, n1, n2);
	}

	public static Node getCollectionTextWithDOM(String collection, String site_name, String lang, String key, Node n1, Node n2, Node n3)
	{
		return getCollectionTextWithDOMMulti(collection, site_name, lang, key, n1, n2, n3);
	}

	public static Node getCollectionTextWithDOM(String collection, String site_name, String lang, String key, Node n1, Node n2, Node n3, Node n4)
	{
		return getCollectionTextWithDOMMulti(collection, site_name, lang, key, n1, n2, n3, n4);
	}

	public static Node getCollectionTextWithDOMMulti(String collection, String site_name, String lang, String key, Node... nodes)
	{
		int num_nodes = nodes.length;
		String[] args = null;
		if (num_nodes != 0)
		{
			args = new String[num_nodes];

			for (int i = 0; i < num_nodes; i++)
			{
				String node_str = XMLConverter.getString(nodes[i]);
				args[i] = node_str;
			}
		}
		return getCollectionTextWithArgs(collection, site_name, "interface_custom", lang, key, args);
	}

    //    public static Node getCollectionTextWithArgs(String collection, String site_name, String lang, String key, String[] args) {
    //	return getCollectionTextWithArgs(String collection, String site_name, "interface_custom", String lang, String key, String[] args);
    // }



    public static Node getCollectionTextWithArgs(String collection, String site_name, String dictionary_name, String lang, String key, String[] args)
    {
	try
	    {
		DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		
		CustomClassLoader class_loader = new CustomClassLoader(XSLTUtil.class.getClassLoader(), GSFile.collectionResourceDir(GSFile.siteHome(GlobalProperties.getGSDL3Home(), site_name), collection));
		String result = Dictionary.createDictionaryAndGetString(dictionary_name, class_loader, key, lang, args); 
		if (result != null)
		    {
			return docBuilder.parse(new ByteArrayInputStream(("<fragment>" + result + "</fragment>").getBytes("UTF-8"))).getDocumentElement();
		    }
		return docBuilder.parse(new ByteArrayInputStream(("<fragment>" + "text:" + site_name+ ":" +collection + ":" + key + "</fragment>").getBytes())).getDocumentElement();
	    }
	catch (Exception ex)
	    {
		return null;
	    }
    }


  public static String getGenericText(String dictionary_name, String lang, String key) {
    return getGenericTextWithArgs(dictionary_name, lang, key, null);
  }

  public String getGenericText(String dictionary_name, String lang, String key, String args_str) {
    String[] args = null;
    if (args_str != null && !args_str.equals("")) {
      args = StringUtils.split(args_str, ";");
    }
    return getGenericTextWithArgs(dictionary_name, lang, key, args);
  }

  public static String getGenericTextWithArgs(String dictionary_name, String lang, String key, String[] args)
  {
    String result = Dictionary.createDictionaryAndGetString(dictionary_name, null, key, lang, args);
    if (result == null) {
      return "_"+dictionary_name+"_"+key+"_";
    }
    return result;
  }

    /** handle displayItems from xslt */
    public static String getCollectionDisplayItemText(Node display_item_list, String lang, String site_name, String collection) {
	
	CustomClassLoader class_loader = new CustomClassLoader(XSLTUtil.class.getClassLoader(), GSFile.collectionResourceDir(GSFile.siteHome(GlobalProperties.getGSDL3Home(), site_name), collection));
	Document doc = XMLConverter.newDOM();
	Element di = DisplayItemUtil.chooseBestMatchDisplayItem(doc, (Element)display_item_list, lang, "en", class_loader) ;
	if (di == null) {
	    return "";
	}
	return GSXML.getNodeText(di);

    }

  public static boolean canEditCollection(String user, String groups, String collection) {
    return GroupsUtil.canEditCollection(user, groups, collection);
  }
  

	public static boolean isImage(String mimetype)
	{
		if (mimetype.startsWith("image/"))
		{
			return true;
		}
		return false;
	}

	// formatting /preprocessing functions
	// some require a language, so we'll have a language param for all
	public static String toLower(String orig, String lang)
	{
		return orig.toLowerCase();
	}

	public static String toUpper(String orig, String lang)
	{
		return orig.toUpperCase();
	}

	public static String tidyWhitespace(String original, String lang)
	{

		if (original == null || original.equals(""))
		{
			return original;
		}
		String new_s = original.replaceAll("\\s+", " ");
		return new_s;
	}

	public static String stripWhitespace(String original, String lang)
	{

		if (original == null || original.equals(""))
		{
			return original;
		}
		String new_s = original.replaceAll("\\s+", "");
		return new_s;
	}

	public static byte[] toUTF8(String orig, String lang)
	{
		try
		{
			byte[] utf8 = orig.getBytes("UTF-8");
			return utf8;
		}
		catch (Exception e)
		{
			logger.error("unsupported encoding");
			return orig.getBytes();
		}
	}

	public static String formatDate(String date, String lang)
	{
		String in_pattern = "yyyyMMdd";
		String out_pattern = "dd MMMM yyyy";
		if (date.length() == 6)
		{
			in_pattern = "yyyyMM";
			out_pattern = "MMMM yyyy";
		}
		// remove the 00
		else if (date.length() == 8 && date.endsWith("00")) {
		  date = date.substring(0,6);
		  in_pattern = "yyyyMM";
		  out_pattern = "MMMM yyyy";
		}

		SimpleDateFormat formatter = new SimpleDateFormat(in_pattern, new Locale(lang));
		try
		{
			Date d = formatter.parse(date);
			formatter.applyPattern(out_pattern);
			String new_date = formatter.format(d);
			return new_date;
		}
		catch (Exception e)
		{
			return date;
		}

	}

  public static final int TS_SECS = 0;
  public static final int TS_MILLISECS = 1;
  public static final int F_DATE = 0;
  public static final int F_TIME = 1;
  public static final int F_DATETIME = 2;
  public static final int F_DAYSAGO = 3;

  public static String formatTimeStamp(String timestamp, int ts_type, int format_type, String lang) {
    try {
      long ts = Long.parseLong(timestamp);
      if (ts_type == TS_SECS) {
	ts = ts * 1000;
      }
      if (format_type == F_DAYSAGO) {
	long current_time = new Date().getTime();
	long days = (current_time - ts)/86400000;
	return String.valueOf(days);
      }
      Date d = new Date(ts);
      DateFormat df;
      switch (format_type) {
      case F_DATE:
	df = DateFormat.getDateInstance(DateFormat.DEFAULT, new Locale(lang));
	break;
      case F_TIME:
	df = DateFormat.getTimeInstance(DateFormat.DEFAULT, new Locale(lang));
	break;
      case F_DATETIME:
	df = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, new Locale(lang));
	break;
      default:
	df = DateFormat.getDateInstance(DateFormat.DEFAULT, new Locale(lang));
	break;
      }
      
      return df.format(d);
    } catch (Exception e) {
      
      return timestamp + e.getMessage();
    }
    
  }
	public static String getDetailFromDate(String date, String detail, String lang)
	{
		String in_pattern = "yyyyMMdd";
		if (date.length() == 6)
		{
			in_pattern = "yyyyMM";
		}
		// remove the 00
		else if (date.length() == 8 && date.endsWith("00")) {
		  date = date.substring(0,6);
		  in_pattern = "yyyyMM";
		}

		SimpleDateFormat formatter = new SimpleDateFormat(in_pattern, new Locale(lang));
		try
		{
			Date d = formatter.parse(date);
			if (detail.toLowerCase().equals("day"))
			{
				formatter.applyPattern("dd");
			}
			else if (detail.toLowerCase().equals("month"))
			{
				formatter.applyPattern("MMMM");
			}
			else if (detail.toLowerCase().equals("year"))
			{
				formatter.applyPattern("yyyy");
			}
			else
			{
				return "";
			}
			return formatter.format(d);
		}
		catch (Exception ex)
		{
			return "";
		}
	}

	public static String formatLanguage(String display_lang, String lang)
	{

		return new Locale(display_lang).getDisplayLanguage(new Locale(lang));
	}
	
	public static boolean checkFileExistence(String site_name, String filePath){

		String gsdl3_home = GlobalProperties.getGSDL3Home();
		//Remove leading file separator
		filePath = filePath.replaceAll("^/+", "");
		//Remove duplicates and replace by separator for current platform
		filePath = filePath.replaceAll("/+", File.separator);
		//Create full path to check
		String fullPath = GSFile.siteHome(gsdl3_home, site_name) + File.separator + filePath;
		File file = new File(fullPath);
		if (file.exists() && file.isFile()) {
			return true;
		}
		return false;
	}
	
	public static String uriEncode(String input)
	{
		String result = "";
		try {
			result = URLEncoder.encode(input, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		
		return result;
		
	}

	public static String cgiSafe(String original, String lang)
	{

		original = original.replace('&', ' ');
		original = original.replaceAll(" ", "%20");
		return original;
	}

	public static String formatBigNumber(String num, String lang)
	{

		String num_str = num;
		char[] num_chars = num_str.toCharArray();
		String zero_str = "";
		String formatted_str = "";

		for (int i = num_chars.length - 4; i >= 0; i--)
		{
			zero_str += '0';
		}

		String sig_str = "";
		for (int i = 0; i < 3 && i < num_chars.length; i++)
		{
			sig_str = sig_str + num_chars[i];
			if (i == 1 && i + 1 < num_chars.length)
			{
				sig_str = sig_str + ".";
			}
		}

		int sig_int = Math.round(Float.parseFloat(sig_str));
		String new_sig_str = sig_int + "";
		if (sig_str.length() > 2)
		{
			new_sig_str = sig_int + "0";
		}

		char[] final_chars = (new_sig_str + zero_str).toCharArray();
		int count = 1;
		for (int i = final_chars.length - 1; i >= 0; i--)
		{
			formatted_str = final_chars[i] + formatted_str;
			if (count == 3 && i != 0)
			{
				formatted_str = "," + formatted_str;
				count = 1;
			}
			else
			{
				count++;
			}
		}
		return formatted_str;
	}

	public static String hashToSectionId(String hashString)
	{
		if (hashString == null || hashString.length() == 0)
		{
			return "";
		}

		int firstDotIndex = hashString.indexOf(".");
		if (firstDotIndex == -1)
		{
			return "";
		}

		String sectionString = hashString.substring(firstDotIndex + 1);

		return sectionString;
	}

	public static String hashToDepthClass(String hashString)
	{
		if (hashString == null || hashString.length() == 0)
		{
			return "";
		}

		String sectionString = hashToSectionId(hashString);

		int count = sectionString.split("\\.").length;

		if (sectionString.equals(""))
		{
			return "sectionHeaderDepthTitle";
		}
		else
		{
			return "sectionHeaderDepth" + count;
		}
	}

    public static String formatNewLines(String str, String lang) {
	if (str == null || str.length() < 1)
		{
			return null;
		}
	return str.replace("\\n", "<br/>");
	}	
	public static String escapeNewLines(String str)
	{
		if (str == null || str.length() < 1)
		{
			return null;
		}
		return str.replace("\n", "\\\n");
	}

	public static String escapeQuotes(String str)
	{
		if (str == null || str.length() < 1)
		{
			return null;
		}
		return str.replace("\"", "\\\"");
	}
        public static String escapeAngleBrackets(String str)
	{
		if (str == null || str.length() < 1)
		{
			return null;
		}
		return str.replace("<", "&lt;").replace(">", "&gt;");
	}
    
	public static String escapeNewLinesAndQuotes(String str)
	{
		if (str == null || str.length() < 1)
		{
			return null;
		}
		return escapeNewLines(escapeQuotes(str));
	}
    
    public static String escapeNewLinesQuotesAngleBracketsForJSString(String str)
	{
	    // The \n and " becomes \\\n and \\\"
	    // but the <> are escaped/encoded for html, i.e. &gt; and &lt;  
		if (str == null || str.length() < 1)
		{
			return null;
		}
		return escapeAngleBrackets(escapeNewLines(escapeQuotes(str)));
	}
	public static String getGlobalProperty(String name)
	{
		return GlobalProperties.getProperty(name);
	}

	public static void clearMetadataStorage()
	{
		_foundTableValues.clear();
	}

	public static boolean checkMetadataNotDuplicate(String name, String value)
	{
		if (_foundTableValues.containsKey(name))
		{
			for (String mapValue : _foundTableValues.get(name))
			{
				if (mapValue.equals(value))
				{
					return false;
				}
			}
			_foundTableValues.get(name).add(value);
			return true;
		}

		ArrayList<String> newList = new ArrayList<String>();
		newList.add(value);

		_foundTableValues.put(name, newList);

		return true;
	}

	public static String reCAPTCHAimage(String publicKey, String privateKey)
	{
		ReCaptcha c = ReCaptchaFactory.newReCaptcha(publicKey, privateKey, false);
		return c.createRecaptchaHtml(null, null);
	}

	/**
	 * Generates a Javascript object graph to store language strings. Leaf nodes are the key, and their value the string.
	 * Further preceding nodes denote the prefix of the string within the language strings property file.
	 * Accessing a language string from the object graph can be done as such: 'const myString = gs.text.{prefix}.{key};'
	 * @param interfaceName The name of the interface to retrieve language strings for.
	 * @param lang The language to retrieve.
	 * @param prefix The prefix to to retrieve strings under. E.g. a value of 'atea.macroniser' will only retrieve strings prefixed with that value.
	 * @return Stringified Javascript code that will generate the language string object graph.
	 */
	public static String getInterfaceStringsAsJavascript(String interfaceName, String lang, String prefix)
	{
		String prependToPrefix = "gs.text";
		return XSLTUtil.getInterfaceStringsAsJavascript(interfaceName, lang, prefix, prependToPrefix);
	}

	/**
	 * Generates a Javascript object graph to store language strings. Leaf nodes are the key, and their value the string.
	 * Further preceding nodes denote the prefix of the string within the language strings property file.
	 * Accessing a language string from the object graph can be done as such: 'const myString = {prependToPrefix}.{prefix}.{key};'
	 * @param interfaceName The name of the interface to retrieve language strings for.
	 * @param lang The language to retrieve.
	 * @param prefix The prefix to to retrieve strings under. E.g. a value of 'atea.macroniser' will only retrieve strings prefixed with that value.
	 * @param prependToPrefix An accessor string to prepend to the generated JS object graph.
	 * @return Stringified Javascript code that will generate the language string object graph.
	 */
	public static String getInterfaceStringsAsJavascript(String interfaceName, String lang, String prefix, String prependToPrefix)
	{
		// now we allow looking for files in the interface's resources folder
		CustomClassLoader my_loader = new CustomClassLoader(
			XSLTUtil.class.getClassLoader(),
			GSFile.interfaceResourceDir(GlobalProperties.getGSDL3Home(), interfaceName)
		);

		StringBuffer outputStr = new StringBuffer();
		HashSet<String> initialisedNodes = new HashSet<>();

		// The dictionaries to pull keys from
                //TODO use interface inheritance
		String[] dictionaries = new String[] {
			"interface_default",
			"interface_aux_default",
			"interface_" + interfaceName,
                        "interface_aux_"+interfaceName

		};

		for (String dictName : dictionaries)
		{
			// get all the *keys* from the english dictionary as this is a complete set
			Dictionary dict = new Dictionary(dictName, "en", my_loader);
			Enumeration<String> keys = dict.getKeys();
			if (keys == null) {
				continue;
			}

			// Get all properties in the language-specific dictionary with the given key prefix
			// Create Javascript strings of the form:
			// prependToPrefix.key="value";\n
			while (keys.hasMoreElements())
			{
				String key = keys.nextElement();
				if (key.startsWith(prefix+"."))
				{
					int lastDotIndex = StringUtils.lastIndexOf(key, '.');
					// If this is true, the key is nested under nodes that we might have to construct
					if (lastDotIndex > 0) {
						// Builds the JS object structure we need to access the key.
						// Also has the side effect of ensuring that any '.' characters in
						// the key are valid once parsed by the JS engine.
						buildJSObjectGraph(
							outputStr,
							prependToPrefix + "." + StringUtils.substring(key, 0, lastDotIndex), // Strip the actual key from the path
							initialisedNodes
						);
					}

					// get the language dependent value for the key. This will return the english if no value found for the given lang
					String value = getInterfaceText(interfaceName, dictName, lang, key, null);
                                        if (value.contains("{")) {
                                            value = value.replaceAll("\\{.*\\}", "...");
                                        }
					outputStr.append(prependToPrefix + "." + key + "=\"" + value + "\";\n");
				}
			}
		}

		return outputStr.toString();
	}
	
	/**
	 * Builds a string that will initialize an empty javascript object.
	 * I.e. 'gs.text??={};'.
	 * @param buffer The buffer to append the string to.
	 * @param objectPath The path to the JS object to initialize. E.g. gs.text.atea.
	 * @param isRootObject A value indicating whether the object is a root object. If not, a logical nullish assignment statement will be built in order to produce cleaner javascript.
	 */
	private static void buildJSObjectInitializer(
		StringBuffer buffer,
		String objectPath,
		Boolean isRootObject
	)
	{

	    // the ?? isn't supported in some older versions of safari and firefox (as run on 32 bit linux test machine)
	    
	    //if (!isRootObject) {
	    ////buffer.append(objectPath + "??={};\n");
	    // Nullish coalescing operator https://plainenglish.io/blog/javascript-operator
	    // Nullish coalescing operator with assignment https://stackoverflow.com/questions/71238309/what-is-double-question-mark-equal
	    //buffer.append("if ("+objectPath+" == null || "+objectPath+" == undefined) {"+objectPath+"={};}\n"); // untested
	    //return;
	    //}
		

		buffer.append("if(typeof " + objectPath + "===\"undefined\"){" + objectPath + "={};}\n");
	}

	/**
	 * Builds a string that will initialize a javascript object graph.
	 * I.e. the structure required to access the final property on the object graph 'gs.text.atea.asr'.
	 * @param buffer The buffer to append the string to.
	 * @param objectGraph The object graph to build.
	 * @param visitedNodes A map of previously built nodes. Updated to add nodes that are produced in this function.
	 */
	private static void buildJSObjectGraph(
		StringBuffer buffer,
		String objectGraph,
		HashSet<String> preBuiltNodes
	)
	{
		if (objectGraph == null) {
			return;
		}

		String[] nodes = StringUtils.split(objectGraph, '.');

		if (!preBuiltNodes.contains(nodes[0])) {
			buildJSObjectInitializer(buffer, nodes[0], true);
			preBuiltNodes.add(nodes[0]);
		}

		if (nodes.length == 1) {
			return;
		}

		String currentDepth = nodes[0];
		for (int i = 1; i < nodes.length; i++) {
			currentDepth += "." + nodes[i];
			if (preBuiltNodes.contains(currentDepth)) {
				continue;
			}

			buildJSObjectInitializer(buffer, currentDepth, false);
			preBuiltNodes.add(currentDepth);
		}
	}

	public static String xmlNodeToString(Node node)
	{
		return GSXML.xmlNodeToString(node);
	}

	// Test from cmdline with:
	// java -classpath /research/ak19/gs3-svn/web/WEB-INF/lib/gsdl3.jar:/research/ak19/gs3-svn/web/WEB-INF/lib/log4j-1.2.8.jar:/research/ak19/gs3-svn/web/WEB-INF/classes/ org.greenstone.gsdl3.util.XSLTUtil
	public static void main(String args[])
	{
		System.out.println("\n@@@@@\n" + XSLTUtil.getInterfaceStringsAsJavascript("default", "en", "dse", "gs.text") + "@@@@@\n");
	}
}
