/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.fusesource.hawtbuf.proto.compiler;

import static org.fusesource.hawtbuf.proto.WireFormat.WIRETYPE_FIXED32;
import static org.fusesource.hawtbuf.proto.WireFormat.WIRETYPE_FIXED64;
import static org.fusesource.hawtbuf.proto.WireFormat.WIRETYPE_LENGTH_DELIMITED;
import static org.fusesource.hawtbuf.proto.WireFormat.WIRETYPE_VARINT;
import static org.fusesource.hawtbuf.proto.WireFormat.makeTag;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.StringTokenizer;

import org.fusesource.hawtbuf.proto.compiler.parser.ParseException;
import org.fusesource.hawtbuf.proto.compiler.parser.ProtoParser;

public class AltJavaGenerator {

    private File out = new File(".");
    private File[] path = new File[]{new File(".")};

    private ProtoDescriptor proto;
    private String javaPackage;
    private String outerClassName;
    private PrintWriter w;
    private int indent;
    private ArrayList<String> errors = new ArrayList<String>();
    private boolean multipleFiles;
    private boolean auto_clear_optional_fields;

    private String factoryFeild = "FACTORY";
    private String getterCN = "Getter";
    private String beanCN = "Bean";
    private String bufferCN = "Buffer";

    public static void main(String[] args) {

        AltJavaGenerator generator = new AltJavaGenerator();
        args = CommandLineSupport.setOptions(generator, args);

        if (args.length == 0) {
            System.out.println("No proto files specified.");
        }
        for (int i = 0; i < args.length; i++) {
            try {
                System.out.println("Compiling: " + args[i]);
                generator.compile(new File(args[i]));
            } catch (CompilerException e) {
                System.out.println("Protocol Buffer Compiler failed with the following error(s):");
                for (String error : e.getErrors()) {
                    System.out.println("");
                    System.out.println(error);
                }
                System.out.println("");
                System.out.println("Compile failed.  For more details see error messages listed above.");
                return;
            }
        }

    }

    interface Closure {
        void execute() throws CompilerException;
    }

    public void compile(File file) throws CompilerException {

        // Parse the proto file
        FileInputStream is = null;
        try {
            is = new FileInputStream(file);
            ProtoParser parser = new ProtoParser(is);
            proto = parser.ProtoDescriptor();
            proto.setName(file.getName());
            loadImports(proto, file.getParentFile());
            proto.validate(errors);
        } catch (FileNotFoundException e) {
            errors.add("Failed to open: " + file.getPath() + ":" + e.getMessage());
        } catch (ParseException e) {
            errors.add("Failed to parse: " + file.getPath() + ":" + e.getMessage());
        } finally {
            try {
                is.close();
            } catch (Throwable ignore) {
            }
        }

        if (!errors.isEmpty()) {
            throw new CompilerException(errors);
        }

        // Load the options..
        javaPackage = javaPackage(proto);
        outerClassName = javaClassName(proto);
//        optimizeFor = getOption(proto.getOptions(), "optimize_for", "SPEED");
        multipleFiles = isMultipleFilesEnabled(proto);
//        deferredDecode = Boolean.parseBoolean(getOption(proto.getOptions(), "deferred_decode", "false"));
        auto_clear_optional_fields = Boolean.parseBoolean(getOption(proto.getOptions(), "auto_clear_optional_fields", "false"));

        if (multipleFiles) {
            generateProtoFile();
        } else {
            writeFile(outerClassName, new Closure() {
                public void execute() throws CompilerException {
                    generateProtoFile();
                }
            });
        }

        if (!errors.isEmpty()) {
            throw new CompilerException(errors);
        }

    }

