/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * License: GNU Lesser General Public License (LGPL), version 2.1 or later.
 * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
 */
package org.hibernate.tool.hbm2ddl;

import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataBuilder;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.model.naming.ImplicitNamingStrategy;
import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
import org.hibernate.boot.registry.BootstrapServiceRegistry;
import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
import org.hibernate.boot.registry.selector.spi.StrategySelector;
import org.hibernate.boot.spi.MetadataImplementor;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.log.DeprecationLogger;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.tool.schema.SourceType;
import org.hibernate.tool.schema.TargetType;
import org.hibernate.tool.schema.internal.ExceptionHandlerCollectingImpl;
import org.hibernate.tool.schema.internal.ExceptionHandlerHaltImpl;
import org.hibernate.tool.schema.internal.Helper;
import org.hibernate.tool.schema.internal.SchemaCreatorImpl;
import org.hibernate.tool.schema.spi.ExceptionHandler;
import org.hibernate.tool.schema.spi.ExecutionOptions;
import org.hibernate.tool.schema.spi.SchemaManagementException;
import org.hibernate.tool.schema.spi.SchemaManagementTool;
import org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator;
import org.hibernate.tool.schema.spi.ScriptSourceInput;
import org.hibernate.tool.schema.spi.ScriptTargetOutput;
import org.hibernate.tool.schema.spi.SourceDescriptor;
import org.hibernate.tool.schema.spi.TargetDescriptor;

/**
 * Command-line tool for exporting (create and/or drop) a database schema.  The export can
 * be sent directly to the database, written to script or both.
 *
 * @author Daniel Bradby
 * @author Gavin King
 * @author Steve Ebersole
 */
public class SchemaExport {
	private static final CoreMessageLogger LOG = CoreLogging.messageLogger( SchemaExport.class );

	public static enum Type {
		CREATE( Action.CREATE ),
		DROP( Action.DROP ),
		NONE( Action.NONE ),
		BOTH( Action.BOTH );

		private final Action actionReplacement;

		Type(Action actionReplacement) {
			this.actionReplacement = actionReplacement;
		}

		public boolean doCreate() {
			return actionReplacement.doCreate();
		}

		public boolean doDrop() {
			return actionReplacement.doDrop();
		}
	}

	public static enum Action {
		/**
		 * None - duh :P
		 */
		NONE,
		/**
		 * Create only
		 */
		CREATE,
		/**
		 * Drop only
		 */
		DROP,
		/**
		 * Drop and then create
		 */
		BOTH;

		public boolean doCreate() {
			return this == BOTH || this == CREATE;
		}

		public boolean doDrop() {
			return this == BOTH || this == DROP;
		}

		private static Action interpret(boolean justDrop, boolean justCreate) {
			if ( justDrop ) {
				return Action.DROP;
			}
			else if ( justCreate ) {
				return Action.CREATE;
			}
			else {
				return Action.BOTH;
			}
		}

		public static Action parseCommandLineOption(String actionText) {
			if ( actionText.equalsIgnoreCase( "create" ) ) {
				return CREATE;
			}
			else if ( actionText.equalsIgnoreCase( "drop" ) ) {
				return DROP;
			}
			else if ( actionText.equalsIgnoreCase( "drop-and-create" ) ) {
				return BOTH;
			}
			else {
				return NONE;
			}
		}
	}

	boolean append = true;
	boolean haltOnError = false;
	boolean format = false;
	boolean manageNamespaces = false;
	String delimiter = null;

	String outputFile = null;

	private String importFiles;

	private final List<Exception> exceptions = new ArrayList<Exception>();


	/**
	 * For generating a export script file, this is the file which will be written.
	 *
	 * @param filename The name of the file to which to write the export script.
	 *
	 * @return this
	 */
	public SchemaExport setOutputFile(String filename) {
		outputFile = filename;
		return this;
	}

	/**
	 * For generating a export script file, by default the content will be appended at the begin or end of the file.
	 *
	 * The sql will be written at the beginning of the file rather append to the end.
	 *
	 * @return this
	 */
	public SchemaExport setOverrideOutputFileContent() {
		append = false;
		return this;
	}

	/**
	 * Comma-separated list of resource names to use for database init commands on create.
	 *
	 * @param importFiles The comma-separated list of init file resources names
	 *
	 * @return this
	 */
	public SchemaExport setImportFiles(String importFiles) {
		this.importFiles = importFiles;
		return this;
	}

	/**
	 * Set the end of statement delimiter
	 *
	 * @param delimiter The delimiter
	 *
	 * @return this
	 */
	public SchemaExport setDelimiter(String delimiter) {
		this.delimiter = delimiter;
		return this;
	}

	/**
	 * Should we format the sql strings?
	 *
	 * @param format Should we format SQL strings
	 *
	 * @return this
	 */
	public SchemaExport setFormat(boolean format) {
		this.format = format;
		return this;
	}

