package com.fasterxml.jackson.core.filter;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.BigInteger;

import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.util.JsonGeneratorDelegate;

/**
 * Specialized {@link JsonGeneratorDelegate} that allows use of
 * {@link TokenFilter} for outputting a subset of content that
 * caller tries to generate.
 * 
 * @since 2.6
 */
public class FilteringGeneratorDelegate extends JsonGeneratorDelegate
{
    /*
    /**********************************************************
    /* Configuration
    /**********************************************************
     */
    
    /**
     * Object consulted to determine whether to write parts of content generator
     * is asked to write or not.
     */
    protected TokenFilter rootFilter;

    /**
     * Flag that determines whether filtering will continue after the first
     * match is indicated or not: if `false`, output is based on just the first
     * full match (returning {@link TokenFilter#INCLUDE_ALL}) and no more
     * checks are made; if `true` then filtering will be applied as necessary
     * until end of content.
     */
    protected boolean _allowMultipleMatches;

    /**
     * Flag that determines whether path leading up to included content should
     * also be automatically included or not. If `false`, no path inclusion is
     * done and only explicitly included entries are output; if `true` then
     * path from main level down to match is also included as necessary.
     */
    protected boolean _includePath;

    /* NOTE: this feature is included in the first version (2.6), but
     * there is no public API to enable it, yet, since there isn't an
     * actual use case. But it seemed possible need could arise, which
     * is feature has not yet been removed. If no use is found within
     * first version or two, just remove.
     * 
     * Marked as deprecated since its status is uncertain.
     */
    @Deprecated
    protected boolean _includeImmediateParent;

    /*
    /**********************************************************
    /* Additional state
    /**********************************************************
     */

    /**
     * Although delegate has its own output context it is not sufficient since we actually
     * have to keep track of excluded (filtered out) structures as well as ones delegate
     * actually outputs.
     */
    protected TokenFilterContext _filterContext;

    /**
     * State that applies to the item within container, used where applicable.
     * Specifically used to pass inclusion state between property name and
     * property, and also used for array elements.
     */
    protected TokenFilter _itemFilter;
    
    /**
     * Number of tokens for which {@link TokenFilter#INCLUDE_ALL}
     * has been returned
     */
    protected int _matchCount;

    /*
    /**********************************************************
    /* Construction, initialization
    /**********************************************************
     */

    public FilteringGeneratorDelegate(JsonGenerator d, TokenFilter f,
            boolean includePath, boolean allowMultipleMatches)
    {
        // By default, do NOT delegate copy methods
        super(d, false);
        rootFilter = f;
        // and this is the currently active filter for root values
        _itemFilter = f;
        _filterContext = TokenFilterContext.createRootContext(f);
        _includePath = includePath;
        _allowMultipleMatches = allowMultipleMatches;
    }

    /*
    /**********************************************************
    /* Extended API
    /**********************************************************
     */

    public TokenFilter getFilter() { return rootFilter; }

    public JsonStreamContext getFilterContext() {
        return _filterContext;
    }
    
    /**
     * Accessor for finding number of matches, where specific token and sub-tree
     * starting (if structured type) are passed.
     */
    public int getMatchCount() {
        return _matchCount;
    }

    /*
    /**********************************************************
    /* Public API, accessors
    /**********************************************************
     */
    
    @Override
    public JsonStreamContext getOutputContext() {
        /* 11-Apr-2015, tatu: Choice is between pre- and post-filter context;
         *   let's expose post-filter context that correlates with the view
         *   of caller.
         */
        return _filterContext;
    }
    
    /*
    /**********************************************************
    /* Public API, write methods, structural
    /**********************************************************
     */
    
    @Override
    public void writeStartArray() throws IOException
    {
        // First things first: whole-sale skipping easy
        if (_itemFilter == null) {
            _filterContext = _filterContext.createChildArrayContext(null, false);
            return;
        }
        if (_itemFilter == TokenFilter.INCLUDE_ALL) { // include the whole sub-tree?
            _filterContext = _filterContext.createChildArrayContext(_itemFilter, true);
            delegate.writeStartArray();
            return;
        }
        // Ok; regular checking state then
        _itemFilter = _filterContext.checkValue(_itemFilter);
        if (_itemFilter == null) {
            _filterContext = _filterContext.createChildArrayContext(null, false);
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            _itemFilter = _itemFilter.filterStartArray();
        }
        if (_itemFilter == TokenFilter.INCLUDE_ALL) {
            _checkParentPath();
            _filterContext = _filterContext.createChildArrayContext(_itemFilter, true);
            delegate.writeStartArray();
        } else {
            _filterContext = _filterContext.createChildArrayContext(_itemFilter, false);
        }
    }
        
