/*
 * Copyright (c) 2008-2009, Zepheira All rights reserved.
 * Copyright (c) 2011 Talis Inc., Some rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * - Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution. 
 * - Neither the name of the openrdf.org nor the names of its contributors may
 *   be used to endorse or promote products derived from this software without
 *   specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 */
package org.openrdf.repository.object.compiler;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.openrdf.annotations.Iri;
import org.openrdf.annotations.Prefix;
import org.openrdf.model.Model;
import org.openrdf.model.Resource;
import org.openrdf.model.URI;
import org.openrdf.model.impl.URIImpl;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.repository.object.exceptions.ObjectStoreConfigException;
import org.openrdf.repository.object.managers.LiteralManager;
import org.openrdf.repository.object.managers.RoleMapper;

/**
 * Resolves appropriate Java names from URIs.
 * 
 * @author James Leigh
 * 
 */
public class JavaNameResolver {
	private static final Set<String> KEYWORDS_SENSITIVE = new HashSet<String>(
			Arrays.asList(new String[] { "abstract", "continue", "for", "new",
					"switch", "assert", "default", "goto", "package",
					"synchronized", "boolean", "do", "if", "private", "this",
					"break", "double", "implements", "protected", "throw",
					"byte", "else", "import", "public", "throws", "case",
					"enum", "instanceof", "return", "transient", "catch",
					"extends", "int", "short", "try", "char", "final",
					"interface", "static", "void", "class", "finally", "long",
					"strictfp", "volatile", "const", "float", "native",
					"super", "while" }));
	private static final Set<String> KEYWORDS_INSENSITIVE = new HashSet<String>(
			Arrays.asList(new String[] { "CON", "PRN", "AUX", "CLOCK$",
					"NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6",
					"COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4",
					"LPT5", "LPT6", "LPT7", "LPT8", "LPT9" }));

	/** namespace -&gt; package */
	private Map<String, String> packages = new HashMap<String, String>();
	/** namespace -&gt; prefix */
	private Map<String, String> prefixes = new HashMap<String, String>();
	private Map<URI, URI> aliases = new HashMap<URI, URI>();
	private Map<String, String> implNames = new HashMap<String, String>();
	private Set<URI> ignore = new HashSet<URI>();
	private Model model;
	private RoleMapper roles;
	private LiteralManager literals;
	private ClassLoaderPackages cl;
	private Set<String> nouns;

	private static class ClassLoaderPackages extends ClassLoader {
		private Set<Package> namespacePackages;

		public ClassLoaderPackages(ClassLoader parent) {
			super(parent);
			namespacePackages = new HashSet<Package>();
			for (Package pkg : getPackages()) {
				if (pkg.isAnnotationPresent(Iri.class)) {
					namespacePackages.add(pkg);
				}
			}
		}

		public Set<Package> getNamespacePackages() {
			return namespacePackages;
		}
	}

	public JavaNameResolver() {
		this(Thread.currentThread().getContextClassLoader());
	}

	public JavaNameResolver(ClassLoader cl) {
		this.cl = new ClassLoaderPackages(cl);
		for (Package pkg : this.cl.getNamespacePackages()) {
			if (pkg.isAnnotationPresent(Prefix.class)) {
				String prefix = pkg.getAnnotation(Prefix.class).value();
				String ns = pkg.getAnnotation(Iri.class).value();
				bindPrefixToNamespace(prefix, ns);
			}
		}
	}

	/**
	 * If an attempt is made to convert Set property names to their plural form.
	 */
	public boolean isPluralForms() {
		return nouns != null;
	}

	public void setPluralForms(boolean enabled) {
		if (enabled && nouns == null) {
			nouns = new HashSet<String>();
		} else if (!enabled) {
			nouns = null;
		}
	}

	public void setLiteralManager(LiteralManager literals) {
		this.literals = literals;
	}

	public void setRoleMapper(RoleMapper roles) {
		this.roles = roles;
	}

	public void setModel(Model model) {
		this.model = model;
		if (nouns != null) {
			Set<String> localNames = new HashSet<String>();
			for (Resource subj : model.filter(null, RDF.TYPE, null).subjects()) {
				if (subj instanceof URI) {
					localNames.add(((URI) subj).getLocalName());
				}
			}
			for (String name : localNames) {
				if (name.matches("^[a-zA-Z][a-z]+$")) {
					nouns.add(name.toLowerCase());
				}
			}
		}
	}

	public void setImplNames(Map<String, String> implNames) {
		this.implNames.putAll(implNames);
	}

	public void assignAlias(URI name, URI alias) {
		aliases.put(name, alias);
	}