	/**
	 * Should we stop once an error occurs?
	 *
	 * @param haltOnError True if export should stop after error.
	 *
	 * @return this
	 */
	public SchemaExport setHaltOnError(boolean haltOnError) {
		this.haltOnError = haltOnError;
		return this;
	}

	public SchemaExport setManageNamespaces(boolean manageNamespaces) {
		this.manageNamespaces = manageNamespaces;
		return this;
	}

	public void drop(EnumSet<TargetType> targetTypes, Metadata metadata) {
		execute( targetTypes, Action.DROP, metadata );
	}

	public void create(EnumSet<TargetType> targetTypes, Metadata metadata) {
		execute( targetTypes, Action.BOTH, metadata );
	}

	public void createOnly(EnumSet<TargetType> targetTypes, Metadata metadata) {
		execute( targetTypes, Action.CREATE, metadata );
	}

	public void execute(EnumSet<TargetType> targetTypes, Action action, Metadata metadata) {
		execute( targetTypes, action, metadata, ( (MetadataImplementor) metadata ).getMetadataBuildingOptions().getServiceRegistry() );
	}

	@SuppressWarnings("unchecked")
	public void execute(EnumSet<TargetType> targetTypes, Action action, Metadata metadata, ServiceRegistry serviceRegistry) {
		if ( action == Action.NONE ) {
			LOG.debug( "Skipping SchemaExport as Action.NONE was passed" );
			return;
		}

		if ( targetTypes.isEmpty() ) {
			LOG.debug( "Skipping SchemaExport as no targets were specified" );
			return;
		}

		exceptions.clear();

		LOG.runningHbm2ddlSchemaExport();

		final TargetDescriptor targetDescriptor = buildTargetDescriptor(
				targetTypes,
				outputFile,
				append,
				serviceRegistry
		);

		doExecution( action, needsJdbcConnection( targetTypes ), metadata, serviceRegistry, targetDescriptor );
	}

	public void doExecution(
			Action action,
			boolean needsJdbc,
			Metadata metadata,
			ServiceRegistry serviceRegistry,
			TargetDescriptor targetDescriptor) {
		Map config = new HashMap( serviceRegistry.getService( ConfigurationService.class ).getSettings() );
		config.put( AvailableSettings.HBM2DDL_DELIMITER, delimiter );
		config.put( AvailableSettings.FORMAT_SQL, format );
		config.put( AvailableSettings.HBM2DDL_IMPORT_FILES, importFiles );

		final SchemaManagementTool tool = serviceRegistry.getService( SchemaManagementTool.class );

		final ExceptionHandler exceptionHandler = haltOnError
				? ExceptionHandlerHaltImpl.INSTANCE
				: new ExceptionHandlerCollectingImpl();
		final ExecutionOptions executionOptions = SchemaManagementToolCoordinator.buildExecutionOptions(
				config,
				exceptionHandler
		);

		final SourceDescriptor sourceDescriptor = new SourceDescriptor() {
			@Override
			public SourceType getSourceType() {
				return SourceType.METADATA;
			}

			@Override
			public ScriptSourceInput getScriptSourceInput() {
				return null;
			}
		};

		try {
			if ( action.doDrop() ) {
				tool.getSchemaDropper( config ).doDrop(
						metadata,
						executionOptions,
						sourceDescriptor,
						targetDescriptor
				);
			}

			if ( action.doCreate() ) {
				tool.getSchemaCreator( config ).doCreation(
						metadata,
						executionOptions,
						sourceDescriptor,
						targetDescriptor
				);
			}
		}
		finally {
			if ( exceptionHandler instanceof ExceptionHandlerCollectingImpl ) {
				exceptions.addAll( ( (ExceptionHandlerCollectingImpl) exceptionHandler ).getExceptions() );
			}
		}
	}

	private boolean needsJdbcConnection(EnumSet<TargetType> targetTypes) {
		return targetTypes.contains( TargetType.DATABASE );
	}

	public static TargetDescriptor buildTargetDescriptor(
			EnumSet<TargetType> targetTypes,
			String outputFile,
			ServiceRegistry serviceRegistry) {
		return buildTargetDescriptor( targetTypes, outputFile, true, serviceRegistry );
	}

	public static TargetDescriptor buildTargetDescriptor(
			EnumSet<TargetType> targetTypes,
			String outputFile,
			boolean append,
			ServiceRegistry serviceRegistry) {
		final ScriptTargetOutput scriptTarget;
		if ( targetTypes.contains( TargetType.SCRIPT ) ) {
			if ( outputFile == null ) {
				throw new SchemaManagementException( "Writing to script was requested, but no script file was specified" );
			}
			scriptTarget = Helper.interpretScriptTargetSetting(
					outputFile,
					serviceRegistry.getService( ClassLoaderService.class ),
					(String) serviceRegistry.getService( ConfigurationService.class ).getSettings().get( AvailableSettings.HBM2DDL_CHARSET_NAME ),
					append
			);
		}
		else {
			scriptTarget = null;
		}

		return new TargetDescriptorImpl( targetTypes, scriptTarget );
	}

