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

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.util.CharsetUtil;
import org.openstreetmap.osmosis.core.OsmosisRuntimeException;
import org.openstreetmap.osmosis.core.lifecycle.Closeable;


/**
 * Provides the ability to read data that has been broken into data chunks. Each
 * chunk is preceded by the number of bytes in the chunk followed by a carriage
 * return line feed pair.
 * 
 * @author Brett Henderson
 */
public class ChunkedDataReceiver implements Closeable {

	private static final Logger LOG = Logger.getLogger(ChunkedDataReceiver.class.getName());

	private ChannelBuffer buffer;
	private File tmpDataFile;
	private FileChannel tmpDataChannel;
	private List<File> readyFiles;
	private boolean chunkInProgress;
	private long bytesRemaining;


	/**
	 * Creates a new instance.
	 */
	public ChunkedDataReceiver() {
		buffer = ChannelBuffers.dynamicBuffer();
		
		readyFiles = new ArrayList<File>();
		chunkInProgress = false;
	}


	/**
	 * Attempts to read a chunk length header from the data currently in the
	 * buffer. If a carriage return line feed pair is found, then the data
	 * preceeding those characters will be converted to a number and returned.
	 * If the carriage return line feed pair cannot be found, then -1 will be
	 * returned to indicate that the data is not yet complete.
	 * 
	 * @return The value of the chunk length header, or -1 if more data is
	 *         required.
	 */
	private long getChunkLength() {
		// Look for a carriage return line feed pair.
		for (int i = buffer.readerIndex() + 1; i < buffer.writerIndex(); i++) {
			if (buffer.getByte(i) == 0x0A) {
				if (buffer.getByte(i - 1) == 0x0D) {
					// All data between the reader index and one before i is the header.
					String chunkSizeString = buffer.toString(buffer.readerIndex(), i - buffer.readerIndex() - 1,
							CharsetUtil.UTF_8);
					long chunkSize = Long.parseLong(chunkSizeString);

					// Move the buffer past the current reader index.
					buffer.readerIndex(i + 1);

					return chunkSize;
				}
			}
		}

		return -1;
	}


	private void initializeChunk() {
		try {
			tmpDataFile = File.createTempFile("change", ".tmp");
		} catch (IOException e) {
			throw new OsmosisRuntimeException("Unable to create replication data temp file", e);
		}
		try {
			tmpDataChannel = new FileOutputStream(tmpDataFile).getChannel();
		} catch (FileNotFoundException e) {
			throw new OsmosisRuntimeException("Unable to open chunk data temp file", e);
		}
		
		chunkInProgress = true;
	}


	private void writeToChunk(ChannelBuffer writeBuffer) {
		try {
			// We can only write the minimum of the number of bytes available
			// and the number of bytes remaining. The bytes available is an
			// integer so if all values are possible the minimum value will also
			// be an integer.
			int bytesToWrite = (int) Math.min(writeBuffer.readableBytes(), bytesRemaining);
			
			// Write the data to the chunk data file.
			tmpDataChannel.write(writeBuffer.toByteBuffer(writeBuffer.readerIndex(), bytesToWrite));
			writeBuffer.skipBytes(bytesToWrite);
			
			bytesRemaining -= bytesToWrite;

		} catch (IOException e) {
			throw new OsmosisRuntimeException("Unable to write chunk data to temp file", e);
		}
		
		// Complete the chunk if it is complete.
		if (bytesRemaining <= 0) {
			try {
				tmpDataChannel.close();
			} catch (IOException e) {
				throw new OsmosisRuntimeException("Unable to close chunk data temp file", e);
			}

			readyFiles.add(tmpDataFile);
			tmpDataFile = null;
			chunkInProgress = false;
		}
	}
	
	
	private List<File> createResultFileList() {
		List<File> resultFiles = new ArrayList<File>(readyFiles);
		readyFiles.clear();
		
		return resultFiles;
	}


	/**
	 * Processes the data in the input buffer. It requires each chunk of data to
	 * be preceeded by a length header. Once all data within each chunk has been
	 * read, a file will be produced which holds the contents of the chunk. When
	 * no more data is available, all files resulting from the input data will
	 * be returned. It is possible that 0 files will be returned. Any leftover
	 * data not comprising a complete chunk will be retained internally until
	 * the next call.
	 * 
	 * @param inputBuffer
	 *            Add data in this buffer will be added.
	 * @return All chunk data files completed during this call.
	 */
	public List<File> processData(ChannelBuffer inputBuffer) {
		while (inputBuffer.readableBytes() > 0 || buffer.readableBytes() > 0) {
			// If there is no chunk in progress so the next data will be the
			// header.
			if (!chunkInProgress) {
				
				// Move all input data to our internal buffer so that we have a
				// single view of all available data.
				buffer.writeBytes(inputBuffer);
				bytesRemaining = getChunkLength();
				
				if (bytesRemaining >= 0) {
					// We have read the chunk header, so now begin processing the
					// chunk body.
					initializeChunk();
				} else {
					return createResultFileList();
				}
			}
			
			// If we've reached this far we know a chunk is in progress so we
			// must write to the data file. We write from the internal buffer if
			// available, otherwise the input buffer.
			if (buffer.readableBytes() > 0) {
				writeToChunk(buffer);
			} else {
				writeToChunk(inputBuffer);
			}
		}
		
		return createResultFileList();
	}


	/**
	 * Gets the buffer.
	 * 
	 * @return The buffer.
	 */
	public ChannelBuffer getBuffer() {
		return buffer;
	}


	@Override
	public void close() {
		if (tmpDataChannel != null) {
			try {
				tmpDataChannel.close();
			} catch (IOException ex) {
				LOG.log(Level.WARNING, "Unable to close the current temporary chunk file", ex);
			}
		}
		
		if (tmpDataFile != null) {
			if (!tmpDataFile.delete()) {
				LOG.log(Level.WARNING, "Unable to delete the current temporary chunk file " + tmpDataFile);
			}
		}
		
		for (File readyFile : readyFiles) {
			if (!readyFile.delete()) {
				LOG.log(Level.WARNING, "Unable to delete the current temporary chunk file " + readyFile);
			}
		}
		readyFiles.clear();
	}
}
