/*
 * Copyright 2004-2006 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package at.molindo.util.terracotta;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.IndexOutput;

/**
 * A Terracota based directory based on Lucene RAM directory improved to support
 * better concurrency.
 * 
 * <p>
 * Basically, the direcotry stores
 * {@link org.compass.needle.terracotta.TerracottaFile} which are broken into
 * one or more byte arrays. The size of the byte array can be configured but
 * should not be changed once the index created.
 * 
 * @author kimchy
 */
public class TerracottaDirectory extends Directory {

	private static final transient Log log = LogFactory.getLog(TerracottaDirectory.class);

	public static final transient int DEFAULT_BUFFER_SIZE = 4096;

	public static final transient int DEFAULT_FLUSH_RATE = 10;

	private final ReentrantReadWriteLock fileMapLock = new ReentrantReadWriteLock();
	private final Map<String, TerracottaFile> fileMap = new HashMap<String, TerracottaFile>();

	private final int bufferSize;

	private final int flushRate;

	public TerracottaDirectory() {
		this(DEFAULT_BUFFER_SIZE, DEFAULT_FLUSH_RATE);
	}

	/**
	 * Constructs an empty {@link Directory}.
	 */
	public TerracottaDirectory(final int bufferSize, final int flushRate) {
		this.bufferSize = bufferSize;
		this.flushRate = flushRate;
		try {
			Class.forName("com.tc.object.bytecode.ManagerUtil", true, Thread.currentThread()
					.getContextClassLoader());
			// setLockFactory(new TerracottaManagerUtilLockFactory());
			// Use the default lock factory for now as there are some problems
			// with ManagerUtil
			setLockFactory(new TerracottaLockFactory());
		} catch (final ClassNotFoundException e) {
			setLockFactory(new TerracottaLockFactory());
		}
		if (log.isDebugEnabled()) {
			log.debug("Using Terracota lock factory [" + getLockFactory() + "]");
		}
	}

	/**
	 */
	public TerracottaDirectory(final Directory dir) throws IOException {
		this(dir, false);
	}

	private TerracottaDirectory(final Directory dir, final boolean closeDir) throws IOException {
		this(DEFAULT_BUFFER_SIZE, DEFAULT_FLUSH_RATE);
		Directory.copy(dir, this, closeDir);
	}

	/**
	 */
	public TerracottaDirectory(final File dir) throws IOException {
		this(FSDirectory.getDirectory(dir), true);
	}

	/**
	 */
	public TerracottaDirectory(final String dir) throws IOException {
		this(FSDirectory.getDirectory(dir), true);
	}

	/**
	 * Returns an array of strings, one for each file in the directory.
	 */
	@Override
	public final String[] list() {
		fileMapLock.readLock().lock();
		final String[] files;
		try {
			files = fileMap.keySet().toArray(new String[fileMap.size()]);
		} finally {
			fileMapLock.readLock().unlock();
		}
		return files;
	}

	/**
	 * Returns true if the named file exists in this directory.
	 */
	@Override
	public final boolean fileExists(final String name) {
		fileMapLock.readLock().lock();
		try {
			return fileMap.containsKey(name);
		} finally {
			fileMapLock.readLock().unlock();
		}
	}

	/**
	 * Returns the time the named file was last modified.
	 * 
	 * @throws IOException
	 *             if the file does not exist
	 */
	@Override
	public final long fileModified(final String name) throws IOException {
		fileMapLock.readLock().lock();
		final TerracottaFile file;
		try {
			file = fileMap.get(name);
		} finally {
			fileMapLock.readLock().unlock();
		}

		if (file == null) {
			throw new FileNotFoundException(name);
		}
		return file.getLastModified();
	}

	/**
	 * Set the modified time of an existing file to now.
	 * 
	 * @throws IOException
	 *             if the file does not exist
	 */
	@Override
	public void touchFile(final String name) throws IOException {
		fileMapLock.readLock().lock();
		final TerracottaFile file;
		try {
			file = fileMap.get(name);
		} finally {
			fileMapLock.readLock().unlock();
		}

		if (file == null) {
			throw new FileNotFoundException(name);
		}

		long ts2;
		final long ts1 = System.currentTimeMillis();
		do {
			try {
				Thread.sleep(0, 1);
			} catch (final InterruptedException e) {
				// do nothing
			}
			ts2 = System.currentTimeMillis();
		} while (ts1 == ts2);

		file.setLastModified(ts2);
	}

	/**
	 * Returns the length in bytes of a file in the directory.
	 * 
	 * @throws IOException
	 *             if the file does not exist
	 */
	@Override
	public final long fileLength(final String name) throws IOException {
		fileMapLock.readLock().lock();
		final TerracottaFile file;
		try {
			file = fileMap.get(name);
		} finally {
			fileMapLock.readLock().unlock();
		}

		if (file == null) {
			throw new FileNotFoundException(name);
		}
		return file.getLength();
	}

	/**
	 * Removes an existing file in the directory.
	 * 
	 * @throws IOException
	 *             if the file does not exist
	 */
	@Override
	public void deleteFile(final String name) throws IOException {
		fileMapLock.writeLock().lock();
		final TerracottaFile file;
		try {
			file = fileMap.remove(name);
		} finally {
			fileMapLock.writeLock().unlock();
		}
		if (file == null) {
			throw new FileNotFoundException(name);
		}
	}

	/**
	 * Renames an existing file in the directory.
	 * 
	 * @throws FileNotFoundException
	 *             if from does not exist
	 * @deprecated
	 */
	@Deprecated
	@Override
	public final void renameFile(final String from, final String to) throws IOException {
		fileMapLock.writeLock().lock();
		try {
			final TerracottaFile fromFile = fileMap.get(from);
			if (fromFile == null) {
				throw new FileNotFoundException(from);
			}
			fileMap.remove(from);
			fileMap.put(to, fromFile);
		} finally {
			fileMapLock.writeLock().unlock();
		}
	}

	/**
	 * Creates a new, empty file in the directory with the given name. Returns a
	 * stream writing this file.
	 */
	@Override
	public IndexOutput createOutput(final String name) throws IOException {
		return new TerracottaIndexOutput(this, name);
	}

	/**
	 * Returns a stream reading an existing file.
	 */
	@Override
	public IndexInput openInput(final String name) throws IOException {
		fileMapLock.readLock().lock();
		final TerracottaFile file;
		try {
			file = fileMap.get(name);
			if (file == null) {
				throw new FileNotFoundException(name);
			}
		} finally {
			fileMapLock.readLock().unlock();
		}
		return new TerracottaIndexInput(file, bufferSize);
	}

	void addFile(final String name, final TerracottaFile file) {
		fileMapLock.writeLock().lock();
		try {
			fileMap.put(name, file);
		} finally {
			fileMapLock.writeLock().unlock();
		}
	}

	int getBufferSize() {
		return bufferSize;
	}

	int getFlushRate() {
		return flushRate;
	}

	/**
	 * Closes the store to future operations, releasing associated memory.
	 */
	@Override
	public void close() {
		// don't null it since we want to keep it shared bettween tc instances.
		// fileMap = null;
	}
}


