/*******************************************************************************
* Copyright (c) 2008 Oracle. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0, which accompanies this distribution
* and is available at http://www.eclipse.org/legal/epl-v10.html.
* 
* Contributors:
*     Oracle - initial API and implementation
*******************************************************************************/
package org.eclipse.jpt.eclipselink.core.internal.ddlgen;

import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationType;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.debug.core.ILaunchesListener2;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.IRuntimeClasspathEntry;
import org.eclipse.jdt.launching.IVMInstall;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jpt.core.JpaPlatform;
import org.eclipse.jpt.core.JpaProject;
import org.eclipse.jpt.core.internal.validation.JpaHelper;
import org.eclipse.jpt.core.internal.validation.JpaValidator;
import org.eclipse.jpt.db.ConnectionProfile;
import org.eclipse.jpt.eclipselink.core.internal.context.connection.Connection;
import org.eclipse.jpt.eclipselink.core.internal.context.customization.Customization;
import org.eclipse.jpt.eclipselink.core.internal.context.logging.Logging;
import org.eclipse.jpt.eclipselink.core.internal.context.logging.LoggingLevel;
import org.eclipse.jpt.eclipselink.core.internal.context.schema.generation.DdlGenerationType;
import org.eclipse.jpt.eclipselink.core.internal.context.schema.generation.OutputMode;
import org.eclipse.jpt.eclipselink.core.internal.context.schema.generation.SchemaGeneration;
import org.eclipse.wst.validation.internal.operations.ValidatorJob;

/**
 *  EclipseLinkDLLGenerator launches the EclipseLink DDL generator in a separate VM.
 *  For this it will create a Java Configuration named "EclipseLink" 
 *  in the target workspace (note: the configuration will be overridden at each run).
 *  It will than launch the configuration created with the login information from 
 *  the current Dali project, and passes it to EclipseLink.
 *  
 *  Pre-req:
 *  	org.eclipse.jpt.eclipselink.core.ddlgen.jar in ECLIPSE_HOME/plugins
 */
public class EclipseLinkDDLGenerator
{
	static public String ECLIPSELINK_DDL_GEN_JAR_VERSION = "1.0.0";	//$NON-NLS-1$
	static public String LAUNCH_CONFIG_NAME = "EclipseLink";   //$NON-NLS-1$
	static public String DDL_GEN_PACKAGE_NAME = "org.eclipse.jpt.eclipselink.core.ddlgen";   //$NON-NLS-1$
	static public String ECLIPSELINK_DDL_GEN_CLASS = DDL_GEN_PACKAGE_NAME + ".Main";	  //$NON-NLS-1$
	static public String ECLIPSELINK_DDL_GEN_JAR = DDL_GEN_PACKAGE_NAME + "_" + ECLIPSELINK_DDL_GEN_JAR_VERSION + ".jar";	//$NON-NLS-1$
	static public String PROPERTIES_FILE_NAME = "login.properties";	  //$NON-NLS-1$
	static public String ECLIPSE_HOME = "ECLIPSE_HOME";	  //$NON-NLS-1$
	static public String PLUGINS_DIR = "plugins";	  //$NON-NLS-1$
	private IVMInstall jre;
	private ILaunchConfigurationWorkingCopy launchConfig;
	private ILaunch launch;
	
	private String puName;
	private JpaProject project;
	private String projectLocation;
	private boolean isDebug = false;
	
	// ********** constructors **********
	
	public static void generate(String puName, JpaProject project, String projectLocation, IProgressMonitor monitor) {
		if (puName == null || puName.length() == 0 || project == null) {
			throw new NullPointerException();
		}
		new EclipseLinkDDLGenerator(puName, project, projectLocation, monitor).generate();
	}

	private EclipseLinkDDLGenerator(String puName, JpaProject project, String projectLocation, IProgressMonitor monitor) {
		super();
		this.puName = puName;
		this.project = project;
		this.projectLocation = projectLocation;
		this.initialize();
	}
	
