/*
 * This file is part of Nuts Framework.
 * Copyright(C) 2009-2012 Nuts Develop Team.
 *
 * Nuts Framework 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 3 of the License any later version.
 * 
 * Nuts Framework 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 Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.core.lang.i18n;

import nuts.core.io.Files;
import nuts.core.lang.Arrays;
import nuts.core.lang.Collections;

import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * utility class for Locale. 
 */
public abstract class Locales {

	/** Concurrent map of language locales by country. */
	private static final ConcurrentMap<String, List<Locale>> cLanguagesByCountry = new ConcurrentHashMap<String, List<Locale>>();

	/** Concurrent map of country locales by language. */
	private static final ConcurrentMap<String, List<Locale>> cCountriesByLanguage = new ConcurrentHashMap<String, List<Locale>>();

	/**
	 * Builds a {@link java.util.Locale} from a String of the form en_US_foo into a Locale with
	 * language "en", country "US" and variant "foo". This will parse the output of
	 * {@link java.util.Locale#toString()}.
	 * 
	 * @param localeStr The locale String to parse.
	 * @return requested Locale
	 */
	public static Locale toLocale(String localeStr) {
		return toLocale(localeStr, null);
	}

	/**
	 * Builds a {@link java.util.Locale} from a String of the form en_US_foo into a Locale with
	 * language "en", country "US" and variant "foo". This will parse the output of
	 * {@link java.util.Locale#toString()}.
	 * 
	 * @param localeStr The locale String to parse.
	 * @param defaultLocale The locale to use if localeStr is <tt>null</tt>.
	 * @return requested Locale
	 * @see #toLocale(String)
	 */
	public static Locale toLocale(String localeStr, Locale defaultLocale) {
		try {
			return parseLocale(localeStr);
		}
		catch (Exception e) {
			return defaultLocale;
		}
	}

	// -----------------------------------------------------------------------
	/**
	 * <p>
	 * Converts a String to a Locale.
	 * </p>
	 * <p>
	 * This method takes the string format of a locale and creates the locale object from it.
	 * </p>
	 * 
	 * <pre>
	 *   parseLocale("en")         = new Locale("en", "")
	 *   parseLocale("en_GB")      = new Locale("en", "GB")
	 *   parseLocale("en_GB_xxx")  = new Locale("en", "GB", "xxx")   (#)
	 * </pre>
	 * <p>
	 * (#) The behaviour of the JDK variant constructor changed between JDK1.3 and JDK1.4. In
	 * JDK1.3, the constructor upper cases the variant, in JDK1.4, it doesn't. Thus, the result from
	 * getVariant() may vary depending on your JDK.
	 * </p>
	 * <p>
	 * This method validates the input strictly. The language code must be lowercase. The country
	 * code must be uppercase. The separator must be an underscore. The length must be correct.
	 * </p>
	 * 
	 * @param str the locale String to convert, null returns null
	 * @return a Locale, null if null input
	 * @throws IllegalArgumentException if the string is an invalid format
	 */
	public static Locale parseLocale(final String str) {
		if (str == null) {
			return null;
		}
		if (str.contains("#")) { // LANG-879 - Cannot handle Java 7 script & extensions
			throw new IllegalArgumentException("Invalid locale format: " + str);
		}
		final int len = str.length();
		if (len < 2) {
			throw new IllegalArgumentException("Invalid locale format: " + str);
		}
		final char ch0 = str.charAt(0);
		if (ch0 == '_') {
			if (len < 3) {
				throw new IllegalArgumentException("Invalid locale format: " + str);
			}
			final char ch1 = str.charAt(1);
			final char ch2 = str.charAt(2);
			if (!Character.isUpperCase(ch1) || !Character.isUpperCase(ch2)) {
				throw new IllegalArgumentException("Invalid locale format: " + str);
			}
			if (len == 3) {
				return new Locale("", str.substring(1, 3));
			}
			if (len < 5) {
				throw new IllegalArgumentException("Invalid locale format: " + str);
			}
			if (str.charAt(3) != '_') {
				throw new IllegalArgumentException("Invalid locale format: " + str);
			}
			return new Locale("", str.substring(1, 3), str.substring(4));
		}
		final char ch1 = str.charAt(1);
		if (!Character.isLowerCase(ch0) || !Character.isLowerCase(ch1)) {
			throw new IllegalArgumentException("Invalid locale format: " + str);
		}
		if (len == 2) {
			return new Locale(str);
		}
		if (len < 5) {
			throw new IllegalArgumentException("Invalid locale format: " + str);
		}
		if (str.charAt(2) != '_') {
			throw new IllegalArgumentException("Invalid locale format: " + str);
		}
		final char ch3 = str.charAt(3);
		if (ch3 == '_') {
			return new Locale(str.substring(0, 2), "", str.substring(4));
		}
		final char ch4 = str.charAt(4);
		if (!Character.isUpperCase(ch3) || !Character.isUpperCase(ch4)) {
			throw new IllegalArgumentException("Invalid locale format: " + str);
		}
		if (len == 5) {
			return new Locale(str.substring(0, 2), str.substring(3, 5));
		}
		if (len < 7) {
			throw new IllegalArgumentException("Invalid locale format: " + str);
		}
		if (str.charAt(5) != '_') {
			throw new IllegalArgumentException("Invalid locale format: " + str);
		}
		return new Locale(str.substring(0, 2), str.substring(3, 5), str.substring(6));
	}

