/* Copyright (C) 2009  CSE,IIT Bombay  http://www.cse.iitb.ac.in

This file is part of the ConStore open source storage facility for concept-nets.

ConStore is free software and distributed under the 
Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License;
you can copy, distribute and transmit the work
with the work attribution in the manner specified by the author or licensor.
You may not use this work for commercial purposes and may not alter, 
transform, or build upon this work.

Please refer the legal code of the license, available at
http://creativecommons.org/licenses/by-nc-nd/3.0/legalcode

ConStore 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.  */

package iitb.con.net.codegenerator;

import iitb.con.core.Attribute;
import iitb.con.core.DataType;
import iitb.con.core.Entity;
import iitb.con.core.Relation;
import iitb.con.core.Type;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * CodeGenerator generates java code for the given <tt>Type</tt>, it may be
 * <tt>Entity</tt> or <tt>Relation</tt>. The generated java class will be 
 * used for instantiating the type. User can import these generated classes
 * into their development environment to instantiate the types.
 * 
 * @author Prathab K
 *
 */

public class CodeGenerator {

    static Logger log = Logger.getLogger("ada.wadk.types.CodeGenerator");

    /** Name of the template file */
    private final String templateFile = "ClassTemplate.txt";
    
    /** index of the array in the strings for entity or relation type */
    private byte index;
    
    private static CodeGenerator instance = null;
    
    private CodeGenerator(){}
    
    /** 
     * Method to facilitate the singelton
     * @return the <tt>CodeGenerator</tt> instance 
     */
    public static CodeGenerator getInstance() {
        if(instance == null)
            instance = new CodeGenerator();
        return instance;
            
    }
    
    /**
     * Generates the code for <tt>Entity</tt> type
     * @param path path location where the java file to be created
     * @param entity {@link Entity}
     * @return <tt>true</tt> on success
     */
    public boolean generateCode(String path, Entity entity) {
        index = ENTITY;
        return generateTypeCode(path, entity);
    }
    
    /**
     * Generates the code for <tt>Relation</tt> type
     * @param path path location where the java file to be created
     * @param relation {@link Relation}
     * @param leftEntityName name of relation's left entity
     * @param rightEntityName name of relation's right entity
     * @return <tt>true</tt> on success
     */
    public boolean generateCode(String path, Relation relation, String leftEntityName,
            String rightEntityName) {
        index = RELATION;
        StringBuilder relSetters = new StringBuilder(RELATION_SETTERS);
        replaceAll(relSetters,L_INSTANCE_NAME, leftEntityName);
        replaceAll(relSetters,R_INSTANCE_NAME, rightEntityName);
        RELATION_SETTERS_STRING = relSetters.toString();
        
        StringBuilder rPackageString = new StringBuilder(IMPORT_RESULT);
        rPackageString.append(IMPORT_TYPE + leftEntityName +";\n");
        if(!leftEntityName.equals(rightEntityName))
            rPackageString.append(IMPORT_TYPE + rightEntityName +";\n");
        R_PACKAGE_STRING = rPackageString.toString();
        
        return generateTypeCode(path, relation);
    }
    
    /**
     * Generates the code for the given type
     * @param path path location where the java files to be created
     * @param type {@link Type}
     * @return <tt>true</tt> on success
     */
    private boolean generateTypeCode(String path, Type type) {
        String tempStr = new String(readFileAsString(this.getClass(),templateFile));

        
        //setting the class name
        tempStr = tempStr.replace(CLASS_NAME, type.name);
        //setting the super type
        tempStr = tempStr.replace(SUPER_TYPE,INSTANCE[index]);
        //setting the package name
        tempStr = tempStr.replace(PACKAGE_NAME,PACKAGE_PATH[index]);
        //setting the import package
        tempStr = tempStr.replace(IMPORT_PACKAGE, generatePackageStrings(type.getAllAttributes()));
        //setting the member variables
        tempStr = tempStr.replace(MEMBER_VARIABLES, generateAttributes(type.getAllAttributes()));
        //setting the member functions
        tempStr = tempStr.replace(MEMBER_FUNCTIONS, generateMethods(type));
        
        //writing the contents to the file
        
        String fullPath = path + CODE_PATH[index];
        //String classpath = path + CODE_PATH[0]; //0 for entities path
        
        File filePath = new File(fullPath);
        boolean dirExists = true;
        if(!filePath.exists()) dirExists = filePath.mkdirs();
        if(dirExists) {
            writeToFile(fullPath + type.name + EXTENSION,tempStr);
            compile(path,fullPath + type.name + EXTENSION);
            return true;
        }
        else {
            log.info("Code generation path: " + fullPath);
            log.severe("Error in file creation and code is not generated");
        }
        return false;
    }