	// ********** Queries **********
	
	protected JpaPlatform getPlatform() {
		return this.project.getJpaPlatform();
	}
	
	protected ConnectionProfile getConnectionProfile() {
		return this.project.getConnectionProfile();
	}

	protected ILaunch getLaunch() {
		return this.launch;
	}
	
	protected ILaunchManager getLaunchManager() {
		return DebugPlugin.getDefault().getLaunchManager();
	}
	
	private IVMInstall getProjectJRE() throws CoreException {
		
		return JavaRuntime.getVMInstall(this.project.getJavaProject());
	}

	// ********** behavior **********
	
	protected void initialize() {
		try {
			this.jre = this.getProjectJRE();
			if(this.jre == null) {
				String message = "Could not identify the VM.";
				throw new RuntimeException(message);
			}
			this.launchConfig = this.buildLaunchConfiguration();
		} 
		catch (CoreException e) {
			throw new RuntimeException(e);
		}
	}

	protected void generate() {
		this.preGenerate();
		String propertiesFile  = this.projectLocation + "/" + PROPERTIES_FILE_NAME;
		try {
			this.initializeLaunchConfiguration(this.projectLocation, propertiesFile);
	
			this.saveLoginProperties(this.projectLocation, propertiesFile);
	
			this.launch = this.saveAndLaunchConfig();
			this.addLaunchListener();
		} 
		catch (CoreException e) {
			throw new RuntimeException(e);
		}
	}
	
	private void initializeLaunchConfiguration(String projectLocation, String propertiesFile) throws CoreException {

		this.specifyJRE(this.jre.getName(), this.jre.getVMInstallType().getId());
		
		this.specifyProject(this.project.getProject().getName()); 
		this.specifyMainType(ECLIPSELINK_DDL_GEN_CLASS);	
		
		this.specifyProgramArguments(this.puName, propertiesFile); 
		this.specifyWorkingDir(projectLocation); 

		this.specifyClasspathProperties(this.project, this.buildJdbcJarPath(), this.buildBootstrapJarPath());
	}
	
	private void addLaunchListener() {

		this.getLaunchManager().addLaunchListener(this.buildLaunchListener());
	}
	
	protected void preGenerate() {
		this.project.getConnectionProfile().disconnect();
	}
	
	protected void postGenerate() {
		try {
			if(! this.isDebug) {
				this.removeLaunchConfiguration(LAUNCH_CONFIG_NAME);
			}
		}
		catch ( CoreException e) {
			throw new RuntimeException(e);
		}
		this.project.getConnectionProfile().connect();
		
		this.validateProject();
	}

	protected void validateProject() {
		 JpaValidator validator = new JpaValidator();
		 IProject project = this.project.getProject();
		 JpaHelper helper = new JpaHelper();
		 helper.setProject(project);

		 ValidatorJob validatorJob = new ValidatorJob(validator, "JPA Validator", "", project, helper);
		 validatorJob.setRule(ResourcesPlugin.getWorkspace().getRoot());
		 validatorJob.setUser(true);
		 validatorJob.schedule();
	}
	
	private IPath buildJdbcJarPath() {
		return new Path(this.project.getConnectionProfile().getDriverJarList());
	}
	
	private IPath buildBootstrapJarPath() {
		IPath path = JavaCore.getClasspathVariable(ECLIPSE_HOME);
		return path.append(PLUGINS_DIR).append(ECLIPSELINK_DDL_GEN_JAR);
	}

	private ILaunchesListener2 buildLaunchListener() {
		return new ILaunchesListener2() {
			
			public void launchesTerminated(ILaunch[] launches) {
				for (int i = 0; i < launches.length; i++) {
					ILaunch launch = launches[i];
					if (launch.equals(EclipseLinkDDLGenerator.this.getLaunch())) {

						EclipseLinkDDLGenerator.this.postGenerate();
						return;
					}
				}
			}

			public void launchesAdded(ILaunch[] launches) {
				// not interested to this event
			}

			public void launchesChanged(ILaunch[] launches) {
				// not interested to this event
			}

			public void launchesRemoved(ILaunch[] launches) {
				// not interested to this event
			}
		};
	}

