package jeffaschenk.commons.parsers;

import javassist.*;
import javassist.bytecode.AnnotationsAttribute;
import jeffaschenk.commons.parsers.objects.ClassAnnotationData;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * Simple Annotation Parser Simple Parsers a Classes Annotations and returns a
 * Collection Map of those attributes and properties of the Object class. This
 * parser uses Javassist to perform introspection of the Java ByteCode.
 *
 * @author jeffaschenk@gmail.com
 * @version $Id: $
 */
public class AnnotationParser {

    // ***************************************
    // Logging
    /**
     * Constant <code>log</code>
     */
    protected static Log log = LogFactory.getLog(AnnotationParser.class);

    // *******************************************
    // ClassPool for Loading Compile time Classes
    ClassPool cp;

    /**
     * Prevent the Instantiation
     */
    public AnnotationParser() {
        cp = ClassPool.getDefault();
        cp.insertClassPath(new ClassClassPath(this.getClass()));
    }

    /**
     * Parse a Classes Annotations and return in an Named Value Association
     * Form, return Class, Field and Method Annotations.
     *
     * @param clazz a {@link java.lang.Class} object.
     * @return  {@link jeffaschenk.commons.parsers.objects.ClassAnnotationData} object.
     */
    public ClassAnnotationData parse(Class<?> clazz) {
        // ************************************
        // Obtain Our Class.
        CtClass cc = null;
        try {
            cc = cp.get(clazz.getName());
        } catch (javassist.NotFoundException nfe) {
            log.error("Class Not Found:[" + clazz.getName() + "], "
                    + nfe.getMessage() + ".");
        }
        // ************************************
        // Did we obtain the Javassist Object?
        if (cc == null) {
            return null;
        }

        // ************************************
        // Obtain the Class Annotations.
        ClassAnnotationData ad = new ClassAnnotationData(cc.getName());
        ad.setSimpleName(cc.getSimpleName());
        ad.setClassAnnotations(this.parseClassAnnotations(cc));
        ad.setFieldAnnotations(this.parseClassFieldAnnotations(cc));
        ad.setMethodAnnotations(this
                .parseClassMethodAnnotations(cc));

        // ************************************
        // Return the Association Map.
        return ad;
    }

    /**
     * Parse a Classes Annotations and return in an Named Value Association
     * Form, return Class Annotations.
     *
     * @param clazz a {@link java.lang.Class} object.
     * @return  {@link java.util.Map} object.
     */
    public Map<String, String> parseClassAnnotations(Class<?> clazz) {
        CtClass cc = null;
        try {
            cc = cp.get(clazz.getName());
            // ************************************
            // Return the Association Map.
            return parseClassAnnotations(cc);
        } catch (javassist.NotFoundException nfe) {
            log.error("Class Not Found:[" + clazz.getName() + "], "
                    + nfe.getMessage() + ".");
            return null;
        }
    }

    /**
     * Parse a Classes Annotations and return in an Named Value Association
     * Form, return Class, Field and Method Annotations.
     *
     * @param cc a {@link javassist.CtClass} object.
     * @return  {@link java.util.Map} object.
     */
    public Map<String, String> parseClassAnnotations(CtClass cc) {
        // *************************************
        // Initialize.
        Map<String, String> annotations = new HashMap<String, String>();
        // *************************************
        // Parse the Annotations for this Class.
        if (log.isTraceEnabled()) {
            log.trace("Class:[" + cc.getSimpleName() + "]");
        }

        // ***************************************************
        // Check for all Visible Annotations.
        AnnotationsAttribute attr = (AnnotationsAttribute) cc.getClassFile()
                .getAttribute(AnnotationsAttribute.visibleTag);
        if (attr != null) {
            for (javassist.bytecode.annotation.Annotation an : attr
                    .getAnnotations()) {
                // **********************************************
                // Check for any Annotation Members
                if (an.getMemberNames() != null) {
                    for (Object name : an.getMemberNames()) {
                        if (log.isTraceEnabled()) {
                            log.trace("*** Class Annotation:["
                                    + an.getTypeName()
                                    + "], Member Name:["
                                    + name
                                    + "], Value:["
                                    + an.getMemberValue(name.toString())
                                    .toString().replace("\042", "").trim() + "]");
                        }
                        // ************************************************
                        // Save Annotations in association Named Value Map.
                        annotations.put(an.getTypeName() + "."
                                + name.toString(), an.getMemberValue(
                                name.toString()).toString().replace("\042", "").trim());
                    } // End of Inner For Each Loop for Annotation Members
                } else {
                    if (log.isTraceEnabled()) {
                        log.trace("*** Class Annotation:[" + an.getTypeName()
                                + "]");
                    }
                    // ************************************************
                    // Save Annotations in association Named Value Map.
                    annotations.put(an.getTypeName(), "");
                }
            } // End of Outer For Loop for Visible Annotations
        } // End of Check for Visible Annotations.

        // ************************************
        // Return the Association Map.
        return annotations;
    }