    private void writeFile(String className, Closure closure) throws CompilerException {
        PrintWriter oldWriter = w;
        // Figure out the java file name..
        File outputFile = out;
        if (javaPackage != null) {
            String packagePath = javaPackage.replace('.', '/');
            outputFile = new File(outputFile, packagePath);
        }
        outputFile = new File(outputFile, className + ".java");

        // Start writing the output file..
        outputFile.getParentFile().mkdirs();

        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(outputFile);
            w = new PrintWriter(fos);
            closure.execute();
            w.flush();
        } catch (FileNotFoundException e) {
            errors.add("Failed to write to: " + outputFile.getPath() + ":" + e.getMessage());
        } finally {
            try {
                fos.close();
            } catch (Throwable ignore) {
            }
            w = oldWriter;
        }
    }

    private void loadImports(ProtoDescriptor proto, File protoDir) {
        LinkedHashMap<String, ProtoDescriptor> children = new LinkedHashMap<String, ProtoDescriptor>();
        for (String imp : proto.getImports()) {
            File file = new File(protoDir, imp);
            for (int i = 0; i < path.length && !file.exists(); i++) {
                file = new File(path[i], imp);
            }
            if (!file.exists()) {
                errors.add("Cannot load import: " + imp);
            }

            FileInputStream is = null;
            try {
                is = new FileInputStream(file);
                ProtoParser parser = new ProtoParser(is);
                ProtoDescriptor child = parser.ProtoDescriptor();
                child.setName(file.getName());
                loadImports(child, file.getParentFile());
                children.put(imp, child);
            } catch (ParseException e) {
                errors.add("Failed to parse: " + file.getPath() + ":" + e.getMessage());
            } catch (FileNotFoundException e) {
                errors.add("Failed to open: " + file.getPath() + ":" + e.getMessage());
            } finally {
                try {
                    is.close();
                } catch (Throwable ignore) {
                }
            }
        }
        proto.setImportProtoDescriptors(children);
    }


    private void generateProtoFile() throws CompilerException {
        if (multipleFiles) {
            for (EnumDescriptor value : proto.getEnums().values()) {
                final EnumDescriptor o = value;
                String className = uCamel(o.getName());
                writeFile(className, new Closure() {
                    public void execute() throws CompilerException {
                        generateFileHeader();
                        generateEnum(o);
                    }
                });
            }
            for (MessageDescriptor value : proto.getMessages().values()) {
                final MessageDescriptor o = value;
                String className = uCamel(o.getName());
                writeFile(className, new Closure() {
                    public void execute() throws CompilerException {
                        generateFileHeader();
                        generateMessageBean(o);
                    }
                });
            }

        } else {
            generateFileHeader();

            p("public class " + outerClassName + " {");
            indent();

            for (EnumDescriptor enumType : proto.getEnums().values()) {
                generateEnum(enumType);
            }
            for (MessageDescriptor m : proto.getMessages().values()) {
                generateMessageBean(m);
            }

            unindent();
            p("}");
        }
    }

    private void generateFileHeader() {
        p("//");
        p("// Generated by protoc, do not edit by hand.");
        p("//");
        if (javaPackage != null) {
            p("package " + javaPackage + ";");
            p("");
        }
    }

    private void generateMessageBean(MessageDescriptor m) {

        String type = uCamel(m.getName());
        p();

        String staticOption = "static ";
        if (multipleFiles && m.getParent() == null) {
            staticOption = "";
        }

        p(staticOption + "public class " + type + " implements org.fusesource.hawtbuf.proto.PBMessageFactory<" + qualified(type, beanCN) + ", " + qualified(type, bufferCN) + "> {");
        p();
        indent();

        for (EnumDescriptor enumType : m.getEnums().values()) {
            generateEnum(enumType);
        }

        // Generate the Nested Messages.
        for (MessageDescriptor subMessage : m.getMessages().values()) {
            generateMessageBean(subMessage);
        }

        // Generate the Group Messages
        for (FieldDescriptor field : m.getFields().values()) {
            if (field.isGroup()) {
                generateMessageBean(field.getGroup());
            }
        }

        p("public static final " + type + " "+factoryFeild+" = new " + type + "();");
        p("public static final org.fusesource.hawtbuf.proto.PBMessageFramedCodec<"+bufferCN+"> FRAMED_CODEC = new org.fusesource.hawtbuf.proto.PBMessageFramedCodec<"+bufferCN+">("+factoryFeild+");");
        p("public static final org.fusesource.hawtbuf.proto.PBMessageUnframedCodec<"+bufferCN+"> UNFRAMED_CODEC = new org.fusesource.hawtbuf.proto.PBMessageUnframedCodec<"+bufferCN+">("+factoryFeild+");");
        p();

        p("public " + beanCN + " create()  {");
        indent();
        p("return new " + beanCN + "();");
        unindent();
        p("}");
        p();
        generateMethodParseFrom(m, bufferCN, beanCN);

        // Generate the field getters
        String gettrsExtendsClause = " extends org.fusesource.hawtbuf.proto.PBMessage<" + qualified(type, beanCN) + ", " + qualified(type, bufferCN) + ">";
        for (EnumFieldDescriptor enumFeild : m.getAssociatedEnumFieldDescriptors()) {
            String name = uCamel(enumFeild.getParent().getName());
            name = name + "." + name + "Creatable";
            gettrsExtendsClause += ", " + name;
        }
        p(staticOption + "public interface " + getterCN + gettrsExtendsClause + " {");
        p();
        indent();
        {
            for (FieldDescriptor field : m.getFields().values()) {
                generateFieldGetterSignatures(field);
            }
            p("public " + beanCN + " copy();");
            p("public " + bufferCN + " freeze();");
            p("public java.lang.StringBuilder toString(java.lang.StringBuilder sb, String prefix);");
        }
        unindent();
        p("}");

        p();
        p("static public final class " + beanCN + " implements " + getterCN + " {");
        p();
        indent();

        p("" + bufferCN + " frozen;");
        p("" + beanCN + " bean;");
        p();
        p("public " + beanCN + "() {");
        indent();
        p("this.bean = this;");
        unindent();
        p("}");
        p();
        p("public " + beanCN + "(" + beanCN + " copy) {");
        indent();
        p("this.bean = copy;");
        unindent();
        p("}");
        p();
        p("public " + beanCN + " copy() {");
        indent();
        p("return new " + beanCN + "(bean);");
        unindent();
        p("}");
        p();

        generateMethodFreeze(m, bufferCN);

        p("private void copyCheck() {");
        indent();
        p("assert frozen==null : org.fusesource.hawtbuf.proto.MessageBufferSupport.FORZEN_ERROR_MESSAGE;");
        p("if (bean != this) {");
        indent();
        p("copy(bean);");
        unindent();
        p("}");
        unindent();
        p("}");
        p();

        generateMethodCopyFromBean(m, beanCN);

        // Generate the field accessors..
        for (FieldDescriptor field : m.getFields().values()) {
            generateFieldAccessor(beanCN, field);
        }

        generateMethodToString(m);

        generateMethodMergeFromStream(m, beanCN);

        generateBeanEquals(m, beanCN);

        generateMethodMergeFromBean(m, getterCN, beanCN);

        generateMethodClear(m);

        generateReadWriteExternal(m);

        for (EnumFieldDescriptor enumFeild : m.getAssociatedEnumFieldDescriptors()) {
            String enumName = uCamel(enumFeild.getParent().getName());
            p("public " + enumName + " to" + enumName + "() {");
            indent();
            p("return " + enumName + "." + enumFeild.getName() + ";");
            unindent();
            p("}");
            p();
        }

        unindent();
        p("}");
        p();

        p("static public final class " + bufferCN + " implements org.fusesource.hawtbuf.proto.MessageBuffer<" + qualified(type, beanCN) + ", " + qualified(type, bufferCN) + ">, " + getterCN + " {");
        p();
        indent();

        p("private " + beanCN + " bean;");
        p("private org.fusesource.hawtbuf.Buffer buffer;");
        p("private int size=-1;");
        p("private int hashCode;");
        p();
        p("private " + bufferCN + "(org.fusesource.hawtbuf.Buffer buffer) {");
        indent();
        p("this.buffer = buffer;");
        unindent();
        p("}");
        p();
        p("private " + bufferCN + "(" + beanCN + " bean) {");
        indent();
        p("this.bean = bean;");
        unindent();
        p("}");
        p();
        p("public " + beanCN + " copy() {");
        indent();
        p("return bean().copy();");
        unindent();
        p("}");
        p();
        p("public " + bufferCN + " freeze() {");
        indent();
        p("return this;");
        unindent();
        p("}");
        p();
        p("private " + beanCN + " bean() {");
        indent();
        p("if (bean == null) {");
        indent();
        p("try {");
        indent();
        p("bean = new " + beanCN + "().mergeUnframed(new org.fusesource.hawtbuf.proto.CodedInputStream(buffer));");
        p("bean.frozen=this;");
        unindent();
        p("} catch (org.fusesource.hawtbuf.proto.InvalidProtocolBufferException e) {");
        indent();
        p("throw new RuntimeException(e);");
        unindent();
        p("} catch (java.io.IOException e) {");
        indent();
        p("throw new RuntimeException(\"An IOException was thrown (should never happen in this method).\", e);");
        unindent();
        p("}");
        unindent();
        p("}");
        p("return bean;");
        unindent();
        p("}");
        p();

        p("public String toString() {");
        indent();
        p("return bean().toString();");
        unindent();
        p("}");
        p();
        p("public java.lang.StringBuilder toString(java.lang.StringBuilder sb, String prefix) {");
        indent();
        p("return bean().toString(sb, prefix);");
        unindent();
        p("}");
        p();

        for (FieldDescriptor field : m.getFields().values()) {
            generateBufferGetters(field);
        }

        generateMethodWrite(m);

        generateMethodSerializedSize(m);

        generateBufferEquals(m, bufferCN);

        p("public boolean frozen() {");
        indent();
        p("return true;");
        unindent();
        p("}");

        for (EnumFieldDescriptor enumFeild : m.getAssociatedEnumFieldDescriptors()) {
            String enumName = uCamel(enumFeild.getParent().getName());
            p("public " + enumName + " to" + enumName + "() {");
            indent();
            p("return " + enumName + "." + enumFeild.getName() + ";");
            unindent();
            p("}");
            p();
        }

        unindent();
        p("}");
        p();


//        generateMethodVisitor(m);
//        generateMethodType(m, className);


        unindent();
        p("}");
        p();

    }


    private void generateMethodFreeze(MessageDescriptor m, String bufferClassName) {
        p("public boolean frozen() {");
        indent();
        p("return frozen!=null;");
        unindent();
        p("}");
        p();
        p("public " + bufferClassName + " freeze() {");
        indent();
        p("if( frozen==null ) {");
        indent();
        p("frozen = new " + bufferClassName + "(bean);");
        p("assert deepFreeze();");
        unindent();
        p("}");
        p("return frozen;");
        unindent();
        p("}");
        p();
        p("private boolean deepFreeze() {");
        indent();
        p("frozen.serializedSizeUnframed();");
        p("return true;");
        unindent();
        p("}");
        p();

    }


    private boolean isPrimitive(String type) {
        return type.equals("int") ||
                type.equals("long") ||
                type.equals("double") ||
                type.equals("float") ||
                type.equals("boolean");
    }

    private boolean isBuferOrString(String type) {
        return type.equals("org.fusesource.hawtbuf.AsciiBuffer") ||
                type.equals("org.fusesource.hawtbuf.UTF8Buffer") ||
                type.equals("org.fusesource.hawtbuf.Buffer") ||
                type.equals("java.lang.String");
    }

    /**
     * @param m
     * @param className
     */
    private void generateMethodCopyFromBean(MessageDescriptor m, String className) {
        p("private void copy(" + className + " other) {");
        indent();
        p("this.bean = this;");
        for (FieldDescriptor field : m.getFields().values()) {
            String lname = lCamel(field.getName());
            String type = field.getRule() == FieldDescriptor.REPEATED_RULE ? javaCollectionType(field) : javaType(field);
            boolean primitive = isPrimitive(javaType(field));
            if (field.isRepeated()) {
                if (primitive || isBuferOrString(type) || field.getTypeDescriptor().isEnum()) {
                    p("this.f_" + lname + " = other.f_" + lname + ";");
                    p("if( this.f_" + lname + " !=null && !other.frozen()) {");
                    indent();
                    p("this.f_" + lname + " = new java.util.ArrayList<" + type + ">(this.f_" + lname + ");");
                    unindent();
                    p("}");
                } else {
                    p("this.f_" + lname + " = other.f_" + lname + ";");
                    p("if( this.f_" + lname + " !=null) {");
                    indent();
                    p("this.f_" + lname + " = new java.util.ArrayList<" + type + ">(other.f_" + lname + ".size());");
                    p("for( " + type + " e :  other.f_" + lname + ") {");
                    indent();
                    p("this.f_" + lname + ".add(e.copy());");
                    unindent();
                    p("}");
                    unindent();
                    p("}");
                }
            } else {
                if (primitive || isBuferOrString(type) || field.getTypeDescriptor().isEnum()) {
                    p("this.f_" + lname + " = other.f_" + lname + ";");
                    if (primitive) {
                        p("this.b_" + lname + " = other.b_" + lname + ";");
                    }
                } else {
                    p("this.f_" + lname + " = other.f_" + lname + ";");
                    p("if( this.f_" + lname + " !=null ) {");
                    indent();
                    p("this.f_" + lname + " = this.f_" + lname + ".copy();");
                    unindent();
                    p("}");
                }
            }
        }
        unindent();
        p("}");
        p();
    }


    /**
     * If the java_visitor message option is set, then this method generates a visitor method.  The option
     * speifiies the class name of the visitor and optionally the return value and exceptions thrown by the visitor.
     * <p/>
     * Examples:
     * <p/>
     * option java_visitor = "org.apache.kahadb.store.Visitor";
     * generates:
     * public void visit(org.apache.kahadb.store.Visitor visitor) {
     * visitor.visit(this);
     * }
     * <p/>
     * option java_visitor = "org.apache.kahadb.store.Visitor:int:java.io.IOException";
     * generates:
     * public int visit(org.apache.kahadb.store.Visitor visitor) throws java.io.IOException {
     * return visitor.visit(this);
     * }
     *
     * @param m
     */
    private void generateMethodVisitor(MessageDescriptor m) {
        String javaVisitor = getOption(m.getOptions(), "java_visitor", null);
        if (javaVisitor != null) {
            String returns = "void";
            String throwsException = null;

            StringTokenizer st = new StringTokenizer(javaVisitor, ":");
            String vistorClass = st.nextToken();
            if (st.hasMoreTokens()) {
                returns = st.nextToken();
            }
            if (st.hasMoreTokens()) {
                throwsException = st.nextToken();
            }

            String throwsClause = "";
            if (throwsException != null) {
                throwsClause = "throws " + throwsException + " ";
            }

            p("public " + returns + " visit(" + vistorClass + " visitor) " + throwsClause + "{");
            indent();
            if ("void".equals(returns)) {
                p("visitor.visit(this);");
            } else {
                p("return visitor.visit(this);");
            }
            unindent();
            p("}");
            p();
        }
    }

    private void generateMethodType(MessageDescriptor m, String className) {
        String typeEnum = getOption(m.getOptions(), "java_type_method", null);
        if (typeEnum != null) {

            TypeDescriptor typeDescriptor = m.getType(typeEnum);
            if (typeDescriptor == null) {
                typeDescriptor = m.getProtoDescriptor().getType(typeEnum);
            }
            if (typeDescriptor == null || !typeDescriptor.isEnum()) {
                errors.add("The java_type_method option on the " + m.getName() + " message does not point to valid enum type");
                return;
            }


            String constant = constantCase(className);
            EnumDescriptor enumDescriptor = (EnumDescriptor) typeDescriptor;
            if (enumDescriptor.getFields().get(constant) == null) {
                errors.add("The java_type_method option on the " + m.getName() + " message does not points to the " + typeEnum + " enum but it does not have an entry for " + constant);
            }

            String type = qualified(javaFactoryType(typeDescriptor), getterCN);

            p("public " + type + " type() {");
            indent();
            p("return " + type + "." + constant + ";");
            unindent();
            p("}");
            p();
        }
    }

    private void generateMethodParseFrom(MessageDescriptor m, String bufferClassName, String beanClassName) {
        p("public " + beanClassName + " parseUnframed(org.fusesource.hawtbuf.proto.CodedInputStream data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException, java.io.IOException {");
        indent();
        p("return new " + beanClassName + "().mergeUnframed(data);");
        unindent();
        p("}");
        p();
        p("public " + beanClassName + " parseUnframed(java.io.InputStream data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException, java.io.IOException {");
        indent();
        p("return parseUnframed(new org.fusesource.hawtbuf.proto.CodedInputStream(data));");
        unindent();
        p("}");
        p();
        p("public " + bufferClassName + " parseUnframed(org.fusesource.hawtbuf.Buffer data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException {");
        indent();
        p("return new " + bufferClassName + "(data);");
        unindent();
        p("}");
        p();
        p("public " + bufferClassName + " parseUnframed(byte[] data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException {");
        indent();
        p("return parseUnframed(new org.fusesource.hawtbuf.Buffer(data));");
        unindent();
        p("}");
        p();
        p("public " + bufferClassName + " parseFramed(org.fusesource.hawtbuf.proto.CodedInputStream data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException, java.io.IOException {");
        indent();
        p("int length = data.readRawVarint32();");
        p("int oldLimit = data.pushLimit(length);");
        p("" + bufferClassName + " rc = parseUnframed(data.readRawBytes(length));");
        p("data.popLimit(oldLimit);");
        p("return rc;");
        unindent();
        p("}");
        p();
        p("public " + bufferClassName + " parseFramed(org.fusesource.hawtbuf.Buffer data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException {");
        indent();
        p("try {");
        indent();
        p("org.fusesource.hawtbuf.proto.CodedInputStream input = new org.fusesource.hawtbuf.proto.CodedInputStream(data);");
        p("" + bufferClassName + " rc = parseFramed(input);");
        p("input.checkLastTagWas(0);");
        p("return rc;");
        unindent();
        p("} catch (org.fusesource.hawtbuf.proto.InvalidProtocolBufferException e) {");
        indent();
        p("throw e;");
        unindent();
        p("} catch (java.io.IOException e) {");
        indent();
        p("throw new RuntimeException(\"An IOException was thrown (should never happen in this method).\", e);");
        unindent();
        p("}");
        unindent();
        p("}");
        p();
        p("public " + bufferClassName + " parseFramed(byte[] data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException {");
        indent();
        p("return parseFramed(new org.fusesource.hawtbuf.Buffer(data));");
        unindent();
        p("}");
        p();
        p("public " + bufferClassName + " parseFramed(java.io.InputStream data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException, java.io.IOException {");
        indent();
        p("return parseUnframed(org.fusesource.hawtbuf.proto.MessageBufferSupport.readFrame(data));");
        unindent();
        p("}");
        p();

    }

    private void generateBeanEquals(MessageDescriptor m, String className) {
        p("public boolean equals(Object obj) {");
        indent();
        p("if( obj==this )");
        p("   return true;");
        p("");
        p("if( obj==null || obj.getClass()!=" + className + ".class )");
        p("   return false;");
        p("");
        p("return equals((" + className + ")obj);");
        unindent();
        p("}");
        p("");

        p("public boolean equals(" + className + " obj) {");
        indent();
        for (FieldDescriptor field : m.getFields().values()) {
            String uname = uCamel(field.getName());
            String getterMethod = "get" + uname + "()";
            String hasMethod = "has" + uname + "()";

            if (field.getRule() == FieldDescriptor.REPEATED_RULE) {
                getterMethod = "get" + uname + "List()";
            }

            p("if (" + hasMethod + " ^ obj." + hasMethod + " ) ");
            p("   return false;");


            if (field.getRule() != FieldDescriptor.REPEATED_RULE && (field.isNumberType() || field.getType() == FieldDescriptor.BOOL_TYPE)) {
                p("if (" + hasMethod + " && ( " + getterMethod + "!=obj." + getterMethod + " ))");
            } else {
                p("if (" + hasMethod + " && ( !" + getterMethod + ".equals(obj." + getterMethod + ") ))");
            }
            p("   return false;");
        }
        p("return true;");
        unindent();
        p("}");
        p("");
        p("public int hashCode() {");
        indent();
        int hc = className.hashCode();
        p("int rc=" + hc + ";");
        int counter = 0;
        for (FieldDescriptor field : m.getFields().values()) {
            counter++;

            String uname = uCamel(field.getName());
            String getterMethod = "get" + uname + "()";
            String hasMethod = "has" + uname + "()";

            if (field.getRule() == FieldDescriptor.REPEATED_RULE) {
                getterMethod = "get" + uname + "List()";
            }

            p("if (" + hasMethod + ") {");
            indent();

            if (field.getRule() == FieldDescriptor.REPEATED_RULE) {
                p("rc ^= ( " + uname.hashCode() + "^" + getterMethod + ".hashCode() );");
            } else if (field.isInteger32Type()) {
                p("rc ^= ( " + uname.hashCode() + "^" + getterMethod + " );");
            } else if (field.isInteger64Type()) {
                p("rc ^= ( " + uname.hashCode() + "^(new Long(" + getterMethod + ")).hashCode() );");
            } else if (field.getType() == FieldDescriptor.DOUBLE_TYPE) {
                p("rc ^= ( " + uname.hashCode() + "^(new Double(" + getterMethod + ")).hashCode() );");
            } else if (field.getType() == FieldDescriptor.FLOAT_TYPE) {
                p("rc ^= ( " + uname.hashCode() + "^(new Double(" + getterMethod + ")).hashCode() );");
            } else if (field.getType() == FieldDescriptor.BOOL_TYPE) {
                p("rc ^= ( " + uname.hashCode() + "^ (" + getterMethod + "? " + counter + ":-" + counter + ") );");
            } else {
                p("rc ^= ( " + uname.hashCode() + "^" + getterMethod + ".hashCode() );");
            }
            unindent();
            p("}");
        }
        p("return rc;");
        unindent();
        p("}");
        p("");
    }

    private void generateBufferEquals(MessageDescriptor m, String className) {
        p("public boolean equals(Object obj) {");
        indent();
        p("if( obj==this )");
        p("   return true;");
        p("");
        p("if( obj==null || obj.getClass()!=" + className + ".class )");
        p("   return false;");
        p("");
        p("return equals((" + className + ")obj);");
        unindent();
        p("}");
        p("");

        p("public boolean equals(" + className + " obj) {");
        indent();
        p("return toUnframedBuffer().equals(obj.toUnframedBuffer());");
        unindent();
        p("}");
        p("");
        p("public int hashCode() {");
        indent();
        int hc = className.hashCode();
        p("if( hashCode==0 ) {");
        p("hashCode=" + hc + " ^ toUnframedBuffer().hashCode();");
        p("}");
        p("return hashCode;");
        unindent();
        p("}");
        p("");
    }

    /**
     * @param m
     */
    private void generateMethodSerializedSize(MessageDescriptor m) {

        p("public int serializedSizeFramed() {");
        indent();
        p("int t = serializedSizeUnframed();");
        p("return org.fusesource.hawtbuf.proto.CodedOutputStream.computeRawVarint32Size(t) + t;");
        unindent();
        p("}");
        p();
        p("public int serializedSizeUnframed() {");
        indent();
        p("if (buffer != null) {");
        indent();
        p("return buffer.length;");
        unindent();
        p("}");
        p("if (size != -1)");
        p("   return size;");
        p();
        p("size = 0;");
        for (FieldDescriptor field : m.getFields().values()) {

            String uname = uCamel(field.getName());
            String getter = "get" + uname + "()";
            String type = javaType(field);

            if (!field.isRequired()) {
                p("if (has" + uname + "()) {");
                indent();
            }

            if (field.getRule() == FieldDescriptor.REPEATED_RULE) {
                p("for (" + type + " i : get" + uname + "List()) {");
                indent();
                getter = "i";
            }

            if (field.getType() == FieldDescriptor.STRING_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeStringSize(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.BYTES_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeBytesSize(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.BOOL_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeBoolSize(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.DOUBLE_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeDoubleSize(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.FLOAT_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeFloatSize(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.INT32_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeInt32Size(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.INT64_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeInt64Size(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.SINT32_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeSInt32Size(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.SINT64_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeSInt64Size(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.UINT32_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeUInt32Size(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.UINT64_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeUInt64Size(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.FIXED32_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeFixed32Size(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.FIXED64_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeFixed64Size(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.SFIXED32_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeSFixed32Size(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.SFIXED64_TYPE) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeSFixed64Size(" + field.getTag() + ", " + getter + ");");
            } else if (field.getTypeDescriptor().isEnum()) {
                p("size += org.fusesource.hawtbuf.proto.CodedOutputStream.computeEnumSize(" + field.getTag() + ", " + getter + ".getNumber());");
            } else if (field.getGroup() != null) {
                errors.add("This code generator does not support group fields.");
//                p("size += org.fusesource.hawtbuf.proto.MessageBufferSupport.computeGroupSize("+field.getTag()+", ("+type+"Buffer)"+getter+");");
            } else {
                p("size += org.fusesource.hawtbuf.proto.MessageBufferSupport.computeMessageSize(" + field.getTag() + ", " + getter + ".freeze());");
            }
            if (field.getRule() == FieldDescriptor.REPEATED_RULE) {
                unindent();
                p("}");
            }

            if (!field.isRequired()) {
                unindent();
                p("}");
            }

        }
        // TODO: handle unknown fields
        // size += getUnknownFields().getSerializedSize();");
        p("return size;");
        unindent();
        p("}");
        p();
    }

    /**
     * @param m
     */
    private void generateMethodWrite(MessageDescriptor m) {

        p("public org.fusesource.hawtbuf.Buffer toUnframedBuffer() {");
        indent();
        p("if( buffer !=null ) {");
        indent();
        p("return buffer;");
        unindent();
        p("}");
        p("return org.fusesource.hawtbuf.proto.MessageBufferSupport.toUnframedBuffer(this);");
        unindent();
        p("}");
        p();
        p("public org.fusesource.hawtbuf.Buffer toFramedBuffer() {");
        indent();
        p("return org.fusesource.hawtbuf.proto.MessageBufferSupport.toFramedBuffer(this);");
        unindent();
        p("}");
        p();
        p("public byte[] toUnframedByteArray() {");
        indent();
        p("return toUnframedBuffer().toByteArray();");
        unindent();
        p("}");
        p();
        p("public byte[] toFramedByteArray() {");
        indent();
        p("return toFramedBuffer().toByteArray();");
        unindent();
        p("}");
        p();
        p("public void writeFramed(org.fusesource.hawtbuf.proto.CodedOutputStream output) throws java.io.IOException {");
        indent();
        p("output.writeRawVarint32(serializedSizeUnframed());");
        p("writeUnframed(output);");
        unindent();
        p("}");
        p();
        p("public void writeFramed(java.io.OutputStream output) throws java.io.IOException {");
        indent();
        p("org.fusesource.hawtbuf.proto.CodedOutputStream codedOutput = new org.fusesource.hawtbuf.proto.CodedOutputStream(output);");
        p("writeFramed(codedOutput);");
        p("codedOutput.flush();");
        unindent();
        p("}");
        p();

        p("public void writeUnframed(java.io.OutputStream output) throws java.io.IOException {");
        indent();
        p("org.fusesource.hawtbuf.proto.CodedOutputStream codedOutput = new org.fusesource.hawtbuf.proto.CodedOutputStream(output);");
        p("writeUnframed(codedOutput);");
        p("codedOutput.flush();");
        unindent();
        p("}");
        p();

        p("public void writeUnframed(org.fusesource.hawtbuf.proto.CodedOutputStream output) throws java.io.IOException {");
        indent();

        p("if (buffer == null) {");
        indent();
        p("int size = serializedSizeUnframed();");
        p("buffer = output.getNextBuffer(size);");
        p("org.fusesource.hawtbuf.proto.CodedOutputStream original=null;");
        p("if( buffer == null ) {");
        indent();
        p("buffer = new org.fusesource.hawtbuf.Buffer(new byte[size]);");
        p("original = output;");
        p("output = new org.fusesource.hawtbuf.proto.CodedOutputStream(buffer);");
        unindent();
        p("}");

        for (FieldDescriptor field : m.getFields().values()) {
            String uname = uCamel(field.getName());
            String getter = "bean.get" + uname + "()";
            String type = javaType(field);

            if (!field.isRequired()) {
                p("if (bean.has" + uname + "()) {");
                indent();
            }

            if (field.getRule() == FieldDescriptor.REPEATED_RULE) {
                p("for (" + type + " i : bean.get" + uname + "List()) {");
                indent();
                getter = "i";
            }

            if (field.getType() == FieldDescriptor.STRING_TYPE) {
                p("output.writeString(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.BYTES_TYPE) {
                p("output.writeBytes(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.BOOL_TYPE) {
                p("output.writeBool(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.DOUBLE_TYPE) {
                p("output.writeDouble(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.FLOAT_TYPE) {
                p("output.writeFloat(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.INT32_TYPE) {
                p("output.writeInt32(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.INT64_TYPE) {
                p("output.writeInt64(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.SINT32_TYPE) {
                p("output.writeSInt32(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.SINT64_TYPE) {
                p("output.writeSInt64(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.UINT32_TYPE) {
                p("output.writeUInt32(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.UINT64_TYPE) {
                p("output.writeUInt64(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.FIXED32_TYPE) {
                p("output.writeFixed32(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.FIXED64_TYPE) {
                p("output.writeFixed64(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.SFIXED32_TYPE) {
                p("output.writeSFixed32(" + field.getTag() + ", " + getter + ");");
            } else if (field.getType() == FieldDescriptor.SFIXED64_TYPE) {
                p("output.writeSFixed64(" + field.getTag() + ", " + getter + ");");
            } else if (field.getTypeDescriptor().isEnum()) {
                p("output.writeEnum(" + field.getTag() + ", " + getter + ".getNumber());");
            } else if (field.getGroup() != null) {
                errors.add("This code generator does not support group fields.");
//                p("writeGroup(output, "+field.getTag()+", "+getter+");");
            } else {
                p("org.fusesource.hawtbuf.proto.MessageBufferSupport.writeMessage(output, " + field.getTag() + ", " + getter + ".freeze());");
            }

            if (field.getRule() == FieldDescriptor.REPEATED_RULE) {
                unindent();
                p("}");
            }

            if (!field.isRequired()) {
                unindent();
                p("}");
            }
        }

        p("if( original !=null ) {");
        indent();
        p("output.checkNoSpaceLeft();");
        p("output = original;");
        p("output.writeRawBytes(buffer);");
        unindent();
        p("}");
        unindent();
        p("} else {");
        indent();
        p("output.writeRawBytes(buffer);");
        unindent();
        p("}");

        unindent();
        p("}");
        p();
    }

    /**
     * @param m
     * @param className
     */
    private void generateMethodMergeFromStream(MessageDescriptor m, String className) {
        p("public " + className + " mergeUnframed(java.io.InputStream input) throws java.io.IOException {");
        indent();
        p("return mergeUnframed(new org.fusesource.hawtbuf.proto.CodedInputStream(input));");
        unindent();
        p("}");
        p();
        p("public " + className + " mergeUnframed(org.fusesource.hawtbuf.proto.CodedInputStream input) throws java.io.IOException {");
        indent();
        {
            p("copyCheck();");
            p("while (true) {");
            indent();
            {
                p("int tag = input.readTag();");
                p("if ((tag & 0x07) == 4) {");
                p("   return this;");
                p("}");

                p("switch (tag) {");
                p("case 0:");
                p("   return this;");
                p("default: {");
                p("   input.skipField(tag);");
                p("   break;");
                p("}");

                for (FieldDescriptor field : m.getFields().values()) {
                    String uname = uCamel(field.getName());
                    String setter = "set" + uname;
                    boolean repeated = field.getRule() == FieldDescriptor.REPEATED_RULE;
                    if (repeated) {
                        setter = "create" + uname + "List().add";
                    }
                    if (field.getType() == FieldDescriptor.STRING_TYPE) {
                        p("case "
                                + makeTag(field.getTag(),
                                WIRETYPE_LENGTH_DELIMITED) + ":");
                        indent();
                        p(setter + "(input.readString());");
                    } else if (field.getType() == FieldDescriptor.BYTES_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_LENGTH_DELIMITED) + ":");
                        indent();
                        String override = getOption(field.getOptions(), "java_override_type", null);
                        if ("AsciiBuffer".equals(override)) {
                            p(setter + "(new org.fusesource.hawtbuf.AsciiBuffer(input.readBytes()));");
                        } else if ("UTF8Buffer".equals(override)) {
                            p(setter + "(new org.fusesource.hawtbuf.UTF8Buffer(input.readBytes()));");
                        } else {
                            p(setter + "(input.readBytes());");
                        }
                    } else if (field.getType() == FieldDescriptor.BOOL_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_VARINT) + ":");
                        indent();
                        p(setter + "(input.readBool());");
                    } else if (field.getType() == FieldDescriptor.DOUBLE_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_FIXED64)
                                + ":");
                        indent();
                        p(setter + "(input.readDouble());");
                    } else if (field.getType() == FieldDescriptor.FLOAT_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_FIXED32)
                                + ":");
                        indent();
                        p(setter + "(input.readFloat());");
                    } else if (field.getType() == FieldDescriptor.INT32_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_VARINT)
                                + ":");
                        indent();
                        p(setter + "(input.readInt32());");
                    } else if (field.getType() == FieldDescriptor.INT64_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_VARINT)
                                + ":");
                        indent();
                        p(setter + "(input.readInt64());");
                    } else if (field.getType() == FieldDescriptor.SINT32_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_VARINT)
                                + ":");
                        indent();
                        p(setter + "(input.readSInt32());");
                    } else if (field.getType() == FieldDescriptor.SINT64_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_VARINT)
                                + ":");
                        indent();
                        p(setter + "(input.readSInt64());");
                    } else if (field.getType() == FieldDescriptor.UINT32_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_VARINT)
                                + ":");
                        indent();
                        p(setter + "(input.readUInt32());");
                    } else if (field.getType() == FieldDescriptor.UINT64_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_VARINT)
                                + ":");
                        indent();
                        p(setter + "(input.readUInt64());");
                    } else if (field.getType() == FieldDescriptor.FIXED32_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_FIXED32)
                                + ":");
                        indent();
                        p(setter + "(input.readFixed32());");
                    } else if (field.getType() == FieldDescriptor.FIXED64_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_FIXED64)
                                + ":");
                        indent();
                        p(setter + "(input.readFixed64());");
                    } else if (field.getType() == FieldDescriptor.SFIXED32_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_FIXED32)
                                + ":");
                        indent();
                        p(setter + "(input.readSFixed32());");
                    } else if (field.getType() == FieldDescriptor.SFIXED64_TYPE) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_FIXED64)
                                + ":");
                        indent();
                        p(setter + "(input.readSFixed64());");
                    } else if (field.getTypeDescriptor().isEnum()) {
                        p("case " + makeTag(field.getTag(), WIRETYPE_VARINT)
                                + ":");
                        indent();
                        String type = javaType(field);
                        p("{");
                        indent();
                        p("int t = input.readEnum();");
                        p("" + type + " value = " + type + ".valueOf(t);");
                        p("if( value !=null ) {");
                        indent();
                        p(setter + "(value);");
                        unindent();
                        p("}");
                        // TODO: else store it as an known

                        unindent();
                        p("}");

                    } else if (field.getGroup() != null) {
                        errors.add("This code generator does not support group fields.");
//                        p("case "+ makeTag(field.getTag(), WIRETYPE_START_GROUP)+ ":");
//                        indent();
//                        String type = javaType(field);
//                        if (repeated) {
//                            p(setter + "(readGroup(input, " + field.getTag()+ ", new " + type + "()));");
//                        } else {
//                            p("if (has" + uname + "()) {");
//                            indent();
//                            p("readGroup(input, " + field.getTag() + ", get"
//                                    + uname + "());");
//                            unindent();
//                            p("} else {");
//                            indent();
//                            p(setter + "(readGroup(input, " + field.getTag()
//                                    + ",new " + type + "()));");
//                            unindent();
//                            p("}");
//                        }
//                        p("");
                    } else {
                        p("case " + makeTag(field.getTag(), WIRETYPE_LENGTH_DELIMITED) + ":");
                        indent();
                        String type = javaFactoryType(field.getTypeDescriptor());
                        if (repeated) {
                            p(setter + "(" + qualified(type, factoryFeild) + ".parseFramed(input));");
                        } else {
                            p("if (has" + uname + "()) {");
                            indent();
                            p("set" + uname + "(get" + uname + "().copy().mergeFrom(" + qualified(type, factoryFeild) + ".parseFramed(input)));");
                            unindent();
                            p("} else {");
                            indent();
                            p(setter + "(" + qualified(type, factoryFeild) + ".parseFramed(input));");
                            unindent();
                            p("}");
                        }
                    }
                    p("break;");
                    unindent();
                }
                p("}");
            }
            unindent();
            p("}");
        }
        unindent();
        p("}");
    }

    /**
     * @param m
     * @param getterClassName
     */
    private void generateMethodMergeFromBean(MessageDescriptor m, String getterClassName, String beanClassName) {
        p("public " + beanClassName + " mergeFrom(" + getterClassName + " other) {");
        indent();
        p("copyCheck();");
        for (FieldDescriptor field : m.getFields().values()) {
            String uname = uCamel(field.getName());
            p("if (other.has" + uname + "()) {");
            indent();

            if (field.isScalarType() || field.getTypeDescriptor().isEnum()) {
                if (field.isRepeated()) {
                    p("get" + uname + "List().addAll(other.get" + uname + "List());");
                } else {
                    p("set" + uname + "(other.get" + uname + "());");
                }
            } else {

                String type = qualified(javaFactoryType(field.getTypeDescriptor()), getterCN);

                // It's complex type...
                if (field.isRepeated()) {
                    p("for(" + type + " element: other.get" + uname + "List() ) {");
                    indent();
                    p("get" + uname + "List().add(element.copy());");
                    unindent();
                    p("}");
                } else {
                    p("if (has" + uname + "()) {");
                    indent();
                    p("set" + uname + "(get" + uname + "().copy().mergeFrom(other.get" + uname + "()));");
                    unindent();
                    p("} else {");
                    indent();
                    p("set" + uname + "(other.get" + uname + "().copy());");
                    unindent();
                    p("}");
                }
            }
            unindent();
            p("}");
        }
        p("return this;");
        unindent();
        p("}");
        p();
    }

    /**
     * @param m
     */
    private void generateMethodClear(MessageDescriptor m) {
        p("public void clear() {");
        indent();
        for (FieldDescriptor field : m.getFields().values()) {
            String uname = uCamel(field.getName());
            p("clear" + uname + "();");
        }
        unindent();
        p("}");
        p();
    }

    private void generateReadWriteExternal(MessageDescriptor m) {

        p("public void readExternal(java.io.DataInput in) throws java.io.IOException {");
        indent();
        p("assert frozen==null : org.fusesource.hawtbuf.proto.MessageBufferSupport.FORZEN_ERROR_MESSAGE;");
        p("bean = this;");
        p("frozen = null;");

        for (FieldDescriptor field : m.getFields().values()) {
            String lname = lCamel(field.getName());
            String type = javaType(field);
            boolean repeated = field.getRule() == FieldDescriptor.REPEATED_RULE;

            // Create the fields..
            if (repeated) {
                p("{");
                indent();
                p("int size = in.readShort();");
                p("if( size>=0 ) {");
                indent();
                p("f_" + lname + " = new java.util.ArrayList<" + javaCollectionType(field) + ">(size);");
                p("for(int i=0; i<size; i++) {");
                indent();
                if (field.isInteger32Type()) {
                    p("f_" + lname + ".add(in.readInt());");
                } else if (field.isInteger64Type()) {
                    p("f_" + lname + ".add(in.readLong());");
                } else if (field.getType() == FieldDescriptor.DOUBLE_TYPE) {
                    p("f_" + lname + ".add(in.readDouble());");
                } else if (field.getType() == FieldDescriptor.FLOAT_TYPE) {
                    p("f_" + lname + ".add(in.readFloat());");
                } else if (field.getType() == FieldDescriptor.BOOL_TYPE) {
                    p("f_" + lname + ".add(in.readBoolean());");
                } else if (field.getType() == FieldDescriptor.STRING_TYPE) {
                    p("f_" + lname + ".add(in.readUTF());");
                } else if (field.getType() == FieldDescriptor.BYTES_TYPE) {
                    p("byte b[] = new byte[in.readInt()];");
                    p("in.readFully(b);");
                    p("f_" + lname + ".add(new " + type + "(b));");
                } else if (field.getTypeDescriptor().isEnum()) {
                    p("f_" + lname + ".add(" + type + ".valueOf(in.readShort()));");
                } else {
                    String beanName = qualified(javaFactoryType(field.getTypeDescriptor()), beanCN);
                    p(beanName + " o = new " + beanName + "();");
                    p("o.readExternal(in);");
                    p("f_" + lname + ".add(o);");
                }
                unindent();
                p("}");
                unindent();
                p("} else {");
                indent();
                p("f_" + lname + " = null;");
                unindent();
                p("}");
                unindent();
                p("}");

            } else {
                if (field.isInteger32Type()) {
                    p("f_" + lname + " = in.readInt();");
                    p("b_" + lname + " = true;");
                } else if (field.isInteger64Type()) {
                    p("f_" + lname + " = in.readLong();");
                    p("b_" + lname + " = true;");
                } else if (field.getType() == FieldDescriptor.DOUBLE_TYPE) {
                    p("f_" + lname + " = in.readDouble();");
                    p("b_" + lname + " = true;");
                } else if (field.getType() == FieldDescriptor.FLOAT_TYPE) {
                    p("f_" + lname + " = in.readFloat();");
                    p("b_" + lname + " = true;");
                } else if (field.getType() == FieldDescriptor.BOOL_TYPE) {
                    p("f_" + lname + " = in.readBoolean();");
                    p("b_" + lname + " = true;");
                } else if (field.getType() == FieldDescriptor.STRING_TYPE) {
                    p("if( in.readBoolean() ) {");
                    indent();
                    p("f_" + lname + " = in.readUTF();");
                    unindent();
                    p("} else {");
                    indent();
                    p("f_" + lname + " = null;");
                    unindent();
                    p("}");
                } else if (field.getType() == FieldDescriptor.BYTES_TYPE) {
                    p("{");
                    indent();
                    p("int size = in.readInt();");
                    p("if( size>=0 ) {");
                    indent();
                    p("byte b[] = new byte[size];");
                    p("in.readFully(b);");
                    p("f_" + lname + " = new " + type + "(b);");
                    unindent();
                    p("} else {");
                    indent();
                    p("f_" + lname + " = null;");
                    unindent();
                    p("}");
                    unindent();
                    p("}");
                } else if (field.getTypeDescriptor().isEnum()) {
                    p("if( in.readBoolean() ) {");
                    indent();
                    p("f_" + lname + " = " + type + ".valueOf(in.readShort());");
                    unindent();
                    p("} else {");
                    indent();
                    p("f_" + lname + " = null;");
                    unindent();
                    p("}");
                } else {
                    p("if( in.readBoolean() ) {");
                    indent();
                    String factoryType = javaFactoryType(field.getTypeDescriptor());
                    p("" + qualified(factoryType, beanCN) + " o = new " + qualified(factoryType, beanCN) + "();");
                    p("o.readExternal(in);");
                    p("f_" + lname + " = o;");
                    unindent();
                    p("} else {");
                    indent();
                    p("f_" + lname + " = null;");
                    unindent();
                    p("}");
                }
            }
        }

        unindent();
        p("}");
        p();
        p("public void writeExternal(java.io.DataOutput out) throws java.io.IOException {");
        indent();
        for (FieldDescriptor field : m.getFields().values()) {
            String lname = lCamel(field.getName());
            boolean repeated = field.getRule() == FieldDescriptor.REPEATED_RULE;

            // Create the fields..
            if (repeated) {
                p("if( bean.f_" + lname + "!=null ) {");
                indent();
                p("out.writeShort(bean.f_" + lname + ".size());");
                p("for(" + javaCollectionType(field) + " o : bean.f_" + lname + ") {");
                indent();

                if (field.isInteger32Type()) {
                    p("out.writeInt(o);");
                } else if (field.isInteger64Type()) {
                    p("out.writeLong(o);");
                } else if (field.getType() == FieldDescriptor.DOUBLE_TYPE) {
                    p("out.writeDouble(o);");
                } else if (field.getType() == FieldDescriptor.FLOAT_TYPE) {
                    p("out.writeFloat(o);");
                } else if (field.getType() == FieldDescriptor.BOOL_TYPE) {
                    p("out.writeBoolean(o);");
                } else if (field.getType() == FieldDescriptor.STRING_TYPE) {
                    p("out.writeUTF(o);");
                } else if (field.getType() == FieldDescriptor.BYTES_TYPE) {
                    p("out.writeInt(o.getLength());");
                    p("out.write(o.getData(), o.getOffset(), o.getLength());");
                } else if (field.getTypeDescriptor().isEnum()) {
                    p("out.writeShort(o.getNumber());");
                } else {
                    p("o.copy().writeExternal(out);");
                }
                unindent();
                p("}");
                unindent();
                p("} else {");
                indent();
                p("out.writeShort(-1);");
                unindent();
                p("}");

            } else {
                if (field.isInteger32Type()) {
                    p("out.writeInt(bean.f_" + lname + ");");
                } else if (field.isInteger64Type()) {
                    p("out.writeLong(bean.f_" + lname + ");");
                } else if (field.getType() == FieldDescriptor.DOUBLE_TYPE) {
                    p("out.writeDouble(bean.f_" + lname + ");");
                } else if (field.getType() == FieldDescriptor.FLOAT_TYPE) {
                    p("out.writeFloat(bean.f_" + lname + ");");
                } else if (field.getType() == FieldDescriptor.BOOL_TYPE) {
                    p("out.writeBoolean(bean.f_" + lname + ");");
                } else if (field.getType() == FieldDescriptor.STRING_TYPE) {
                    p("if( bean.f_" + lname + "!=null ) {");
                    indent();
                    p("out.writeBoolean(true);");
                    p("out.writeUTF(bean.f_" + lname + ");");
                    unindent();
                    p("} else {");
                    indent();
                    p("out.writeBoolean(false);");
                    unindent();
                    p("}");
                } else if (field.getType() == FieldDescriptor.BYTES_TYPE) {
                    p("if( bean.f_" + lname + "!=null ) {");
                    indent();
                    p("out.writeInt(bean.f_" + lname + ".getLength());");
                    p("out.write(bean.f_" + lname + ".getData(), bean.f_" + lname + ".getOffset(), bean.f_" + lname + ".getLength());");
                    unindent();
                    p("} else {");
                    indent();
                    p("out.writeInt(-1);");
                    unindent();
                    p("}");
                } else if (field.getTypeDescriptor().isEnum()) {
                    p("if( bean.f_" + lname + "!=null ) {");
                    indent();
                    p("out.writeBoolean(true);");
                    p("out.writeShort(bean.f_" + lname + ".getNumber());");
                    unindent();
                    p("} else {");
                    indent();
                    p("out.writeBoolean(false);");
                    unindent();
                    p("}");
                } else {
                    p("if( bean.f_" + lname + "!=null ) {");
                    indent();
                    p("out.writeBoolean(true);");
                    p("bean.f_" + lname + ".copy().writeExternal(out);");
                    unindent();
                    p("} else {");
                    indent();
                    p("out.writeBoolean(false);");
                    unindent();
                    p("}");
                }
            }
        }

        unindent();
        p("}");
        p();

    }


