package com.fasterxml.jackson.databind;

import java.io.Closeable;
import java.io.IOException;
import java.util.*;

import com.fasterxml.jackson.core.*;

/**
 * Iterator exposed by {@link ObjectMapper} when binding sequence of
 * objects. Extension is done to allow more convenient exposing of
 * {@link IOException} (which basic {@link Iterator} does not expose)
 */
public class MappingIterator<T> implements Iterator<T>, Closeable
{
    protected final static MappingIterator<?> EMPTY_ITERATOR =
        new MappingIterator<Object>(null, null, null, null, false, null);
    
    protected final JavaType _type;

    protected final DeserializationContext _context;
    
    protected final JsonDeserializer<T> _deserializer;

    protected JsonParser _parser;
    
    /**
     * Flag that indicates whether input {@link JsonParser} should be closed
     * when we are done or not; generally only called when caller did not
     * pass JsonParser.
     */
    protected final boolean _closeParser;

    /**
     * Flag that is set when we have determined what {@link #hasNextValue()}
     * should value; reset when {@link #nextValue} is called
     */
    protected boolean _hasNextChecked;
    
    /**
     * If not null, "value to update" instead of creating a new instance
     * for each call.
     */
    protected final T _updatedValue;

    /**
     * @deprecated Since 2.1, to be removed
     */
    @Deprecated
    protected MappingIterator(JavaType type, JsonParser jp, DeserializationContext ctxt,
            JsonDeserializer<?> deser) {
        this(type, jp, ctxt, deser, true, null);
    }

    /**
     * @param managedParser Whether we "own" the {@link JsonParser} passed or not:
     *   if true, it was created by {@link ObjectReader} and code here needs to
     *   close it; if false, it was passed by calling code and should not be
     *   closed by iterator.
     */
    @SuppressWarnings("unchecked")
    protected MappingIterator(JavaType type, JsonParser jp, DeserializationContext ctxt,
            JsonDeserializer<?> deser,
            boolean managedParser, Object valueToUpdate)
    {
        _type = type;
        _parser = jp;
        _context = ctxt;
        _deserializer = (JsonDeserializer<T>) deser;
        _closeParser = managedParser;
        if (valueToUpdate == null) {
            _updatedValue = null;
        } else {
            _updatedValue = (T) valueToUpdate;
        }

        /* Ok: one more thing; we may have to skip START_ARRAY, assuming
         * "wrapped" sequence; but this is ONLY done for 'managed' parsers
         * and never if JsonParser was directly passed by caller (if it
         * was, caller must have either positioned it over first token of
         * the first element, or cleared the START_ARRAY token explicitly).
         * Note, however, that we do not try to guess whether this could be
         * an unwrapped sequence of arrays/Lists: we just assume it is wrapped;
         * and if not, caller needs to hand us JsonParser instead, pointing to
         * the first token of the first element.
         */
        if (managedParser && jp != null && jp.getCurrentToken() == JsonToken.START_ARRAY) {
            jp.clearCurrentToken();
        }
    }

    @SuppressWarnings("unchecked")
    protected static <T> MappingIterator<T> emptyIterator() {
        return (MappingIterator<T>) EMPTY_ITERATOR;
    }
    
    /*
    /**********************************************************
    /* Basic iterator impl
    /**********************************************************
     */

    @Override
    public boolean hasNext()
    {
        try {
            return hasNextValue();
        } catch (JsonMappingException e) {
            return (Boolean) _handleMappingException(e);
        } catch (IOException e) {
            return (Boolean) _handleIOException(e);
        }
    }
    
