/**
 * Copyright (c) 2011 - 2015, Lunifera GmbH (Gross Enzersdorf), Loetz KG (Heidelberg)
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *         Florian Pirchner - Initial implementation
 */
package org.lunifera.ecview.xtext.builder.participant.impl;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.collections.map.LRUMap;
import org.eclipse.core.runtime.FileLocator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.inject.Singleton;

@Singleton
public class I18nRegistry {

	private static final Logger LOGGER = LoggerFactory
			.getLogger(I18nRegistry.class);

	private final LRUMap fastAccess = new LRUMap(1000);

	private final Map<Locale, List<ResourceDescription>> cache = new HashMap<Locale, List<ResourceDescription>>();

	public I18nRegistry() {

	}

	/**
	 * Tries to find the translation for the given locale and key.
	 * 
	 * @param locale
	 * @param key
	 * @return
	 */
	public String findTranslation(Locale locale, String key) {
		String result = tryFastAccess(locale, key);
		if (result != null) {
			return result;
		}

		AccessPath accessPath = computeBestMatchAccessPath(locale, key);
		Translation translation = accessPath.getTranslation();
		if (translation == null) {
			return "";
		}

		return putFastAccess(translation.getLocale(), translation.getI18nKey(),
				translation.getI18nValue());
	}

	/**
	 * Try to find the translation in the fast access map.
	 * 
	 * @param locale
	 * @param key
	 * @return
	 */
	private String tryFastAccess(Locale locale, String key) {
		List<Locale> computedLocales = computeLocales(locale);
		for (Locale temp : computedLocales) {
			String fastAccessKey = createFastAccessKey(temp, key);
			String result = (String) fastAccess.get(fastAccessKey);
			if (result != null) {
				return result;
			}
		}
		return null;
	}

	/**
	 * Try to find the translation in the fast access map.
	 * 
	 * @param locale
	 * @param key
	 * @return
	 */
	private String putFastAccess(Locale locale, String key, String translation) {
		fastAccess.put(createFastAccessKey(locale, key), translation);
		return translation;
	}

	/**
	 * Creates the key for the fast access map access.
	 * 
	 * @param temp
	 * @param key
	 * @return
	 */
	private String createFastAccessKey(Locale temp, String key) {
		return temp.toLanguageTag() + ":" + key;
	}

	/**
	 * Computes the access url.
	 * <p>
	 * Following order will be used:
	 * <ol>
	 * <li>Use given locale</li>
	 * <li>Create more specific locale and repeat until default locale is
	 * reached.</li>
	 * </ul> </li>
	 * </ol>
	 * 
	 * @param locale
	 * @param packageName
	 * @param key
	 * @return
	 */
	private AccessPath computeAccessPath(Locale locale, String key) {

		Matcher valueMatcher = null;
		if (key != null && !key.equals("")) {
			key = Pattern.quote(key);
			Pattern valuePattern = Pattern.compile(key.toString());
			valueMatcher = valuePattern.matcher("");
			valueMatcher.reset();
		}

		AccessPath url = new AccessPath();
		int prio = 0;

		List<Locale> computedLocales = computeLocales(locale);

		for (Locale computedLocale : computedLocales) {
			Accessor accessor = new Accessor(computedLocale, ++prio, key,
					valueMatcher);
			url.addAccessor(accessor);
		}

		return url;
	}

	/**
	 * Computes the access url to find the best matching element. See
	 * {@link #computeAccessPath(IProject, Locale, String, String)}.
	 *
	 * @param locale
	 * @param packageName
	 * @param key
	 * @return
	 */
	private AccessPath computeBestMatchAccessPath(Locale locale, String key) {
		return computeAccessPath(locale, key);
	}

	/**
	 * Computes all locales that should be added to AccessPath
	 * 
	 * @param locale
	 * @return
	 */
	private List<Locale> computeLocales(Locale locale) {
		List<Locale> locales = new LinkedList<Locale>();

		// Add first locale
		locales.add(locale);

		Locale temp = locale;
		while (true) {
			String tag = temp.toLanguageTag();
			String[] segments = tag.split("-");
			if (segments.length > 1) {
				StringBuilder builder = new StringBuilder();
				for (int i = 0; i < segments.length - 1; i++) {
					if (builder.length() != 0) {
						builder.append("-");
					}
					builder.append(segments[i]);
				}
				Locale moreGeneral = Locale.forLanguageTag(builder.toString());
				locales.add(moreGeneral);
				temp = moreGeneral;
			} else {
				break;
			}
		}

		locales.add(new Locale(""));

		return locales;
	}

	private boolean isValid(String result) {
		return result != null && !result.equals("");
	}

	/**
	 * Add a new resource to the cache.
	 * 
	 * @param description
	 */
	public void addResource(URL url) {

		Properties properties = new Properties();
		try {
			properties.load(url.openStream());
		} catch (IOException e) {
			LOGGER.error("{}", e);
			return;
		}

		Locale locale = getLocale(url);
		if (locale == null) {
			LOGGER.error("Could not determine locale for " + url.toString());
			return;
		}

		ResourceDescription description = new ResourceDescription(locale, url,
				properties);
		addResource(description);
	}

	private Locale getLocale(URL url) {
		File file = null;
		try {
			file = new File(FileLocator.toFileURL(url).toURI());
		} catch (URISyntaxException e) {
			LOGGER.error("{}", e);
			return null;
		} catch (IOException e) {
			LOGGER.error("{}", e);
			return null;
		}

		String[] tokens = file.getName().replace(".properties", "").split("_");
		StringBuilder builder = new StringBuilder();
		if (tokens.length >= 2) {
			for (int i = 1; i < tokens.length; i++) {
				if (builder.length() > 0) {
					builder.append("-");
				}
				builder.append(tokens[i]);
			}
		}
		Locale locale = Locale.forLanguageTag(builder.toString());
		return locale;
	}