	/**
	 * For testing use
	 */
	public void perform(Action action, Metadata metadata, ScriptTargetOutput target) {
		doExecution(
				action,
				false,
				metadata,
				( (MetadataImplementor) metadata ).getMetadataBuildingOptions().getServiceRegistry(),
				new TargetDescriptorImpl( EnumSet.of( TargetType.SCRIPT ), target )
		);
	}

	public static void main(String[] args) {
		try {
			final CommandLineArgs commandLineArgs = CommandLineArgs.parseCommandLineArgs( args );
			execute( commandLineArgs );
		}
		catch (Exception e) {
			LOG.unableToCreateSchema( e );
		}
	}

	public static void execute(CommandLineArgs commandLineArgs) throws Exception {
		StandardServiceRegistry serviceRegistry = buildStandardServiceRegistry( commandLineArgs );
		try {
			final MetadataImplementor metadata = buildMetadata( commandLineArgs, serviceRegistry );

			new SchemaExport()
					.setHaltOnError( commandLineArgs.halt )
					.setOutputFile( commandLineArgs.outputFile )
					.setDelimiter( commandLineArgs.delimiter )
					.setFormat( commandLineArgs.format )
					.setManageNamespaces( commandLineArgs.manageNamespaces )
					.setImportFiles( commandLineArgs.importFile )
					.execute( commandLineArgs.targetTypes, commandLineArgs.action, metadata, serviceRegistry );
		}
		finally {
			StandardServiceRegistryBuilder.destroy( serviceRegistry );
		}
	}

	private static StandardServiceRegistry buildStandardServiceRegistry(CommandLineArgs commandLineArgs)
			throws Exception {
		final BootstrapServiceRegistry bsr = new BootstrapServiceRegistryBuilder().build();
		final StandardServiceRegistryBuilder ssrBuilder = new StandardServiceRegistryBuilder( bsr );

		if ( commandLineArgs.cfgXmlFile != null ) {
			ssrBuilder.configure( commandLineArgs.cfgXmlFile );
		}

		Properties properties = new Properties();
		if ( commandLineArgs.propertiesFile != null ) {
			try ( final FileInputStream fis = new FileInputStream( commandLineArgs.propertiesFile ) ) {
				properties.load( fis );
			}
		}
		ssrBuilder.applySettings( properties );

		return ssrBuilder.build();
	}

	private static MetadataImplementor buildMetadata(
			CommandLineArgs parsedArgs,
			StandardServiceRegistry serviceRegistry) throws Exception {
		final MetadataSources metadataSources = new MetadataSources( serviceRegistry );

		for ( String filename : parsedArgs.hbmXmlFiles ) {
			metadataSources.addFile( filename );
		}

		for ( String filename : parsedArgs.jarFiles ) {
			metadataSources.addJar( new File( filename ) );
		}


		final MetadataBuilder metadataBuilder = metadataSources.getMetadataBuilder();
		final StrategySelector strategySelector = serviceRegistry.getService( StrategySelector.class );
		if ( parsedArgs.implicitNamingStrategyImplName != null ) {
			metadataBuilder.applyImplicitNamingStrategy(
					strategySelector.resolveStrategy(
							ImplicitNamingStrategy.class,
							parsedArgs.implicitNamingStrategyImplName
					)
			);
		}
		if ( parsedArgs.physicalNamingStrategyImplName != null ) {
			metadataBuilder.applyPhysicalNamingStrategy(
					strategySelector.resolveStrategy(
							PhysicalNamingStrategy.class,
							parsedArgs.physicalNamingStrategyImplName
					)
			);
		}

		return (MetadataImplementor) metadataBuilder.build();
	}

	/**
	 * Intended for test usage only.  Builds a Metadata using the same algorithm  as
	 * {@link #main}
	 *
	 * @param args The "command line args"
	 *
	 * @return The built Metadata
	 *
	 * @throws Exception Problems building the Metadata
	 */
	public static MetadataImplementor buildMetadataFromMainArgs(String[] args) throws Exception {
		final CommandLineArgs commandLineArgs = CommandLineArgs.parseCommandLineArgs( args );
		StandardServiceRegistry serviceRegistry = buildStandardServiceRegistry( commandLineArgs );
		try {
			return buildMetadata( commandLineArgs, serviceRegistry );
		}
		finally {
			StandardServiceRegistryBuilder.destroy( serviceRegistry );
		}
	}