	// -----------------------------------------------------------------------
	/**
	 * <p>
	 * Obtains the list of locales to search through when performing a locale search.
	 * </p>
	 * 
	 * <pre>
	 * localeLookupList(Locale("fr","CA","xxx"))
	 *   = [Locale("fr","CA","xxx"), Locale("fr","CA"), Locale("fr")]
	 * </pre>
	 * 
	 * @param locale the locale to start from
	 * @return the unmodifiable list of Locale objects, 0 being locale, not null
	 */
	public static List<Locale> localeLookupList(final Locale locale) {
		return localeLookupList(locale, locale);
	}

	// -----------------------------------------------------------------------
	/**
	 * <p>
	 * Obtains the list of locales to search through when performing a locale search.
	 * </p>
	 * 
	 * <pre>
	 * localeLookupList(Locale("fr", "CA", "xxx"), Locale("en"))
	 *   = [Locale("fr","CA","xxx"), Locale("fr","CA"), Locale("fr"), Locale("en"]
	 * </pre>
	 * <p>
	 * The result list begins with the most specific locale, then the next more general and so on,
	 * finishing with the default locale. The list will never contain the same locale twice.
	 * </p>
	 * 
	 * @param locale the locale to start from, null returns empty list
	 * @param defaultLocale the default locale to use if no other is found
	 * @return the unmodifiable list of Locale objects, 0 being locale, not null
	 */
	public static List<Locale> localeLookupList(final Locale locale, final Locale defaultLocale) {
		final List<Locale> list = new ArrayList<Locale>(4);
		if (locale != null) {
			list.add(locale);
			if (locale.getVariant().length() > 0) {
				list.add(new Locale(locale.getLanguage(), locale.getCountry()));
			}
			if (locale.getCountry().length() > 0) {
				list.add(new Locale(locale.getLanguage(), ""));
			}
			if (list.contains(defaultLocale) == false) {
				list.add(defaultLocale);
			}
		}
		return Collections.unmodifiableList(list);
	}

	// -----------------------------------------------------------------------
	/**
	 * <p>
	 * Obtains an unmodifiable list of installed locales.
	 * </p>
	 * <p>
	 * This method is a wrapper around {@link Locale#getAvailableLocales()}. It is more efficient,
	 * as the JDK method must create a new array each time it is called.
	 * </p>
	 * 
	 * @return the unmodifiable list of available locales
	 */
	public static List<Locale> availableLocaleList() {
		return SyncAvoid.AVAILABLE_LOCALE_LIST;
	}

	// -----------------------------------------------------------------------
	/**
	 * <p>
	 * Obtains an unmodifiable set of installed locales.
	 * </p>
	 * <p>
	 * This method is a wrapper around {@link Locale#getAvailableLocales()}. It is more efficient,
	 * as the JDK method must create a new array each time it is called.
	 * </p>
	 * 
	 * @return the unmodifiable set of available locales
	 */
	public static Set<Locale> availableLocaleSet() {
		return SyncAvoid.AVAILABLE_LOCALE_SET;
	}

	// -----------------------------------------------------------------------
	/**
	 * <p>
	 * Checks if the locale specified is in the list of available locales.
	 * </p>
	 * 
	 * @param locale the Locale object to check if it is available
	 * @return true if the locale is a known locale
	 */
	public static boolean isAvailableLocale(final Locale locale) {
		return availableLocaleList().contains(locale);
	}

	// -----------------------------------------------------------------------
	/**
	 * <p>
	 * Obtains the list of languages supported for a given country.
	 * </p>
	 * <p>
	 * This method takes a country code and searches to find the languages available for that
	 * country. Variant locales are removed.
	 * </p>
	 * 
	 * @param countryCode the 2 letter country code, null returns empty
	 * @return an unmodifiable List of Locale objects, not null
	 */
	public static List<Locale> languagesByCountry(final String countryCode) {
		if (countryCode == null) {
			return Collections.emptyList();
		}
		List<Locale> langs = cLanguagesByCountry.get(countryCode);
		if (langs == null) {
			langs = new ArrayList<Locale>();
			final List<Locale> locales = availableLocaleList();
			for (int i = 0; i < locales.size(); i++) {
				final Locale locale = locales.get(i);
				if (countryCode.equals(locale.getCountry()) && locale.getVariant().isEmpty()) {
					langs.add(locale);
				}
			}
			langs = Collections.unmodifiableList(langs);
			cLanguagesByCountry.putIfAbsent(countryCode, langs);
			langs = cLanguagesByCountry.get(countryCode);
		}
		return langs;
	}