    @Override
    public T next()
    {
        try {
            return nextValue();
        } catch (JsonMappingException e) {
            throw new RuntimeJsonMappingException(e.getMessage(), e);
        } catch (IOException e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    @Override
    public void remove() {
        throw new UnsupportedOperationException();
    }
    
    @Override
    public void close() throws IOException{
        if (_parser != null) {
            _parser.close();
        }
    }

    /*
    /**********************************************************
    /* Extended API, iteration
    /**********************************************************
     */

    /**
     * Equivalent of {@link #next} but one that may throw checked
     * exceptions from Jackson due to invalid input.
     */
    public boolean hasNextValue() throws IOException
    {
        if (_parser == null) {
            return false;
        }
        if (!_hasNextChecked) {
            JsonToken t = _parser.getCurrentToken();
            _hasNextChecked = true;
            if (t == null) { // un-initialized or cleared; find next
                t = _parser.nextToken();
                // If EOF, no more, or if we hit END_ARRAY (although we don't clear the token).
                if (t == null || t == JsonToken.END_ARRAY) {
                    JsonParser jp = _parser;
                    _parser = null;
                    if (_closeParser) {
                        jp.close();
                    }
                    return false;
                }
            }
        }
        return true;
    }
    
    public T nextValue() throws IOException
    {
        // caller should always call 'hasNext[Value]' first; but let's ensure:
        if (!_hasNextChecked) {
            if (!hasNextValue()) {
                return _throwNoSuchElement();
            }
        }
        if (_parser == null) {
            return _throwNoSuchElement();
        }
        _hasNextChecked = false;
        T result;
        
        if (_updatedValue == null) {
            result = _deserializer.deserialize(_parser, _context);
        } else{
            _deserializer.deserialize(_parser, _context, _updatedValue);
            result = _updatedValue;
        }
        // Need to consume the token too
        _parser.clearCurrentToken();
        return result;
    }

    /**
     * Convenience method for reading all entries accessible via
     * this iterator; resulting container will be a {@link java.util.ArrayList}.
     * 
     * @return List of entries read
     * 
     * @since 2.2
     */
    public List<T> readAll() throws IOException {
        return readAll(new ArrayList<T>());
    }

    /**
     * Convenience method for reading all entries accessible via
     * this iterator
     * 
     * @return List of entries read (same as passed-in argument)
     * 
     * @since 2.2
     */
    public <L extends List<? super T>> L readAll(L resultList) throws IOException
    {
        while (hasNextValue()) {
            resultList.add(nextValue());
        }
        return resultList;
    }

    /**
     * Convenience method for reading all entries accessible via
     * this iterator
     * 
     * @since 2.5
     */
    public <C extends Collection<? super T>> C readAll(C results) throws IOException
    {
        while (hasNextValue()) {
            results.add(nextValue());
        }
        return results;
    }
    
    /*
    /**********************************************************
    /* Extended API, accessors
    /**********************************************************
     */

    /**
     * Accessor for getting underlying parser this iterator uses.
     * 
     * @since 2.2
     */
    public JsonParser getParser() {
    	return _parser;
    }

    /**
     * Accessor for accessing {@link FormatSchema} that the underlying parser
     * (as per {@link #getParser}) is using, if any; only parser of schema-aware
     * formats use schemas.
     * 
     * @since 2.2
     */
    public FormatSchema getParserSchema() {
    	return _parser.getSchema();
    }

    /**
     * Convenience method, functionally equivalent to:
     *<code>
     *   iterator.getParser().getCurrentLocation()
     *</code>
     * 
     * @return Location of the input stream of the underlying parser
     * 
     * @since 2.2.1
     */
    public JsonLocation getCurrentLocation() {
        return _parser.getCurrentLocation();
    }

    /*
    /**********************************************************
    /* Helper methods
    /**********************************************************
     */

    protected <R> R _throwNoSuchElement() {
        throw new NoSuchElementException();
    }
    
    protected <R> R _handleMappingException(JsonMappingException e) {
        throw new RuntimeJsonMappingException(e.getMessage(), e);
    }

    protected <R> R _handleIOException(IOException e) {
        throw new RuntimeException(e.getMessage(), e);
    }
}
