package com.hammurapi.common.concurrent;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import com.hammurapi.common.Context;
import com.hammurapi.convert.Converter;

public class LocalStringPropertySet extends	LocalAbstractPropertySet<String, String> {

	private static final String SETTER_PREFIX = "set";
	private static final int SETTER_PREFIX_LENGTH = SETTER_PREFIX.length();
	private static final String GETTER_PREFIX = "get";
	private static final int GETTER_PREFIX_LENGTH = GETTER_PREFIX.length();
	
	private String separator;
	private ClassLoader classLoader;
	private ReadWriteLock lock; 
	private Set<String> immutableClasses = new HashSet<String>();

	public LocalStringPropertySet(
			ExecutorService executorService,
			Converter converter, 
			Context context, 
			ClassLoader classLoader,
			PropertySet<String>... chain) {
		this(executorService, converter, context, "/", classLoader, chain);
	}
	
	public LocalStringPropertySet(
			ExecutorService executorService,
			Converter converter, 
			Context context,
			String separator,
			ClassLoader classLoader,
			PropertySet<String>... chain) {
		this(executorService, converter, context, separator, classLoader, new ReentrantReadWriteLock(), chain);
		this.separator = separator;
		this.classLoader = classLoader==null ? getClass().getClassLoader() : classLoader;
	}

	protected LocalStringPropertySet(
			ExecutorService executorService,
			Converter converter, 
			Context context,
			String separator,
			ClassLoader classLoader,
			ReadWriteLock lock,
			PropertySet<String>... chain) {
		super(executorService, converter, context, chain);
		this.separator = separator;
		this.classLoader = classLoader==null ? getClass().getClassLoader() : classLoader;
		try {
			Enumeration<URL> immutableResources = this.classLoader.getResources("META-INF/services/"+Immutable.class.getName());
			while (immutableResources.hasMoreElements()) {
				BufferedReader br = new BufferedReader(new InputStreamReader(immutableResources.nextElement().openStream()));
				String l;
				while ((l=br.readLine())!=null) {
					immutableClasses.add(l.trim());
				}
			}
		} catch (IOException e) {
			throw new PropertySetException("Cannot load list of immutable classes: "+e, e); 
		}
		this.lock = lock; 
	}

	@Override
	protected String buildPath(int startIdx, Object... elements) {
		StringBuilder sb = new StringBuilder();
		for (int i=startIdx; i < elements.length; ++i) {
			if (i>startIdx) {
				sb.append(separator);
			}
			sb.append(elements[i]);
		}
		return sb.toString();
	}

	@Override
	protected String buildPath(String prefix, Object... elements) {
		StringBuilder sb = new StringBuilder(prefix);
		for (int i=0; i < elements.length; ++i) {
			sb.append(separator);
			sb.append(elements[i]);
		}
		return sb.toString();
	}

	@Override
	protected String buildPath(String prefix, String suffix) {
		return prefix+separator+suffix;
	}

	@Override
	protected String[] tokenize(String path) {
		List<String> ret = new ArrayList<String>();
		for (int idx = path.indexOf(separator); idx!=-1; idx = path.indexOf(separator)) {
			ret.add(path.substring(0, idx));
			path = path.substring(idx+separator.length());
		}
		if (!path.isEmpty()) {
			ret.add(path);
		}
		return ret.toArray(new String[ret.size()]);
	}

	@Override
	public <T> T getAdapter(final Class<T> targetType, final Context context) {
		if (targetType.isInterface()) {
			InvocationHandler invocationHandler = new InvocationHandler() {

				@Override
				public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
					if (method.getDeclaringClass().equals(targetType)) {
						Class<?>[] parameterTypes = method.getParameterTypes();
						Class<?> returnType = method.getReturnType();
						String name = method.getName();
						if (name.startsWith(GETTER_PREFIX) && name.length()>GETTER_PREFIX.length() && parameterTypes.length==0 && !void.class.equals(returnType)) { // Getters
							String firstChar = name.substring(GETTER_PREFIX_LENGTH, GETTER_PREFIX_LENGTH+1).toLowerCase();
							String pName = name.length()>GETTER_PREFIX.length()+1 ? firstChar + name.substring(GETTER_PREFIX.length()+1) : firstChar;
							Object ret = get(pName, returnType);
							if (ret==null || returnType.isInstance(ret)) {
								return ret;
							}
							Object cRet = converter.convert(ret, returnType, context);
							if (cRet==null) {
								throw new PropertySetException("Cannot convert from "+ret+" to "+returnType);
							}
							return cRet;
						}
						
						if (name.startsWith(SETTER_PREFIX) && name.length()>SETTER_PREFIX.length() && parameterTypes.length==1 && void.class.equals(returnType)) { // Setters
							String firstChar = name.substring(SETTER_PREFIX_LENGTH, SETTER_PREFIX_LENGTH+1).toLowerCase();
							String pName = name.length()>SETTER_PREFIX.length()+1 ? firstChar + name.substring(SETTER_PREFIX.length()+1) : firstChar;
							set(pName, args[0]);
							return null;
						} 
						
						// Invoke invocable
						Object ret = LocalStringPropertySet.this.invoke(name, args);
						if (ret==null || returnType.isInstance(ret)) {
							return ret;
						}
						Object cRet = converter.convert(ret, returnType, context);
						if (cRet==null) {
							throw new PropertySetException("Cannot convert from "+ret+" to "+returnType);
						}
						return cRet;
					} 
					// Object methods.
					return method.invoke(proxy, args);
				}
				
			};
			return (T) Proxy.newProxyInstance(classLoader, new Class[] {targetType}, invocationHandler);
		}
		
		return null;
	}
	
	@Override
	protected <T> T version(T source) {
		if (source==null) {
			return source;
		}
		Class<? extends Object> sourceClass = source.getClass();
		if (immutableClasses.contains(sourceClass.getName())) {
			return source;
		}
		if (sourceClass.getAnnotation(Immutable.class)!=null) {
			return source;
		}
		Versionable<T> v = converter.convert(source, Versionable.class, context);
		if (v!=null) {
			return v.createVersion();
		}
		if (source instanceof Cloneable) {
			try {
				return (T) sourceClass.getMethod("clone").invoke(source);
			} catch (Exception e) {
				throw new PropertySetException("Cannot version "+source+" by cloning", e);
			}
		}
		
		return source; // Not immutable, not versionable, not cloneable - return AS IS.
	}

	@Override
	protected AbstractPropertySet<String, String> newInstance(String key) {
		PropertySet<String>[] subChain = Arrays.copyOf(shadows, shadows.length);
		for (int i=0; i<subChain.length; ++i) {
			subChain[i] = new StringSubSet(shadows[i], key, separator);
		}
		return new LocalStringPropertySet(
				executorService, 
				converter, 
				context, 
				separator, 
				classLoader, 
				lock, 
				subChain);
	}

	@Override
	public Lock readLock() {
		return lock.readLock();
	}
	
	@Override
	public Lock writeLock() {
		return lock.writeLock();
	}

	@Override
	public PropertySet<String> createVersion() {
		throw new UnsupportedOperationException();
	}
}