	// -----------------------------------------------------------------------
	/**
	 * <p>
	 * Obtains the list of countries supported for a given language.
	 * </p>
	 * <p>
	 * This method takes a language code and searches to find the countries available for that
	 * language. Variant locales are removed.
	 * </p>
	 * 
	 * @param languageCode the 2 letter language code, null returns empty
	 * @return an unmodifiable List of Locale objects, not null
	 */
	public static List<Locale> countriesByLanguage(final String languageCode) {
		if (languageCode == null) {
			return Collections.emptyList();
		}
		List<Locale> countries = cCountriesByLanguage.get(languageCode);
		if (countries == null) {
			countries = new ArrayList<Locale>();
			final List<Locale> locales = availableLocaleList();
			for (int i = 0; i < locales.size(); i++) {
				final Locale locale = locales.get(i);
				if (languageCode.equals(locale.getLanguage()) && locale.getCountry().length() != 0
						&& locale.getVariant().isEmpty()) {
					countries.add(locale);
				}
			}
			countries = Collections.unmodifiableList(countries);
			cCountriesByLanguage.putIfAbsent(languageCode, countries);
			countries = cCountriesByLanguage.get(languageCode);
		}
		return countries;
	}

	// -----------------------------------------------------------------------
	// class to avoid synchronization (Init on demand)
	static class SyncAvoid {
		/** Unmodifiable list of available locales. */
		private static final List<Locale> AVAILABLE_LOCALE_LIST;
		/** Unmodifiable set of available locales. */
		private static final Set<Locale> AVAILABLE_LOCALE_SET;

		static {
			final List<Locale> list = new ArrayList<Locale>(Arrays.asList(Locale.getAvailableLocales())); // extra
																											// safe
			AVAILABLE_LOCALE_LIST = Collections.unmodifiableList(list);
			AVAILABLE_LOCALE_SET = Collections.unmodifiableSet(new HashSet<Locale>(list));
		}
	}

	/**
	 * Builds a {@link java.util.Locale} from a filename String of the form en_US_foo into a Locale
	 * with language "en", country "US" and variant "foo". This will parse the output of
	 * {@link java.util.Locale#toString()}.
	 * 
	 * @param filename filename
	 * @return Locale
	 */
	public static Locale localeFromFileName(String filename) {
		return localeFromFileName(filename, null);
	}

	/**
	 * Builds a {@link java.util.Locale} from a filename String of the form en_US_foo into a Locale
	 * with language "en", country "US" and variant "foo". This will parse the output of
	 * {@link java.util.Locale#toString()}.
	 * 
	 * @param filename filename
	 * @param defaultLocale The locale to use
	 * @return Locale
	 */
	public static Locale localeFromFileName(String filename, Locale defaultLocale) {
		return localeFromFileName(new File(filename), null);
	}
	
	/**
	 * Builds a {@link java.util.Locale} from a filename String of the form en_US_foo into a Locale
	 * with language "en", country "US" and variant "foo". This will parse the output of
	 * {@link java.util.Locale#toString()}.
	 * 
	 * @param file file
	 * @return Locale
	 */
	public static Locale localeFromFileName(File file) {
		return localeFromFileName(file, null);
	}

	/**
	 * Builds a {@link java.util.Locale} from a filename String of the form en_US_foo into a Locale
	 * with language "en", country "US" and variant "foo". This will parse the output of
	 * {@link java.util.Locale#toString()}.
	 * 
	 * @param file file
	 * @param defaultLocale The locale to use
	 * @return Locale
	 */
	public static Locale localeFromFileName(File file, Locale defaultLocale) {
		String b = Files.getFileNameBase(file);
		
		String[] sa = b.split("\\_");
		
		if (sa.length > 3) {
			if (sa[sa.length - 3].length() == 2 && sa[sa.length - 2].length() == 2) {
				return new Locale(sa[sa.length - 3], sa[sa.length - 2], sa[sa.length - 1]); 
			}
			else if (sa[sa.length - 2].length() == 2 && sa[sa.length - 1].length() == 2) {
				return new Locale(sa[sa.length - 2], sa[sa.length - 1]); 
			}
			else if (sa[sa.length - 1].length() == 2) {
				return new Locale(sa[sa.length - 1]); 
			}
		}
		else if (sa.length == 3) {
			if (sa[sa.length - 2].length() == 2 && sa[sa.length - 1].length() == 2) {
				return new Locale(sa[sa.length - 2], sa[sa.length - 1]); 
			}
			else if (sa[sa.length - 1].length() == 2) {
				return new Locale(sa[sa.length - 1]); 
			}
		}
		else if (sa.length == 2) {
			if (sa[sa.length - 1].length() == 2) {
				return new Locale(sa[sa.length - 1]); 
			}
		}
		return defaultLocale;
	}
}
