package org.hamcrest.generator;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.StringTokenizer;

/**
 * {@link FactoryWriter} that outputs Java code which simply delegates all
 * Hamcrest factory methods to factory methods elsewhere. This is useful for
 * grouping lots of matcher classes into one class, so you only have to look
 * in one place for matchers.
 *
 * @author Joe Walnes
 * @see FactoryWriter
 */
public class HamcrestFactoryWriter implements FactoryWriter {

    private final PrintWriter output;
    private final String javaPackageName;
    private final String javaClassName;

    private String indentationString = "  ";
    private String newLine = "\n";

    private int indentation = 1;

    public HamcrestFactoryWriter(String javaPackageName, String javaClassName, Writer output) {
        this.javaPackageName = javaPackageName;
        this.javaClassName = javaClassName;
        this.output = new PrintWriter(output);
    }

    @Override
    public void writeHeader() throws IOException {
        output.append("// Generated source.").append(newLine)
                .append("package ").append(javaPackageName).append(';').append(newLine).append(newLine);
        output.append("public class ").append(javaClassName).append(" {").append(newLine).append(newLine);
    }

    @Override
    public void writeFooter() throws IOException {
        output.append('}').append(newLine);
    }

    @Override
    public void close() throws IOException {
        output.close();
    }

    @Override
    public void flush() throws IOException {
        output.flush();
    }

    @Override
    public void writeMethod(String generatedMethodName, FactoryMethod factoryMethodToDelegateTo)
            throws IOException {
        writeJavaDoc(factoryMethodToDelegateTo);
        indent();
        output.append("public static ");
        writeGenericTypeParameters(factoryMethodToDelegateTo);
        output.append(factoryMethodToDelegateTo.getReturnType());
        if (factoryMethodToDelegateTo.getGenerifiedType() != null) {
            output.append('<').append(factoryMethodToDelegateTo.getGenerifiedType()).append('>');
        }
        output.append(' ').append(generatedMethodName);
        writeParameters(factoryMethodToDelegateTo);
        writeExceptions(factoryMethodToDelegateTo);
        output.append(" {").append(newLine);
        indentation++;
        writeMethodBody(factoryMethodToDelegateTo);
        indentation--;
        indent();
        output.append('}').append(newLine).append(newLine);
    }

    private void writeGenericTypeParameters(FactoryMethod factoryMethod) {
        if (!factoryMethod.getGenericTypeParameters().isEmpty()) {
            output.append('<');
            boolean seenFirst = false;
            for (String type : factoryMethod.getGenericTypeParameters()) {
                if (seenFirst) {
                    output.append(", ");
                } else {
                    seenFirst = true;
                }
                output.append(type);
            }
            output.append("> ");
        }
    }

    private void writeMethodBody(FactoryMethod factoryMethod) {
        indent();
        output.append("return ").append(factoryMethod.getMatcherClass());
        output.append('.');

        // lets write the generic types
        if (!factoryMethod.getGenericTypeParameters().isEmpty()) {
            output.append('<');
            boolean seenFirst = false;
            for (String type : factoryMethod.getGenericTypeParameters()) {
                if (seenFirst) {
                    output.append(",");
                } else {
                    seenFirst = true;
                }
                // lets only print the first word of the type
                // so if its 'T extends Cheese' we just print T
                //output.append(type);

                StringTokenizer iter = new StringTokenizer(type);
                iter.hasMoreElements();
                output.append(iter.nextToken());
            }
            output.append(">");
        }

        output.append(factoryMethod.getName());
        output.append('(');
        boolean seenFirst = false;
        for (FactoryMethod.Parameter parameter : factoryMethod.getParameters()) {
            if (seenFirst) {
                output.append(", ");
            } else {
                seenFirst = true;
            }
            output.append(parameter.getName());
        }
        output.append(')');
        output.append(';').append(newLine);
    }

    private void writeExceptions(FactoryMethod factoryMethod) {
        boolean seenFirst = false;
        for (String exception : factoryMethod.getExceptions()) {
            if (seenFirst) {
                output.append(", ");
            } else {
                output.append(" throws ");
                seenFirst = true;
            }
            output.append(exception);
        }
    }

    private void writeParameters(FactoryMethod factoryMethod) {
        output.append('(');
        boolean seenFirst = false;
        for (FactoryMethod.Parameter parameter : factoryMethod.getParameters()) {
            if (seenFirst) {
                output.append(", ");
            } else {
                seenFirst = true;
            }
            output.append(parameter.getType()).append(' ').append(parameter.getName());
        }
        output.append(')');
    }

    private void writeJavaDoc(FactoryMethod factoryMethod) {
        if (factoryMethod.getJavaDoc() != null) {
            String[] lines = factoryMethod.getJavaDoc().split("\n");
            if (lines.length > 0) {
                indent();
                output.append("/**").append(newLine);
                for (String line : lines) {
                    indent();
                    output.append(" * ").append(line).append(newLine);
                }
                indent();
                output.append(" */").append(newLine);
            }
        }
    }

    private void indent() {
        for (int i = 0; i < indentation; i++) {
            output.append(indentationString);
        }
    }
}
