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 jakarta.persistence.Id;
import jakarta.xml.bind.annotation.XmlID;

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;

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.getParameterCount() == 0;
    }
}