    /**
     * Parse a Classes Annotations and return in an Named Value Association
     * Form, return Field Annotations.
     *
     * @param clazz a {@link java.lang.Class} object.
     * @return  {@link java.util.Map} object.
     */
    public Map<String, String> parseClassFieldAnnotations(Class<?> clazz) {
        CtClass cc = null;
        try {
            cc = cp.get(clazz.getName());
            // ************************************
            // Return the Association Map.
            return parseClassFieldAnnotations(cc);
        } catch (javassist.NotFoundException nfe) {
            log.error("Class Not Found:[" + clazz.getName() + "], "
                    + nfe.getMessage() + ".");
            return null;
        }
    }

    /**
     * Parse a Classes Annotations and return in an Named Value Association
     * Form, return Field Annotations.
     *
     * @param cc a {@link javassist.CtClass} object.
     * @return  {@link java.util.Map} object.
     */
    public Map<String, String> parseClassFieldAnnotations(CtClass cc) {
        // *************************************
        // Initialize.
        Map<String, String> annotations = new HashMap<String, String>();
        // *******************************************
        // Parse the Field Annotations for this Class.

        CtField[] fields = cc.getDeclaredFields();
        if (fields == null) {
            return annotations;
        }
        for (CtField field : fields) {
            // ******************************************************
            // Skip standard serialVersionUID Fields.
            if (field.getName().equalsIgnoreCase("serialVersionUID")) {
                continue;
            }
            if (log.isTraceEnabled()) {
                log.trace("*** Field:[" + field.getName() + "]");
            }
            // ***************************************************
            // Check for all Visible Field Annotations.
            AnnotationsAttribute attr = (AnnotationsAttribute) field
                    .getFieldInfo().getAttribute(
                            AnnotationsAttribute.visibleTag);
            if (attr != null) {
                for (javassist.bytecode.annotation.Annotation an : attr
                        .getAnnotations()) {
                    // **********************************************
                    // Check for any Annotation Members
                    if (an.getMemberNames() != null) {
                        for (Object name : an.getMemberNames()) {
                            if (log.isTraceEnabled()) {
                                log.trace("*** Field:["
                                        + field.getName()
                                        + "] Annotation:["
                                        + an.getTypeName()
                                        + "], Member Name:["
                                        + name
                                        + "], Value:["
                                        + an.getMemberValue(name.toString())
                                        .toString().replace("\042", "").trim() + "]");
                            }
                            // ************************************************
                            // Save Annotations in association Named Value Map.
                            annotations.put(field.getName() + ":" + an.getTypeName() + "."
                                    + name.toString(), an.getMemberValue(
                                    name.toString()).toString().replace("\042", "").trim());
                        } // End of Inner For Each Loop for Annotation
                        // Members
                    } else {
                        if (log.isTraceEnabled()) {
                            log
                                    .trace("*** Field:[" + field.getName()
                                            + "] Annotation:["
                                            + an.getTypeName() + "]");
                        }
                        // ************************************************
                        // Save Annotations in association Named Value Map.
                        annotations.put(an.getTypeName(), "");
                    }
                } // End of Outer For Loop for Visible Annotations
            } // End of Check for Visible Annotations.

        } // End of Fields For Each Loop.

        // ************************************
        // Return the Association Map.
        return annotations;
    }

    /**
     * Parse a Classes Annotations and return in an Named Value Association
     * Form, return Method Annotations.
     *
     * @param clazz a {@link java.lang.Class} object.
     * @return  {@link java.util.Map} object.
     */
    public Map<String, String> parseClassMethodAnnotations(Class<?> clazz) {
        CtClass cc = null;
        try {
            ClassPool cp = new ClassPool();
            cc = cp.get(clazz.getName());
            // ************************************
            // Return the Association Map.
            return parseClassMethodAnnotations(cc);
        } catch (javassist.NotFoundException nfe) {
            log.error("Class Not Found:[" + clazz.getName() + "], "
                    + nfe.getMessage() + ".");
            return null;
        }
    }

