/*
 * Hibernate, Relational Persistence for Idiomatic Java
 *
 * Copyright (c) 2010, Red Hat, Inc. and/or its affiliates or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat, Inc.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * This program 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.  See the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package org.hibernate.search.store.impl;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Properties;

import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.store.Directory;
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.NativeFSLockFactory;
import org.apache.lucene.store.NoLockFactory;
import org.apache.lucene.store.SimpleFSDirectory;
import org.apache.lucene.store.SimpleFSLockFactory;
import org.apache.lucene.store.SingleInstanceLockFactory;
import org.apache.lucene.util.Version;
import org.hibernate.annotations.common.util.StringHelper;
import org.hibernate.search.Environment;
import org.hibernate.search.SearchException;
import org.hibernate.search.store.LockFactoryProvider;
import org.hibernate.search.util.configuration.impl.ConfigurationParseHelper;
import org.hibernate.search.util.impl.ClassLoaderHelper;
import org.hibernate.search.util.impl.FileHelper;
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";
	private static final String COPY_BUFFER_SIZE_PROP_NAME = "buffer_size_on_copy";
	private static final String LOCKING_STRATEGY_PROP_NAME = "locking_strategy";
	private static final String FS_DIRECTORY_TYPE_PROP_NAME = "filesystem_access_type";
	private static final String INDEX_BASE_PROP_NAME = "indexBase";
	private static final String INDEX_NAME_PROP_NAME = "indexName";
	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 File getSourceDirectory(String indexName, Properties properties, boolean needWritePermissions) {
		String root = properties.getProperty( ROOT_INDEX_PROP_NAME );
		String relative = properties.getProperty( RELATIVE_INDEX_PROP_NAME );
		File 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 = new File( relative );
			if ( !sourceDirectory.isDirectory() ) { // this also tests for existence
				throw new SearchException( "Unable to read source directory: " + relative );
			}
			//else keep source as it
		}
		else {
			File rootDir = new File( root );
			makeSanityCheckedDirectory( rootDir, indexName, needWritePermissions );
			sourceDirectory = new File( root, 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
	 * @return the created {@code FSDirectory} instance
	 * @throws java.io.IOException if an error
	 */
	public static FSDirectory createFSIndex(File indexDir, Properties properties) throws IOException {
		LockFactory lockFactory = createLockFactory( indexDir, properties );
		FSDirectoryType fsDirectoryType = FSDirectoryType.getType( properties );
		FSDirectory fsDirectory = fsDirectoryType.getDirectory( indexDir, null );

		// must use the setter (instead of using the constructor) to set the lockFactory, or Lucene will
		// throw an exception if it's different than a previous setting.
		fsDirectory.setLockFactory( lockFactory );
		log.debugf( "Initialize index: '%s'", indexDir.getAbsolutePath() );
		initializeIndexIfNeeded( fsDirectory );
		return fsDirectory;
	}

	/**
	 * Initialize the Lucene Directory if it isn't already.
	 *
	 * @param directory the Directory to initialize
	 * @throws SearchException in case of lock acquisition timeouts, IOException, or if a corrupt index is found
	 */
	public static void initializeIndexIfNeeded(Directory directory) {
		//version doesn't really matter as we won't use the Analyzer
		Version version =  Environment.DEFAULT_LUCENE_MATCH_VERSION;
		SimpleAnalyzer analyzer = new SimpleAnalyzer( version );
		try {
			if ( ! IndexReader.indexExists( directory ) ) {
				IndexWriterConfig iwriterConfig = new IndexWriterConfig( version, analyzer ).setOpenMode( OpenMode.CREATE );
				IndexWriter iw = new IndexWriter( directory, iwriterConfig );
				iw.close();
			}
		}
		catch (IOException e) {
			throw new SearchException( "Could not initialize index", e );
		}
		finally {
			analyzer.close();
		}
	}

	/**
	 * Creates a LockFactory as selected in the configuration for the
	 * DirectoryProvider.
	 * The SimpleFSLockFactory and NativeFSLockFactory need a File to know
	 * where to stock the filesystem based locks; other implementations
	 * ignore this parameter.
	 *
	 * @param indexDir the directory to use to store locks, if needed by implementation
	 * @param dirConfiguration the configuration of current DirectoryProvider
	 * @return the LockFactory as configured, or a SimpleFSLockFactory
	 *         in case of configuration errors or as a default.
	 * @throws IOException if any.
	 */
	public static LockFactory createLockFactory(File indexDir, Properties dirConfiguration) {
		//For FS-based indexes default to "native", default to "single" otherwise.
		String defaultStrategy = indexDir == null ? "single" : "native";
		String lockFactoryName = dirConfiguration.getProperty( LOCKING_STRATEGY_PROP_NAME, defaultStrategy );
		if ( "simple".equals( lockFactoryName ) ) {
			if ( indexDir == null ) {
				throw new SearchException( "To use \"simple\" as a LockFactory strategy an indexBase path must be set" );
			}
			try {
				return new SimpleFSLockFactory( indexDir );
			}
			catch ( IOException e ) {
				throw new SearchException( "Could not initialize SimpleFSLockFactory", e );
			}
		}
		else if ( "native".equals( lockFactoryName ) ) {
			if ( indexDir == null ) {
				throw new SearchException( "To use \"native\" as a LockFactory strategy an indexBase path must be set" );
			}
			try {
				return new NativeFSLockFactory( indexDir );
			}
			catch ( IOException e ) {
				throw new SearchException( "Could not initialize NativeFSLockFactory", e );
			}
		}
		else if ( "single".equals( lockFactoryName ) ) {
			return new SingleInstanceLockFactory();
		}
		else if ( "none".equals( lockFactoryName ) ) {
			return NoLockFactory.getNoLockFactory();
		}
		else {
			LockFactoryProvider lockFactoryFactory = ClassLoaderHelper.instanceFromName(
					LockFactoryProvider.class,
					lockFactoryName, DirectoryProviderHelper.class, LOCKING_STRATEGY_PROP_NAME
			);
			return lockFactoryFactory.createLockFactory( indexDir, dirConfiguration );
		}
	}

	/**
	 * Verify the index directory exists and is writable,
	 * or creates it if not existing.
	 *
	 * @param annotatedIndexName The index name declared on the @Indexed annotation
	 * @param properties The properties may override the indexname.
	 * @param verifyIsWritable Verify the directory is writable
	 * @return the File representing the Index Directory
	 * @throws SearchException if any.
	 */
	public static File getVerifiedIndexDir(String annotatedIndexName, Properties properties, boolean verifyIsWritable) {
		String indexBase = properties.getProperty( INDEX_BASE_PROP_NAME, "." );
		String indexName = properties.getProperty( INDEX_NAME_PROP_NAME, annotatedIndexName );
		File baseIndexDir = new File( indexBase );
		makeSanityCheckedDirectory( baseIndexDir, indexName, verifyIsWritable );
		File indexDir = new File( baseIndexDir, indexName );
		makeSanityCheckedDirectory( indexDir, indexName, verifyIsWritable );
		return indexDir;
	}

	/**
	 * @param directory The directory to create or verify
	 * @param indexName To label exceptions
	 * @param verifyIsWritable Verify the directory is writable
	 *
	 * @throws SearchException
	 */
	private static void makeSanityCheckedDirectory(File directory, String indexName, boolean verifyIsWritable) {
		if ( !directory.exists() ) {
			log.indexDirectoryNotFoundCreatingNewOne( directory.getAbsolutePath() );
			//if not existing, create the full path
			if ( !directory.mkdirs() ) {
				throw new SearchException(
						"Unable to create index directory: "
								+ directory.getAbsolutePath() + " for index "
								+ indexName
				);
			}
		}
		else {
			// else check it is not a file
			if ( !directory.isDirectory() ) {
				throw new SearchException(
						"Unable to initialize index: "
								+ indexName + ": "
								+ directory.getAbsolutePath() + " is a file."
				);
			}
		}
		// and ensure it's writable
		if ( verifyIsWritable && ( !directory.canWrite() ) ) {
			throw new SearchException(
					"Cannot write into index directory: "
							+ directory.getAbsolutePath() + " 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", 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.
	 *
	 * @param indexName the index name
	 * @param properties the configuration properties
	 * @return the number of Bytes to use as "chunk size" in file copy operations.
	 */
	public static long getCopyBufferSize(String indexName, Properties properties) {
		String value = properties.getProperty( COPY_BUFFER_SIZE_PROP_NAME );
		long size = FileHelper.DEFAULT_COPY_BUFFER_SIZE;
		if ( value != null ) {
			try {
				size = Long.parseLong( value ) * 1024 * 1024; //from MB to B.
			}
			catch ( NumberFormatException nfe ) {
				throw new SearchException(
						"Unable to initialize index " +
								indexName + "; " + COPY_BUFFER_SIZE_PROP_NAME + " is not numeric.", nfe
				);
			}
			if ( size <= 0 ) {
				throw new SearchException(
						"Unable to initialize index " +
								indexName + "; " + COPY_BUFFER_SIZE_PROP_NAME + " needs to be greater than zero."
				);
			}
		}
		return size;
	}

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

		private Class<?> fsDirectoryClass;

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

		public FSDirectory getDirectory(File indexDir, LockFactory factory) throws IOException {
			FSDirectory directory;
			if ( fsDirectoryClass == null ) {
				directory = FSDirectory.open( indexDir, factory );
			}
			else {
				try {
					Constructor constructor = fsDirectoryClass.getConstructor( File.class, LockFactory.class );
					directory = (FSDirectory) 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() );
				}
				catch ( IllegalArgumentException e ) {
					throw new SearchException( "Invalid option value for " + FS_DIRECTORY_TYPE_PROP_NAME + ": " + fsDirectoryTypeValue );
				}
			}
			else {
				fsDirectoryType = AUTO;
			}
			return fsDirectoryType;
		}
	}
}
