/**
 * 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 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;

import static org.fusesource.hawtbuf.proto.WireFormat.*;

public class JavaGenerator {

    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 deferredDecode;
    private boolean auto_clear_optional_fields;

    public static void main(String[] args) {
        
        JavaGenerator generator = new JavaGenerator();
        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 className = uCamel(m.getName());
        p();
        
        String staticOption = "static ";
        if( multipleFiles && m.getParent()==null ) {
            staticOption="";
        }
        
        String javaImplements = getOption(m.getOptions(), "java_implements", null);
        if (javaImplements == null) {
            // Provide compatibility with previous versions which had implements misspelled
            javaImplements = getOption(m.getOptions(), "java_implments", null);
        }
        
        String implementsExpression = "";
        if( javaImplements!=null ) {
            implementsExpression = "implements "+javaImplements+" ";
        }
        
        String baseClass = "org.fusesource.hawtbuf.proto.BaseMessage";
        if( deferredDecode ) {
            baseClass = "org.fusesource.hawtbuf.proto.DeferredDecodeMessage";
        }
        if( m.getBaseType()!=null ) {
            baseClass = javaType(m.getBaseType())+"Base";
        }
        
        p(staticOption+"public final class " + className + " extends "+className+"Base<"+className+"> "+implementsExpression+"{");
        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( isInBaseClass(m, field) ) {
                continue;
            }
            if( field.isGroup() ) {
                generateMessageBean(field.getGroup());
            }
        }

        
        generateMethodAssertInitialized(m, className);

        generateMethodClear(m);

        p("public "+className+" clone() {");
        p("   return new "+className+"().mergeFrom(this);");
        p("}");
        p();
        
        generateMethodMergeFromBean(m, className);

        generateMethodSerializedSize(m);
        
        generateMethodMergeFromStream(m, className);

        generateMethodWriteTo(m);

        generateMethodParseFrom(m, className);

        generateMethodToString(m);
        
        generateMethodVisitor(m);
                
        generateMethodType(m, className);
        
        generateMethodEquals(m, className);
                
        unindent();
        p("}");
        p();
        
        p(staticOption+"abstract class " + className + "Base<T> extends "+baseClass+"<T> {");
        p();
        indent();
        
        // Generate the field accessors..
        for (FieldDescriptor field : m.getFields().values()) {
            if( isInBaseClass(m, field) ) {
                continue;
            }
            generateFieldAccessor(field);
        }
        
        unindent();
        p("}");
        p();
    }

    private boolean isInBaseClass(MessageDescriptor m, FieldDescriptor field) {
        if( m.getBaseType() ==null )
            return false;
        return m.getBaseType().getFields().containsKey(field.getName());
    }

    /**
     * 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.
     * 
     * Examples:
     * 
     *   option java_visitor = "org.apache.kahadb.store.Visitor";
     *   generates:
     *     public void visit(org.apache.kahadb.store.Visitor visitor) {
     *       visitor.visit(this);
     *     }
     *   
     *   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 = javaType(typeDescriptor);
            
            p("public "+type+" type() {");
            indent();
                p("return "+type+"."+constant+";");
            unindent();
            p("}");
            p();
        }
    }
    
    private void generateMethodParseFrom(MessageDescriptor m, String className) {

        String postMergeProcessing = ".checktInitialized()";
        if( deferredDecode ) {
            postMergeProcessing="";
        }
        
        p("public static "+className+" parseUnframed(org.fusesource.hawtbuf.proto.CodedInputStream data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException, java.io.IOException {");
        indent();
        p("return new "+className+"().mergeUnframed(data)"+postMergeProcessing+";");
        unindent();
        p("}");
        p();

        p("public static "+className+" parseUnframed(org.fusesource.hawtbuf.Buffer data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException {");
        indent();
        p("return new "+className+"().mergeUnframed(data)"+postMergeProcessing+";");
        unindent();
        p("}");
        p();

        p("public static "+className+" parseUnframed(byte[] data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException {");
        indent();
        p("return new "+className+"().mergeUnframed(data)"+postMergeProcessing+";");
        unindent();
        p("}");
        p();
        
        p("public static "+className+" parseUnframed(java.io.InputStream data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException, java.io.IOException {");
        indent();
        p("return new "+className+"().mergeUnframed(data)"+postMergeProcessing+";");
        unindent();
        p("}");
        p();
        
        p("public static "+className+" parseFramed(org.fusesource.hawtbuf.proto.CodedInputStream data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException, java.io.IOException {");
        indent();
        p("return new "+className+"().mergeFramed(data)"+postMergeProcessing+";");
        unindent();
        p("}");
        p();
        
        p("public static "+className+" parseFramed(org.fusesource.hawtbuf.Buffer data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException {");
        indent();
        p("return new "+className+"().mergeFramed(data)"+postMergeProcessing+";");
        unindent();
        p("}");
        p();

        p("public static "+className+" parseFramed(byte[] data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException {");
        indent();
        p("return new "+className+"().mergeFramed(data)"+postMergeProcessing+";");
        unindent();
        p("}");
        p();
        
        p("public static "+className+" parseFramed(java.io.InputStream data) throws org.fusesource.hawtbuf.proto.InvalidProtocolBufferException, java.io.IOException {");
        indent();
        p("return new "+className+"().mergeFramed(data)"+postMergeProcessing+";");
        unindent();
        p("}");
        p();
    }

    private void generateMethodEquals(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();
        if( deferredDecode ) {
            p("return toUnframedBuffer().equals(obj.toUnframedBuffer());");
        } else {        
            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();
        if( deferredDecode ) {
            p("return "+hc+" ^ toUnframedBuffer().hashCode();");
        } else {
            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("");
    }
    
    /**
     * @param m
     */
    private void generateMethodSerializedSize(MessageDescriptor m) {
        p("public int serializedSizeUnframed() {");
        indent();
        if( deferredDecode ) {
            p("if (encodedForm != null) {");
            indent();
            p("return encodedForm.length;");
            unindent();
            p("}");
        }
        p("if (memoizedSerializedSize != -1)");
        p("   return memoizedSerializedSize;");
        p();
        p("int size = 0;");
        for (FieldDescriptor field : m.getFields().values()) {
            
            String uname = uCamel(field.getName());
            String getter="get"+uname+"()";            
            String type = javaType(field);
            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 ) {
                p("size += computeGroupSize("+field.getTag()+", "+getter+");");
            } else {
                p("size += computeMessageSize("+field.getTag()+", "+getter+");");
            }
            if( field.getRule() == FieldDescriptor.REPEATED_RULE ) {
                unindent();
                p("}");
            }
            //TODO: finish this up.
            unindent();
            p("}");

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

    /**
     * @param m
     */
    private void generateMethodWriteTo(MessageDescriptor m) {
        p("public void writeUnframed(org.fusesource.hawtbuf.proto.CodedOutputStream output) throws java.io.IOException {");
        indent();
        
        if( deferredDecode ) {
            p("if (encodedForm == null) {");
            indent();
            p("int size = serializedSizeUnframed();");
            p("encodedForm = output.getNextBuffer(size);");
            p("org.fusesource.hawtbuf.proto.CodedOutputStream original=null;");
            p("if( encodedForm == null ) {");
            indent();
            p("encodedForm = new org.fusesource.hawtbuf.Buffer(new byte[size]);");
            p("original = output;");
            p("output = new org.fusesource.hawtbuf.proto.CodedOutputStream(encodedForm);");
            unindent();
            p("}");
        }
        

        for (FieldDescriptor field : m.getFields().values()) {
            String uname = uCamel(field.getName());
            String getter="get"+uname+"()";            
            String type = javaType(field);
            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("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 ) {
                p("writeGroup(output, "+field.getTag()+", "+getter+");");
            } else {
                p("writeMessage(output, "+field.getTag()+", "+getter+");");
            }
            
            if( field.getRule() == FieldDescriptor.REPEATED_RULE ) {
                unindent();
                p("}");
            }
            
            unindent();
            p("}");
        }
        
        if( deferredDecode ) {
            p("if( original !=null ) {");
            indent();
            p("output.checkNoSpaceLeft();");
            p("output = original;");
            p("output.writeRawBytes(encodedForm);");
            unindent();
            p("}");
            unindent();
            p("} else {");
            indent();
            p("output.writeRawBytes(encodedForm);");
            unindent();
            p("}");
        }

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

    /**
     * @param m
     * @param className
     */
    private void generateMethodMergeFromStream(MessageDescriptor m, String className) {
        p("public "+className+" mergeUnframed(org.fusesource.hawtbuf.proto.CodedInputStream input) throws java.io.IOException {");
        indent();
        {
            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 = "get" + 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();
                        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) {
                        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 = javaType(field);
                        if (repeated) {
                            p(setter + "(new " + type
                                    + "().mergeFramed(input));");
                        } else {
                            p("if (has" + uname + "()) {");
                            indent();
                            p("get" + uname + "().mergeFramed(input);");
                            unindent();
                            p("} else {");
                            indent();
                            p(setter + "(new " + type
                                    + "().mergeFramed(input));");
                            unindent();
                            p("}");
                        }
                    }
                    p("break;");
                    unindent();
                }
                p("}");
            }
            unindent();
            p("}");
        }
        unindent();
        p("}");
    }

    /**
     * @param m
     * @param className
     */
    private void generateMethodMergeFromBean(MessageDescriptor m, String className) {
        p("public "+className+" mergeFrom("+className+" other) {");
        indent();
        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 = javaType(field);
                // It's complex type...
                if( field.isRepeated() ) {
                    p("for("+type+" element: other.get"+uname+"List() ) {");
                    indent();
                        p("get"+uname+"List().add(element.clone());");
                    unindent();
                    p("}");
                } else {
                    p("if (has"+uname+"()) {");
                    indent();
                    p("get"+uname+"().mergeFrom(other.get"+uname+"());");
                    unindent();
                    p("} else {");
                    indent();
                    p("set"+uname+"(other.get"+uname+"().clone());");
                    unindent();
                    p("}");
                }
            }
            unindent();
            p("}");
        }
        p("return this;");
        unindent();
        p("}");
        p();
    }

    /**
     * @param m
     */
    private void generateMethodClear(MessageDescriptor m) {
        p("public void clear() {");
        indent();
        p("super.clear();");
        for (FieldDescriptor field : m.getFields().values()) {
            String uname = uCamel(field.getName());
            p("clear" + uname + "();");
        }
        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();
        
        if( deferredDecode ) {
            p("load();");
        }        
        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
     * @param className 
     */
    private void generateFieldAccessor(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 = field.getTypeDescriptor()==null || field.getTypeDescriptor().isEnum();
        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();
            if( deferredDecode ) {
                p("load();");
            }        
            p("return this.f_" + lname + "!=null && !this.f_" + lname + ".isEmpty();");
            unindent();
            p("}");
            p();

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

            p("public T set" + uname + "List(java.util.List<" + type + "> " + lname + ") {");
            indent();
              p("loadAndClear();");
            p("this.f_" + lname + " = " + lname + ";");
            p("return (T)this;");
            unindent();
            p("}");
            p();
            
            p("public int get" + uname + "Count() {");
            indent();
            if( deferredDecode ) {
                p("load();");
            }        
            p("if( this.f_" + lname + " == null ) {");
            indent();
            p("return 0;");
            unindent();
            p("}");
            p("return this.f_" + lname + ".size();");
            unindent();
            p("}");
            p();
            
            p("public " + type + " get" + uname + "(int index) {");
            indent();
            if( deferredDecode ) {
                p("load();");
            }        
            p("if( this.f_" + lname + " == null ) {");
            indent();
            p("return null;");
            unindent();
            p("}");
            p("return this.f_" + lname + ".get(index);");
            unindent();
            p("}");
            p();
                            
            p("public T set" + uname + "(int index, " + type + " value) {");
            indent();
              p("loadAndClear();");
            p("get" + uname + "List().set(index, value);");
            p("return (T)this;");
            unindent();
            p("}");
            p();
            
            p("public T add" + uname + "(" + type + " value) {");
            indent();
              p("loadAndClear();");
            p("get" + uname + "List().add(value);");
            p("return (T)this;");
            unindent();
            p("}");
            p();
            
            p("public T addAll" + uname + "(java.lang.Iterable<? extends " + type + "> collection) {");
            indent();
              p("loadAndClear();");
            p("super.addAll(collection, get" + uname + "List());");
            p("return (T)this;");
            unindent();
            p("}");
            p();

            p("public void clear" + uname + "() {");
            indent();
              p("loadAndClear();");
            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( deferredDecode ) {
                p("load();");
            }        
            if (primitive) {
                p("return this.b_" + lname + ";");
            } else {
                p("return this.f_" + lname + "!=null;");
            }
            unindent();
            p("}");
            p();

            p("public " + type + " get" + uname + "() {");
            indent();
            if( deferredDecode ) {
                p("load();");
            }        
            if( field.getTypeDescriptor()!=null && !field.getTypeDescriptor().isEnum()) {
                p("if( this.f_" + lname + " == null ) {");
                indent();
                p("this.f_" + lname + " = new " + type + "();");
                unindent();
                p("}");
            }
            p("return this.f_" + lname + ";");
            unindent();
            p("}");
            p();

            p("public T set" + uname + "(" + type + " " + lname + ") {");
            indent();
              p("loadAndClear();");
            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 (T)this;");
            unindent();
            p("}");
            p();

            p("public void clear" + uname + "() {");
            indent();
              p("loadAndClear();");
            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 org.fusesource.hawtbuf.Buffer(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 org.fusesource.hawtbuf.proto.Message createMessage() {");
            indent();
            p("switch (this) {");
            indent();
            for (EnumFieldDescriptor field : ed.getFields().values()) {
                p("case "+field.getName()+":");
                String type = constantToUCamelCase(field.getName());
                p("   return new "+type+"();");
            }
            p("default:");
            p("   return null;");
            unindent();
            p("}");
            unindent();
            p("}");
            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 ) {
            return "java.lang.String";
        }
        if( field.getType() == FieldDescriptor.BYTES_TYPE ) {
            return "org.fusesource.hawtbuf.Buffer";
        }
        if( field.getType() == FieldDescriptor.BOOL_TYPE ) {
            return "java.lang.Boolean";
        }
        
        TypeDescriptor descriptor = field.getTypeDescriptor();
        return javaType(descriptor);
    }

    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 ) {
            return "java.lang.String";
        }
        if( field.getType() == FieldDescriptor.BYTES_TYPE ) {
            return "org.fusesource.hawtbuf.Buffer";
        }
        if( field.getType() == FieldDescriptor.BOOL_TYPE ) {
            return "boolean";
        }
        
        TypeDescriptor descriptor = field.getTypeDescriptor();
        return javaType(descriptor);
    }

    private String javaType(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 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;
    }
    
}