//    private void generateMethodAssertInitialized(MessageDescriptor m, String className) {
//        
//        p("public java.util.ArrayList<String> missingFields() {");
//        indent();
//        p("java.util.ArrayList<String> missingFields = super.missingFields();");
//        
//        for (FieldDescriptor field : m.getFields().values()) {
//            String uname = uCamel(field.getName());
//            if( field.isRequired() ) {
//                p("if(  !has" + uname + "() ) {");
//                indent();
//                p("missingFields.add(\""+field.getName()+"\");");
//                unindent();
//                p("}");
//            }
//        }
//        
//        if( !deferredDecode ) {
//            for (FieldDescriptor field : m.getFields().values()) {
//                if( field.getTypeDescriptor()!=null && !field.getTypeDescriptor().isEnum()) {
//                    String uname = uCamel(field.getName());
//                    p("if( has" + uname + "() ) {");
//                    indent();
//                    if( !field.isRepeated() ) {
//                        p("try {");
//                        indent();
//                        p("get" + uname + "().assertInitialized();");
//                        unindent();
//                        p("} catch (org.fusesource.hawtbuf.proto.UninitializedMessageException e){");
//                        indent();
//                        p("missingFields.addAll(prefix(e.getMissingFields(),\""+field.getName()+".\"));");
//                        unindent();
//                        p("}");
//                    } else {
//                        String type = javaCollectionType(field);
//                        p("java.util.List<"+type+"> l = get" + uname + "List();");
//                        p("for( int i=0; i < l.size(); i++ ) {");
//                        indent();
//                        p("try {");
//                        indent();
//                        p("l.get(i).assertInitialized();");
//                        unindent();
//                        p("} catch (org.fusesource.hawtbuf.proto.UninitializedMessageException e){");
//                        indent();
//                        p("missingFields.addAll(prefix(e.getMissingFields(),\""+field.getName()+"[\"+i+\"]\"));");
//                        unindent();
//                        p("}");
//                        unindent();
//                        p("}");
//                    }
//                    unindent();
//                    p("}");
//                }
//            }
//        }
//        p("return missingFields;");
//        unindent();
//        p("}");
//        p();
//    }

    private void generateMethodToString(MessageDescriptor m) {

        p("public String toString() {");
        indent();
        p("return toString(new java.lang.StringBuilder(), \"\").toString();");
        unindent();
        p("}");
        p();

        p("public java.lang.StringBuilder toString(java.lang.StringBuilder sb, String prefix) {");
        indent();
        for (FieldDescriptor field : m.getFields().values()) {
            String uname = uCamel(field.getName());
            p("if(  has" + uname + "() ) {");
            indent();
            if (field.isRepeated()) {
                String type = javaCollectionType(field);
                p("java.util.List<" + type + "> l = get" + uname + "List();");
                p("for( int i=0; i < l.size(); i++ ) {");
                indent();
                if (field.getTypeDescriptor() != null && !field.getTypeDescriptor().isEnum()) {
                    p("sb.append(prefix+\"" + field.getName() + "[\"+i+\"] {\\n\");");
                    p("l.get(i).toString(sb, prefix+\"  \");");
                    p("sb.append(prefix+\"}\\n\");");
                } else {
                    p("sb.append(prefix+\"" + field.getName() + "[\"+i+\"]: \");");
                    p("sb.append(l.get(i));");
                    p("sb.append(\"\\n\");");
                }
                unindent();
                p("}");
            } else {
                if (field.getTypeDescriptor() != null && !field.getTypeDescriptor().isEnum()) {
                    p("sb.append(prefix+\"" + field.getName() + " {\\n\");");
                    p("get" + uname + "().toString(sb, prefix+\"  \");");
                    p("sb.append(prefix+\"}\\n\");");
                } else {
                    p("sb.append(prefix+\"" + field.getName() + ": \");");
                    p("sb.append(get" + uname + "());");
                    p("sb.append(\"\\n\");");
                }
            }
            unindent();
            p("}");
        }
        p("return sb;");
        unindent();
        p("}");
        p();

    }

    /**
     * @param field
     */
    private void generateBufferGetters(FieldDescriptor field) {
        String uname = uCamel(field.getName());
        String type = field.getRule() == FieldDescriptor.REPEATED_RULE ? javaCollectionType(field) : javaType(field);
        boolean repeated = field.getRule() == FieldDescriptor.REPEATED_RULE;

        // Create the fields..
        p("// " + field.getRule() + " " + field.getType() + " " + field.getName() + " = " + field.getTag() + ";");
        if (repeated) {
            // Create the field accessors
            p("public boolean has" + uname + "() {");
            indent();
            p("return bean().has" + uname + "();");
            unindent();
            p("}");
            p();
            p("public java.util.List<" + type + "> get" + uname + "List() {");
            indent();
            p("return bean().get" + uname + "List();");
            unindent();
            p("}");
            p();
            p("public int get" + uname + "Count() {");
            indent();
            p("return bean().get" + uname + "Count();");
            unindent();
            p("}");
            p();
            p("public " + type + " get" + uname + "(int index) {");
            indent();
            p("return bean().get" + uname + "(index);");
            unindent();
            p("}");
            p();
        } else {
            // Create the field accessors
            p("public boolean has" + uname + "() {");
            indent();
            p("return bean().has" + uname + "();");
            unindent();
            p("}");
            p();
            p("public " + type + " get" + uname + "() {");
            indent();
            p("return bean().get" + uname + "();");
            unindent();
            p("}");
            p();
        }
    }

    /**
     * @param field
     */
    private void generateFieldGetterSignatures(FieldDescriptor field) {
        String uname = uCamel(field.getName());
        String type = field.getRule() == FieldDescriptor.REPEATED_RULE ? javaCollectionType(field) : javaType(field);
        boolean repeated = field.getRule() == FieldDescriptor.REPEATED_RULE;

        // Create the fields..
        p("// " + field.getRule() + " " + field.getType() + " " + field.getName() + " = " + field.getTag() + ";");
        if (repeated) {
            // Create the field accessors
            p("public boolean has" + uname + "();");
            p("public java.util.List<" + type + "> get" + uname + "List();");
            p("public int get" + uname + "Count();");
            p("public " + type + " get" + uname + "(int index);");
        } else {
            // Create the field accessors
            p("public boolean has" + uname + "();");
            p("public " + type + " get" + uname + "();");
        }
    }


    /**
     * @param field
     */
    private void generateFieldAccessor(String beanClassName, FieldDescriptor field) {

        String lname = lCamel(field.getName());
        String uname = uCamel(field.getName());
        String type = field.getRule() == FieldDescriptor.REPEATED_RULE ? javaCollectionType(field) : javaType(field);
        String typeDefault = javaTypeDefault(field);
        boolean primitive = isPrimitive(type);
        boolean repeated = field.getRule() == FieldDescriptor.REPEATED_RULE;

        // Create the fields..
        p("// " + field.getRule() + " " + field.getType() + " " + field.getName() + " = " + field.getTag() + ";");

        if (repeated) {
            p("private java.util.List<" + type + "> f_" + lname + ";");
            p();

            // Create the field accessors
            p("public boolean has" + uname + "() {");
            indent();
            p("return bean.f_" + lname + "!=null && !bean.f_" + lname + ".isEmpty();");
            unindent();
            p("}");
            p();

            p("public java.util.List<" + type + "> get" + uname + "List() {");
            indent();
            p("return bean.f_" + lname + ";");
            unindent();
            p("}");
            p();

            p("public java.util.List<" + type + "> create" + uname + "List() {");
            indent();
            p("copyCheck();");
            p("if( this.f_" + lname + " == null ) {");
            indent();
            p("this.f_" + lname + " = new java.util.ArrayList<" + type + ">();");
            unindent();
            p("}");
            p("return bean.f_" + lname + ";");
            unindent();
            p("}");
            p();

            p("public " + beanClassName + " set" + uname + "List(java.util.List<" + type + "> " + lname + ") {");
            indent();
            p("copyCheck();");
            p("this.f_" + lname + " = " + lname + ";");
            p("return this;");
            unindent();
            p("}");
            p();

            p("public int get" + uname + "Count() {");
            indent();
            p("if( bean.f_" + lname + " == null ) {");
            indent();
            p("return 0;");
            unindent();
            p("}");
            p("return bean.f_" + lname + ".size();");
            unindent();
            p("}");
            p();

            p("public " + type + " get" + uname + "(int index) {");
            indent();
            p("if( bean.f_" + lname + " == null ) {");
            indent();
            p("return null;");
            unindent();
            p("}");
            p("return bean.f_" + lname + ".get(index);");
            unindent();
            p("}");
            p();

            p("public " + beanClassName + " set" + uname + "(int index, " + type + " value) {");
            indent();
            p("this.create" + uname + "List().set(index, value);");
            p("return this;");
            unindent();
            p("}");
            p();

            p("public " + beanClassName + " add" + uname + "(" + type + " value) {");
            indent();
            p("this.create" + uname + "List().add(value);");
            p("return this;");
            unindent();
            p("}");
            p();

            p("public " + beanClassName + " addAll" + uname + "(java.lang.Iterable<? extends " + type + "> collection) {");
            indent();
            p("org.fusesource.hawtbuf.proto.MessageBufferSupport.addAll(collection, this.create" + uname + "List());");
            p("return this;");
            unindent();
            p("}");
            p();

            p("public void clear" + uname + "() {");
            indent();
            p("copyCheck();");
            p("this.f_" + lname + " = null;");
            unindent();
            p("}");
            p();

        } else {

            p("private " + type + " f_" + lname + " = " + typeDefault + ";");
            if (primitive) {
                p("private boolean b_" + lname + ";");
            }
            p();

            // Create the field accessors
            p("public boolean has" + uname + "() {");
            indent();
            if (primitive) {
                p("return bean.b_" + lname + ";");
            } else {
                p("return bean.f_" + lname + "!=null;");
            }
            unindent();
            p("}");
            p();

            p("public " + type + " get" + uname + "() {");
            indent();
            p("return bean.f_" + lname + ";");
            unindent();
            p("}");
            p();

            p("public " + beanClassName + " set" + uname + "(" + type + " " + lname + ") {");
            indent();
            p("copyCheck();");
            if (primitive) {
                if (auto_clear_optional_fields && field.isOptional()) {
                    if (field.isStringType() && !"null".equals(typeDefault)) {
                        p("this.b_" + lname + " = (" + lname + " != " + typeDefault + ");");
                    } else {
                        p("this.b_" + lname + " = (" + lname + " != " + typeDefault + ");");
                    }
                } else {
                    p("this.b_" + lname + " = true;");
                }
            }
            p("this.f_" + lname + " = " + lname + ";");
            p("return this;");
            unindent();
            p("}");
            p();

            p("public void clear" + uname + "() {");
            indent();
            p("copyCheck();");
            if (primitive) {
                p("this.b_" + lname + " = false;");
            }
            p("this.f_" + lname + " = " + typeDefault + ";");
            unindent();
            p("}");
            p();
        }

    }

    private String javaTypeDefault(FieldDescriptor field) {
        OptionDescriptor defaultOption = field.getOptions().get("default");
        if (defaultOption != null) {
            if (field.isStringType()) {
                return asJavaString(defaultOption.getValue());
            } else if (field.getType() == FieldDescriptor.BYTES_TYPE) {
                return "new " + javaType(field) + "(org.fusesource.hawtbuf.UTF8Buffer.encode(" + asJavaString(defaultOption.getValue()) + "))";
            } else if (field.isInteger32Type()) {
                int v;
                if (field.getType() == FieldDescriptor.UINT32_TYPE) {
                    v = TextFormat.parseUInt32(defaultOption.getValue());
                } else {
                    v = TextFormat.parseInt32(defaultOption.getValue());
                }
                return "" + v;
            } else if (field.isInteger64Type()) {
                long v;
                if (field.getType() == FieldDescriptor.UINT64_TYPE) {
                    v = TextFormat.parseUInt64(defaultOption.getValue());
                } else {
                    v = TextFormat.parseInt64(defaultOption.getValue());
                }
                return "" + v + "l";
            } else if (field.getType() == FieldDescriptor.DOUBLE_TYPE) {
                double v = Double.valueOf(defaultOption.getValue());
                return "" + v + "d";
            } else if (field.getType() == FieldDescriptor.FLOAT_TYPE) {
                float v = Float.valueOf(defaultOption.getValue());
                return "" + v + "f";
            } else if (field.getType() == FieldDescriptor.BOOL_TYPE) {
                boolean v = Boolean.valueOf(defaultOption.getValue());
                return "" + v;
            } else if (field.getTypeDescriptor() != null && field.getTypeDescriptor().isEnum()) {
                return javaType(field) + "." + defaultOption.getValue();
            }
            return defaultOption.getValue();
        } else {
            if (field.isNumberType()) {
                return "0";
            }
            if (field.getType() == FieldDescriptor.BOOL_TYPE) {
                return "false";
            }
            return "null";
        }
    }

    static final char HEX_TABLE[] = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

    private String asJavaString(String value) {
        StringBuilder sb = new StringBuilder(value.length() + 2);
        sb.append("\"");
        for (int i = 0; i < value.length(); i++) {

            char b = value.charAt(i);
            switch (b) {
                // Java does not recognize \a or \v, apparently.
                case '\b':
                    sb.append("\\b");
                    break;
                case '\f':
                    sb.append("\\f");
                    break;
                case '\n':
                    sb.append("\\n");
                    break;
                case '\r':
                    sb.append("\\r");
                    break;
                case '\t':
                    sb.append("\\t");
                    break;
                case '\\':
                    sb.append("\\\\");
                    break;
                case '\'':
                    sb.append("\\\'");
                    break;
                case '"':
                    sb.append("\\\"");
                    break;
                default:
                    if (b >= 0x20 && b < 'Z') {
                        sb.append((char) b);
                    } else {
                        sb.append("\\u");
                        sb.append(HEX_TABLE[(b >>> 12) & 0x0F]);
                        sb.append(HEX_TABLE[(b >>> 8) & 0x0F]);
                        sb.append(HEX_TABLE[(b >>> 4) & 0x0F]);
                        sb.append(HEX_TABLE[b & 0x0F]);
                    }
                    break;
            }

        }
        sb.append("\"");
        return sb.toString();
    }

    private void generateEnum(EnumDescriptor ed) {
        String uname = uCamel(ed.getName());

        String staticOption = "static ";
        if (multipleFiles && ed.getParent() == null) {
            staticOption = "";
        }

        // TODO Auto-generated method stub
        p();
        p("public " + staticOption + "enum " + uname + " {");
        indent();


        p();
        int counter = 0;
        for (EnumFieldDescriptor field : ed.getFields().values()) {
            boolean last = counter + 1 == ed.getFields().size();
            p(field.getName() + "(\"" + field.getName() + "\", " + field.getValue() + ")" + (last ? ";" : ","));
            counter++;
        }
        p();
        p("private final String name;");
        p("private final int value;");
        p();
        p("private " + uname + "(String name, int value) {");
        p("   this.name = name;");
        p("   this.value = value;");
        p("}");
        p();
        p("public final int getNumber() {");
        p("   return value;");
        p("}");
        p();
        p("public final String toString() {");
        p("   return name;");
        p("}");
        p();
        p("public static " + uname + " valueOf(int value) {");
        p("   switch (value) {");

        // It's possible to define multiple ENUM fields with the same value.. 
        //   we only want to put the first one into the switch statement.
        HashSet<Integer> values = new HashSet<Integer>();
        for (EnumFieldDescriptor field : ed.getFields().values()) {
            if (!values.contains(field.getValue())) {
                p("   case " + field.getValue() + ":");
                p("      return " + field.getName() + ";");
                values.add(field.getValue());
            }

        }
        p("   default:");
        p("      return null;");
        p("   }");
        p("}");
        p();


        String createMessage = getOption(ed.getOptions(), "java_create_message", null);
        if ("true".equals(createMessage)) {

            p("public interface " + uname + "Creatable {");
            indent();
            p("" + uname + " to" + uname + "();");
            unindent();
            p("}");
            p();

            p("public " + uname + "Creatable createBean() {");
            indent();
            p("switch (this) {");
            indent();
            for (EnumFieldDescriptor field : ed.getFields().values()) {
                p("case " + field.getName() + ":");
                String type = field.getAssociatedType().getName();
                p("   return new " + qualified(type, beanCN) + "();");
            }
            p("default:");
            p("   return null;");
            unindent();
            p("}");
            unindent();
            p("}");
            p();

            generateParseDelegate(ed, "parseUnframed", "org.fusesource.hawtbuf.Buffer", "org.fusesource.hawtbuf.proto.InvalidProtocolBufferException");
            generateParseDelegate(ed, "parseFramed", "org.fusesource.hawtbuf.Buffer", "org.fusesource.hawtbuf.proto.InvalidProtocolBufferException");
            generateParseDelegate(ed, "parseUnframed", "byte[]", "org.fusesource.hawtbuf.proto.InvalidProtocolBufferException");
            generateParseDelegate(ed, "parseFramed", "byte[]", "org.fusesource.hawtbuf.proto.InvalidProtocolBufferException");
            generateParseDelegate(ed, "parseFramed", "org.fusesource.hawtbuf.proto.CodedInputStream", "org.fusesource.hawtbuf.proto.InvalidProtocolBufferException, java.io.IOException");
            generateParseDelegate(ed, "parseFramed", "java.io.InputStream", "org.fusesource.hawtbuf.proto.InvalidProtocolBufferException, java.io.IOException");
        }

        unindent();
        p("}");
        p();
    }

    private void generateParseDelegate(EnumDescriptor descriptor, String methodName, String inputType, String exceptions) {
        p("public org.fusesource.hawtbuf.proto.MessageBuffer " + methodName + "(" + inputType + " data) throws " + exceptions + " {");
        indent();
        p("switch (this) {");
        indent();
        for (EnumFieldDescriptor field : descriptor.getFields().values()) {
            p("case " + field.getName() + ":");
            String type = constantToUCamelCase(field.getName());
            p("   return " + qualified(type, factoryFeild) + "." + methodName + "(data);");
        }
        p("default:");
        p("   return null;");
        unindent();
        p("}");
        unindent();
        p("}");
        p();
    }


    private String javaCollectionType(FieldDescriptor field) {
        if (field.isInteger32Type()) {
            return "java.lang.Integer";
        }
        if (field.isInteger64Type()) {
            return "java.lang.Long";
        }
        if (field.getType() == FieldDescriptor.DOUBLE_TYPE) {
            return "java.lang.Double";
        }
        if (field.getType() == FieldDescriptor.FLOAT_TYPE) {
            return "java.lang.Float";
        }
        if (field.getType() == FieldDescriptor.STRING_TYPE) {
            // TODO: support handling string fields as buffers.
//            String override = getOption(field.getOptions(), "java_override_type", null);
//            if( "AsciiBuffer".equals(override) ) {
//                return "org.fusesource.hawtbuf.AsciiBuffer";
//            } else if( "UTF8Buffer".equals(override) ) {
//                return "org.fusesource.hawtbuf.UTF8Buffer";
//            } else if( "Buffer".equals(override) ) {
//                return "org.fusesource.hawtbuf.Buffer";
//            } else {
            return "java.lang.String";
//            }
        }
        if (field.getType() == FieldDescriptor.BYTES_TYPE) {
            String override = getOption(field.getOptions(), "java_override_type", null);
            if ("AsciiBuffer".equals(override)) {
                return "org.fusesource.hawtbuf.AsciiBuffer";
            } else if ("UTF8Buffer".equals(override)) {
                return "org.fusesource.hawtbuf.UTF8Buffer";
            } else {
                return "org.fusesource.hawtbuf.Buffer";
            }
        }
        if (field.getType() == FieldDescriptor.BOOL_TYPE) {
            return "java.lang.Boolean";
        }

        TypeDescriptor descriptor = field.getTypeDescriptor();
        return qualified(javaFactoryType(descriptor), getterCN);
    }

    private String javaType(FieldDescriptor field) {
        if (field.isInteger32Type()) {
            return "int";
        }
        if (field.isInteger64Type()) {
            return "long";
        }
        if (field.getType() == FieldDescriptor.DOUBLE_TYPE) {
            return "double";
        }
        if (field.getType() == FieldDescriptor.FLOAT_TYPE) {
            return "float";
        }
        if (field.getType() == FieldDescriptor.STRING_TYPE) {
            // TODO: support handling string fields as buffers.
//            String override = getOption(field.getOptions(), "java_override_type", null);
//            if( "AsciiBuffer".equals(override) ) {
//                return "org.fusesource.hawtbuf.AsciiBuffer";
//            } else if( "UTF8Buffer".equals(override) ) {
//                return "org.fusesource.hawtbuf.UTF8Buffer";
//            } else if( "Buffer".equals(override) ) {
//                return "org.fusesource.hawtbuf.Buffer";
//            } else {
            return "java.lang.String";
//            }
        }
        if (field.getType() == FieldDescriptor.BYTES_TYPE) {
            String override = getOption(field.getOptions(), "java_override_type", null);
            if ("AsciiBuffer".equals(override)) {
                return "org.fusesource.hawtbuf.AsciiBuffer";
            } else if ("UTF8Buffer".equals(override)) {
                return "org.fusesource.hawtbuf.UTF8Buffer";
            } else {
                return "org.fusesource.hawtbuf.Buffer";
            }
        }
        if (field.getType() == FieldDescriptor.BOOL_TYPE) {
            return "boolean";
        }

        TypeDescriptor descriptor = field.getTypeDescriptor();
        return qualified(javaFactoryType(descriptor), getterCN);
    }

    private String javaFactoryType(TypeDescriptor descriptor) {
        ProtoDescriptor p = descriptor.getProtoDescriptor();
        if (p != proto) {
            // Try to keep it short..
            String othePackage = javaPackage(p);
            if (equals(othePackage, javaPackage(proto))) {
                return javaClassName(p) + "." + descriptor.getQName();
            }
            // Use the fully qualified class name.
            return othePackage + "." + javaClassName(p) + "." + descriptor.getQName();
        }
        return descriptor.getQName();
    }

    private String qualified(String type, String suffix) {
//        int ix = type.lastIndexOf(".");
//        if (ix == -1) {
//            // type = Foo, result = Foo.FooBean
//            return type+"."+type+suffix;
//        }
//        // type = Foo.Bar, result = Foo.Bar.BarBean
//        return type+"."+type.substring(ix+1)+suffix;
        return type + "." + suffix;
    }

    private boolean equals(String o1, String o2) {
        if (o1 == o2)
            return true;
        if (o1 == null || o2 == null)
            return false;
        return o1.equals(o2);
    }

    private String javaClassName(ProtoDescriptor proto) {
        return getOption(proto.getOptions(), "java_outer_classname", uCamel(removeFileExtension(proto.getName())));
    }

    private boolean isMultipleFilesEnabled(ProtoDescriptor proto) {
        return "true".equals(getOption(proto.getOptions(), "java_multiple_files", "false"));
    }


    private String javaPackage(ProtoDescriptor proto) {
        String name = proto.getPackageName();
        if (name != null) {
            name = name.replace('-', '.');
            name = name.replace('/', '.');
        }
        return getOption(proto.getOptions(), "java_package", name);
    }


    // ----------------------------------------------------------------
    // Internal Helper methods
    // ----------------------------------------------------------------

    private void indent() {
        indent++;
    }

    private void unindent() {
        indent--;
    }

    private void p(String line) {
        // Indent...
        for (int i = 0; i < indent; i++) {
            w.print("   ");
        }
        // Then print.
        w.println(line);
    }

    private void p() {
        w.println();
    }

    private String getOption(Map<String, OptionDescriptor> options, String optionName, String defaultValue) {
        OptionDescriptor optionDescriptor = options.get(optionName);
        if (optionDescriptor == null) {
            return defaultValue;
        }
        return optionDescriptor.getValue();
    }

    static private String removeFileExtension(String name) {
        return name.replaceAll("\\..*", "");
    }

    static private String uCamel(String name) {
        boolean upNext = true;
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < name.length(); i++) {
            char c = name.charAt(i);
            if (Character.isJavaIdentifierPart(c) && Character.isLetterOrDigit(c)) {
                if (upNext) {
                    c = Character.toUpperCase(c);
                    upNext = false;
                }
                sb.append(c);
            } else {
                upNext = true;
            }
        }
        return sb.toString();
    }

    static private String lCamel(String name) {
        if (name == null || name.length() < 1)
            return name;
        String uCamel = uCamel(name);
        return uCamel.substring(0, 1).toLowerCase() + uCamel.substring(1);
    }


    private String constantToUCamelCase(String name) {
        boolean upNext = true;
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < name.length(); i++) {
            char c = name.charAt(i);
            if (Character.isJavaIdentifierPart(c) && Character.isLetterOrDigit(c)) {
                if (upNext) {
                    c = Character.toUpperCase(c);
                    upNext = false;
                } else {
                    c = Character.toLowerCase(c);
                }
                sb.append(c);
            } else {
                upNext = true;
            }
        }
        return sb.toString();
    }


    private String constantCase(String name) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < name.length(); i++) {
            char c = name.charAt(i);
            if (i != 0 && Character.isUpperCase(c)) {
                sb.append("_");
            }
            sb.append(Character.toUpperCase(c));
        }
        return sb.toString();
    }

    public File getOut() {
        return out;
    }

    public void setOut(File outputDirectory) {
        this.out = outputDirectory;
    }

    public File[] getPath() {
        return path;
    }

    public void setPath(File[] path) {
        this.path = path;
    }

}