	public void assignAnonymous(URI name) {
		aliases.put(name, null);
	}

	public void ignoreExistingClass(URI name) {
		ignore.add(name);
	}

	public void bindPackageToNamespace(String packageName, String namespace) {
		packages.put(namespace, packageName(packageName));
	}

	public void bindPrefixToNamespace(String prefix, String namespace) {
		if (prefix == null || prefix.length() == 0) {
			prefixes.remove(namespace);
		} else {
			prefixes.put(namespace, prefix);
		}
	}

	public Collection<String> getRootPackages() {
		Set<String> set = new HashSet<String>();
		for (String pkg : packages.values()) {
			if (pkg.contains(".")) {
				set.add(pkg.substring(0, pkg.indexOf('.')));
			} else {
				set.add(pkg);
			}
		}
		return set;
	}

	public URI getType(URI name) {
		if (aliases.containsKey(name))
			return aliases.get(name);
		return name;
	}

	public boolean isAnonymous(URI name) {
		return getType(name) == null;
	}

	public boolean isJavaClass(String className) {
		try {
			synchronized (cl) {
				return Class.forName(className, true, cl) != null;
			}
		} catch (ClassNotFoundException e) {
			return false;
		}
	}

	public String getClassName(URI name) throws ObjectStoreConfigException {
		if (name == null)
			return Object.class.getName();
		if (!ignore.contains(name)) {
			Class javaClass = findJavaClass(name);
			if (javaClass != null) {
				// TODO support n-dimension arrays
				if (javaClass.isArray())
					return javaClass.getComponentType().getName() + "[]";
				if (javaClass.getPackage() != null)
					return javaClass.getName();
			}
		}
		if (!packages.containsKey(name.getNamespace()))
			throw new ObjectStoreConfigException("Unknown type: " + name);
		String pkg = getPackageName(name);
		String simple = word(name.getLocalName());
		if (pkg == null)
			return simple;
		return pkg + '.' + simple;
	}

	public String getAnnotationAttributeName(URI uri) throws ObjectStoreConfigException {
		if (!ignore.contains(uri)) {
			Method m = roles.findAnnotationMethod(uri);
			if (m != null)
				return m.getName();
		}
		return "value";
	}

	public boolean isCompiledAnnotation(URI name) {
		return roles.isRecordedAnnotation(name);
	}

	public boolean isCompiledAnnotationFunctional(URI name) {
		Method ann = roles.findAnnotationMethod(name);
		if (ann == null)
			return false;
		return !ann.getReturnType().isArray();
	}

	public boolean isAnnotationOfClasses(URI name) {
		Method m = roles.findAnnotationMethod(name);
		if (m == null)
			return false;
		Class<?> type = m.getReturnType();
		return type.equals(Class.class) || type.getComponentType() != null
				&& type.getComponentType().equals(Class.class);
	}

	public String getMethodName(URI name) {
		String ns = name.getNamespace();
		String localPart = name.getLocalName();
		if (prefixes.containsKey(ns))
			return word(getMemberPrefix(ns) + initcap(localPart));
		return word(localPart);
	}

	public String getBoundPackageName(String namespace) {
		return packages.get(namespace);
	}

	public String getPackageName(URI uri) {
		if (packages.containsKey(uri.getNamespace()))
			return packages.get(uri.getNamespace());
		Class javaClass = findJavaClass(uri);
		if (javaClass == null || javaClass.getPackage() == null)
			return null;
		return javaClass.getPackage().getName();
	}

	public String getSinglePropertyName(URI name) {
		return getMemberName(name);
	}

	public String getSingleParameterName(URI name) {
		return word(name.getLocalName());
	}

	public String getPluralParameterName(URI name) {
		String ns = name.getNamespace();
		String localPart = name.getLocalName();
		String plural = plural(localPart);
		if (model.contains(new URIImpl(ns + plural), null, null)) {
			plural = localPart;
		}
		return word(plural);
	}

	public String getMemberPrefix(String ns) {
		if (prefixes.containsKey(ns))
			return enc(prefixes.get(ns));
		return "";
	}

	public String getPluralPropertyName(URI name) {
		String ns = name.getNamespace();
		String localPart = name.getLocalName();
		String plural = plural(localPart);
		if (model.contains(new URIImpl(ns + plural), null, null)) {
			plural = localPart;
		}
		if (prefixes.containsKey(ns))
			return getMemberPrefix(ns) + initcap(plural);
		return enc(plural);
	}

	public String getSimpleName(URI name) {
		if ("".equals(name.getLocalName())) {
			String ns = name.getNamespace();
			if (ns.indexOf(':') == ns.length() - 1) {
				return word(ns.substring(0, ns.length() - 1));
			}
			return getSimpleName(new URIImpl(ns.substring(0, ns.length() - 1)));
		}
		return word(name.getLocalName());
	}