    @Override
    public void writeStartArray(int size) throws IOException
    {
        if (_itemFilter == null) {
            _filterContext = _filterContext.createChildArrayContext(null, false);
            return;
        }
        if (_itemFilter == TokenFilter.INCLUDE_ALL) {
            _filterContext = _filterContext.createChildArrayContext(_itemFilter, true);
            delegate.writeStartArray(size);
            return;
        }
        _itemFilter = _filterContext.checkValue(_itemFilter);
        if (_itemFilter == null) {
            _filterContext = _filterContext.createChildArrayContext(null, false);
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            _itemFilter = _itemFilter.filterStartArray();
        }
        if (_itemFilter == TokenFilter.INCLUDE_ALL) {
            _checkParentPath();
            _filterContext = _filterContext.createChildArrayContext(_itemFilter, true);
            delegate.writeStartArray(size);
        } else {
            _filterContext = _filterContext.createChildArrayContext(_itemFilter, false);
        }
    }
    
    @Override
    public void writeEndArray() throws IOException
    {
        _filterContext = _filterContext.closeArray(delegate);

        if (_filterContext != null) {
            _itemFilter = _filterContext.getFilter();
        }
    }

    @Override
    public void writeStartObject() throws IOException
    {
        if (_itemFilter == null) {
            _filterContext = _filterContext.createChildObjectContext(_itemFilter, false);
            return;
        }
        if (_itemFilter == TokenFilter.INCLUDE_ALL) {
            _filterContext = _filterContext.createChildObjectContext(_itemFilter, true);
            delegate.writeStartObject();
            return;
        }

        TokenFilter f = _filterContext.checkValue(_itemFilter);
        if (f == null) {
            return;
        }
        
        if (f != TokenFilter.INCLUDE_ALL) {
            f = f.filterStartObject();
        }
        if (f == TokenFilter.INCLUDE_ALL) {
            _checkParentPath();
            _filterContext = _filterContext.createChildObjectContext(f, true);
            delegate.writeStartObject();
        } else { // filter out
            _filterContext = _filterContext.createChildObjectContext(f, false);
        }
    }
    
    @Override
    public void writeEndObject() throws IOException
    {
        _filterContext = _filterContext.closeObject(delegate);
        if (_filterContext != null) {
            _itemFilter = _filterContext.getFilter();
        }
    }

    @Override
    public void writeFieldName(String name) throws IOException
    {
        TokenFilter state = _filterContext.setFieldName(name);
        if (state == null) {
            _itemFilter = null;
            return;
        }
        if (state == TokenFilter.INCLUDE_ALL) {
            _itemFilter = state;
            delegate.writeFieldName(name);
            return;
        }
        state = state.includeProperty(name);
        _itemFilter = state;
        if (state == TokenFilter.INCLUDE_ALL) {
            _checkPropertyParentPath();
        }
    }

    @Override
    public void writeFieldName(SerializableString name) throws IOException
    {
        TokenFilter state = _filterContext.setFieldName(name.getValue());
        if (state == null) {
            _itemFilter = null;
            return;
        }
        if (state == TokenFilter.INCLUDE_ALL) {
            _itemFilter = state;
            delegate.writeFieldName(name);
            return;
        }
        state = state.includeProperty(name.getValue());
        _itemFilter = state;
        if (state == TokenFilter.INCLUDE_ALL) {
            _checkPropertyParentPath();
        }
    }

    /*
    /**********************************************************
    /* Public API, write methods, text/String values
    /**********************************************************
     */

    @Override
    public void writeString(String value) throws IOException
    {
        if (_itemFilter == null) {
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            TokenFilter state = _filterContext.checkValue(_itemFilter);
            if (state == null) {
                return;
            }
            if (state != TokenFilter.INCLUDE_ALL) {
                if (!state.includeString(value)) {
                    return;
                }
            }
            _checkParentPath();
        } 
        delegate.writeString(value);
    }