    /**
     * Parse a Classes Annotations and return in an Named Value Association
     * Form, return Method Annotations.
     *
     * @param cc a {@link javassist.CtClass} object.
     * @return  {@link java.util.Map} object.
     */
    public Map<String, String> parseClassMethodAnnotations(CtClass cc) {
        // *************************************
        // Initialize.
        Map<String, String> annotations = new HashMap<String, String>();
        // *******************************************
        // Parse the Field Annotations for this Class.

        CtMethod[] methods = cc.getDeclaredMethods();
        if (methods == null) {
            return annotations;
        }
        for (CtMethod method : methods) {
            if (log.isTraceEnabled()) {
                log.trace("*** Method:[" + method.getName() + "]");
            }
            // ***************************************************
            // Check for all Visible Field Annotations.
            AnnotationsAttribute attr = (AnnotationsAttribute) method
                    .getMethodInfo().getAttribute(
                            AnnotationsAttribute.visibleTag);
            if (attr != null) {
                for (javassist.bytecode.annotation.Annotation an : attr
                        .getAnnotations()) {
                    // **********************************************
                    // Check for any Annotation Members
                    if (an.getMemberNames() != null) {
                        for (Object name : an.getMemberNames()) {
                            if (log.isTraceEnabled()) {
                                log.trace("*** Method:["
                                        + method.getName()
                                        + "] Annotation:["
                                        + an.getTypeName()
                                        + "], Member Name:["
                                        + name
                                        + "], Value:["
                                        + an.getMemberValue(name.toString())
                                        .toString().replace("\042", "").trim() + "]");
                            }
                            // ************************************************
                            // Save Annotations in association Named Value Map.
                            annotations.put(method.getName() + ":" + an.getTypeName() + "."
                                    + name.toString(), an.getMemberValue(
                                    name.toString()).toString().replace("\042", "").trim());
                        } // End of Inner For Each Loop for Annotation
                        // Members
                    } else {
                        if (log.isTraceEnabled()) {
                            log
                                    .trace("*** Method:[" + method.getName()
                                            + "] Annotation:["
                                            + an.getTypeName() + "]");
                        }
                        // ************************************************
                        // Save Annotations in association Named Value Map.
                        annotations.put(an.getTypeName(), "");
                    }
                } // End of Outer For Loop for Visible Annotations
            } // End of Check for Visible Annotations.

        } // End of Fields For Each Loop.

        // ************************************
        // Return the Association Map.
        return annotations;
    }

    /**
     * Parse a Classes Annotations and return in an Named Value Association
     * Form, return Method Annotations.
     *
     * @param method a {@link java.lang.reflect.Method} object.
     * @return  {@link java.util.Map} object.
     */
    public Map<String, String> parseSpecificClassMethodAnnotations(Method method) {
        // *************************************
        // Initialize.
        Map<String, String> annotations = new HashMap<String, String>();
        // ************************************
        // Obtain Our Class.
        CtClass cc = null;
        try {
            cc = cp.get(method.getDeclaringClass().getName());
        } catch (javassist.NotFoundException nfe) {
            log.error("Class Not Found:[" + method.getDeclaringClass().getName() + "], "
                    + nfe.getMessage() + ".");
        }
        // ************************************
        // Did we obtain the Javassist Object?
        if (cc == null) {
            return null;
        }
        // *******************************************
        // Parse the Annotations for this Method.
        CtMethod[] methods = cc.getDeclaredMethods();
        if (methods == null) {
            return annotations;
        }
        for (CtMethod ctmethod : methods) {
            if (log.isTraceEnabled()) {
                log.trace("*** Method:[" + method.getName() + "]");
            }
            // ***************************************************
            // Check for all Visible Field Annotations.
            AnnotationsAttribute attr = (AnnotationsAttribute) ctmethod
                    .getMethodInfo().getAttribute(
                            AnnotationsAttribute.visibleTag);
            if (attr != null) {
                for (javassist.bytecode.annotation.Annotation an : attr
                        .getAnnotations()) {
                    // **********************************************
                    // Check for any Annotation Members
                    if (an.getMemberNames() != null) {
                        for (Object name : an.getMemberNames()) {
                            if (log.isTraceEnabled()) {
                                log.trace("*** Method:["
                                        + method.getName()
                                        + "] Annotation:["
                                        + an.getTypeName()
                                        + "], Member Name:["
                                        + name
                                        + "], Value:["
                                        + an.getMemberValue(name.toString())
                                        .toString().replace("\042", "").trim() + "]");
                            }
                            // ************************************************
                            // Save Annotations in association Named Value Map.
                            annotations.put(method.getName() + ":" + an.getTypeName() + "."
                                    + name.toString(), an.getMemberValue(
                                    name.toString()).toString().replace("\042", "").trim());
                        } // End of Inner For Each Loop for Annotation
                        // Members
                    } else {
                        if (log.isTraceEnabled()) {
                            log
                                    .trace("*** Method:[" + method.getName()
                                            + "] Annotation:["
                                            + an.getTypeName() + "]");
                        }
                        // ************************************************
                        // Save Annotations in association Named Value Map.
                        annotations.put(an.getTypeName(), "");
                    }
                } // End of Outer For Loop for Visible Annotations
            } // End of Check for Visible Annotations.

        } // End of Fields For Each Loop.

        // ************************************
        // Return the Association Map.
        return annotations;
    }

}
