/*
 * Copyright (c) 2004-2017, by Alex Leigh.
 * All rights reserved.
 *
 * THIS IS UNPUBLISHED PROPRIETARY SOURCE CODE
 * The copyright notice above does not evidence any
 * actual or intended publication of such source code.
 */

package lc.mecha.lang;

import lc.mecha.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * The Introspector is a coordinating utility class that provides access into
 * objects at runtime using the Java introspection/reflection functionality. The
 * implementation in this class is a foundation of the dynamic object system
 * present in the ashpool codebase.
 *
 * @author Alex Leigh
 */
public class Introspector {
    private final static Logger logger = LoggerFactory.getLogger(Introspector.class);

    public static Object findByParameter(final List list, final String parameter, final String value) {
        for (final Object o : list) {
            final Object v = Introspector.getValueInObject(o, parameter);
            if (v != null) if (v.equals(value)) return o;
        }
        return null;
    }

    /**
     * This method will search the provided object for the provided annotation
     * class, which is expected to be associated with a property of the given
     * name.
     *
     * @param property   The name of the property in the object.
     * @param object     The object.
     * @param annotation The annotation class.
     * @return The annotation (if present in the object), else null.
     */
    public static Annotation getAnnotationForProperty(final String property,
                                                      final Class<?> object, final Class annotation) {
        // First check for a field.
        try {
            final Field f = object.getField(property);
            final Annotation a = f.getAnnotation(annotation);
            if (a != null) {
                return a;
            }
        } catch (Exception e) {
            // Do nothing
        }

        // If not, then try the methods for getters
        final Method[] methods = object.getMethods();
        for (final Method m : methods) {
            if (m.getName().equals(buildEtterName("get", property))) {
                // Matching method
                final Annotation a = m.getAnnotation(annotation);
                if (a != null) {
                    return a;
                }
            }
        }

        return null;
    }

    /**
     * For the given prefix and propety title, returns the name of the
     * appropriate 'etter method.
     *
     * @param prefix   The prefix: get/is/set
     * @param property The name of the property.
     * @return The method name
     */
    private static String buildEtterName(final String prefix, final String property) {
        return prefix + StringUtil.capitalize(property);
    }

    /**
     * Return a map of all the properties and their types present in the given
     * class. The properties are provided if they are accessible as fields or if
     * they have accessible getters.
     *
     * @param c
     * @return
     */
    private static Map<String, Class<?>> getProperties(final Class<?> c) {
        final HashMap<String, Class<?>> props = new HashMap<>();

        for (final Field f : c.getFields()) {
            logger.debug("Determined field: " + f.getName());
            props.put(f.getName(), f.getType());
        }

        for (final Method m : c.getMethods()) {
            final String n = m.getName();

            logger.debug("Evaluating as potential field: " + n);

            if (n.length() > 3 && !n.equals("getClass") && !n.equals("getName")
                    && n.substring(0, 3).equals("get")) {
                logger.debug("Determined field: " + n);
                props.put(StringUtil.decapitalize(n.substring(3)), m
                        .getReturnType());
            }
        }
        return props;
    }

    /**
     * Return all of the property names for the given class. The method provides
     * the names of properties that are accessible either directly or via a
     * getter.
     * <p>
     * If the provided object is a
     * <code>Map</code>, then all of it's non-null keys
     * are returned along with the class that their containing data happens to be.
     * <p>
     * If the passed object is both a DynamicObject and a Map only the Map's
     * properties will be returned.
     */
    public static Map<String, Class<?>> getProperties(final Object o) {
        if (o instanceof Map<?, ?>) {
            final Map<String, Class<?>> props = new HashMap<>();
            for (final Object key : ((Map<?, ?>) o).keySet()) {

                props.put(key.toString(), ((Map<?, ?>) o).get(key).getClass());
            }
            return props;
        }

        return getProperties(o.getClass());
    }

