// This software is released into the Public Domain.  See copying.txt for details.
package org.openstreetmap.osmosis.core.store;

import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.openstreetmap.osmosis.core.OsmosisRuntimeException;
import org.openstreetmap.osmosis.core.lifecycle.Completable;


/**
 * Provides a store for writing objects to a file for later retrieval. The
 * number of objects is limited only by disk space.
 * 
 * @param <T>
 *            The object type to be stored.
 * @author Brett Henderson
 */
public class RandomAccessObjectStore<T extends Storeable> implements Completable {
	
	private static final Logger LOG = Logger.getLogger(RandomAccessObjectStore.class.getName());
	
	private ObjectSerializationFactory serializationFactory;
	private StorageStage stage;
	private String tempFilePrefix;
	private File tempFile;
	private File storageFile;
	private OffsetTrackingOutputStream offsetTrackingStream;
	private StoreClassRegister storeClassRegister;
	private ObjectWriter objectWriter;
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param serializationFactory
	 *            The factory defining the object serialisation implementation.
	 * @param tempFilePrefix
	 *            The prefix of the temporary file.
	 */
	public RandomAccessObjectStore(ObjectSerializationFactory serializationFactory, String tempFilePrefix) {
		this.serializationFactory = serializationFactory;
		this.tempFilePrefix = tempFilePrefix;
		
		storeClassRegister = new DynamicStoreClassRegister();
		
		stage = StorageStage.NotStarted;
	}
	
	
	/**
	 * Creates a new instance.
	 * 
	 * @param serializationFactory
	 *            The factory defining the object serialisation implementation.
	 * @param storageFile
	 *            The storage file to use.
	 */
	public RandomAccessObjectStore(ObjectSerializationFactory serializationFactory, File storageFile) {
		this.serializationFactory = serializationFactory;
		this.storageFile = storageFile;
		
		storeClassRegister = new DynamicStoreClassRegister();
		
		stage = StorageStage.NotStarted;
	}
	
	
	/**
	 * Initialises the output file and configures the class for adding data.
	 */
	private void initializeAddStage() {
		// We can't add if we've passed the add stage.
		if (stage.compareTo(StorageStage.Add) > 0) {
			throw new OsmosisRuntimeException("Cannot add to storage in stage " + stage + ".");
		}
		
		// If we're not up to the add stage, initialise for adding.
		if (stage.compareTo(StorageStage.Add) < 0) {
			FileOutputStream fileStream = null;
			try {
				if (storageFile == null) {
					tempFile = File.createTempFile(tempFilePrefix, null);
					storageFile = tempFile;
				} 
				
				fileStream = new FileOutputStream(storageFile);
				offsetTrackingStream = new OffsetTrackingOutputStream(new BufferedOutputStream(fileStream, 65536));
				
				// Clear reference so that the stream doesn't get closed at the end of this method.
				fileStream = null;
				
				objectWriter = serializationFactory.createObjectWriter(
					new DataOutputStoreWriter(new DataOutputStream(offsetTrackingStream)),
					storeClassRegister
				);
				
				stage = StorageStage.Add;
				
			} catch (IOException e) {
				throw new OsmosisRuntimeException(
						"Unable to create object stream writing to file " + storageFile + ".", e);
			} finally {
				if (fileStream != null) {
					try {
						fileStream.close();
					} catch (IOException e) {
						// We are already in an error condition so log and continue.
						LOG.log(Level.WARNING, "Unable to close file stream.", e);
					}
				}
			}
		}
	}
	
	
	/**
	 * Adds the specified object to the store.
	 * 
	 * @param data
	 *            The object to be added.
	 * @return The offset within the output file of the object written.
	 */
	public long add(T data) {
		long objectFileOffset;
		
		initializeAddStage();
		
		objectFileOffset = offsetTrackingStream.getByteCount();
		
		// Write the object to the store.
		objectWriter.writeObject(data);
		
		return objectFileOffset;
	}
	
	
	/**
	 * Configures the state of this object instance for reading mode. If the
	 * current state doesn't allow reading, an exception will be thrown.
	 */
	private void initializeReadingStage() {
		// If we're already in the reading stage there's nothing to do.
		if (stage.equals(StorageStage.Reading)) {
			return;
		}
		
		if (stage.equals(StorageStage.Add)) {
			throw new OsmosisRuntimeException(
					"Cannot begin reading in " + StorageStage.Add + " stage, must call complete first.");
		}
		
		// If we haven't reached the reading stage yet, configure for output
		// first to ensure a file is available for reading.
		if (stage.compareTo(StorageStage.Reading) < 0) {
			stage = StorageStage.Reading;
		}
		
		// If we've passed the reading stage, we can't continue.
		if (stage.compareTo(StorageStage.Reading) > 0) {
			throw new OsmosisRuntimeException("Cannot read from storage once we've reached stage " + stage + ".");
		}
	}
	
	
	/**
	 * Creates a new reader capable of accessing the contents of this store. The
	 * reader must be explicitly released when no longer required. Readers must
	 * be released prior to this store.
	 * 
	 * @return A store reader.
	 */
	public RandomAccessObjectStoreReader<T> createReader() {
		initializeReadingStage();
		
		try {
			BufferedRandomAccessFileInputStream randomFileReader;
			
			randomFileReader = new BufferedRandomAccessFileInputStream(storageFile);
			
			return new RandomAccessObjectStoreReader<T>(
				randomFileReader,
				serializationFactory.createObjectReader(
						new DataInputStoreReader(
								new DataInputStream(randomFileReader)), storeClassRegister)
			);
			
		} catch (FileNotFoundException e) {
			throw new OsmosisRuntimeException(
					"Unable to create object stream reading from file " + storageFile + ".", e);
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void complete() {
		// If we're already in the reading stage, we don't need to perform a
		// complete.
		if (stage.compareTo(StorageStage.Reading) != 0) {
			// We need to make sure we pass through the add stage to ensure an
			// output file is created.
			initializeAddStage();
			
			try {
				offsetTrackingStream.close();
				offsetTrackingStream = null;
				
			} catch (IOException e) {
				throw new OsmosisRuntimeException("Unable to close the file " + storageFile + ".");
			}
			
			stage = StorageStage.Reading;
		}
	}
	
	
	/**
	 * {@inheritDoc}
	 */
	public void close() {
		if (offsetTrackingStream != null) {
			try {
				offsetTrackingStream.close();
			} catch (Exception e) {
				// We cannot throw an exception within a release statement.
				LOG.log(Level.WARNING, "Unable to close offset tracking output stream.", e);
			}
			offsetTrackingStream = null;
		}
		
		if (tempFile != null) {
			if (!tempFile.delete()) {
				// We cannot throw an exception within a release statement.
				LOG.warning("Unable to delete file " + tempFile);
			}
			tempFile = null;
		}
		
		stage = StorageStage.Released;
	}
}