	/**
	 * Add a new resource to the cache.
	 * 
	 * @param description
	 */
	public void addResource(ResourceDescription description) {
		synchronized (cache) {
			if (!cache.containsKey(description.getLocale())) {
				cache.put(description.getLocale(),
						new ArrayList<ResourceDescription>());
			}

			List<ResourceDescription> descs = cache
					.get(description.getLocale());
			if (!descs.contains(description)) {
				descs.add(description);
			}
		}

		// clear the fast access cache
		fastAccess.clear();
	}

	private List<ResourceDescription> getResourceDescriptions(Locale locale) {
		if (cache.containsKey(locale)) {
			return new ArrayList<I18nRegistry.ResourceDescription>(
					cache.get(locale));
		} else {
			return Collections.emptyList();
		}
	}

	/**
	 * Removes the given translations.
	 * 
	 * @param locale
	 * @param url
	 */
	public void removeResource(Locale locale, URL url) {
		if (!cache.containsKey(locale)) {
			return;
		}

		// clear the fast access cache
		fastAccess.clear();

		// remove the resource from the builder
		synchronized (cache) {
			List<ResourceDescription> descs = cache.get(locale);
			for (Iterator<ResourceDescription> iterator = descs.iterator(); iterator
					.hasNext();) {
				ResourceDescription desc = iterator.next();
				if (desc.getURL().equals(url)) {
					iterator.remove();
				}
			}
		}
	}

	/**
	 * Removes the translations with the given URL.
	 * 
	 * @param url
	 */
	public void removeResource(URL url) {
		Locale locale = getLocale(url);
		if (locale == null) {
			LOGGER.error("Could not determine locale for " + url.toString());
			return;
		}
		removeResource(locale, url);

	}

	/**
	 * Defines how the registry should be searched. For instance the ordering of
	 * locales...
	 */
	private static class AccessPath {
		private List<Accessor> accessors = new LinkedList<I18nRegistry.Accessor>();

		public AccessPath() {
		}

		public void addAccessor(Accessor accessor) {
			accessors.add(accessor);
		}

		/**
		 * Returns the translation. The key matches the entire key for an i18n
		 * record.
		 * 
		 * @return
		 */
		public Translation getTranslation() {
			for (Accessor accessor : accessors) {
				Translation result = accessor.getTranslation();
				if (result != null) {
					return result;
				}
			}

			return null;
		}
	}

	/**
	 * This class will access the registry.
	 */
	private class Accessor {

		private final Locale locale;
		private final Matcher matcher;
		@SuppressWarnings("unused")
		private final String key;
		private final int prio;

		public Accessor(Locale locale, int prio, String key, Matcher matcher) {
			super();
			this.locale = locale;
			this.key = key;
			this.matcher = matcher;
			this.prio = prio;
		}

		/**
		 * Returns the first translation found. The matcher must match the given
		 * key.
		 * 
		 * @return
		 */
		public Translation getTranslation() {
			List<ResourceDescription> descs = getResourceDescriptions(locale);
			for (ResourceDescription desc : descs) {
				for (Map.Entry<Object, Object> entry : desc.getProperties()
						.entrySet()) {
					if (matcher.reset(((String) entry.getKey())).matches()) {
						if (isValid((String) entry.getValue())) {
							return new Translation((String) entry.getKey(),
									(String) entry.getValue(), desc, prio);
						}
					}
				}
			}
			return null;
		}
	}

	/**
	 * A resource is a file containing all translations for a file.
	 */
	public static class ResourceDescription {
		private final Locale locale;
		private final URL url;
		private final Properties properties;

		public ResourceDescription(Locale locale, URL url, Properties properties) {
			this.locale = locale;
			this.url = url;
			this.properties = properties;
		}

		public Locale getLocale() {
			return locale;
		}

		public URL getURL() {
			return url;
		}

		public Properties getProperties() {
			return properties;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((url == null) ? 0 : url.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			ResourceDescription other = (ResourceDescription) obj;
			if (url == null) {
				if (other.url != null)
					return false;
			} else if (!url.equals(other.url))
				return false;
			return true;
		}
	}

	public static class Translation {

		private final String i18nKey;
		private final String i18nValue;
		private final ResourceDescription resourceDescription;
		@SuppressWarnings("unused")
		private final int priority;

		public Translation(String i18nKey, String i18nValue,
				ResourceDescription resourceDescription, int priority) {
			super();
			this.i18nKey = i18nKey;
			this.i18nValue = i18nValue;
			this.resourceDescription = resourceDescription;
			this.priority = priority;
		}

		/**
		 * Returns the i18nKey.
		 * 
		 * @return
		 */
		public String getI18nKey() {
			return i18nKey;
		}

		/**
		 * Returns the i18nValue.
		 * 
		 * @return
		 */
		public String getI18nValue() {
			return i18nValue;
		}

		/**
		 * Returns the locale where key and value had been found.
		 * 
		 * @return
		 */
		public Locale getLocale() {
			return resourceDescription.getLocale();
		}

		/**
		 * Returns the resource description, where the i18n entry was contained.
		 * 
		 * @return
		 */
		public ResourceDescription getResourceDescription() {
			return resourceDescription;
		}

	}
}
