/*
 * HamNameMap.java		2001-07-03
 *
 * Ralph Elliott  <mailto:re@cmp.uea.ac.uk>
 */
package util;


import java.util.Properties;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Set;
import java.util.Arrays;
import java.util.Comparator;

import util.ResourceUtils;


/** Provides mapping in both directions between HamNoSys token
 * names and the corresponding octet values (classic HNS 3 character
 * values), based on data from a Properties file.
 * Alternative spellings are allowed for a couple of tokens,
 * as specified by an auxiliary Properties file.
 * This caters for discrepancies between UH and UEA spellings,
 * especially when doing HNS-Sign SiGML input.
 * There is a singleton static factory method, which gives the standard
 * instance of this class.
 */
public class HamNameMap {


	protected static final String		HNS_TAG_FILE =
										"util/octetToHam4Tags.txt";
	protected static final String		HNS_ALT_SPELL_FILE =
										"util/altSpellingsHam4Tags.txt";

	protected static final String		HAM_VERSION40 = "hamversion40";
	protected static final String		HAM_SPACE = "hamspace";

	protected static HamNameMap			hamNames = null;


//########  Static (singleton) factory method.  ########

	public static HamNameMap getStdHamNameMap() {

		// Start with an unsynchronized test, to make the common case
		// fast.
		if (hamNames == null) {
			synchronized (HamNameMap.class) {
				if (hamNames == null) {
					Properties hnprops =
						ResourceUtils.getProperties(HNS_TAG_FILE);
					Properties hnaltspellprops =
						ResourceUtils.getProperties(HNS_ALT_SPELL_FILE);
					hamNames =
						new HamNameMap(hnprops, null, hnaltspellprops);
				}
			}
		}

		return hamNames;
	}


//########  HamNameMap class implementation.  ########

	protected static final int			O_NULL = 0;
	protected static final int			O_MAX = 256;

	protected String[]					o2h;
	protected HashMap<String,Integer>	h2o;

	protected final int					O_HAMVERSION;
	protected final int					O_HAMSPACE;

	protected HamNameMap(Properties hnmain, Properties hnextra) {

		this(hnmain, hnextra, null); 
	}

	protected HamNameMap( 
		Properties hnmain, Properties hnextra, Properties hnaltspell) {

		this.o2h = new String[O_MAX];
		this.updateO2HMap(hnmain);
		if (hnextra != null) { this.updateO2HMap(hnextra); }
		
		this.h2o = new HashMap<String,Integer>();
		this.makeH2OMap();
		if (hnaltspell != null) { this.addAltSpellingsToH2OMap(hnaltspell); }

		// Set up support for whitespace elimination.
		final int HVER = this.octet(HAM_VERSION40);
		if (HVER == O_NULL) { badOperand(HAM_VERSION40); }

		final int HSPACE = this.octet(HAM_SPACE);
		if (HSPACE == O_NULL) { badOperand(HAM_SPACE); }

		this.O_HAMVERSION = HVER;
		this.O_HAMSPACE = HSPACE;
	}

	public String hamName(int o) {

		//	return this.o2h[o];
		return (0 <= o && o < O_MAX ? this.o2h[o] : null);
	}

	public int octet(String hamname) {

		final Integer O_OBJ = this.h2o.get(hamname);
		return (O_OBJ == null ? O_NULL : O_OBJ);
	}

	public int cleanOctet(String hamname) {

		final int OCT = this.octet(hamname);

		// Eliminate whitespace characters.
		return
			O_NULL < OCT &&
			OCT <= O_HAMSPACE &&
			OCT != O_HAMVERSION  ? O_NULL : OCT;
	}

	public String[] hamNameSet() {

		return this.h2o.keySet().toArray(new String[0]);
	}

