/*
 * Hibernate Search, full-text search for your domain model
 *
 * 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.search.store.impl;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Properties;

import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.LockFactory;
import org.apache.lucene.store.MMapDirectory;
import org.apache.lucene.store.NIOFSDirectory;
import org.apache.lucene.store.SimpleFSDirectory;
import org.hibernate.search.engine.service.spi.ServiceManager;
import org.hibernate.search.exception.SearchException;
import org.hibernate.search.store.spi.DirectoryHelper;
import org.hibernate.search.store.spi.LockFactoryCreator;
import org.hibernate.search.util.StringHelper;
import org.hibernate.search.util.configuration.impl.ConfigurationParseHelper;
import org.hibernate.search.util.logging.impl.Log;
import org.hibernate.search.util.logging.impl.LoggerFactory;

/**
 * @author Emmanuel Bernard
 * @author Sanne Grinovero
 * @author Hardy Ferentschik
 */
public final class DirectoryProviderHelper {
	private static final Log log = LoggerFactory.make();

	private static final String ROOT_INDEX_PROP_NAME = "sourceBase";
	private static final String RELATIVE_INDEX_PROP_NAME = "source";

	@Deprecated //Legacy - no longer used:
	private static final String COPY_BUFFER_SIZE_PROP_NAME = "buffer_size_on_copy";

	private static final String FS_DIRECTORY_TYPE_PROP_NAME = "filesystem_access_type";
	private static final String REFRESH_PROP_NAME = "refresh";
	private static final String RETRY_INITIALIZE_PROP_NAME = "retry_initialize_period";

	private DirectoryProviderHelper() {
	}

	/**
	 * Build a directory name out of a root and relative path, guessing the significant part
	 * and checking for the file availability
	 *
	 * @param indexName the name of the index (directory) to create
	 * @param properties the configuration properties
	 * @param needWritePermissions when true the directory will be tested for read-write permissions.
	 * @return The file representing the source directory
	 */
	public static Path getSourceDirectoryPath(String indexName, Properties properties, boolean needWritePermissions) {
		String root = properties.getProperty( ROOT_INDEX_PROP_NAME );
		String relative = properties.getProperty( RELATIVE_INDEX_PROP_NAME );
		Path sourceDirectory;
		if ( log.isTraceEnabled() ) {
			log.trace(
					"Guess source directory from " + ROOT_INDEX_PROP_NAME + " " +
							( root != null ? root : "<null>" ) +
							" and " +
							RELATIVE_INDEX_PROP_NAME + " " +
							( relative != null ? relative : "<null>" )
					);
		}
		if ( relative == null ) {
			relative = indexName;
		}
		if ( StringHelper.isEmpty( root ) ) {
			log.debug( "No root directory, go with relative " + relative );
			sourceDirectory = FileSystems.getDefault().getPath( relative );
			if ( Files.notExists( sourceDirectory ) ) {
				throw log.sourceDirectoryNotExisting( relative );
			}
			else if ( ! Files.isReadable( sourceDirectory ) ) {
				throw log.directoryIsNotReadable( relative );
			}
			else if ( ! Files.isDirectory( sourceDirectory ) ) {
				throw log.fileIsADirectory( relative );
			}
			//else keep source as it
		}
		else {
			Path rootDir = FileSystems.getDefault().getPath( root );
			makeSanityCheckedDirectory( rootDir, indexName, needWritePermissions );
			sourceDirectory = rootDir.resolve( relative );
			makeSanityCheckedDirectory( sourceDirectory, indexName, needWritePermissions );
			log.debug( "Got directory from root + relative" );
		}
		return sourceDirectory;
	}

	/**
	 * Creates an FSDirectory in provided directory and initializes
	 * an index if not already existing.
	 *
	 * @param indexDir the directory where to write a new index
	 * @param properties the configuration properties
	 * @param serviceManager provides access to services
	 * @return the created {@code FSDirectory} instance
	 * @throws java.io.IOException if an error
	 */
	public static FSDirectory createFSIndex(Path indexDir, Properties properties, ServiceManager serviceManager) throws IOException {
		LockFactory lockFactory = getLockFactory( indexDir, properties, serviceManager );
		FSDirectoryType fsDirectoryType = FSDirectoryType.getType( properties );
		FSDirectory fsDirectory = fsDirectoryType.getDirectory( indexDir, lockFactory );
		log.debugf( "Initialize index: '%s'", indexDir.toAbsolutePath() );
		DirectoryHelper.initializeIndexIfNeeded( fsDirectory );
		return fsDirectory;
	}

	private static LockFactory getLockFactory(Path indexDir, Properties properties, ServiceManager serviceManager) {
		try {
			return serviceManager.requestService( LockFactoryCreator.class ).createLockFactory( indexDir, properties );
		}
		finally {
			serviceManager.releaseService( LockFactoryCreator.class );
		}
	}

