package dbcodegen;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;

import util.FileUtils;
import dbcodegen.db.Database;
import dbcodegen.db.Table;

/**
 * This is the entry class for generating the java persistence model based on a
 * database schema.  It uses the Empire DB open-source framework to build a 
 * java persistence layer for an application.  The Apache Velocity template 
 * engine is used to create the output interfaces and classes.  
 * 
 * The Empire DB framework doesn't try to hide the underlying database and data 
 * model but instead embraces its power by modeling it within java.  The result 
 * is a persistence layer that uses a more "object-oriented, type safe" SQL to
 * access persistent data. 
 * 
 * NOTE:  THIS VERSION HAS SEVERE RESTRICTIONS:
 * 1. Only tables are currently modeled (we'll add views to a later version).
 * 2. Table indexes are not yet modeled (exception is primary key).  Again, this
 * 		will be added to later editions.
 * 3. It is assumed that each table has a single INTEGER auto-generated primary
 * 		key column that has the same name for all tables.
 * 4. It is assumed that each table has a single TIMESTAMP optimistic locking
 * 		column that has the same name for all tables.
 */
public class DbCodeGenerator {
	private Database db;
	private String basePackageName;
	private File baseDir;
	private File tableDir;
	private File recordDir;
	private Map<String, EnumType> enumMap = new HashMap<String, EnumType>();
	
	/**
	 * Constructs an instance of the database code generator.
	 * @param db The database.
	 * @param srcLocation The location of the generated source code.
	 * @param packageName The package name for the generated persistence layer.
	 */
	public DbCodeGenerator(Database db) {
		this.db = db;
		try {
			Velocity.init();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Adds a new enumerated type.  Use this when a database column is a
	 * VARCHAR (string) which constrains the possible values permitted.
	 * Use the Column class's convertToEnum method to indicate which column
	 * has these constraints.  
	 * @param name The enumerated type's name.
	 * @return The enumerated type created.
	 */
	public EnumType addEnumType(String name) {
		EnumType type = new EnumType(name);
		this.enumMap.put(name, type);
		return type;
	}

	/**
	 * Generates the source code for the persistence layer.
	 */
	public void generateCode(String srcLocation, String packageName) {
		this.basePackageName = packageName;
		
		// Prepare directories for generated source files
		this.initDirectories(srcLocation, packageName);
		
		// Create the DB class
		this.createDatabaseClass();
	
		// Create base record interface and class
		this.createBaseRecordInterface();
		this.createBaseRecordClass();
		
		// Create table classes, record interfaces and record classes
		for (Table table: this.db.getTables()) {
			this.createTableClass(table);
			this.createRecordInterface(table);
			this.createRecordClass(table);
		}
	}
	
	private void initDirectories(String srcLocation, String packageName) {
		// Create the directory structure for the generated source code.
		File baseDir = new File(srcLocation);
		if (!baseDir.exists()) {
			baseDir.mkdirs();
		}		
		StringBuilder sb = new StringBuilder();
		sb.append(srcLocation).append("/");
		sb.append(packageName.replaceAll("\\.", "/"));
		this.baseDir = new File(sb.toString());
		if (!this.baseDir.exists()) {
			this.baseDir.mkdirs();
		}
		
		// Clean out the directory so old code is wiped out.
		FileUtils.cleanDirectory(this.baseDir);
		
		// Create the table package directory
		this.tableDir = new File(this.baseDir, "tables");
		this.tableDir.mkdir();

		// Create the record package directory
		this.recordDir = new File(this.baseDir, "records");
		this.recordDir.mkdir();
	}
	private void createDatabaseClass() {
		File file = new File(this.baseDir, this.db.getClassName() + 
				".java");
		VelocityContext context = new VelocityContext();
		context.put("basePackageName", this.basePackageName);		
		context.put("tableSubPackage", "tables");
		context.put("database", this.db);
		this.writeFile(file, DATABASE_TEMPLATE, context);
	}
	
	private void createTableClass(Table table) {
		File file = new File(this.tableDir, 
				table.getClassName() + ".java");
		VelocityContext context = new VelocityContext();
		context.put("tablePackageName", this.basePackageName + ".tables");		
		context.put("table", table);
		this.writeFile(file, TABLE_TEMPLATE, context);
	}
	
	private void createBaseRecordInterface() {
		File file = new File(this.recordDir, 
				"IBaseRecord.java");
		VelocityContext context = new VelocityContext();
		context.put("recordPackage", this.basePackageName + ".records");		
		this.writeFile(file, I_BASE_RECORD_TEMPLATE, context);
	}
	
	private void createBaseRecordClass() {
		File file = new File(this.recordDir, 
				"BaseRecord.java");
		VelocityContext context = new VelocityContext();
		context.put("basePackageName", this.basePackageName);		
		context.put("recordPackage", this.basePackageName + ".records");		
		context.put("database", this.db);
		this.writeFile(file, BASE_RECORD_TEMPLATE, context);
	}

	private void createRecordInterface(Table table) {
		File file = new File(this.recordDir, 
				"I" + table.getRecordClassName() + ".java");
		VelocityContext context = new VelocityContext();
		context.put("recordPackage", this.basePackageName + ".records");		
		context.put("table", table);		
		this.writeFile(file, I_RECORD_TEMPLATE, context);
	}
	
	private void createRecordClass(Table table) {
		File file = new File(this.recordDir, 
				table.getRecordClassName() + ".java");
		VelocityContext context = new VelocityContext();
		context.put("recordPackage", this.basePackageName + ".records");		
		context.put("table", table);		
		this.writeFile(file, RECORD_TEMPLATE, context);
	}

	private void writeFile(File file, String templatePath, 
			VelocityContext context) {
		try {
			Template template = Velocity.getTemplate(templatePath);
			Writer writer = new FileWriter(file);
			template.merge(context, writer);
			writer.close();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (ResourceNotFoundException e) {
			e.printStackTrace();
		} catch (ParseErrorException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		Database db = new Database("com.mysql.jdbc.Driver", 
				"jdbc:mysql://localhost:3306/addressbook", 
				"root", "password", "addressbook");
		db.populateTableMetaData("ID", "LOCK_TS");
		DbCodeGenerator generator = new DbCodeGenerator(db);
		generator.generateCode(
				"src", "addressbook.persistence.generated");
	}
	
	public static final String DATABASE_TEMPLATE =
		"resources/templates/Database.vm";
	public static final String TABLE_TEMPLATE =
		"resources/templates/Table.vm";
	public static final String I_BASE_RECORD_TEMPLATE =
		"resources/templates/IBaseRecord.vm";
	public static final String BASE_RECORD_TEMPLATE =
		"resources/templates/BaseRecord.vm";
	public static final String I_RECORD_TEMPLATE =
		"resources/templates/IRecord.vm";
	public static final String RECORD_TEMPLATE =
		"resources/templates/Record.vm";
}