    /**
     * Reads the specified file
     * @param fullPathFilename file name with path
     * @return contents of the file as a String
     */
    private String readFileAsString(String fullPathFilename) {
        try{
            StringBuilder buffer = new StringBuilder();
            BufferedReader reader = new BufferedReader(new FileReader(fullPathFilename));

            char[] chars = new char[1024];
            while((reader.read(chars)) > -1){
                buffer.append(String.valueOf(chars));
            }
            reader.close();
            return buffer.toString();
        }catch (IOException ioException) {
            log.info("Application Location: " + new File(".").getAbsolutePath());
            log.log(Level.SEVERE,"The template file for class generation NOT found" +
                    "in the location: " + fullPathFilename , ioException);
            return null;
        }
    }
    
    /**
     * Reads the specified file (in case the file is present inside the package)
     * @param clazz package class
     * @param filename name of the file
     * @return contents of the file as a String
     */
    private String readFileAsString(Class clazz, String filename) {
        try{
            StringBuilder contents = new StringBuilder();
            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(getStream(clazz, filename)));
            String line;
            while ((line = reader.readLine()) != null) {  
                contents.append(line + "\n");
              }
            reader.close();
            return contents.toString();
        }catch (IOException ioException) {
            log.log(Level.SEVERE,"The template file for class generation NOT found" +
                    "in the location: " + clazz.toString() + "/" + filename , ioException);
            return null;
        }
    }
    
    /**
     * Creates an input stream for the file found inside the package
     * @param clazz package class
     * @param filename file name
     * @return file InputStream
     */
    private InputStream getStream(Class clazz, String filename) {
        InputStream is = null;

        if (clazz != null) {
            is = clazz.getResourceAsStream(filename);

        } else {
            try {
                is = new FileInputStream(filename);
            } catch (FileNotFoundException e) {
                return null;
            }
        }
        if (is == null) {
            return null;
        } else {
            return new BufferedInputStream(is);
        }
    }

    /**
     * Writes the given contents to the file
     * @param fullPathFilename file name along with path
     * @param contents file contents
     */
    private void writeToFile(String fullPathFilename, String contents) {
        try{
            BufferedWriter writer = new BufferedWriter(new FileWriter(fullPathFilename));
            writer.write(contents);
            writer.flush();
            writer.close();
        }catch (IOException ioException) {
            log.log(Level.SEVERE,"Error in writing the auto code generation file", ioException);
        }
    }
    
    /**
     * Compiles the generated Java code
     * @param fullPathFileName file name along with path
     */
    private void compile(String classpath, String fullPathFileName) {
        com.sun.tools.javac.Main javac = new com.sun.tools.javac.Main();
        //String[] options = new String[] {  "-classpath", classpath, fullPathFileName };
        String[] options = new String[] {  fullPathFileName };
        javac.compile(options);
    }

    /**
     * Generates the data members of the class code
     * @param attributeList data members list of the class
     * @return data members part code as string
     */
    private String generateAttributes(List<Attribute> attributeList) {
        StringBuilder attributeCode = new StringBuilder("");
        for(Attribute attribute : attributeList) {
            String strTemp = new String("\t public ");
            attributeCode.append(strTemp);
            
            String dataType = DataType.getDataTypeString(attribute.dataType);

            if(attribute.repeating) {
                attributeCode.append("List<" + dataType + "> " 
                                        + attribute.name + " = new ArrayList<" + dataType + ">();\n");
            }else {
                if(attribute.defaultValue != null) {
                    attributeCode.append(dataType + " " + attribute.name + " = " +
                            DataType.getDataValue(attribute.dataType, attribute.defaultValue) + ";\n");
                }else {
                    attributeCode.append(dataType + " " + attribute.name + ";\n");
                }
            }
        }
        return attributeCode.toString();
    }
    
    /**
     * Generates the member functions of the class code
     * @param type <tt>Type</tt> for which the code to be generated
     * @return member functions code as string
     */
    private String generateMethods(Type type) {
        List<Attribute> attributeList = type.getAllAttributes();
        boolean flag = false;
        StringBuilder strCode = new StringBuilder("");
        
        if(attributeList.size() > 0)
        {
            StringBuilder strTemp = new StringBuilder("");
    
            strCode.append(setterTemplate);
            if(index == RELATION) strCode.append(superSetterCall);
            
            for(Attribute attribute : attributeList) {
                String dataType = DataType.getDataTypeString(attribute.dataType);
                if(flag)
                    strTemp.append("else ");
                else
                    strTemp.append("\n\t\t");
                
                if(attribute.repeating)
                    strTemp.append(SETTER_IF_R);
                else
                    strTemp.append(SETTER_IF);
                
                replaceAll(strTemp,DATA_TYPE, dataType);
                replaceAll(strTemp,ATTR_NAME,attribute.name);
    
                //append to the main code string
                strCode.append(strTemp);
                
                flag= true;
                strTemp.setLength(0);//clear the contents of string builder
              }
            strCode.append("else { \n \t\t}");
            strCode.append("\n\t}"); //closing the method
            
            strTemp.setLength(0);
            flag = false;
            strCode.append(getterTemplate);
            if(index == RELATION) strCode.append(superGetterCall);
            
            for(Attribute attribute : attributeList) {
                if(flag)
                    strTemp.append("else ");
                else
                    strTemp.append("\n\t\t");
                
                strTemp.append(GETTER_IF);
                
                replaceAll(strTemp,ATTR_NAME,attribute.name);
    
                //append to the main code string
                strCode.append(strTemp);
                
                flag= true;
                strTemp.setLength(0);//clear the contents of string builder
              }
            strCode.append("else { \n \t\t\treturn null;\n \t\t}");
            strCode.append("\n\t}"); //closing the method
        }
        
        //Generating the relation setter methods
        if(index == RELATION) {
            strCode.append(RELATION_SETTERS_STRING);
        }
        
        return strCode.toString();
    }

    /**
     * Generates the import package string based on the class attributes
     * @param attributeList attribute list for which import strings to be generated
     * @return import-package string
     */
    private String generatePackageStrings(List<Attribute> attributeList) {
        StringBuilder strPackages = new StringBuilder(INSTANCE_PKG_STRING[index]);
        if(index == RELATION)
            strPackages.append(R_PACKAGE_STRING);
        for(Attribute attr: attributeList) {
            if(attr.repeating) {
                strPackages.append(PACKAGE_STRING);
                return strPackages.toString();
            }
        }
        return strPackages.toString();
    }
    
    /**
     * Replaces the given substring with the specified substring for the given string 
     * @param source string in which replacement has to done
     * @param target the substring that has to be replaced
     * @param replacement the replacement substring
     */
    private void replaceAll(StringBuilder source, String target, String replacement) {
        int index = source.indexOf(target);
        while(index > 0) {
            source.replace(index, index + target.length(), replacement);
            index = source.indexOf(target);
        }
    }
    
    private static final String setterTemplate = "\tpublic void " +
            "setAttributeValue(String name, Object value) {";
    
    private static final String getterTemplate = "\n\tpublic Object getAttributeValue(String name) {";
    
    private static final String superSetterCall = "\n\t\tsuper.setAttributeValue(name,value);";
    private static final String superGetterCall = "\n\t\tObject obj = super.getAttributeValue(name);" +
            "\n\t\tif(obj != null) return obj;";
    
    private static final String SETTER_IF ="if(name.equals(\"$Attr_Name$\")) {" +
                "\n\t\t\tthis.$Attr_Name$ = ($DT$) value;" + 
                "\n\t\t}";
    
    //for repeating attribute
    private static final String SETTER_IF_R ="if(name.equals(\"$Attr_Name$\")) {" +
    "\n\t\t\tthis.$Attr_Name$ = (List<$DT$>) value;" + 
    "\n\t\t}";
    
    private static final String GETTER_IF ="if(name.equals(\"$Attr_Name$\")) {" +
    "\n\t\t\treturn this.$Attr_Name$;" + 
    "\n\t\t}";
    
    private static final byte RELATION = 1;
    private static final byte ENTITY = 0;
    private static final String ATTR_NAME = "$Attr_Name$";
    private static final String DATA_TYPE = "$DT$";
    private static final String PACKAGE_NAME = "$PACKAGE_NAME$";
    private static final String CLASS_NAME = "$CLASS_NAME$";
    private static final String SUPER_TYPE = "$SUPER_TYPE$";
    private static final String IMPORT_PACKAGE = "$IMPORT_PACKAGE$";
    private static final String MEMBER_VARIABLES = "$MEMBER_VARIABLES$";
    private static final String MEMBER_FUNCTIONS = "$MEMBER_FUNCTIONS$";
    private static final String EXTENSION = ".java";
    private static final String L_INSTANCE_NAME = "$L_INSTANCE_NAME$";
    private static final String R_INSTANCE_NAME = "$R_INSTANCE_NAME$";
    
    private static final String[] PACKAGE_PATH = {"types.entities","types.relations"};
    private static final String[] INSTANCE = {"EntityInstance","RelationInstance"};
    private static final String[] INSTANCE_PKG_STRING = 
        {"import iitb.con.core.EntityInstance;\n","import iitb.con.core.RelationInstance;\n"};
    
    private static final String PACKAGE_STRING = "import java.util.ArrayList; \nimport java.util.List;";
    private static final String[] CODE_PATH = 
        {"/types/entities/","/types/relations/"};
    
    private static final String IMPORT_TYPE = "import types.entities.";
    private static final String IMPORT_RESULT = "import iitb.con.util.Result;\n";
    private String R_PACKAGE_STRING = "";
    private String RELATION_SETTERS_STRING = "";
    private static String RELATION_SETTERS = "\tpublic Result setLeftInstance($L_INSTANCE_NAME$ instance) {\n" +
            "\t\treturn super.setLeftInstance(instance);\n" +
            "\t}\n\n" +
            "\tpublic Result setRightInstance($R_INSTANCE_NAME$ instance) {\n" +
            "\t\treturn super.setRightInstance(instance);\n" +
            "\t}\n\n";
}