/*
 * 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.apache.openjpa.enhance;

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.lib.util.ClassUtil;
import org.apache.openjpa.lib.util.CodeFormat;
import org.apache.openjpa.lib.util.Files;
import org.apache.openjpa.lib.util.ParameterTemplate;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;

/**
 * Generates Java class code from metadata.
 *
 * @author Abe White
 * @author Stephen Kim
 * @since 0.3.0
 */
public class CodeGenerator {

    private File _dir = null;
    private CodeFormat _format = null;
    private ClassMetaData _meta = null;
    private Class _type = null;
    private ParameterTemplate _code = null;

    /**
     * Constructor. Supply configuration and class to generate code for.
     */
    public CodeGenerator(OpenJPAConfiguration conf, Class type) {
        this(conf.newMetaDataRepositoryInstance().
            getMetaData(type, null, true));
    }

    /**
     * Constructor. Supply configuration and metadata to generate code for.
     */
    public CodeGenerator(ClassMetaData meta) {
        _meta = meta;
        _type = meta.getDescribedType();
    }

    /**
     * The directory to write source to. Defaults to the current directory.
     * If the given directory does not match the package of the metadata, the
     * package structure will be created below the directory.
     */
    public File getCodeDirectory() {
        return _dir;
    }

    /**
     * The directory to write source to. Defaults to the current directory.
     * If the given directory does not match the package of the metadata, the
     * package structure will be created below the directory.
     */
    public void setDirectory(File dir) {
        _dir = dir;
    }

    /**
     * The code formatter for the generated Java code.
     */
    public CodeFormat getCodeFormat() {
        return _format;
    }

    /**
     * Set the code formatter for the generated Java code.
     */
    public void setCodeFormat(CodeFormat format) {
        _format = format;
    }

    /**
     * Return the type being generated.
     */
    public Class getType() {
        return _type;
    }

    /**
     * Return metadata for the type being generated.
     */
    public ClassMetaData getMetaData() {
        return _meta;
    }

    /**
     * Return the generated code, or null if {@link #generateCode} has not
     * been called.
     */
    public String getCode() {
        return (_code == null) ? null : _code.toString();
    }

    /**
     * Writes the generated code to the proper directory.
     */
    public void generateCode() {
        // setup parameters
        String className = ClassUtil.getClassName(_type);
        String packageName = ClassUtil.getPackageName(_type);
        String packageDec = "";
        if (packageName.length() > 0)
            packageDec = "package " + packageName + ";";

        String extendsDec = "";
        String extendsName = "";
        if (!_type.getSuperclass().getName().equals(Object.class.getName())) {
            extendsName = ClassUtil.getClassName(_type.getSuperclass());
            extendsDec = "extends " + extendsName;
        }

        String imports = getImports();
        String[] fieldCode = getFieldCode();
        String constructor = getConstructor();

        // get code template
        _code = new ParameterTemplate();
        String codeStr = getClassCode();
        if (codeStr != null) {
            _code.append(codeStr);
            _code.setParameter("packageDec", packageDec);
            _code.setParameter("imports", imports);
            _code.setParameter("className", className);
            _code.setParameter("extendsDec", extendsDec);
            _code.setParameter("constructor", constructor);
            _code.setParameter("fieldDecs", fieldCode[0]);
            _code.setParameter("fieldCode", fieldCode[1]);
        } else
            _code.append(getClassCode(packageDec, imports, className,
                extendsName, constructor, fieldCode[0], fieldCode[1]));
    }

    /**
     * Write the generated code to the proper file.
     */
    public void writeCode()
        throws IOException {
        if (_code == null)
            return;

        File file = getFile();
        Files.backup(file, false);
        _code.write(file);
    }

    /**
     * Write the code to the specified {@link Writer}.
     */
    public void writeCode(Writer out)
        throws IOException {
        if (_code == null)
            return;

        _code.write(out);
    }

    /**
     * Return the necessary imports for the class.
     */
    private String getImports() {
        Set pkgs = getImportPackages();

        CodeFormat imports = newCodeFormat();
        String base = ClassUtil.getPackageName(_type);
        String pkg;
        for (Object o : pkgs) {
            pkg = (String) o;
            if (pkg.length() > 0 && !"java.lang".equals(pkg)
                    && !base.equals(pkg)) {
                if (imports.length() > 0)
                    imports.endl();
                imports.append("import ").append(pkg).append(".*;");
            }
        }
        return imports.toString();
    }