	// ********** launch configuration **********
	
	private void specifyJRE(String jreName, String vmId) {

		this.launchConfig.setAttribute(IJavaLaunchConfigurationConstants.ATTR_JRE_CONTAINER_PATH, jreName);
		this.launchConfig.setAttribute(IJavaLaunchConfigurationConstants.ATTR_JRE_CONTAINER_PATH, vmId);
	}
	
	private void specifyProject(String projectName) {

		this.launchConfig.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, projectName);
	}
	
	private void specifyMainType(String mainType) {

		this.launchConfig.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, mainType);
	}
	
	private void specifyProgramArguments(String puName, String propertiesPath) {

		StringBuffer programArguments = new StringBuffer();
		programArguments.append(this.buildPuNameArgument(puName));
		programArguments.append(this.buildPropertiesPathArgument(propertiesPath));
		programArguments.append(this.buildDebugArgument());
		
		this.launchConfig.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, programArguments.toString());
	}

	private void specifyClasspathProperties(JpaProject project, IPath jdbcJar, IPath bootstrapJar) throws CoreException {
		// DDL_GEN jar
		IRuntimeClasspathEntry bootstrapEntry = JavaRuntime.newArchiveRuntimeClasspathEntry(bootstrapJar);
		bootstrapEntry.setClasspathProperty(IRuntimeClasspathEntry.USER_CLASSES);
		// Default Project classpath
		IRuntimeClasspathEntry defaultEntry = JavaRuntime.newDefaultProjectClasspathEntry(project.getJavaProject());
		defaultEntry.setClasspathProperty(IRuntimeClasspathEntry.USER_CLASSES);
		// JDBC jar
		IRuntimeClasspathEntry jdbcEntry = JavaRuntime.newArchiveRuntimeClasspathEntry(jdbcJar);
		jdbcEntry.setClasspathProperty(IRuntimeClasspathEntry.USER_CLASSES);
		// System Library  
		IPath systemLibsPath = new Path(JavaRuntime.JRE_CONTAINER);
		IRuntimeClasspathEntry systemLibsEntry = JavaRuntime.newRuntimeContainerClasspathEntry(systemLibsPath, IRuntimeClasspathEntry.STANDARD_CLASSES);

		List<String> classpath = new ArrayList<String>();
		classpath.add(bootstrapEntry.getMemento());
		classpath.add(defaultEntry.getMemento());
		classpath.add(jdbcEntry.getMemento());
		classpath.add(systemLibsEntry.getMemento());
		
		this.launchConfig.setAttribute(IJavaLaunchConfigurationConstants.ATTR_CLASSPATH, classpath);
		this.launchConfig.setAttribute(IJavaLaunchConfigurationConstants.ATTR_DEFAULT_CLASSPATH, false);
	}
	
	private void specifyWorkingDir(String projectLocation) {
		
		File workingDir = new Path(projectLocation).toFile();
		this.launchConfig.setAttribute(IJavaLaunchConfigurationConstants.ATTR_WORKING_DIRECTORY, workingDir.getAbsolutePath());
	}
	
	private ILaunch saveAndLaunchConfig() throws CoreException {
		ILaunchConfiguration configuration = this.launchConfig.doSave();
		
		return configuration.launch(ILaunchManager.RUN_MODE, new NullProgressMonitor());
	}

	private ILaunchConfigurationWorkingCopy buildLaunchConfiguration() throws CoreException {
		ILaunchConfigurationWorkingCopy launchConfig = null;
		this.removeLaunchConfiguration( LAUNCH_CONFIG_NAME);

		ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
		ILaunchConfigurationType type = manager.getLaunchConfigurationType(IJavaLaunchConfigurationConstants.ID_JAVA_APPLICATION);

		launchConfig = type.newInstance(null, LAUNCH_CONFIG_NAME);
		return launchConfig;
	}

	private void removeLaunchConfiguration(String launchConfigurationName) throws CoreException {

		ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
		ILaunchConfigurationType type = manager.getLaunchConfigurationType(IJavaLaunchConfigurationConstants.ID_JAVA_APPLICATION);
	
		ILaunchConfiguration[] configurations = manager.getLaunchConfigurations(type);
				for (int i = 0; i < configurations.length; i++) {
			ILaunchConfiguration configuration = configurations[i];
			if (configuration.getName().equals(launchConfigurationName)) {
				configuration.delete();
				break;
			}
		}
	}

	// ********** EclipseLink properties **********
	
	private void buildProjectLocationProperty(Properties properties, String location) {
		this.putProperty(properties, 
			SchemaGeneration.ECLIPSELINK_APPLICATION_LOCATION,
			location);
	}
	
	private void buildDDLModeProperties(Properties properties) {
		this.putProperty(properties,  
			SchemaGeneration.ECLIPSELINK_DDL_GENERATION_TYPE,
			DdlGenerationType.DROP_AND_CREATE_TABLES);
		this.putProperty(properties,  
			SchemaGeneration.ECLIPSELINK_DDL_GENERATION_OUTPUT_MODE,
			OutputMode.DATABASE);
	}
	
	private void buildLoginProperties(Properties properties) {
		ConnectionProfile profile = this.getConnectionProfile();

		this.putProperty(properties,  
			Connection.ECLIPSELINK_BIND_PARAMETERS,
			"false");
		this.putProperty(properties, 
			Connection.ECLIPSELINK_DRIVER,
			profile.getDriverClassName());
		this.putProperty(properties,
			Connection.ECLIPSELINK_URL,
			profile.getUrl());
		this.putProperty(properties,
			Connection.ECLIPSELINK_USER,
			profile.getUserName());
		this.putProperty(properties,
			Connection.ECLIPSELINK_PASSWORD,
			profile.getUserPassword());
		this.putProperty(properties,
			Logging.ECLIPSELINK_LEVEL,
			LoggingLevel.FINEST);
		this.putProperty(properties,
			Logging.ECLIPSELINK_TIMESTAMP,
			"false");
		this.putProperty(properties,
			Logging.ECLIPSELINK_THREAD,
			"false");
		this.putProperty(properties,
			Logging.ECLIPSELINK_SESSION,
			"false");
		this.putProperty(properties,
			Logging.ECLIPSELINK_EXCEPTIONS,
			"true");
		this.putProperty(properties,
			Customization.ECLIPSELINK_THROW_EXCEPTIONS,
			"true");
	}
	
	private void putProperty(Properties properties, String key, String value) {
		properties.put(key, (value == null) ? "" : value);
	}
	
	private void saveLoginProperties(String projectLocation, String propertiesFile) {
		Properties elProperties = new Properties();
		
		this.buildLoginProperties(elProperties);
		
		this.buildDDLModeProperties(elProperties);

		this.buildProjectLocationProperty(elProperties, projectLocation);
	    try {
	        File file = new File(propertiesFile);
	        file.createNewFile();
	        FileOutputStream stream = new FileOutputStream(file);
	        elProperties.store(stream, null);
		    stream.close();
	    }
	    catch (Exception e) {
	    	String message = "Error saving: " + propertiesFile;
			throw new RuntimeException(message, e);
		}
	}

	// ********** Main arguments **********

	private String buildPuNameArgument(String puName) {
		return " -pu \"" + puName + "\"";
	}
	
	private String buildPropertiesPathArgument(String propertiesFile) {
		return " -p \"" + propertiesFile + "\"";
	}
	
	private String buildDebugArgument() {
		return (this.isDebug) ? " -debug" : "";
	}
}