	/**
	 * Returns a List of all Exceptions which occurred during the export.
	 *
	 * @return A List containing the Exceptions occurred during the export
	 */
	public List getExceptions() {
		return exceptions;
	}

	private static class CommandLineArgs {
		EnumSet<TargetType> targetTypes;
		Action action;

		boolean halt = false;
		boolean format = false;

		boolean manageNamespaces = false;

		String delimiter = null;

		String outputFile = null;
		String importFile = SchemaCreatorImpl.DEFAULT_IMPORT_FILE;

		String propertiesFile = null;
		String cfgXmlFile = null;
		String implicitNamingStrategyImplName = null;
		String physicalNamingStrategyImplName = null;

		List<String> hbmXmlFiles = new ArrayList<String>();
		List<String> jarFiles = new ArrayList<String>();

		public static CommandLineArgs parseCommandLineArgs(String[] args) {
			String targetText = null;
			boolean script = true;
			boolean export = true;

			String actionText = null;
			boolean drop = false;
			boolean create = false;

			CommandLineArgs parsedArgs = new CommandLineArgs();

			for ( String arg : args ) {
				if ( arg.startsWith( "--" ) ) {
					if ( arg.equals( "--quiet" ) ) {
						script = false;
					}
					else if ( arg.equals( "--text" ) ) {
						export = false;
					}
					else if ( arg.equals( "--drop" ) ) {
						drop = true;
					}
					else if ( arg.equals( "--create" ) ) {
						create = true;
					}
					else if ( arg.startsWith( "--action=" ) ) {
						actionText = arg.substring( 9 );
					}
					else if ( arg.startsWith( "--target=" ) ) {
						targetText = arg.substring( 9 );
					}
					else if ( arg.equals( "--schemas" ) ) {
						parsedArgs.manageNamespaces = true;
					}
					else if ( arg.equals( "--haltonerror" ) ) {
						parsedArgs.halt = true;
					}
					else if ( arg.startsWith( "--output=" ) ) {
						parsedArgs.outputFile = arg.substring( 9 );
					}
					else if ( arg.startsWith( "--import=" ) ) {
						parsedArgs.importFile = arg.substring( 9 );
					}
					else if ( arg.startsWith( "--properties=" ) ) {
						parsedArgs.propertiesFile = arg.substring( 13 );
					}
					else if ( arg.equals( "--format" ) ) {
						parsedArgs.format = true;
					}
					else if ( arg.startsWith( "--delimiter=" ) ) {
						parsedArgs.delimiter = arg.substring( 12 );
					}
					else if ( arg.startsWith( "--config=" ) ) {
						parsedArgs.cfgXmlFile = arg.substring( 9 );
					}
					else if ( arg.startsWith( "--naming=" ) ) {
						DeprecationLogger.DEPRECATION_LOGGER.logDeprecatedNamingStrategyArgument();
					}
					else if ( arg.startsWith( "--implicit-naming=" ) ) {
						parsedArgs.implicitNamingStrategyImplName = arg.substring( 18 );
					}
					else if ( arg.startsWith( "--physical-naming=" ) ) {
						parsedArgs.physicalNamingStrategyImplName = arg.substring( 18 );
					}
				}
				else {
					if ( arg.endsWith( ".jar" ) ) {
						parsedArgs.jarFiles.add( arg );
					}
					else {
						parsedArgs.hbmXmlFiles.add( arg );
					}
				}
			}

			if ( actionText == null ) {
				parsedArgs.action = Action.interpret( drop, create );
			}
			else {
				if ( drop || create ) {
					LOG.warn( "--drop or --create was used; prefer --action=none|create|drop|drop-and-create instead" );
				}
				parsedArgs.action = Action.parseCommandLineOption( actionText );
			}

			if ( targetText == null ) {
				parsedArgs.targetTypes = TargetTypeHelper.parseLegacyCommandLineOptions( script, export, parsedArgs.outputFile );
			}
			else {
				if ( !script || !export ) {
					LOG.warn( "--text or --quiet was used; prefer --target=none|(stdout|database|script)*" );
				}
				parsedArgs.targetTypes = TargetTypeHelper.parseCommandLineOptions( targetText );
			}

			return parsedArgs;
		}
	}

	private static class TargetDescriptorImpl implements TargetDescriptor {
		private final EnumSet<TargetType> targetTypes;
		private final ScriptTargetOutput scriptTarget;

		public TargetDescriptorImpl(
				EnumSet<TargetType> targetTypes,
				ScriptTargetOutput scriptTarget) {

			this.targetTypes = targetTypes;
			this.scriptTarget = scriptTarget;
		}

		@Override
		public EnumSet<TargetType> getTargetTypes() {
			return targetTypes;
		}

		@Override
		public ScriptTargetOutput getScriptTargetOutput() {
			return scriptTarget;
		}
	}
}