    /**
     * Returns the set of packages that needs to be imported for this code.
     */
    public Set getImportPackages() {
        Set pkgs = new TreeSet();
        pkgs.add(ClassUtil.getPackageName(_type.getSuperclass()));

        FieldMetaData[] fields = _meta.getDeclaredFields();
        for (FieldMetaData fieldMetaData : fields) {
            pkgs.add(ClassUtil.getPackageName(fieldMetaData.getDeclaredType()));
        }

        fields = _meta.getPrimaryKeyFields();
        for (FieldMetaData field : fields) {
            pkgs.add(ClassUtil.getPackageName(field.getDeclaredType()));
        }

        return pkgs;
    }

    /**
     * Return code for a primary key constructor for the given class.
     */
    private String getConstructor() {
        FieldMetaData[] fields = _meta.getPrimaryKeyFields();
        if (fields.length == 0)
            return "";

        CodeFormat cons = newCodeFormat();
        CodeFormat body = newCodeFormat();

        // public <class> (
        cons.tab().append("public ").append(ClassUtil.getClassName(_type));
        cons.openParen(true);

        // append args to constructor, and build up body at same time
        String propertyName;
        String fieldType;
        for (int i = 0; i < fields.length; i++) {
            propertyName = fields[i].getName();
            if (propertyName.startsWith("_"))
                propertyName = propertyName.substring(1);
            fieldType = ClassUtil.getClassName(fields[i].getDeclaredType());

            if (i > 0)
                cons.append(", ");
            cons.append(fieldType).append(" ").append(propertyName);

            if (_meta.getPCSuperclass() == null) {
                if (i > 0)
                    body.endl();
                body.tab(2);
                if (propertyName.equals(fields[i].getName()))
                    body.append("this.");
                body.append(fields[i].getName());
                body.append(" = ").append(propertyName).append(";");
            } else {
                // super (...);
                if (i == 0)
                    body.tab(2).append("super").openParen(true);
                else
                    body.append(", ");
                body.append(propertyName);
                if (i == fields.length - 1)
                    body.closeParen().append(";");
            }
        }
        cons.closeParen();

        cons.openBrace(2).endl();
        cons.append(body.toString()).endl();
        cons.closeBrace(2);
        return cons.toString();
    }

    /**
     * Returns the Java declaration and access method code for all declared
     * fields.
     */
    private String[] getFieldCode() {
        CodeFormat decs = newCodeFormat();
        CodeFormat code = newCodeFormat();

        FieldMetaData[] fields = _meta.getDeclaredFields();
        for (FieldMetaData fieldMetaData : fields) {
            appendFieldCode(fieldMetaData, decs, code);
        }
        fields = _meta.getDeclaredUnmanagedFields();
        for (FieldMetaData field : fields) {
            appendFieldCode(field, decs, code);
        }
        return new String[]{ decs.toString(), code.toString() };
    }

