package org.jboss.resteasy.links.impl;

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

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

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;

public class BeanUtils {
   @SuppressWarnings(value = "unchecked")
   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(Messages.MESSAGES.failedToFindBeanProperty(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(Messages.MESSAGES.failedToReadProperty(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(Messages.MESSAGES.failedToReadProperty(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(Messages.MESSAGES.failedToReadPropertyFromMethod(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(Messages.MESSAGES.failedToReadField(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;
   }
}