    @Override
    public void writeString(char[] text, int offset, int len) throws IOException
    {
        if (_itemFilter == null) {
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            String value = new String(text, offset, len);
            TokenFilter state = _filterContext.checkValue(_itemFilter);
            if (state == null) {
                return;
            }
            if (state != TokenFilter.INCLUDE_ALL) {
                if (!state.includeString(value)) {
                    return;
                }
            }
            _checkParentPath();
        } 
        delegate.writeString(text, offset, len);
    }

    @Override
    public void writeString(SerializableString value) throws IOException
    {
        if (_itemFilter == null) {
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            TokenFilter state = _filterContext.checkValue(_itemFilter);
            if (state == null) {
                return;
            }
            if (state != TokenFilter.INCLUDE_ALL) {
                if (!state.includeString(value.getValue())) {
                    return;
                }
            }
            _checkParentPath();
        } 
        delegate.writeString(value);
    }

    @Override
    public void writeRawUTF8String(byte[] text, int offset, int length) throws IOException
    {
        if (_checkRawValueWrite()) {
            delegate.writeRawUTF8String(text, offset, length);
        }
    }

    @Override
    public void writeUTF8String(byte[] text, int offset, int length) throws IOException
    {
        // not exact match, but best we can do
        if (_checkRawValueWrite()) {
            delegate.writeUTF8String(text, offset, length);
        }
    }

    /*
    /**********************************************************
    /* Public API, write methods, binary/raw content
    /**********************************************************
     */

    @Override
    public void writeRaw(String text) throws IOException
    {
        if (_checkRawValueWrite()) {
            delegate.writeRaw(text);
        }
    }

    @Override
    public void writeRaw(String text, int offset, int len) throws IOException
    {
        if (_checkRawValueWrite()) {
            delegate.writeRaw(text);
        }
    }

    @Override
    public void writeRaw(SerializableString text) throws IOException
    {
        if (_checkRawValueWrite()) {
            delegate.writeRaw(text);
        }
    }

    @Override
    public void writeRaw(char[] text, int offset, int len) throws IOException
    {
        if (_checkRawValueWrite()) {
            delegate.writeRaw(text, offset, len);
        }
    }

    @Override
    public void writeRaw(char c) throws IOException
    {
        if (_checkRawValueWrite()) {
            delegate.writeRaw(c);
        }
    }

    @Override
    public void writeRawValue(String text) throws IOException
    {
        if (_checkRawValueWrite()) {
            delegate.writeRaw(text);
        }
    }

    @Override
    public void writeRawValue(String text, int offset, int len) throws IOException
    {
        if (_checkRawValueWrite()) {
            delegate.writeRaw(text, offset, len);
        }
    }

    @Override
    public void writeRawValue(char[] text, int offset, int len) throws IOException
    {
        if (_checkRawValueWrite()) {
            delegate.writeRaw(text, offset, len);
        }
    }

    @Override
    public void writeBinary(Base64Variant b64variant, byte[] data, int offset, int len) throws IOException
    {
        if (_checkBinaryWrite()) {
            delegate.writeBinary(b64variant, data, offset, len);
        }
    }

    @Override
    public int writeBinary(Base64Variant b64variant, InputStream data, int dataLength) throws IOException
    {
        if (_checkBinaryWrite()) {
            return delegate.writeBinary(b64variant, data, dataLength);
        }
        return -1;
    }

    /*
    /**********************************************************
    /* Public API, write methods, other value types
    /**********************************************************
     */

    @Override
    public void writeNumber(short v) throws IOException
    {
        if (_itemFilter == null) {
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            TokenFilter state = _filterContext.checkValue(_itemFilter);
            if (state == null) {
                return;
            }
            if (state != TokenFilter.INCLUDE_ALL) {
                if (!state.includeNumber(v)) {
                    return;
                }
            }
            _checkParentPath();
        } 
        delegate.writeNumber(v);
    }

