package de.brightbyte.yates;

import java.util.Iterator;
import java.util.Locale;
import java.util.Map;

import de.brightbyte.abstraction.Abstractor;
import de.brightbyte.util.StructuredDataCodec;

public class YatesParameters implements YatesScope {
	
	protected static class ParameterReference {
		public final String reference;

		public ParameterReference(final String reference) {
			this.reference = reference;
		}
		
		@Override
		public String toString() {
			return "$"+reference;
		}
	}
	
	public static final StructuredDataCodec.Lookup referenceLookup = new StructuredDataCodec.Lookup() {
		public Object get(String key) {
			return new ParameterReference(key);
		}
	};
	

	public static final Abstractor abstractor = new Abstractor<YatesParameters>(){
	
		public void setProperty(YatesParameters obj, String name, Object v) throws IllegalArgumentException {
			throw new UnsupportedOperationException();
		}
	
		public boolean isPropertyMutable(String name) {
			return false;
		}
	
		public Class<YatesParameters> getTargetType() {
			return YatesParameters.class;
		}
	
		public Class getPropertyType(String property) {
			return Object.class;
		}
	
		public Object getProperty(YatesParameters obj, String name) throws IllegalArgumentException {
			try {
				return obj.resolve(name);
			} catch (YatesException e) {
				throw new RuntimeException(e);
			}
		}

		public boolean hasProperty(YatesParameters obj, String name) {
			return obj.hasParameter(name);
		}
	
	};
	
	protected Map<String, Object> parameters;

	protected YatesScope parent;	
	protected Object subject;
	
	public YatesParameters(YatesScope parent, Map<String, Object> parameters, Object subject) {
		this(parent, parameters);
		setSubject(subject);
	}
	
	public YatesParameters(YatesScope parent, Map<String, Object> parameters) {
		if (parent==null) throw new NullPointerException();
		
		this.parent = parent;
		this.parameters = parameters;
	}
	
	@Override
	public String toString() {
		return parameters.toString();
	}

	/**
	 * @throws YatesException 
	 * @see de.brightbyte.yates.YatesScope#resolve(java.lang.String)
	 */
	public Object resolve(String name) throws YatesException {
		String[] path = null;
		int pofs = 0;
		
		int idx = name.indexOf('.');
		if (idx>=0 && name.length()>1) {
			path = name.split("\\.");
			name = path[0];
			pofs = 1;

			if (name.equals("")) name = ".";			
		}

		String n = "&" + name;
		Object v;
		
		if (name.equals(".")) {
			if (subject!=null) v = subject;
			else v = parent.getSubject();
		}
		else {
			if (parameters!=null && parameters.containsKey(n)) v = getParameter(n, null);
			else if (subject!=null && hasProperty(subject, name)) {
				v = getSubject();
				if (path==null) path = new String[] { name };
				name = ".";
				pofs = 0;
			}
			else v = parent.resolve(name);
		}
		
		if (path!=null && pofs<path.length) {
			if (v==null) throw new YatesException("can't dereference null: "+name);
			v = getContext().resolve(v, path, pofs); //HACK: fugly!
		}
		
		return v;
	}

	private boolean hasProperty(Object v, String n) {
		return getContext().hasProperty(v, n);
	}

	private YatesContext getContext() { //HACK: fugly!
		YatesScope p = parent;
		while (!(p instanceof YatesContext)) {
			p = ((YatesParameters)p).parent;
		}
		
		return (YatesContext)p;
	}

	public <T>T getParameter(String key, T def) throws YatesException {
		if (parameters!=null && parameters.containsKey(key)) {
			Object v = parameters.get(key);
			if (v!=null && v instanceof ParameterReference) {
				String r = ((ParameterReference)v).reference;
				v = parent.resolve(r); //NOTE: this always refers to the parent context!
			}
			return (T)v;
		}
		else return def;
	}
	
	public <T>T fallbackParameter(String key, T value) throws YatesException {
		if (value==null && parameters!=null) return getParameter(key, (T)null);
		else return value;
	}
	
	public boolean hasParameter(String key) {
		return parameters!=null && parameters.containsKey(key);
	}

	public Object requireParameter(String key, Object def) throws YatesException {
		if (parameters==null || !parameters.containsKey(key)) {
			if (def==null) throw new YatesException("missing required parameter ´"+key+"´");
			else return def;
		}
		
		Object v = parameters.get(key);
		if (v!=null && v instanceof ParameterReference) {
			String r = ((ParameterReference)v).reference;
			v = parent.resolve(r); //NOTE: this always refers to the parent context!
			
			if (v==null) 
				throw new YatesException("required parameter ´"+key+"´ was null: reference was $"+r);
		}
		else {
			if (v==null) 
				throw new YatesException("required parameter ´"+key+"´ was null");
		}
		
		return v;
	}

	public YatesMacro getMacro(String name) throws YatesException {
		Object v = resolve(name);

		if (v!=null && !(v instanceof YatesMacro)) {
			if (v instanceof String) return new LiteralMacro((String)v); 
			else throw new YatesException(name+" is not a macro: found "+v.getClass());
		}
		
		if (v==null) v = parent.getMacro(name); 
		return (YatesMacro)v;
	}

	public Object getSubject() throws YatesException {
		if (subject!=null) return subject;
		else return parent.getSubject();
	}

	public void setSubject(Object subject) {
		if (this.subject!=null) throw new IllegalStateException("subject already defined");
		this.subject = subject;
	}
	
	public Object getSetting(String name) throws YatesException {
		if (parameters!=null && parameters.containsKey("settings."+name)) { //XXX: HACK!
			return getParameter("settings."+name, null); 
		}
		
		return parent.getSetting(name);
	}

	public Locale getLocale() throws YatesException {
		return (Locale)getSetting("locale");
	}

	public StringMangler getEscaper() throws YatesException {
		return (StringMangler)getSetting("escaper");
	}

	public YatesMacro getNullRenderer() throws YatesException {
		return (YatesMacro)getSetting("nullRenderer");
	}

	public YatesMacro guessRenderer(Object subject) throws YatesException {
		return parent.guessRenderer(subject);
	}

	public Iterable<String> getParameterNames(final String prefix) {
		final Iterator<String> it = parameters.keySet().iterator();
		
		final Iterator<String> fit = new Iterator<String>(){
			protected String next = null;
			
			/*anon initializer*/ { 
				find();
			}
		
			protected void find() {
				while (true) {
					if (!it.hasNext()) {
						next = null;
						return;
					}
					
					next = it.next();
					if (next.startsWith("&")) continue;
					if (prefix!=null && !next.startsWith(prefix)) continue;
					
					return;
				} 
			}
			
			public void remove() {
				throw new UnsupportedOperationException();
			}
		
			public String next() {
				String n = next;
				find();
				return n;
			}
		
			public boolean hasNext() {
				return next!=null;
			}
		
		};
		
		return new Iterable<String>() {
		
			public Iterator<String> iterator() {
				return fit;
			}
		
		};
	}
}