    /**
     * Append the declaration and code for the given field to the given buffers.
     */
    private void appendFieldCode(FieldMetaData fmd, CodeFormat decs,
        CodeFormat code) {
        String fieldName = fmd.getName();
        String capFieldName = StringUtil.capitalize(fieldName);
        String propertyName = fieldName;
        if (propertyName.startsWith("_"))
            propertyName = propertyName.substring(1);
        String fieldType = ClassUtil.getClassName(fmd.getDeclaredType());

        String keyType = null;
        String elementType = null;
        String paramType = "";
        if (useGenericCollections()) {
            if (fmd.getDeclaredTypeCode() == JavaTypes.COLLECTION) {
                Class elmCls = fmd.getElement().getDeclaredType();
                elementType = ClassUtil.getClassName(elmCls);
                paramType = decs.getParametrizedType(
                    new String[] {elementType});
            } else if (fmd.getDeclaredTypeCode() == JavaTypes.MAP) {
                Class keyCls = fmd.getKey().getDeclaredType();
                Class elmCls = fmd.getElement().getDeclaredType();
                keyType = ClassUtil.getClassName(keyCls);
                elementType = ClassUtil.getClassName(elmCls);
                paramType = decs.getParametrizedType(
                    new String[] {keyType, elementType});
            }
        }

        String fieldValue = getInitialValue(fmd);
        if (fieldValue == null) {
            if ("Set".equals(fieldType))
                fieldValue = "new HashSet" + paramType + decs.getParens();
            else if ("TreeSet".equals(fieldType))
                fieldValue = "new TreeSet" + paramType + decs.getParens();
            else if ("Collection".equals(fieldType))
                fieldValue = "new ArrayList" + paramType + decs.getParens();
            else if ("Map".equals(fieldType))
                fieldValue = "new HashMap" + paramType + decs.getParens();
            else if ("TreeMap".equals(fieldType))
                fieldValue = "new TreeMap" + paramType + decs.getParens();
            else if (fmd.getDeclaredTypeCode() == JavaTypes.COLLECTION ||
                fmd.getDeclaredTypeCode() == JavaTypes.MAP)
                fieldValue = "new " + fieldType + paramType + decs.getParens();
            else
                fieldValue = "";
        }
        if (fieldValue.length() > 0)
            fieldValue = " = " + fieldValue;

        boolean fieldAccess = !usePropertyBasedAccess();
        String custom = getDeclaration(fmd);
        if (decs.length() > 0)
            decs.endl();
        ParameterTemplate templ;
        if (custom != null) {
            templ = new ParameterTemplate();
            templ.append(custom);
            templ.setParameter("fieldName", fieldName);
            templ.setParameter("capFieldName", capFieldName);
            templ.setParameter("propertyName", propertyName);
            templ.setParameter("fieldType", fieldType);
            templ.setParameter("keyType", keyType);
            templ.setParameter("elementType", elementType);
            templ.setParameter("fieldValue", fieldValue);
            decs.append(templ.toString());
        } else {
            if (fieldAccess)
                writeAnnotations(decs, getFieldAnnotations(fmd), 1);
            decs.tab().append("private ").append(fieldType).
                append(paramType).append(" ").append(fieldName).
                append(fieldValue).append(";");
            if (fieldAccess)
                decs.endl();
        }

        custom = getFieldCode(fmd);
        if (code.length() > 0)
            code.afterSection();
        if (custom != null) {
            templ = new ParameterTemplate();
            templ.append(custom);
            templ.setParameter("fieldName", fieldName);
            templ.setParameter("capFieldName", capFieldName);
            templ.setParameter("propertyName", propertyName);
            templ.setParameter("fieldType", fieldType);
            templ.setParameter("keyType", keyType);
            templ.setParameter("elementType", elementType);
            templ.setParameter("fieldValue", fieldValue);
            code.append(templ.toString());
        } else {
            // getter
            if (!fieldAccess)
                writeAnnotations(code, getFieldAnnotations(fmd), 1);
            code.tab().append("public ").append(fieldType).append(paramType).
                 append(" ");
            if ("boolean".equalsIgnoreCase(fieldType))
                code.append("is");
            else
                code.append("get");
            if (fieldName.length() > 1 && Character.isLowerCase(fieldName.charAt(0))
                && Character.isUpperCase(fieldName.charAt(1))) {
                code.append(fieldName).parens();
            } else {
                code.append(capFieldName).parens();
            }
            code.openBrace(2).endl();
            code.tab(2).append("return ").append(fieldName).
                append(";").endl();
            code.closeBrace(2).afterSection();

            // setter
            if (fieldName.length() > 1 && Character.isLowerCase(fieldName.charAt(0))
                && Character.isUpperCase(fieldName.charAt(1))) {
                code.tab().append("public void set").append(fieldName);
            } else {
                code.tab().append("public void set").append(capFieldName);
            }
            code.openParen(true).append(fieldType).append(paramType).
                append(" ").append(propertyName).closeParen();
            code.openBrace(2).endl();
            code.tab(2);
            if (propertyName.equals(fieldName))
                code.append("this.");
            code.append(fieldName).append(" = ").append(propertyName).
                append(";").endl();
            code.closeBrace(2);
        }
    }

    /**
     * Return a code template for a generated Java class.
     */
    private String getClassCode(String packageDec, String imports,
        String className, String extendsName, String constructor,
        String fieldDecs, String fieldCode) {
        CodeFormat code = newCodeFormat();
        if (packageDec.length() > 0)
            code.append(packageDec).afterSection();
        if (imports.length() > 0)
            code.append(imports).afterSection();

        code.append("/**").endl().
            append(" * Auto-generated by:").endl().
            append(" * ").append(getClass().getName()).endl().
            append(" */").endl();

        writeAnnotations(code, getClassAnnotations(), 0);
        code.append("public class ").append(className);
        if (extendsName.length() > 0)
            code.extendsDec(1).append(" ").append(extendsName);
        openClassBrace(code);

        if (fieldDecs.length() > 0)
            code.append(fieldDecs).afterSection();

        // default constructor
        code.tab().append("public ").append(className).parens();
        code.openBrace(2).endl().closeBrace(2);

        if (constructor.length() > 0)
            code.afterSection().append(constructor);
        if (fieldCode.length() > 0)
            code.afterSection().append(fieldCode);
        code.endl();

        closeClassBrace(code);

        return code.toString();
    }

    /**
     * Appends the given list of annotations to code buffer.
     */
    private void writeAnnotations (CodeFormat code, List ann,
        int tabLevel) {
        if (ann == null || ann.size() == 0)
            return;
        for (Object o : ann) {
            if (tabLevel > 0)
                code.tab(tabLevel);
            String s = (String) o;
            code.append(s).endl();
        }
    }