	public String[] hamNameSetAscending() {

		// Comparator expects HamNames and orders them according
		// to their external encodings:
		Comparator<String> bcomp =
			new Comparator<String>() {
				public int compare(String o1, String o2) {
					final Integer I1 = HamNameMap.this.h2o.get(o1);
					final Integer I2 = HamNameMap.this.h2o.get(o2);
					int cmp = 0;
					if (I1!=null && I2==null) { cmp = -1; }
					else if (I1==null && I2!=null) { cmp = +1; }
					else if (I1!=null && I2!=null) { cmp = I1 - I2; }
					return cmp;
				}
			};
		final Set<String> NAMES = this.h2o.keySet();
		final int N = NAMES.size();
		String[] hnms = new String[N];
		NAMES.toArray(hnms);
		Arrays.sort(hnms, bcomp);

		return(hnms);
	}

	protected void updateO2HMap(Properties hnprops) {

		// Use the given properties to update the internal
		// byte-to-HamNoSys-name mapping array:
		// For each properties entry, extract the first token string
		// (from the property value), convert the key to int, and if
		// the former is a valid "ham..." name then record
		// the mapping from the one to the other.
		Enumeration<?>	hnen = hnprops.propertyNames();
		while (hnen.hasMoreElements()) {
			String ostr = (String)(hnen.nextElement());
			String hn = getFirstPropsToken(hnprops, ostr);
			if (hn.startsWith("ham")) { this.o2h[s2i(ostr)] = hn; }
		}
	}

	protected void makeH2OMap() {

		for (int o=0; o!=O_MAX; ++o) {
			String hn = this.o2h[o];
			if (hn != null) {
				if (this.h2o.get(hn)==null) {
					this.h2o.put(new String(hn), new Integer(o));
				}
			}
		}
	}

	protected void addAltSpellingsToH2OMap(Properties hnaltspell) {

		// We know they're strings, but Properties won't admit it.
		Enumeration<?> asen = hnaltspell.propertyNames();
		while (asen.hasMoreElements()) {
			String altstr = (String)asen.nextElement();
			String stdstr = getFirstPropsToken(hnaltspell, altstr);
			int stdval = this.octet(stdstr);
			if (stdval != O_NULL) { this.h2o.put(altstr, stdval); }
		}
	}

	protected static String getFirstPropsToken(
			Properties props, String istr) {

		String pstr = (String)(props.getProperty(istr));
		String pfirst = pstr.split("[ \t]+", 2)[0];

		return pfirst;
	}

	protected static int s2i(String s) {

		return Integer.decode(s).intValue();
	}

/** Returns a clean version of the given string, eliminating any invalid
 * HNS-UCS characters, and outputs a message mentioning the given index
 * for each invalid character.
 */
	public String[] cleanHamNameVec(String[] hamnv, int ix) {

		String[] hnsnvclean = hamnv;

		// Count invalid HNS token names in the string hnsnv.
		final int N = hamnv.length;
		int nbad = 0;
		for (String hamn : hamnv) {
			if (this.cleanOctet(hamn) == O_NULL) { ++ nbad; }
		}

		if (nbad != 0) {
			// Create a new clean array and copy the good HNS symbol
			// names into it.
			final int NG = N - nbad;
			hnsnvclean = new String[NG];
			int g = 0, j = 0;
			for (String hname : hamnv) {
				if (this.cleanOctet(hname) != O_NULL) {
					hnsnvclean[g] = hname;  ++ g;
				}
				else  {
					invalidMsg(ix, j, "HNS symbol name \""+hname+"\"");
				}
				++ j;
			}
		}

		return hnsnvclean;
	}

/** Outputs a message on the standard error string, saying that within
 * the entry with the given first index the item at the second given
 * index is invalid.
 */
	private static void invalidMsg(int i, int j, String item) {

		final String MSG =
			"In item "+i+": rejected "+item+" at position "+j+".";
		System.err.println(MSG);
	}

	protected static final void badOperand(String tag) {

		throw new IllegalArgumentException("HamNameMap: invalid name: "+tag);
	}

}
