package org.hamcrest.generator;

import com.thoughtworks.qdox.model.DocletTag;
import com.thoughtworks.qdox.model.JavaClass;
import com.thoughtworks.qdox.model.JavaMethod;
import com.thoughtworks.qdox.model.JavaParameter;
import com.thoughtworks.qdox.model.Type;

import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;

/**
 * Wraps an existing sequence of FactoryMethods, and attempts to pull in
 * parameter names and JavaDoc (which aren't available using reflection) using
 * QDox.
 *
 * @see <a href="http://qdox.codehaus.org/">QDox</a>
 * @author Joe Walnes
 */
public class QDoxFactoryReader implements Iterable<FactoryMethod> {

    private final Iterable<FactoryMethod> wrapped;
    private final JavaClass classSource;

    private static final Pattern GENERIC_REGEX = Pattern.compile("<.*>");
    private static final Pattern VARARGS_REGEX = Pattern.compile("...", Pattern.LITERAL);

    public QDoxFactoryReader(Iterable<FactoryMethod> wrapped, QDox qdox, String className) {
        this.wrapped = wrapped;
        this.classSource = qdox.getClassByName(className);
    }

    public Iterator<FactoryMethod> iterator() {
        final Iterator<FactoryMethod> iterator = wrapped.iterator();
        return new Iterator<FactoryMethod>() {
            public boolean hasNext() {
                return iterator.hasNext();
            }

            public FactoryMethod next() {
                return enhance(iterator.next());
            }

            public void remove() {
                iterator.remove();
            }
        };
    }

    private FactoryMethod enhance(FactoryMethod factoryMethod) {
        JavaMethod methodSource = findMethodInSource(factoryMethod);
        if (methodSource != null) {
            factoryMethod.setJavaDoc(createJavaDocComment(methodSource));
            JavaParameter[] parametersFromSource
                    = methodSource.getParameters();
            List<FactoryMethod.Parameter> parametersFromReflection
                    = factoryMethod.getParameters();

            if (parametersFromReflection.size() == parametersFromSource.length) {
                for (int i = 0; i < parametersFromSource.length; i++) {
                    parametersFromReflection.get(i).setName(
                            parametersFromSource[i].getName());
                }
            }
        }
        return factoryMethod;
    }

    /**
     * Attempts to locate the source code for a specific method, by cross-referencing
     * the signature returned by reflection with the list of methods parsed by QDox.
     */
    private JavaMethod findMethodInSource(FactoryMethod factoryMethod) {
        // Note, this doesn't always work - it struggles with some kinds of generics.
        // This seems to cover most cases though.
        List<FactoryMethod.Parameter> params = factoryMethod.getParameters();
        Type[] types = new Type[params.size()];
        for (int i = 0; i < types.length; i++) {
            // QDox ignores varargs and generics, so we strip them out to help QDox.
            String type = params.get(i).getType();
            type = GENERIC_REGEX.matcher(type).replaceAll("");
            type = VARARGS_REGEX.matcher(type).replaceAll("");
            types[i] = new Type(type);
        }
        JavaMethod[] methods = classSource.getMethodsBySignature(factoryMethod.getName(), types, false);
        return methods.length == 1 ?  methods[0] : null;
    }

    /**
     * Reconstructs the JavaDoc as a string for a particular method.
     */
    private String createJavaDocComment(JavaMethod methodSource) {
        String comment = methodSource.getComment();
        DocletTag[] tags = methodSource.getTags();
        if ((comment == null || comment.trim().length() == 0) && tags.length == 0) {
            return null;
        }
        StringBuilder result = new StringBuilder();
        result.append(comment);
        result.append("\n\n");
        for (DocletTag tag : tags) {
            result.append('@').append(tag.getName())
                    .append(' ').append(tag.getValue())
                    .append('\n');
        }
        return result.toString();
    }

}
