package ac.essex.ooechs.ecj.ecj2java;

import ec.gp.GPIndividual;

import java.util.Vector;
import java.util.Date;
import java.io.File;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

import ac.essex.ooechs.ecj.ecj2java.nodes.ParseableNode;
import ac.essex.ooechs.ecj.ecj2java.lang.Constant;

/**
 * <p/>
 * Takes an ECJ individual and outputs it as Java Code.
 * For this class to work properly, all your GP functions must
 * implement the functionality in the ParseableNode interface. This
 * means either extending the ParseableGPNode adapter class (for functions and terminals)
 * or extending the ParseableERC adapter class (for ERCs)
 * </p>
 * <p/>
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version,
 * provided that any use properly credits the author.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details at http://www.gnu.org
 * </p>
 *
 * @see ParseableNode
 * @author Olly Oechsle, University of Essex, Date: 05-Sep-2006
 * @version 1.0
 */
public class JavaWriter {

    /**
     * The class modifiers, default is public
     * although you may possibly want to generate a different kind of class,
     * abstract for example or inner class.
     */
    protected String classModifiers = "public";

    /**
     * Comments that appear just before the class. They are entered JavaDoc style.
     */
    protected String comments = "";

    /**
     * The package to which this class belongs, set to null if the class is package-less.
     */
    protected String classPackage;

    /**
     * The name of the class. The class will be saved to ${classname}.java
     * in the directory of your choosing if you use the <code>saveJavaCode()</code>
     * function.
      */
    protected String className;

    /**
     * If this class extends another class. It is quite often useful for the GP generated
     * class to extend an a abstract class. For example, the GP generated class has an
     * evaluatePixel(int x, int y) method, and extends an abstract class which has a function
     * to load an image and then call the evaluatePixel() function on each pixel.
     */
    protected String classExtends;

    /**
     * Similar to class extends - if this class implements functions defined in an interface.
     */
    protected String classImplements;

    private Vector<String> imports;
    private Vector<String> functionSignatures;
    private Vector<Constant> intConstants;
    private int funcCounter, ercCounter;

    public JavaWriter(String className, String GPFunctionSignature) {
        this(className, GPFunctionSignature, null, null);
    }

    public JavaWriter(String className, String GPFunctionSignature, String comments) {
        this(className, GPFunctionSignature, comments, null);
    }

    public JavaWriter(String className, String GPFunctionSignature, String comments, String classPackage) {

        imports = new Vector<String>(5);
        functionSignatures = new Vector<String>(5);
        intConstants = new Vector<Constant>(5);

        functionSignatures.add(GPFunctionSignature.trim());

        this.classPackage = classPackage.trim();
        this.className = className.trim();
        this.comments = comments.trim();

    }

    /**
     * Adds a classname to be added to the list of import statements
     * @param className
     */
    public void addImport(String className) {
        imports.add(className);
    }

    public void addComments(String comments) {
        this.comments += "\n" + comments;
    }

    public void setImplements(String classImplements) {
        this.classImplements = classImplements;
    }

    public void setExtends(String classExtends) {
        this.classExtends = classExtends;
    }

    public void addIntConstant(String name, int value) {
        intConstants.add(new Constant(name, value));
    }

    /**
     * Adds another function signature, in the case that the individual
     * consists of more than one tree. Function signatures do not need
     * to include the brace at the end, for example<br />
     * <i>public void evaluate(int x, int y)</i>
     */
    public void addFunctionSignature(String functionSignature) {
        functionSignatures.add(functionSignature);
    }

    /**
     * Converts the individual to a String, containing a complete class declaration
     * that can then be saved to disk or do whatever you want with. You need to
     * pass it a GPIndividual class from within ECJ, preferably from the <code>describe</code>
     * method in your Problem class.
     **/
    public String getJavaCode(GPIndividual ind) {

        StringBuffer buffer = new StringBuffer(1024);

        // package declaration
        if (classPackage != null) {
            buffer.append("package ");
            buffer.append(classPackage);
            if (!classPackage.endsWith(";")) buffer.append(';');
            buffer.append("\n\n");
        }

        // import statements
        for (int i = 0; i < imports.size(); i++) {
            String importStatement = imports.elementAt(i).trim();
            buffer.append("import ");
            buffer.append(importStatement);
            if (!importStatement.endsWith(";")) buffer.append(';');
            buffer.append('\n');
        }

        // put in a new line for good luck after imports
        if (imports.size() > 0) buffer.append('\n');

        // comments
        buffer.append("/**\n");
        buffer.append(comments);
        buffer.append("\nGenerated by ECJ2Java on ");
        buffer.append(new Date().toString());
        buffer.append("\n**/\n\n");

        // start the class
        buffer.append(classModifiers);
        buffer.append(" class ");
        buffer.append(className);

        if (classExtends != null) {
            buffer.append(" extends ");
            buffer.append(classExtends);
        }

        if (classImplements != null) {
            buffer.append(" implements ");
            buffer.append(classImplements);
        }

        buffer.append(" {\n\n");

        // add any constants

        for (int i = 0; i < intConstants.size(); i++) {
            Constant constant = intConstants.elementAt(i);
            buffer.append("\t");
            buffer.append(constant.toString());
            buffer.append(";\n");
        }

        if (intConstants.size() > 0) {
            buffer.append("\n");
        }

        // count the trees
        for (int i = 0; i < ind.trees.length; i++) {

            // Extract the first tree
            ac.essex.ooechs.ecj.ecj2java.nodes.ParseableNode tree = (ParseableNode) ((GPIndividual) ind).trees[i].child;

            // Start with the function signature for this tree
            String functionSignature = functionSignatures.elementAt(i);

            append(buffer, functionSignature, 1);
            buffer.append(" {\n");

            // create variable names
            funcCounter = 0;
            ercCounter = 0;
            nameChildren(tree, 0);

            // now append the rest of the code in the tree
            appendCode(tree, buffer);

            append(buffer, "}\n\n", 1);

        }

        // finish the class
        buffer.append("}");

        return buffer.toString();

    }