	/**
	 * @param directory The directory to create or verify
	 * @param indexName To label exceptions
	 * @param verifyIsWritable Verify the directory is writable
	 * @throws SearchException if the index cannot be created, it's not a directory or it's not writeable
	 */
	public static void makeSanityCheckedDirectory(Path directory, String indexName, boolean verifyIsWritable) {
		if ( Files.notExists( directory ) ) {
			log.indexDirectoryNotFoundCreatingNewOne( directory.toString() );
			//if not existing, create the full path
			try {
				Files.createDirectories( directory );
			}
			catch (IOException e) {
				throw new SearchException(
						"Unable to create index directory: "
								+ directory + " for index " + indexName );
			}
		}
		else {
			// else check it is not a file
			if ( ! Files.isDirectory( directory ) ) {
				throw new SearchException(
						"Unable to initialize index: "
								+ indexName + ": " + directory + " is a file." );
			}
		}
		// and ensure it's writable
		if ( verifyIsWritable && ( ! Files.isWritable( directory ) ) ) {
			throw new SearchException(
					"Cannot write into index directory: "
							+ directory + " for index " + indexName );
		}
	}

	/**
	 * @param properties the configuration of the DirectoryProvider
	 * @param directoryProviderName the name of the DirectoryProvider, used for error reporting
	 * @return The period in milliseconds to keep retrying initialization of a DirectoryProvider
	 */
	static long getRetryInitializePeriod(Properties properties, String directoryProviderName) {
		int retry_period_seconds = ConfigurationParseHelper.getIntValue( properties, RETRY_INITIALIZE_PROP_NAME, 0 );
		log.debugf( "Retry initialize period for Directory %s: %d seconds", directoryProviderName, retry_period_seconds );
		if ( retry_period_seconds < 0 ) {
			throw new SearchException( RETRY_INITIALIZE_PROP_NAME + " for Directory " + directoryProviderName + " must be a positive integer" );
		}
		return retry_period_seconds * 1000; //convert into milliseconds
	}

	static long getRefreshPeriod(Properties properties, String directoryProviderName) {
		String refreshPeriod = properties.getProperty( REFRESH_PROP_NAME, "3600" );
		long period;
		try {
			period = Long.parseLong( refreshPeriod );
		}
		catch (NumberFormatException nfe) {
			throw new SearchException(
					"Unable to initialize index: " + directoryProviderName + "; refresh period is not numeric.", nfe
			);
		}
		log.debugf( "Refresh period: %d seconds", (Long) period );
		return period * 1000; //per second
	}

	/**
	 * Users may configure the number of MB to use as
	 * "chunk size" for large file copy operations performed
	 * by DirectoryProviders.
	 *
	 * @deprecated the configured chunk size is actually no longer useful and will be ignored.
	 *
	 * @param indexName the index name
	 * @param properties the configuration properties
	 * @return the number of Bytes to use as "chunk size" in file copy operations.
	 */
	@Deprecated
	public static long getCopyBufferSize(String indexName, Properties properties) {
		String value = properties.getProperty( COPY_BUFFER_SIZE_PROP_NAME );
		if ( value != null ) {
			log.deprecatedConfigurationPropertyIsIgnored( COPY_BUFFER_SIZE_PROP_NAME );
		}
		return 0l;
	}

	private enum FSDirectoryType {
		AUTO( null ),
		SIMPLE( SimpleFSDirectory.class ),
		NIO( NIOFSDirectory.class ),
		MMAP( MMapDirectory.class );

		private Class<? extends FSDirectory> fsDirectoryClass;

		FSDirectoryType(Class<? extends FSDirectory> fsDirectoryClass) {
			this.fsDirectoryClass = fsDirectoryClass;
		}

		public FSDirectory getDirectory(Path indexDir, LockFactory factory) throws IOException {
			FSDirectory directory;
			if ( fsDirectoryClass == null ) {
				directory = FSDirectory.open( indexDir, factory );
			}
			else {
				try {
					Constructor<? extends FSDirectory> constructor = fsDirectoryClass.getConstructor( Path.class, LockFactory.class );
					directory = constructor.newInstance( indexDir, factory );
				}
				catch (NoSuchMethodException e) {
					throw new SearchException( "Unable to find appropriate FSDirectory constructor", e );
				}
				catch (InstantiationException e) {
					throw new SearchException(
							"Unable to instantiate FSDirectory class " + fsDirectoryClass.getName(), e
					);
				}
				catch (IllegalAccessException e) {
					throw new SearchException(
							"Unable to instantiate FSDirectory class " + fsDirectoryClass.getName(), e
					);
				}
				catch (InvocationTargetException e) {
					throw new SearchException(
							"Unable to instantiate FSDirectory class " + fsDirectoryClass.getName(), e
					);
				}
			}
			return directory;
		}

		public static FSDirectoryType getType(Properties properties) {
			FSDirectoryType fsDirectoryType;
			String fsDirectoryTypeValue = properties.getProperty( FS_DIRECTORY_TYPE_PROP_NAME );
			if ( StringHelper.isNotEmpty( fsDirectoryTypeValue ) ) {
				try {
					fsDirectoryType = Enum.valueOf( FSDirectoryType.class, fsDirectoryTypeValue.toUpperCase( Locale.ROOT ) );
				}
				catch (IllegalArgumentException e) {
					throw new SearchException( "Invalid option value for " + FS_DIRECTORY_TYPE_PROP_NAME + ": " + fsDirectoryTypeValue );
				}
			}
			else {
				fsDirectoryType = AUTO;
			}
			return fsDirectoryType;
		}
	}
}