    @Override
    public void writeNumber(int v) throws IOException
    {
        if (_itemFilter == null) {
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            TokenFilter state = _filterContext.checkValue(_itemFilter);
            if (state == null) {
                return;
            }
            if (state != TokenFilter.INCLUDE_ALL) {
                if (!state.includeNumber(v)) {
                    return;
                }
            }
            _checkParentPath();
        } 
        delegate.writeNumber(v);
    }

    @Override
    public void writeNumber(long v) throws IOException
    {
        if (_itemFilter == null) {
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            TokenFilter state = _filterContext.checkValue(_itemFilter);
            if (state == null) {
                return;
            }
            if (state != TokenFilter.INCLUDE_ALL) {
                if (!state.includeNumber(v)) {
                    return;
                }
            }
            _checkParentPath();
        } 
        delegate.writeNumber(v);
    }

    @Override
    public void writeNumber(BigInteger v) throws IOException
    {
        if (_itemFilter == null) {
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            TokenFilter state = _filterContext.checkValue(_itemFilter);
            if (state == null) {
                return;
            }
            if (state != TokenFilter.INCLUDE_ALL) {
                if (!state.includeNumber(v)) {
                    return;
                }
            }
            _checkParentPath();
        } 
        delegate.writeNumber(v);
    }

    @Override
    public void writeNumber(double v) throws IOException
    {
        if (_itemFilter == null) {
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            TokenFilter state = _filterContext.checkValue(_itemFilter);
            if (state == null) {
                return;
            }
            if (state != TokenFilter.INCLUDE_ALL) {
                if (!state.includeNumber(v)) {
                    return;
                }
            }
            _checkParentPath();
        } 
        delegate.writeNumber(v);
    }

    @Override
    public void writeNumber(float v) throws IOException
    {
        if (_itemFilter == null) {
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            TokenFilter state = _filterContext.checkValue(_itemFilter);
            if (state == null) {
                return;
            }
            if (state != TokenFilter.INCLUDE_ALL) {
                if (!state.includeNumber(v)) {
                    return;
                }
            }
            _checkParentPath();
        } 
        delegate.writeNumber(v);
    }

    @Override
    public void writeNumber(BigDecimal v) throws IOException
    {
        if (_itemFilter == null) {
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            TokenFilter state = _filterContext.checkValue(_itemFilter);
            if (state == null) {
                return;
            }
            if (state != TokenFilter.INCLUDE_ALL) {
                if (!state.includeNumber(v)) {
                    return;
                }
            }
            _checkParentPath();
        } 
        delegate.writeNumber(v);
    }

    @Override
    public void writeNumber(String encodedValue) throws IOException, UnsupportedOperationException
    {
        if (_itemFilter == null) {
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            TokenFilter state = _filterContext.checkValue(_itemFilter);
            if (state == null) {
                return;
            }
            if (state != TokenFilter.INCLUDE_ALL) {
                if (!state.includeRawValue()) { // close enough?
                    return;
                }
            }
            _checkParentPath();
        }
        delegate.writeNumber(encodedValue);
    }

    @Override
    public void writeBoolean(boolean v) throws IOException
    {
        if (_itemFilter == null) {
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            TokenFilter state = _filterContext.checkValue(_itemFilter);
            if (state == null) {
                return;
            }
            if (state != TokenFilter.INCLUDE_ALL) {
                if (!state.includeBoolean(v)) {
                    return;
                }
            }
            _checkParentPath();
        } 
        delegate.writeBoolean(v);
    }

    @Override
    public void writeNull() throws IOException
    {
        if (_itemFilter == null) {
            return;
        }
        if (_itemFilter != TokenFilter.INCLUDE_ALL) {
            TokenFilter state = _filterContext.checkValue(_itemFilter);
            if (state == null) {
                return;
            }
            if (state != TokenFilter.INCLUDE_ALL) {
                if (!state.includeNull()) {
                    return;
                }
            }
            _checkParentPath();
        } 
        delegate.writeNull();
    }

    /*
    /**********************************************************
    /* Overridden field methods
    /**********************************************************
     */

    @Override
    public void writeOmittedField(String fieldName) throws IOException {
        // Hmmh. Not sure how this would work but...
        if (_itemFilter != null) {
            delegate.writeOmittedField(fieldName);
        }
    }
    