	private String getMemberName(URI name) {
		String ns = name.getNamespace();
		String localPart = name.getLocalName();
		if (prefixes.containsKey(ns))
			return getMemberPrefix(ns) + initcap(localPart);
		return enc(localPart);
	}

	private String packageName(String pkgName) {
		StringBuilder sb = new StringBuilder();
		String[] dots = pkgName.split("\\.");
		for (int i=0;i<dots.length;i++) {
			if (i>0) {
				sb.append('.');
			}
			sb.append(word(dots[i]));
		}
		return sb.toString();
	}

	private String word(String str) {
		String enc = enc(str);
		if (KEYWORDS_SENSITIVE.contains(enc))
			return "_" + enc;
		if (KEYWORDS_INSENSITIVE.contains(enc.toUpperCase()))
			return "_" + enc;
		char first = enc.charAt(0);
		if (!Character.isLetter(first) && '_' != first && '$' != first)
			return "_" + enc;
		return enc;
	}

	private String enc(String str) {
		if (str.length() == 0)
			return "_";
		char[] name = str.toCharArray();
		StringBuffer sb = new StringBuffer(name.length);
		for (int i = 0; i < name.length; i++) {
			if ('A' <= name[i] && name[i] <= 'Z') {
				sb.append(name[i]);
			} else if ('a' <= name[i] && name[i] <= 'z') {
				sb.append(name[i]);
			} else if ('0' <= name[i] && name[i] <= '9') {
				sb.append(name[i]);
			} else if ('$' == name[i]) {
				sb.append(name[i]);
			} else if ('*' == name[i]) {
				sb.append("Star");
			} else if ('#' == name[i]) {
				sb.append("Hash");
			} else {
				sb.append('_');
			}
		}
		return sb.toString();
	}

	private Class findJavaClass(URI uri) {
		if (uri.equals(RDF.XMLLITERAL))
			return literals.findClass(uri);
		Class klass = roles.findConcept(uri, cl);
		if (klass != null)
			return klass;
		klass = findLoadedMethod(uri);
		if (klass != null)
			return klass;
		Method m = roles.findAnnotationMethod(uri);
		if (m != null)
			return m.getDeclaringClass();
		return literals.findClass(uri);
	}

	private Class findLoadedMethod(URI URI) {
		if (cl == null)
			return null;
		String sn = getSimpleName(URI);
		for (Package pkg : cl.getNamespacePackages()) {
			String namespace = pkg.getAnnotation(Iri.class).value();
			if (URI.getNamespace().equals(namespace)) {
				try {
					synchronized (cl) {
						return Class.forName(pkg.getName() + '.' + sn, true, cl);
					}
				} catch (ClassNotFoundException e) {
					continue;
				}
			}
		}
		return null;
	}

	private String plural(String singular) {
		if (nouns == null) {
			return singular;
		} else if (singular.matches(".+[A-Z_-].*")
				&& !isNoun(singular.replaceAll(".*(?=[A-Z])|.*[_-]", ""))) {
			return singular;
		} else if (singular.endsWith("s") && !singular.endsWith("ss")) {
			return singular;
		} else if (singular.endsWith("ed")) {
			return singular;
		} else if (singular.endsWith("y") && (singular.length() > 1)) {
			char c = singular.charAt(singular.length() - 2);
			if (c == 'a' || c == 'o' || c == 'e' || c == 'u' || c == 'i') {
				return singular + "s";
			} else {
				return singular.substring(0, singular.length() - 1) + "ies";
			}
		} else if (singular.endsWith("s") || singular.endsWith("x")) {
			return singular + "es";
		} else {
			return singular + "s";
		}
	}

	/**
	 * If this is word is a thing in our repository it is a noun. An alternative
	 * is to use a wordnet database.
	 */
	private boolean isNoun(String word) {
		if (nouns == null)
			return false;
		return nouns.contains(word.toLowerCase());
	}

	private String initcap(String str) {
		if (str.length() == 0)
			return "";
		char[] name = str.toCharArray();
		StringBuffer sb = new StringBuffer(name.length);
		for (int i = 0; i < name.length; i++) {
			if (i == 0) {
				sb.append(Character.toUpperCase(name[i]));
			} else if (name[i] == '-' || name[i] == '.') {
				if (i == name.length - 1) {
					sb.append('_');
				} else {
					name[i + 1] = Character.toUpperCase(name[i + 1]);
				}
			} else {
				sb.append(name[i]);
			}
		}
		return enc(sb.toString());
	}
}