//$Id: BasicRenderer.java,v 1.6 2003/01/24 13:30:19 oneovthafew Exp $
package net.sf.hibernate.tool.hbm2java;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

import net.sf.hibernate.util.StringHelper;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class BasicRenderer implements Renderer {

    static private Log log = LogFactory.getLog(BasicRenderer.class); 	
	/**
	 * Returns the true name for the given class name. By true name is
	 * that it will return the Proxy for the class name if the class was
	 * defined with a proxy attribute.
	 * @param field class name that we use to serach in class2classmap
	 * @param class2classmap a map from classname to classmappings
	 * @return String return either name or the proxy name of the classmap
	 */
	String getTrueTypeName(Field  field, Map class2classmap) {
        String name = ( field.getClassType()!=null ) ?
        	field.getClassType().getFullyQualifiedName():
        	field.getType();
        ClassMapping cmap = (ClassMapping) class2classmap.get(name);
        
        if(cmap!=null) {
            if (cmap.getProxy()!=null) {
                return cmap.getProxy();
            }   
        } 
        return name;
    }
    
    String getTrueTypeName(ClassName cn, Map class2classmap) {
        String name = cn.getFullyQualifiedName();
        ClassMapping cmap = (ClassMapping) class2classmap.get(name);
        
        
        if(cmap!=null) {
                    if (cmap.getProxy()!=null) {
                        return cmap.getProxy();
                    }   
                } 
        return name;
    }
    
    
    public void render(String packageName, ClassMapping classMapping, Map class2classmap, PrintWriter mainwriter) throws Exception {
        if ( classMapping.getGeneratedPackageName()!=null ) {
        	mainwriter.println("package " + classMapping.getGeneratedPackageName() + ";");
        }
        else {
        	mainwriter.println("// default package");
        }
        mainwriter.println();
    
        // switch to another writer to be able to insert the actually
        // used imports when whole class has been rendered. 
        StringWriter strWriter = new StringWriter();
        PrintWriter writer = new PrintWriter(strWriter);
        
    
        // class declaration
        if(classMapping.getMeta("description")==null) {
            writer.println("/** @author Hibernate CodeGenerator */");
        } else {
            writer.println("/** \n" + toJavaDoc(classMapping.getMetaAsString("description"),0)  + "*/");   
        }
        
        String classScope = "public";
        if(classMapping.getMeta("scope-class")!=null) {
            classScope = classMapping.getMetaAsString("scope-class").trim();
        }
        if(classMapping.shouldBeAbstract()) {
            writer.print( "abstract " + classScope + " class " + classMapping.getGeneratedName() );
        } else {
            writer.print( classScope + " class " + classMapping.getGeneratedName() );    
        }
    
        // subclass
        if (classMapping.getSuperClass() != null) {
            writer.print( " extends " + classMapping.getSuperClass() );
        } else if (classMapping.getMeta("extends")!=null) {
            writer.print( " extends " + classMapping.getMetaAsString("extends"));
        }
    
        // always implements Serializable
        writer.print(" implements Serializable");
        
    	// implement proxy, but NOT if the proxy is the class it self!
    	if ( 
    		classMapping.getProxy()!=null && 
    		( !classMapping.getProxy().equals( classMapping.getCanonicalName() ) )
    	) {
        	writer.print(StringHelper.COMMA_SPACE);
        	writer.print( classMapping.getProxy() );
        }
        
        if(classMapping.getMeta("implements")!=null) {
            Collection implementz = classMapping.getMeta("implements");
            for (Iterator iter = implementz.iterator(); iter.hasNext();) {
                String iface = (String) iter.next();
                writer.print(StringHelper.COMMA_SPACE);
                writer.print(iface);
            }
        }
        
        writer.println(" {");
        writer.println();
    
        // fields
        for ( Iterator fields = classMapping.getFields().iterator(); fields.hasNext(); ) {
            Field field = (Field) fields.next();
            
            String fieldScope = getFieldScope(field, "scope-field");
            writer.println( 
            	"    /** " + 
            	( field.isNullable() && !field.isIdentifier() ? "nullable " : StringHelper.EMPTY_STRING ) +
            	( field.isIdentifier() ? "identifier" : "persistent" )
            	+ " field */");
            writer.println(
            	"    " + fieldScope + " " + 
            	shortenType( field.getType(), classMapping.getImports() ) + 
            	' ' + 
            	field.getName() +
            	';'
            );
                       
        	writer.println();
        }
        
        // full constructor
        List allFieldsForFullConstructor = classMapping.getAllFieldsForFullConstructor();
        
        writer.println("    /** full constructor */");
        String fullCons = "    public " + classMapping.getGeneratedName() + StringHelper.OPEN_PAREN;
        
        
        for(Iterator fields = allFieldsForFullConstructor.iterator(); fields.hasNext();) {
            Field field = (Field) fields.next();
                fullCons = fullCons + shortenType(getTrueTypeName(field, class2classmap), classMapping.getImports()) + " " + field.getName();
                if(fields.hasNext()) {
                  fullCons = fullCons + ", ";
                }
        }
        
        writer.println(fullCons + ") {");
        //invoke super to initialize superclass...
        List supersConstructorFields = classMapping.getFieldsForSupersFullConstructor();
        if (!supersConstructorFields.isEmpty()) {
            writer.print("        super(");
            for (Iterator fields = supersConstructorFields.iterator(); fields.hasNext();) {
                Field field = (Field) fields.next();
                writer.print(field.getName());
                if(fields.hasNext()) {
                    writer.print(StringHelper.COMMA_SPACE);
                }
            }
            writer.println(");");
        }
        
        // initialisation of localfields
        for(Iterator fields = classMapping.getLocalFieldsForFullConstructor().iterator(); fields.hasNext();) {
            Field field = (Field) fields.next();
            writer.println("        this." + field.getName() + " = " + field.getName() + ";");
        }
        writer.println("    }");
        writer.println();
    
        // no args constructor (if fullconstructor had any arguments!)
        if (allFieldsForFullConstructor.size() > 0) {
            writer.println("    /** default constructor */");
    		writer.println("    public " + classMapping.getGeneratedName() + "() {");
    		writer.println("    }");
    		writer.println();
    	}
        
        // minimal constructor (only if the fullconstructor had any arguments)
        if ((allFieldsForFullConstructor.size() > 0) && classMapping.needsMinimalConstructor()) {
    
            List allFieldsForMinimalConstructor = classMapping.getAllFieldsForMinimalConstructor();
            writer.println("    /** minimal constructor */"); 
        
            String minCons = "    public " + classMapping.getGeneratedName() + StringHelper.OPEN_PAREN;
            for (Iterator fields = allFieldsForMinimalConstructor.iterator(); fields.hasNext();) {
                Field field = (Field) fields.next();
                minCons = minCons + shortenType(getTrueTypeName(field, class2classmap), classMapping.getImports()) + " " + field.getName();
                if (fields.hasNext()) {
                    minCons = minCons + ", ";
                }
            }
    
            writer.println(minCons + ") {");
            // invoke super to initialize superclass...
                  List supersMinConstructorFields = classMapping.getFieldsForSupersMinimalConstructor();
                  if (!supersMinConstructorFields.isEmpty()) {
                      writer.print("      super(");
                      for (Iterator fields = supersMinConstructorFields.iterator(); fields.hasNext();) {
                          Field field = (Field) fields.next();
                          writer.print(field.getName());
                          if(fields.hasNext()) {
                              writer.print(StringHelper.COMMA_SPACE);
                          }
                      }
                      writer.println(");");
                  }
        
            // initialisation of localfields
            for (Iterator fields = classMapping.getLocalFieldsForMinimalConstructor().iterator(); fields.hasNext();) {
                Field field = (Field) fields.next();
                writer.println("        this." + field.getName() + " = " + field.getName() + ";");
            }
            writer.println("    }");
            writer.println();
        }
    
    
        doFieldAccessors(classMapping, class2classmap, writer);
            
        writer.println();
        writer.println("    public String toString() {");
        //easier to use reflectionToString() than worry about superclasses
        writer.println("        return ToStringBuilder.reflectionToString(this);");
        writer.println("    }");
        writer.println();
        
        if ( classMapping.mustImplementEquals() ) {
        	writer.println("    public boolean equals(Object other) {");
        	writer.println("        if ( !(other instanceof " + classMapping.getGeneratedName() + ") ) return false;");
        	writer.println("        " + classMapping.getGeneratedName() + " castOther = (" + classMapping.getGeneratedName() + ") other;");
        	writer.println("        return new EqualsBuilder()");
        	for (Iterator fields = classMapping.getFields().iterator(); fields.hasNext();) {
        		Field field = (Field) fields.next();
        		if ( field.isIdentifier() ) {
        			writer.println("            .append(this." + field.getName() + ", castOther." + field.getName() + StringHelper.CLOSE_PAREN);
        		}
        	}
        	writer.println("            .isEquals();");
        	writer.println("    }");
    		writer.println();
    		
        	writer.println("    public int hashCode() {");
        	writer.println("        return new HashCodeBuilder()");
        	for (Iterator fields = classMapping.getFields().iterator(); fields.hasNext();) {
        		Field field = (Field) fields.next();
        		if ( field.isIdentifier() ) {
        			writer.println("            .append(" + field.getName() + StringHelper.CLOSE_PAREN);
        		}
        	}
        	writer.println("            .toHashCode();");
        	writer.println("    }");
    		writer.println();
        }
    	
        writer.println("}");
        
        
        // finally write the imports
        doImports(classMapping, mainwriter);
        mainwriter.print(strWriter.toString());
        
        
    }

    public void doFieldAccessors(ClassMapping classMapping, Map class2classmap, PrintWriter writer) {
        // field accessors
        for (Iterator fields = classMapping.getFields().iterator(); fields.hasNext();) {
            Field field = (Field) fields.next();
        
            // getter
            String getAccessScope = getFieldScope(field, "scope-get", "public");
            String getterType = ( field.getType().toLowerCase().equals("boolean") ) ? " is" : " get";
        
            if(field.getMeta("description")!=null) {
            writer.println("    /** \n" + toJavaDoc(field.getMetaAsString("description"), 4) + "     */"); 
            }
            writer.println("    " + getAccessScope + " " + getTrueTypeName(field, class2classmap) + getterType + field.getAsSuffix() + "() {");
            writer.println("        return this." + field.getName() + ";");
            writer.println("    }");
            writer.println();
        
            // setter
            String setAccessScope = getFieldScope(field, "scope-set", "public");
            writer.println("    " + setAccessScope + " void set" + field.getAsSuffix() + StringHelper.OPEN_PAREN + getTrueTypeName(field, class2classmap) + " " + field.getName() + ") {");
            writer.println("        this." + field.getName() + " = " + field.getName() + ";");
            writer.println("    }");
            writer.println();
            
            // add/remove'rs (commented out for now)
            /* 
            if(field.getForeignClass()!=null) { 
                ClassName foreignClass = field.getForeignClass();
                
                String trueforeign = getTrueTypeName(foreignClass, class2classmap);
                classMapping.addImport(trueforeign);
                
                // Try to identify the matching set method on the child.
                ClassMapping forignMap = (ClassMapping) class2classmap.get(foreignClass.getFullyQualifiedName());
                
                if(forignMap!=null) {
                  Iterator foreignFields = forignMap.getFields().iterator();
                  while (foreignFields.hasNext()) {
                    Field ffield = (Field) foreignFields.next();
                    if(ffield.isIdentifier()) {
                       log.debug("Trying to match " + ffield.getName() + " with " + field.getForeignKeys());   
                    }
                }
                  
                } else {
                  log.error("Could not find foreign class's mapping - cannot provide bidirectional setters!");   
                }
                
                String addAccessScope = getFieldScope(field, "scope", "scope-add");
                writer.println("    " + setAccessScope + " void add" + field.getAsSuffix() + StringHelper.OPEN + shortenType(trueforeign, classMapping.getImports()) + " a" + field.getName() + ") {");
                writer.println("        this." + getterType + field.getAsSuffix() + "().add(a" + field.getName() + ");");
                writer.println("        a" + field.getName() + ".setXXX(this);");
                writer.println("    }");
                writer.println();
            
            
            }
            */
        }
        
    }

    public void doImports(ClassMapping classMapping, PrintWriter writer) {
           // imports
            classMapping.getImports().add("java.io.Serializable");
            classMapping.getImports().add("org.apache.commons.lang.builder.ToStringBuilder");
            if ( classMapping.mustImplementEquals() ) {
            	classMapping.getImports().add("org.apache.commons.lang.builder.EqualsBuilder");
            	classMapping.getImports().add("org.apache.commons.lang.builder.HashCodeBuilder");
            }
        
            for ( Iterator imports = classMapping.getImports().iterator(); imports.hasNext(); ) {
                writer.println("import " + imports.next() + ";");
            }
            writer.println();
    }
    
    public String getFieldScope(Field field, String localScopeName) {
        return ( field.getMeta(localScopeName)==null )? "private" : field.getMetaAsString(localScopeName);
    }
    
    public String getFieldScope(Field field, String localScopeName, String defaultScope) {
        return ( field.getMeta(localScopeName)==null )? defaultScope : field.getMetaAsString(localScopeName);
    }
    
        
    /**
     * Convert string into something that can be rendered nicely into a javadoc
     * comment.
     * Prefix each line with a star ('*').
     * @param string
     */
    private String toJavaDoc(String string, int indent) {
        StringBuffer result = new StringBuffer();
        
        if(string!=null) {
            String[] lines = StringUtils.split(string, "\n\r\f");
            for (int i = 0; i < lines.length; i++) {
                String docline = " * " + lines[i] + "\n";
                result.append(StringUtils.leftPad(docline, docline.length() + indent));
            }
        }
        
        return result.toString();
    }
    
    
    /**
     * Returns the last part of type if it is in the set of imports.
     * e.g. java.util.Date would become Date, if imports contains 
     * java.util.Date.
     * 
     * @param type
     * @param imports
     * @return String
     */
    private String shortenType(String type, TreeSet imports) {
        if( imports.contains(type) ) {
          return type.substring( type.lastIndexOf(StringHelper.DOT)+1 );
        } 
        else {
          if( type.endsWith("[]") ) {
            return shortenType( type.substring(0, type.length()-2), imports ) + "[]";    
          } 
          else {
            return type;   
          }
        }
    }
}