    /*
    /**********************************************************
    /* Public API, write methods, Native Ids
    /**********************************************************
     */

    // 25-Mar-2015, tatu: These are tricky as they sort of predate actual filtering calls.
    //   Let's try to use current state as a clue at least...
    
    @Override
    public void writeObjectId(Object id) throws IOException {
        if (_itemFilter != null) {
            delegate.writeObjectId(id);
        }
    }

    @Override
    public void writeObjectRef(Object id) throws IOException {
        if (_itemFilter != null) {
            delegate.writeObjectRef(id);
        }
    }
    
    @Override
    public void writeTypeId(Object id) throws IOException {
        if (_itemFilter != null) {
            delegate.writeTypeId(id);
        }
    }

    /*
    /**********************************************************
    /* Public API, write methods, serializing Java objects
    /**********************************************************
     */

    // Base class definitions for these seems correct to me, iff not directly delegating:

    /*
    @Override
    public void writeObject(Object pojo) throws IOException,JsonProcessingException {
        if (delegateCopyMethods) {
            delegate.writeObject(pojo);
            return;
        }
        // NOTE: copied from 
        if (pojo == null) {
            writeNull();
        } else {
            if (getCodec() != null) {
                getCodec().writeValue(this, pojo);
                return;
            }
            _writeSimpleObject(pojo);
        }
    }
    
    @Override
    public void writeTree(TreeNode rootNode) throws IOException {
        if (delegateCopyMethods) {
            delegate.writeTree(rootNode);
            return;
        }
        // As with 'writeObject()', we are not check if write would work
        if (rootNode == null) {
            writeNull();
        } else {
            if (getCodec() == null) {
                throw new IllegalStateException("No ObjectCodec defined");
            }
            getCodec().writeValue(this, rootNode);
        }
    }
    */

    /*
    /**********************************************************
    /* Public API, copy-through methods
    /**********************************************************
     */

    // Base class definitions for these seems correct to me, iff not directly delegating:

    /*
    @Override
    public void copyCurrentEvent(JsonParser jp) throws IOException {
        if (delegateCopyMethods) delegate.copyCurrentEvent(jp);
        else super.copyCurrentEvent(jp);
    }

    @Override
    public void copyCurrentStructure(JsonParser jp) throws IOException {
        if (delegateCopyMethods) delegate.copyCurrentStructure(jp);
        else super.copyCurrentStructure(jp);
    }
    */

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

    protected void _checkParentPath() throws IOException
    {
        ++_matchCount;
        // only need to construct path if parent wasn't written
        if (_includePath) {
            _filterContext.writePath(delegate);
        }
        // also: if no multiple matches desired, short-cut checks
        if (!_allowMultipleMatches) {
            // Mark parents as "skip" so that further check calls are not made
            _filterContext.skipParentChecks();
        }
    }

    /**
     * Specialized variant of {@link #_checkParentPath} used when checking
     * parent for a property name to be included with value: rules are slightly
     * different.
     */
    protected void _checkPropertyParentPath() throws IOException
    {
        ++_matchCount;
        if (_includePath) {
            _filterContext.writePath(delegate);
        } else if (_includeImmediateParent) {
            // 21-Apr-2015, tatu: Note that there is no API to enable this currently...
            //    retained for speculative future use
            _filterContext.writeImmediatePath(delegate);
        }

        // also: if no multiple matches desired, short-cut checks
        if (!_allowMultipleMatches) {
            // Mark parents as "skip" so that further check calls are not made
            _filterContext.skipParentChecks();
        }
    }
    
    protected boolean _checkBinaryWrite() throws IOException
    {
        if (_itemFilter == null) {
            return false;
        }
        if (_itemFilter == TokenFilter.INCLUDE_ALL) {
            return true;
        }
        if (_itemFilter.includeBinary()) { // close enough?
            _checkParentPath();
            return true;
        }
        return false;
    }
    
    protected boolean _checkRawValueWrite() throws IOException
    {
        if (_itemFilter == null) {
            return false;
        }
        if (_itemFilter == TokenFilter.INCLUDE_ALL) {
            return true;
        }
        if (_itemFilter.includeRawValue()) { // close enough?
            _checkParentPath();
            return true;
        }
        return false;
    }
}