    /**
     * Append the opening code-level brace to the code; this can be
     * overridden to add code to the top of the class.
     */
    protected void openClassBrace(CodeFormat code) {
        code.openBrace(1).endl();
    }

    /**
     * Append the closing code-level brace to the code; this can be
     * overridden to add code to the bottom of the class.
     */
    protected void closeClassBrace(CodeFormat code) {
        code.closeBrace(1);
    }

    /**
     * Return Java file to write to.
     */
    public File getFile() {
        String packageName = ClassUtil.getPackageName(_type);
        String fileName = ClassUtil.getClassName(_type) + ".java";

        File dir = Files.getPackageFile(_dir, packageName, true);
        return new File(dir, fileName);
    }

    /**
     * Return a copy of the internal code format.
     */
    protected CodeFormat newCodeFormat() {
        if (_format == null)
            return new CodeFormat();
        return (CodeFormat) _format.clone();
    }

    /**
     * Return a code template for the given class, or null to use the standard
     * system-generated Java code. To facilitate template reuse, the
     * following parameters can appear in the template; the proper values
     * will be subtituted by the system:
     * <ul>
     * <li>${packageDec}: The package declaration, in the form
     * "package &lt;package name &gt;;", or empty string if no package.</li>
     * <li>${imports}: Imports for the packages used by the declared
     * field types.</li>
     * <li>${className}: The name of the class, without package.</li>
     * <li>${extendsDec}: Extends declaration, in the form
     * "extends &lt;superclass&gt;", or empty string if no superclass.</li>
     * <li>${constructor}: A constructor that takes in all primary key fields
     * of the class, or empty string if the class uses datastore identity.</li>
     * <li>${fieldDecs}: Declarations of all the declared fields.</li>
     * <li>${fieldCode}: Get/set methods for all the declared fields.</li>
     * </ul> Returns null by default.
     */
    protected String getClassCode() {
        return null;
    }

    /**
     * Return code for the initial value for the given field, or null to use
     * the default generated by the system. Returns null by default.
     */
    protected String getInitialValue(FieldMetaData field) {
        return null;
    }

    /**
     * Return a code template for the declaration of the given field, or null
     * to use the system-generated default Java code.
     * To facilitate template reuse, the following parameters can appear in
     * your template; the proper values will be subtituted by the system:
     * <ul>
     * <li>${fieldName}: The name of the field.</li>
     * <li>${capFieldName}: The capitalized field name.</li>
     * <li>${propertyName}: The field name without leading '_', if any.</li>
     * <li>${fieldType}: The field's type name.</li>
     * <li>${keyType}: Key type name for maps, null otherwise.</li>
     * <li>${elementType}: Element type name for collections, null otherwise.
     * </li>
     * <li>${fieldValue}: The field's initial value, in the form
     * " = &lt;value&gt;", or empty string if none.</li>
     * </ul> Returns null by default.
     */
    protected String getDeclaration(FieldMetaData field) {
        return null;
    }

    /**
     * Return a code template for the get/set methods of the given field, or
     * null to use the system-generated default Java code.
     * To facilitate template reuse, the following parameters can appear in
     * your template; the proper values will be subtituted by the system:
     * <ul>
     * <li>${fieldName}: The name of the field.</li>
     * <li>${capFieldName}: The capitalized field name.</li>
     * <li>${propertyName}: The field name without leading '_', if any.</li>
     * <li>${fieldType}: The field's type name.</li>
     * <li>${keyType}: Key type name for maps, null otherwise.</li>
     * <li>${elementType}: Element type name for collections, null otherwise.
     * </li>
     * <li>${fieldValue}: The field's initial value, in the form
     * "= &lt;value&gt;", or empty string if none.</li>
     * </ul>
     */
    protected String getFieldCode (FieldMetaData field)
	{
		return null;
	}

    /**
     * Whether to use property-based access on generated code.
     * Defaults to false (field-based).
     */
    protected boolean usePropertyBasedAccess () {
        return false;
    }

    /**
     * Return class-level annotations. Returns null by default.
     */
    protected List getClassAnnotations() {
        return null;
    }

    /**
     * Return field-level annotations. Returns null by default.
     */
    protected List getFieldAnnotations(FieldMetaData field) {
        return null;
    }

    /**
     * Whether to use generic collections on one-to-many and many-to-many
     * relations instead of untyped collections.
     *
     * Override in descendants to change default behavior.
     */
    protected boolean useGenericCollections() {
        return false;
    }

}