    /**
     * Generates the java code (using getJavaCode) saves the code in a given directory. Since there is no flexibility
     * in the naming of the Java file (it will always be "${classname}.java", the function doesn't allow the filename
     * to be specified.
     */
    public void saveJavaCode(GPIndividual ind, File directory) throws IOException {
        if (directory.isDirectory()) {

            File f = new File(directory, className + ".java");

            BufferedWriter writer = new BufferedWriter(new FileWriter(f));

            String output = getJavaCode(ind);

            writer.write(output);

            writer.close();

        } else {
            System.err.println("Cannot save Java Code - not a directory");
        }
    }

    private void nameChildren(ac.essex.ooechs.ecj.ecj2java.nodes.ParseableNode tree, int depth) {

        // name the children first
        if (tree.countChildren() > 0) {
            for (int i = 0; i < tree.countChildren(); i++)
                nameChildren(tree.getChild(i), depth + 1);
        }

        // assist in readability by assigning names to non void functions
        // prefix functions with "func_" and ERC values with "val_"
        switch (tree.getType()) {
            case ac.essex.ooechs.ecj.ecj2java.nodes.ParseableNode.FUNCTION:
                funcCounter++;
                tree.setVariableName("func_" + funcCounter);
                break;
            case ParseableNode.ERC:
                ercCounter++;
                tree.setVariableName("val_" + ercCounter);
                break;
        }

        if (depth == 0 && tree.getType() != ac.essex.ooechs.ecj.ecj2java.nodes.ParseableGPNode.VOID) {
            tree.setType(ac.essex.ooechs.ecj.ecj2java.nodes.ParseableNode.RETURN);
        }

    }


    private void appendCode(ac.essex.ooechs.ecj.ecj2java.nodes.ParseableNode tree, StringBuffer buffer) {

        if (tree.countChildren() > 0) {
            for (int i = 0; i < tree.countChildren(); i++) {

                /**
                 * Abbreviation - if we just declare a variable
                 * which is redundant, we can abbreviate it.
                 */

                ParseableNode child = tree.getChild(i);

                if (child.countChildren() == 0) {
                    // replace the variable name with the java code
                    // and don't print it out on its own line.
                    child.setVariableName(child.getJavaCode());
                } else {
                    appendCode(tree.getChild(i), buffer);
                }

            }
        }

        String java = tree.getJavaCode();

        // now print out the node
        switch (tree.getType()) {
            case ParseableNode.RETURN:
                append(buffer, "return ", 2);
                buffer.append(java);
                if (!java.endsWith(";")) buffer.append(";");
                if (tree.getLineComment() != null) {
                    buffer.append(" //");
                    buffer.append(tree.getLineComment());
                }
                buffer.append("\n");
                break;
            default:
                switch (tree.getObjectType()) {
                    case ParseableNode.DOUBLE:
                        append(buffer, "double", 2);
                        break;
                    case ParseableNode.BOOLEAN:
                        append(buffer, "boolean", 2);
                        break;
                    case ParseableNode.INT:
                        append(buffer, "int", 2);
                        break;
                }
                buffer.append(' ');
                buffer.append(tree.getVariableName());
                buffer.append(" = ");
                buffer.append(java);
                if (!java.endsWith(";")) buffer.append(";");
                if (tree.getLineComment() != null) {
                    buffer.append(" //");
                    buffer.append(tree.getLineComment());
                }
                buffer.append("\n");
                break;
        }

    }

    private void append(StringBuffer buffer, String text, int tabs) {
        for (int i = 0; i < tabs; i++) {
            buffer.append('\t');
        }
        buffer.append(text);
    }

}