    /**
     * This method tries to set the provided value in the provided object. It
     * uses several strategies in a guaranteed order;
     * <p>
     * 1) It tries to find an etter method with the correct signature. For
     * example for the property "foo", "setFoo()". If found the etter will be
     * used.
     * <p>
     * 2) It tries to find an accessible (e.g., public) variable in the object
     * matching the property name. If found, the value will be set directly.
     * <p>
     * 3) If the object is a <code>DynamicObject</code>, the method stores
     * the value using the <code>setUnboundKey()</code> method.
     * <p>
     * 4) If the object is a <code>Map</code>, the method stores the value by
     * adding it to the map.
     */
    @SuppressWarnings("unchecked")
    public static void setValueInObject(final Object o, final String property, final Object value) {
        // Check to see if we have a compound path; if we do then we are going
        // to
        // strip off the leading value and call that property with the
        // remainder.

        // logger.info(o + " " + property + " " + value);

        final String[] path = property.split("\\.", 2);

        if (path.length == 2) {
            // Get the target (without the remainder)
            final Object target;

            target = o instanceof DynamicObject ? ((DynamicObject) o).get(path[0]) : getValueInObject(o, path[0]);

            if (target == null) {
                throw new RuntimeException("Invalid property path.");
            } else {
                // With the remainder
                // logger.info("Calling setValueInObject for child");

                if (target instanceof DynamicObject) {
                    ((DynamicObject) target).set(path[1], value);
                } else {
                    setValueInObject(target, path[1], value);
                }
            }
            return;
        }

        final Class<?> c = o.getClass();
        final Class<?>[] args;

        args = value == null ? new Class[]{Object.class} : new Class[]{value.getClass()};

        // Attempt to find a setter who's argument matches the provided
        // value's class perfectly
        try {
            c.getMethod(buildEtterName("set", property), args).invoke(o,
                    value);
            return;
        } catch (Exception e) {
            // e.printStackTrace();
        }

        try {
            final Field f = c.getField(property);
            f.set(o, value);
            return;
        } catch (Exception e) {
            // e.printStackTrace();
        }

        if (o instanceof DynamicObject) {
            ((DynamicObject) o).setUnbound(property, value);
            return;
        }

        if (o instanceof Map<?, ?>) {
            ((Map<String, Object>) o).put(property, value);
            return;
        }

        throw new RuntimeException("No set strategy for " + property + " in " + o);
    }

    /**
     * Fetch the value for the given property out of the provided object. The
     * method will attempt several approaches to retrieve the value;
     * <p>
     * 1) Attempt to get the value using an etter; for example if the properity
     * name is foo than getFoo() will be called.
     * <p>
     * 2) Attempt to locate a public variable with the same-name as the
     * property.
     *
     * @param o
     * @param property
     * @return
     */

    public static Object getValueInObject(final Object o, final String property) {
        // Check to see if we have a compound path; if we do then we are going
        // to
        // strip off the leading value and call that property with the
        // remainder.

        final String[] path = property.split("\\.", 2);
        if (path.length == 2) {
            // Get the target (without the remainder)
            final Object target;
            target = o instanceof DynamicObject ? ((DynamicObject) o).get(path[0]) : getValueInObject(o, path[0]);

            if (target == null) {
                return null;
            } else {
                // With the remainder
                return target instanceof DynamicObject ? ((DynamicObject) target).get(path[1]) : getValueInObject(target, path[1]);
            }
        }

        final Class<?> c = o.getClass();
        final Class<?>[] args = new Class[0];

        try {
            final Method m = c.getMethod(buildEtterName("get", property), args);
            final Object result = m.invoke(o);
            return result;
        } catch (Exception e) {
            // Do nothing
        }

        try {
            final Method m = c.getMethod(buildEtterName("is", property), args);

            return m.invoke(o);
        } catch (Exception e) {
            // Do nothing
        }

        try {
            logger.info("Attempting field case");
            final Field f = c.getField(property);
            return f.get(o);
        } catch (Exception e) {
            // Do nothing
        }

        if (o instanceof DynamicObject) {
            return ((DynamicObject) o).unbound(property);
        }

        if (o instanceof Map<?, ?>) {
            return ((Map<?, ?>) o).get(property);
        }

        throw new RuntimeException("Get Failed for " + property + " in " + o);
    }
}
