package org.jboss.resteasy.links.impl;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.persistence.Id;
import javax.xml.bind.annotation.XmlID;

import org.jboss.resteasy.links.ParentResource;
import org.jboss.resteasy.links.ResourceID;
import org.jboss.resteasy.links.ResourceIDs;

public class BeanUtils {

	private static final Class<Annotation>[] IdAnnotationList = new Class[]{ResourceID.class, XmlID.class, Id.class};

	public static List<Object> findIDs(Object entity){
		Class<? extends Object> klass = entity.getClass();
		ResourceIDs resourceIDs = findTypeAnnotation(klass, ResourceIDs.class);
		if(resourceIDs != null){
			// return those properties
			String[] names = resourceIDs.value();
			List<Object> values = new ArrayList<Object>();
			for (String name : names) {
				try {
					values.add(getPropertyValue(entity, entity.getClass(), name));
				} catch (NotFoundException e) {
					throw new RuntimeException("Failed to find bean property "+name);
				}
			}
			return values;
		}
		for(Class<Annotation> idAnnotationClass : IdAnnotationList){
			try {
				return Collections.singletonList(findAnnotatedProperty(entity, klass, idAnnotationClass));
			} catch (NotFoundException e) {
				// ignore
			}
		}
		// we got nothing
		return Collections.emptyList();
	}

	private static Object getPropertyValue(Object entity,
			Class<?> klass, String name) throws NotFoundException {
		// easiest is a public property:
		try {
			return readPropertyMethods(entity, klass, name, false);
		} catch (NotFoundException e) {
			// ignore
		}
		// not found, try private properties
		do{
			try {
				return readPropertyMethods(entity, klass, name, true);
			} catch (NotFoundException e) {
				// ignore
			}
			// try the field
			try {
				Field f = klass.getDeclaredField(name);
				return readField(f, entity);
			} catch (SecurityException e) {
				// there's one but it's not accessible?
				throw new RuntimeException("Failed to read property "+name, e);
			} catch (NoSuchFieldException e) {
				// ignore
			}
			// go up
			klass = klass.getSuperclass();
		}while(klass != null);
		// we got nothing
		throw new NotFoundException();
	}

	private static Object readPropertyMethods(Object entity, Class<?> klass,
			String propertyName, boolean b) throws NotFoundException {
		try {
			return readPropertyMethod(entity, klass, "is"+capitalise(propertyName), false);
		} catch (NotFoundException e) {
			// ignore
		}
		// let this one throw
		return readPropertyMethod(entity, klass, "get"+capitalise(propertyName), false);
	}

	private static Object readPropertyMethod(Object entity, Class<?> klass,
			String methodName, boolean declared) throws NotFoundException {
		try{
			Method getter = declared ? klass.getDeclaredMethod(methodName) : klass.getMethod(methodName);
			return readMethod(getter, entity);
		} catch (SecurityException e) {
			// there's one but it's not accessible?
			throw new RuntimeException("Failed to read property "+methodName, e);
		} catch (NoSuchMethodException e) {
			throw new NotFoundException();
		}
	}

	private static String capitalise(String name) {
		return name.substring(0, 1).toUpperCase() + name.substring(1);
	}

	private static <T extends Annotation> T findTypeAnnotation(Class<?> klass,
			Class<T> annotationClass) {
		do{
			if(klass.isAnnotationPresent(annotationClass))
				return klass.getAnnotation(annotationClass);
			klass = klass.getSuperclass();
		}while(klass != null);
		return null;
	}

	public static Object findParentResource(Object entity) {
		try {
			return findAnnotatedProperty(entity, entity.getClass(), ParentResource.class);
		} catch (NotFoundException e) {
			return null;
		}
	}

	private static Object findAnnotatedProperty(Object entity, Class<?> type, Class<? extends Annotation> annotation) throws NotFoundException {
		for(Field f : type.getDeclaredFields()){
			if(f.isAnnotationPresent(annotation)){
				return readField(f, entity);
			}
		}
		for(Method m : type.getDeclaredMethods()){
			if(m.isAnnotationPresent(annotation) && isBeanAccessor(m)){
				return readMethod(m, entity);
			}
		}
		if(type.getSuperclass() != null)
			return findAnnotatedProperty(entity, type.getSuperclass(), annotation);
		throw new NotFoundException();
	}

	private static Object readMethod(Method m, Object entity) {
		// read that property
		m.setAccessible(true);
		try {
			return m.invoke(entity);
		} catch (Exception e) {
			throw new RuntimeException("Failed to read property from method "+m.getName(), e);
		}finally{
			m.setAccessible(false);
		}
	}

	private static Object readField(Field f, Object entity) {
		// read that field
		f.setAccessible(true);
		try {
			return f.get(entity);
		} catch (Exception e) {
			throw new RuntimeException("Failed to read field "+f.getName(), e);
		}finally{
			f.setAccessible(false);
		}
	}

	private static boolean isBeanAccessor(Method m) {
		String name = m.getName();
		return (name.startsWith("get") || name.startsWith("is")) && m.getParameterTypes().length == 0;
	}
}
