package net.filebot.format;

import static net.filebot.util.ExceptionUtilities.*;

import java.lang.reflect.Method;
import java.util.AbstractMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.script.Bindings;

public class ExpressionBindings extends AbstractMap<String, Object> implements Bindings {

	protected final Object bindingBean;

	protected final Map<String, Method> bindings = new TreeMap<String, Method>(String.CASE_INSENSITIVE_ORDER);

	protected final Method undefined;

	public ExpressionBindings(Object bindingBean) {
		this.bindingBean = bindingBean;

		// get method bindings
		for (Method method : bindingBean.getClass().getMethods()) {
			Define define = method.getAnnotation(Define.class);

			if (define != null) {
				for (String name : define.value()) {
					Method existingBinding = bindings.put(name, method);

					if (existingBinding != null) {
						throw new IllegalArgumentException(String.format("Illegal binding {%s} on %s", name, method.getName()));
					}
				}
			}
		}

		// extract mapping that handles undefined bindings
		undefined = bindings.remove(Define.undefined);
	}

	protected boolean isUndefined(Object value) {
		return value == null || ExpressionFormatFunctions.isEmptyValue(null, value);
	}

	public Object getBindingBean() {
		return bindingBean;
	}

	@Override
	public Object get(Object key) {
		Method method = bindings.get(key);

		if (method != null) {
			try {
				Object value = method.invoke(bindingBean);
				if (!isUndefined(value)) {
					return value;
				}
				if (undefined != null) {
					return undefined.invoke(bindingBean, key); // invoke fallback method
				}
			} catch (Exception e) {
				// check InvocationTargetException cause
				if (e.getCause() instanceof BindingException) {
					throw (BindingException) e.getCause();
				}
				throw new BindingException(key, getRootCauseMessage(e), e);
			}
		}
		return null;
	}

	@Override
	public Object put(String key, Object value) {
		// bindings are immutable
		return null;
	}

	@Override
	public Object remove(Object key) {
		// bindings are immutable
		return null;
	}

	@Override
	public boolean containsKey(Object key) {
		return bindings.containsKey(key);
	}

	@Override
	public Set<String> keySet() {
		return bindings.keySet();
	}

	@Override
	public boolean isEmpty() {
		return bindings.isEmpty();
	}

	@Override
	public Set<Entry<String, Object>> entrySet() {
		Set<Entry<String, Object>> entrySet = new HashSet<Entry<String, Object>>();

		for (final String key : keySet()) {
			entrySet.add(new Entry<String, Object>() {

				@Override
				public String getKey() {
					return key;
				}

				@Override
				public Object getValue() {
					return get(key);
				}

				@Override
				public Object setValue(Object value) {
					return put(key, value);
				}
			});
		}

		return entrySet;
	}

}
