/*
 * Copyright 2006-2010 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 org.springframework.batch.item.support;

import java.util.Map.Entry;

import org.springframework.batch.item.ExecutionContext;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemStream;
import org.springframework.batch.item.ItemStreamException;
import org.springframework.batch.item.ItemStreamReader;
import org.springframework.batch.item.ParseException;
import org.springframework.batch.item.PeekableItemReader;
import org.springframework.batch.item.UnexpectedInputException;

/**
 * <p>
 * A {@link PeekableItemReader} that allows the user to peek one item ahead.
 * Repeated calls to {@link #peek()} will return the same item, and this will be
 * the next item returned from {@link #read()}.
 * </p>
 * 
 * <p>
 * Intentionally not thread safe: it wouldn't be possible to honour the peek in
 * multiple threads because only one of the threads that peeked would get that
 * item in the next call to read.
 * </p>
 * 
 * @author Dave Syer
 * 
 */
public class SingleItemPeekableItemReader<T> implements ItemStreamReader<T>, PeekableItemReader<T> {

	private ItemReader<T> delegate;

	private T next;

	private ExecutionContext executionContext = new ExecutionContext();

	/**
	 * The item reader to use as a delegate. Items are read from the delegate
	 * and passed to the caller in {@link #read()}.
	 * 
	 * @param delegate the delegate to set
	 */
	public void setDelegate(ItemReader<T> delegate) {
		this.delegate = delegate;
	}

	/**
	 * Get the next item from the delegate (whether or not it has already been
	 * peeked at).
	 * 
	 * @see ItemReader#read()
	 */
    @Override
	public T read() throws Exception, UnexpectedInputException, ParseException {
		if (next != null) {
			T item = next;
			next = null;
			// executionContext = new ExecutionContext();
			return item;
		}
		return delegate.read();
	}

	/**
	 * Peek at the next item, ensuring that if the delegate is an
	 * {@link ItemStream} the state is stored for the next call to
	 * {@link #update(ExecutionContext)}.
	 * 
	 * @return the next item (or null if there is none).
	 * 
	 * @see PeekableItemReader#peek()
	 */
    @Override
	public T peek() throws Exception, UnexpectedInputException, ParseException {
		if (next == null) {
			updateDelegate(executionContext);
			next = delegate.read();
		}
		return next;
	}

	/**
	 * If the delegate is an {@link ItemStream}, just pass the call on,
	 * otherwise reset the peek cache.
	 * 
	 * @throws ItemStreamException if there is a problem
	 * @see ItemStream#close()
	 */
    @Override
	public void close() throws ItemStreamException {
		next = null;
		if (delegate instanceof ItemStream) {
			((ItemStream) delegate).close();
		}
		executionContext = new ExecutionContext();
	}

	/**
	 * If the delegate is an {@link ItemStream}, just pass the call on,
	 * otherwise reset the peek cache.
	 * 
	 * @param executionContext the current context
	 * @throws ItemStreamException if there is a problem
	 * @see ItemStream#open(ExecutionContext)
	 */
    @Override
	public void open(ExecutionContext executionContext) throws ItemStreamException {
		next = null;
		if (delegate instanceof ItemStream) {
			((ItemStream) delegate).open(executionContext);
		}
		executionContext = new ExecutionContext();
	}

	/**
	 * If there is a cached peek, then retrieve the execution context state from
	 * that point. If there is no peek cached, then call directly to the
	 * delegate.
	 * 
	 * @param executionContext the current context
	 * @throws ItemStreamException if there is a problem
	 * @see ItemStream#update(ExecutionContext)
	 */
    @Override
	public void update(ExecutionContext executionContext) throws ItemStreamException {
		if (next != null) {
			// Get the last state from the delegate instead of using
			// current value.
			for (Entry<String, Object> entry : this.executionContext.entrySet()) {
				executionContext.put(entry.getKey(), entry.getValue());
			}
			return;
		}
		updateDelegate(executionContext);
	}

	private void updateDelegate(ExecutionContext executionContext) {
		if (delegate instanceof ItemStream) {
			((ItemStream) delegate).update(executionContext);
		}
	}

}
