/* Locales.java


	Purpose:
	Description:
	History:
	2002/01/25 11:58:34, Create, Tom M. Yeh.

Copyright (C) 2001 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
	This program is distributed under LGPL Version 2.1 in the hope that
	it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.util;

import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.zkoss.lang.Objects;

/**
 * The locale relevant utilities.
 *
 * @author tomyeh
 */
public class Locales {
	private final static
		InheritableThreadLocal<Locale> _thdLocale = new InheritableThreadLocal<>();

	/** Returns the current locale; never null.
	 * This is the locale that every other objects shall use,
	 * unless they have special consideration.
	 *
	 * <p>Default: If {@link #setThreadLocal} was called with non-null,
	 * the value is returned. Otherwise, Locale.getDefault() is returned,
	 */
	public static final Locale getCurrent() {
		final Locale l = _thdLocale.get();
		return l != null ? l: Locale.getDefault();
	}
	/** Returns whether the current locale ({@link #getCurrent}) belongs
	 * to the specified language and/or country.
	 *
	 * @param lang the language code, e.g., en and zh. Ignored if null.
	 * @param country the country code, e.g., US. Ignored if null.
	 * If empty, it means no country code at all.
	 */
	public static final boolean testCurrent(String lang, String country) {
		final Locale l = getCurrent();
		return (lang == null || lang.equals(l.getLanguage()))
			&& (country == null || country.equals(l.getCountry()));
	}
	/**
	 * Sets the locale for the current thread only.
	 *
	 * <p>Each thread could have an independent locale, called
	 * the thread locale.
	 *
	 * <p>When Invoking this method under a thread that serves requests,
	 * remember to clean up the setting upon completing each request.
	 *
	 * <pre><code>Locale old = Locales.setThreadLocal(newValue);
	 *try { 
	 *  ...
	 *} finally {
	 *  Locales.setThreadLocal(old);
	 *}</code></pre>
	 *
	 * @param locale the thread locale; null to denote no thread locale
	 * @return the previous thread locale
	 */
	public static final Locale setThreadLocal(Locale locale) {
		final Locale old = _thdLocale.get();
		_thdLocale.set(locale);
		return old;
	}
	/**
	 * Returns the locale defined by {@link #setThreadLocal}.
	 *
	 * @since 3.0.0
	 * @see #getCurrent
	 */
	public static final Locale getThreadLocal() {
		return _thdLocale.get();
	}

	/** Converts a string that consists of language, country and variant
	 * to a locale.
	 *
	 * <p>The separator between language, country and variant
	 * is customizable, and whitespaces are ignored, e.g.,
	 * "zh_TW" and "zh, TW".
	 *
	 * <p>Thus, locale.equals(Locales.getLocale(locale.toString(), '_')).
	 *
	 * @param localeString the locale in string; null is OK
	 * @param separator the separator; ((char)0) means to decide automatically
	 * (either ',', '-' or '_')
	 * @return the locale or null if locale is null or empty
	 */
	public static final Locale getLocale(String localeString, char separator) {
		if (localeString == null)
			return null;

		if (separator == (char)0) {
			if (localeString.indexOf('_') >= 0) {
				separator = '_';
			} else if (localeString.indexOf('-') >= 0) {
				separator = '-';
			} else {
				separator = ',';
			}
		}
		localeString = localeString.replace(separator, '-');
		Locale locale = Locale.forLanguageTag(localeString);

		// fix for ZK-5214
		if (!java.util.Objects.equals(localeString, locale.toLanguageTag())) {
			String[] split = localeString.split("-");
			switch (split.length) {
			case 1: locale = new Locale(split[0]);
				break;
			case 2: locale = new Locale(split[0], split[1]);
				break;
			case 3: locale = new Locale(split[0], split[1], split[2]);
				break;
			}
		}
		return getLocale(locale);
	}
	/** Converts a string that consists of language, country and variant
	 * to a locale.
	 *
	 * <p>A shortcut: getLocale(localeString, (char)0).
	 */
	public static final Locale getLocale(String localeString) {
		return getLocale(localeString, (char)0);
	}

	/** Converts a Locale to one of them being used before.
	 * To save memory (since locale is used frequently), it is suggested
	 * to pass thru this method after creating a new instance of Locale.<br>
	 * Example, getLocale(new Locale(...)).
	 *
	 * <p>This method first look for any locale
	 */
	public static final Locale getLocale(Locale locale) {
		return _founds.computeIfAbsent(locale, locale1 -> locale);
	}
	/** Locales that are found so far. */
	private static final Map<Locale, Locale> _founds = new ConcurrentHashMap<>(16);
	static {
		final Locale[] ls = new Locale[] {
			Locale.TRADITIONAL_CHINESE, Locale.SIMPLIFIED_CHINESE,
			Locale.ENGLISH, Locale.US,
			Locale.JAPAN, Locale.JAPANESE,
			Locale.KOREA, Locale.KOREAN,
			Locale.FRANCE, Locale.FRENCH,
			Locale.GERMANY, Locale.GERMAN,
			Locale.CHINESE
		};
		for (Locale l : ls)
			_founds.put(l, l);
	}

	/** Returns any occurrence of the specified Locale or any its fallback
	 * in the value collection, or null if not found.
	 * By fallback, we mean will try without variant and country.
	 * Example, if locale is zh_TW, it will try zh_TW and then zh.
	 */
	public static Locale getByFallback(Collection<Locale> values, Locale locale) {
		if (values.contains(locale))
			return locale;

		final String lang = locale.getLanguage();
		final String cnty = locale.getCountry();
		final String variant = locale.getVariant();
		if (variant != null && !variant.isEmpty()) {
			locale = new Locale(lang, cnty);
			if (values.contains(locale))
				return locale;
		}
		if (cnty != null && !cnty.isEmpty()) {
			locale = new Locale(lang, "");
			if (values.contains(locale))
				return locale;
		}

		//search the first one that matches partially
		Locale rtn = null;
		for (Locale l: values) {
			if (l.getLanguage().equals(lang)) {
				//case 1: it matches all but the last element -> done
				if (variant == null || variant.isEmpty()
				|| Objects.equals(l.getCountry(), cnty))//country might null
					return l;

				//case 2: it matches only language, we seeek for any case 1
				if (rtn == null)
					rtn = l;
			}
		}
		return rtn;
	}

	/** Returns the index of '_' preceding the country part, starting from j.
	 * It is similar to s.indexOf('_', j), except it detects country part
	 * (which must be only two letter in lower cases.
	 */
	public static int indexOfUnderline(String s, int j) {
		int k, last = s.length() - 2;
		for (;(k = s.indexOf('_', j)) >= 0 && k < last; j = k + 1) {
			char cc = s.charAt(k + 1);
			if (cc < 'a' || cc > 'z') continue; //not found
			cc = s.charAt(k + 2);
			if (cc < 'a' || cc > 'z') continue; //not found
			cc = s.charAt(k + 3);
			if (cc < 'a' || cc > 'z') return k; //found
		}
		return -1;
	}
	/** Returns the index of '_' preceding the country part.
	 * It is similar to s.indexOf('_'), except it detects country part
	 * (which must be only two letter in lower cases.
	 */
	public static int indexOfUnderline(String s) {
		return indexOfUnderline(s, 0);
	}
}
