/*
 * Copyright 2007-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * or in the "license" file accompanying this file. This file 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 com.amazon.ion.impl;

import static com.amazon.ion.impl._Private_IonConstants.tidDATAGRAM;
import static com.amazon.ion.impl._Private_IonConstants.tidList;
import static com.amazon.ion.impl._Private_IonConstants.tidSexp;
import static com.amazon.ion.impl._Private_IonConstants.tidStruct;
import static com.amazon.ion.impl.lite._Private_LiteDomTrampoline.reverseEncode;

import com.amazon.ion.IonException;
import com.amazon.ion.IonType;
import com.amazon.ion.SymbolTable;
import com.amazon.ion.Timestamp;
import com.amazon.ion.impl.BlockedBuffer.BlockedByteInputStream;
import com.amazon.ion.impl.IonBinary.BufferManager;
import com.amazon.ion.system.IonWriterBuilder.InitialIvmHandling;
import com.amazon.ion.system.IonWriterBuilder.IvmMinimizing;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.LinkedList;
import java.util.Queue;


final class IonWriterSystemBinary
    extends IonWriterSystem
    implements _Private_ListWriter
{

    /**
     * Implements patching of the internal OutputStream with actual value
     * lengths and types. Every instance of this class represents a container
     * (including the datagram). Information about the children of the container
     * is stored in _types/_positions/_lengths arrays.
     *
     * The patching mechanism works as follows. For every primitive value, we store
     * its type, position in internal OutputStream and the serialized lengths
     * in this class. With every value insertion, its length is then summed to the
     * length of the @_parent. Upon seeing a container, a new PatchedValue is created
     * and inserted into @_children queue, then all subsequent values are written
     * as described above. As the container is finished, its @_parent becomes active
     * and values are continued to be written. In the end, a general tree is created
     * with information about all the values in the datagram. The values themselves
     * are serialized into internal OutputStream
     *
     * To finalize the internal OutputStream into a user stream, the arrays in
     * PatchedValues are traversed from the beginning. For every value in
     * in @_types[i], it's type descriptor is written into user stream. If the type
     * is primitive, content of the internal OutputStream is copied starting
     * from @_positions[i] with length @_lengths[i]. If @_types[i] is a container,
     * a child is popped from the @_children queue and recursively written as
     * described above. Whenever TID_SYMBOL_TABLE_PATCH is seen in @_types, the
     * current symbol table gets reset with the one on the top of @_symtabs queue.
     */
    static class PatchedValues {

        private final static int DEFAULT_PATCH_COUNT    = 10;

        /** first free position in the _types/_positions/_lengths arrays */
        int _freePos;
        /** types of the container */
        int[] _types;
        /** positions where values start in buffer */
        int[] _positions;
        /**
         * lengths of the values. The high 32bits of this value store the length
         * of the field name, the low 32bits store the actual value length. We need the
         * field name length to properly adjust the total @_parent length in @endPatch
         */
        long[] _lengths;
        // the parent
        PatchedValues _parent;
        Queue<PatchedValues> _children;
        Queue<SymbolTable> _symtabs;

        PatchedValues() {
            _freePos = -1;
            _types = new int[DEFAULT_PATCH_COUNT];
            _positions = new int[DEFAULT_PATCH_COUNT];
            _lengths = new long[DEFAULT_PATCH_COUNT];
        }

        void reset() {
            _freePos = -1;
            _children = null;
            _symtabs = null;
        }

        /**
         * Add a new PatchedValues instance to _children and return it
         */
        PatchedValues addChild() {
            PatchedValues pv = new PatchedValues();
            pv._parent = this;
            if (_children == null) {
                _children = new LinkedList<PatchedValues>();
            }
            _children.add(pv);
            return pv;
        }

        /**
         * Inject a symbol table at the specified position in internal OutputStream. Most of the
         * times, the injection will happen upon seeing the first non-system symbol. At that point
         * the information about the symbol is already stored here by @startPatch call, so we need
         * to be able to inject a symtab before this value (injectBeforeCurrent)
         *
         * @param st
         * @param injectBeforeCurrent Flags if symbol table must be injected before the current value
         *                            which essentially shifts the array by 1
         * @throws IllegalStateException if the PatchedValues is not a top-level one
         */
        void injectSymbolTable(SymbolTable st, boolean injectBeforeCurrent) {
            if (_parent != null) {
                // we're not on top-level
                throw new IllegalStateException("Cannot inject a symbol table when not on top-level");
            }
            if (_symtabs == null) {
                _symtabs = new LinkedList<SymbolTable>();
            }
            ++_freePos;
            if (_freePos == _positions.length) {
                grow();
            }
            if (injectBeforeCurrent) {
                // move current value to the right
                _types[_freePos] = _types[_freePos - 1];
                _lengths[_freePos] = _lengths[_freePos - 1];
                // set previous value to symbol table type
                _types[_freePos - 1] = TID_SYMBOL_TABLE_PATCH;
                _lengths[_freePos - 1] = 0;
            } else {
                // add symbol table to the next free position as a usual value
                _types[_freePos] = TID_SYMBOL_TABLE_PATCH;
                _lengths[_freePos] = 0;
            }
            // inject the symbol table to the current position on the top
            _symtabs.add(st);
        }

        int getType() {
            return _types[_freePos];
        }

        PatchedValues getParent() {
            return _parent;
        }

        /** Start the patch for given @type at given @pos */
        void startPatch(int type, int pos) {
            ++_freePos;
            if (_freePos == _positions.length) {
                grow();
            }
            _types[_freePos] = type;
            _lengths[_freePos] = 0;
            _positions[_freePos] = pos;
        }

        /** Set the field name length as high 32bits of @_lengths item */
        void patchFieldName(int fieldNameLength) {
            _lengths[_freePos] = ((long)fieldNameLength) << 32;
        }

        /**
         * Set the value length as low 32bits of @_lengths item. The low 32bits will be
         * summed with the given value (say, it's a list that is in struct)
         * @param len
         */
        void patchValue(int len) {
            long memLen = (_lengths[_freePos] & 0xFFFFFFFF00000000L);
            long curLen = (_lengths[_freePos] & 0x00000000FFFFFFFFL);
            _lengths[_freePos] = memLen | (curLen + len);
        }

        /**
         * End this patch. If there's a @_parent available, it's @_length will be summed
         * with the length of current patched value
         */
        void endPatch() {
            if (_parent != null) {
                int memberLen = (int)(_lengths[_freePos] >> 32);
                int valueLen = (int)(_lengths[_freePos] & 0xFFFFFFFF);
                int totalLen = memberLen + valueLen;
                switch (_types[_freePos]) {
                case TID_SYMBOL_TABLE_PATCH:
                case TID_RAW:
                    break;
                case TID_ANNOTATION_PATCH:
                    totalLen += IonBinary.lenVarUInt(valueLen);
                    break;
                default:
                    // add the type if it's specified
                    ++totalLen;
                    // add actual length of @totalLen :)
                    if (valueLen >= _Private_IonConstants.lnIsVarLen) {
                        totalLen += IonBinary.lenVarUInt(valueLen);
                    }
                }
                _parent.patchValue(totalLen);
            }
        }

        // grow all related arrays
        private void grow() {
            int newSize = _positions.length * 2;
            _types = growOne(_types, newSize);
            _positions = growOne(_positions, newSize);
            _lengths = growOne(_lengths, newSize);
        }
        // grow single array and return the result
        static int[] growOne(int[] source, int newSize) {
            int[] dest = new int[newSize];
            System.arraycopy(source, 0, dest, 0, source.length);
            return dest;
        }
        // grow single array and return the result
        static long[] growOne(long[] source, int newSize) {
            long[] dest = new long[newSize];
            System.arraycopy(source, 0, dest, 0, source.length);
            return dest;
        }
    }

    // top-level patch
    PatchedValues _patch = new PatchedValues();

    private final static int TID_ANNOTATION_PATCH = _Private_IonConstants.tidDATAGRAM + 1;
    private final static int TID_SYMBOL_TABLE_PATCH = _Private_IonConstants.tidDATAGRAM + 2;
    private final static int TID_RAW = _Private_IonConstants.tidDATAGRAM + 3;

    // private static final boolean _verbose_debug = false;

    static final int UNKNOWN_LENGTH = -1;

    BufferManager     _manager;
    IonBinary.Writer  _writer;

    /** Not null */
    private final OutputStream _user_output_stream;

    /**
     * Do we {@link #flush()} after each top-level value?
     * @see #closeValue()
     */
    private final boolean _auto_flush;

    boolean           _in_struct;

    /** Ensure we don't use a closed {@link #output} stream. */
    private boolean _closed;

    private final static int TID_FOR_SYMBOL_TABLE_PATCH = _Private_IonConstants.tidDATAGRAM + 1;
    private final static int DEFAULT_PATCH_COUNT        = 10;
    private final static int DEFAULT_PATCH_DEPTH        = 10;
    private final static int NOT_A_SYMBOL_TABLE_IDX     = -1;

    // Patch:
    //          offset in data stream
    //          accumulated length    -- combine w offset in long? offset:len (allows len+=more)
    //          type of data (should this be in the data stream?
    //          patched value's parent is struct flag (low nibble in data stream?)
    //
    // the patches are the accumulated list of patch points and are
    // in position order (which is conveniently the order they are
    // encountered and created).
    int        _patch_count     = 0;
    int []     _patch_lengths   = new int[DEFAULT_PATCH_COUNT];     // TODO: should these be merged? (since array access is expensive)
    int []     _patch_offsets   = new int[DEFAULT_PATCH_COUNT];     // should patch lengths and patch offsets be longs?
    int []     _patch_table_idx = new int[DEFAULT_PATCH_COUNT];
    int []     _patch_types     = new int[DEFAULT_PATCH_COUNT];
    boolean [] _patch_in_struct = new boolean[DEFAULT_PATCH_COUNT];

    // this is only loaded by the User writer, but it is read
    // by the "get byte" operations and must be coordinated
    // with the patch list that the system writer maintains here.
    int           _patch_symbol_table_count = 0;
    SymbolTable[] _patch_symbol_tables      = new SymbolTable[DEFAULT_PATCH_COUNT];

    // the patch stack is the list of patch points that currently
    // need updating.  The value is the index into the patch arrays.
    // As a value requiring patches is closed its patch idx is removed
    // from the stack.
    int    _top;
    int [] _patch_stack = new int[DEFAULT_PATCH_DEPTH];

    /**
     * This is the depth as seen by the user.  Since there are cases where we
     * don't push onto the patch stack and cases where we push non-user
     * containers onto the patch stack we compute this separately during
     * stepIn and stepOut.
     */
    private int _user_depth;


    /**
     * @param out OutputStream the users output byte stream, if specified
     * @param autoFlush when true the writer flushes to the output stream
     *  between top level values
     * @param ensureInitialIvm when true, an initial IVM will be emitted even
     *  when the user doesn't explicitly write one. When false, an initial IVM
     *  won't be emitted unless the user does it. That can result in an invalid
     *  Ion stream if not used carefully.
     * @throws NullPointerException if any parameter is null.
     */
    IonWriterSystemBinary(SymbolTable defaultSystemSymtab,
                          OutputStream out,
                          boolean autoFlush,
                          boolean ensureInitialIvm)
    {
        super(defaultSystemSymtab,
              (ensureInitialIvm ? InitialIvmHandling.ENSURE : null),
              IvmMinimizing.ADJACENT);

        out.getClass(); // Efficient null check
        _user_output_stream = out;

        // the buffer manager and writer
        // are used to hold the buffered
        // binary values pending flush().
        _manager = new BufferManager();
        _writer = _manager.openWriter();
        _auto_flush = autoFlush;
    }

    /**
     * Empty our buffers, assuming it is safe to do so.
     * This is called by {@link #flush()} and {@link #finish()}.
     */
    private void writeAllBufferedData()
        throws IOException
    {
        writeBytes(_user_output_stream);

        clearFieldName();
        clearAnnotations();

        _in_struct = false;
        _patch_count = 0;
        _patch_symbol_table_count = 0;
        _top = 0;
        try {
            _writer.setPosition(0);
            _writer.truncate();
        }
        catch (IOException e) {
            throw new IonException(e);
        }
    }

    @Override
    public void finish() throws IOException
    {
        if (getDepth() != 0) {
            throw new IllegalStateException(ERROR_FINISH_NOT_AT_TOP_LEVEL);
        }

        writeAllBufferedData();
        super.finish();
    }

    final OutputStream getOutputStream()
    {
        return _user_output_stream;
    }

    public final boolean isInStruct()
    {
        return _in_struct;
    }

    private final boolean topInStruct() {
        if (_top == 0) return false;
        boolean in_struct = _patch_in_struct[_patch_stack[_top - 1]];
        return in_struct;
    }
    protected final boolean atDatagramLevel()
    {
        return (topType() == _Private_IonConstants.tidDATAGRAM);
//        return is_datagram;
    }

    @Override
    public final int getDepth()
    {
        return _user_depth;
    }

    protected final IonType getContainer()
    {
        IonType type;
        int tid = parentType();
        switch (tid) {
        case tidList:
            type = IonType.LIST;
            break;
        case tidSexp:
            type = IonType.SEXP;
            break;
        case tidStruct:
            type = IonType.STRUCT;
            break;
        case tidDATAGRAM:
            type = IonType.DATAGRAM;
            break;
        default:
            throw new IonException("unexpected parent type "+tid+" does not represent a container");
        }
        return type;
    }

    @Override
    final SymbolTable inject_local_symbol_table() throws IOException
    {
        SymbolTable symbols = super.inject_local_symbol_table();
        PatchedValues top;
        // find the parent
        for (top = _patch; top.getParent() != null; top = top.getParent()) {}
        // inject the symbol table; if @_patch is not the top element, then inject
        // the symtab before this top-level value begins
        super.startValue();
        top.injectSymbolTable(symbols, _patch.getParent() != null);
        super.endValue();
        return symbols;
    }

    private final int topLength() {
        return _patch_lengths[_patch_stack[_top - 1]];
    }

    private final int topType() {
        if (_top == 0) return _Private_IonConstants.tidDATAGRAM;
        return _patch_types[_patch_stack[_top - 1]];
    }

    private final int parentType() {
        int ii = _top - 2;
        while (ii >= 0) {
            int type = _patch_types[_patch_stack[ii]];
            if (type != _Private_IonConstants.tidTypedecl) return type;
            ii--;
        }
        return _Private_IonConstants.tidDATAGRAM;
    }

    private final void startValue(int value_type)
        throws IOException
    {
        super.startValue();

        int[] sids = null;
        int sid_count = annotationCount();
        if (sid_count > 0) {
            // prepare the SIDs of annotations before doing the patch as this may
            // fail and leave the Writer in undefined state
            sids = super.internAnnotationsAndGetSids();
            _patch.startPatch(_Private_IonConstants.tidTypedecl, _writer.position());
        } else {
            _patch.startPatch(value_type, _writer.position());
        }
        // write field name
        if (_in_struct) {
            if (!isFieldNameSet()) {
                throw new IllegalStateException(ERROR_MISSING_FIELD_NAME);
            }
            int sid = super.getFieldId();
            if (sid < 0) {
                throw new UnsupportedOperationException("symbol resolution must be handled by the user writer");
            }
            int fieldNameLength = _writer.writeVarUIntValue(sid, true);
            _patch.patchFieldName(fieldNameLength);
            clearFieldName();
        }

        // write annotations
        if (sid_count > 0) {
            _patch = _patch.addChild();
            // add all annotations as if it's a value with type = -1
            _patch.startPatch(TID_ANNOTATION_PATCH, _writer.position());
            int len = 0;
            for (int ii=0; ii<sid_count; ii++) {
                len += _writer.writeVarUIntValue(sids[ii], true);
            }
            _patch.patchValue(len);
            _patch.endPatch();
            clearAnnotations();
            // add the actual value now
            _patch.startPatch(value_type, _writer.position());
        }
    }

    private final void closeValue()
        throws IOException
    {
        super.endValue();
        _patch.endPatch();
        if (_patch.getParent() != null
            && _patch.getParent().getType() == _Private_IonConstants.tidTypedecl)
        {
            // this is an annotated value, gotta get out
            _patch = _patch.getParent();
            _patch.endPatch();
            assert _patch != null;
        }
    }


    /**
     * {@inheritDoc}
     * <p>
     * The {@link OutputStream} spec is mum regarding the behavior of flush on
     * a closed stream, so we shouldn't assume that our stream can handle that.
     */
    public final void flush() throws IOException
    {
        if (! _closed)
        {
            if (atDatagramLevel() && ! hasAnnotations())
            {
                SymbolTable symtab = getSymbolTable();

                if (symtab != null &&
                    symtab.isReadOnly() &&
                    symtab.isLocalTable())
                {
                    // It's no longer possible to add more symbols to the local
                    // symtab, so we can safely write everything out.
                    writeAllBufferedData();
                }
            }

            _user_output_stream.flush();
        }
    }

    public final void close() throws IOException
    {
        if (! _closed) {
            try
            {
                if (getDepth() == 0) {
                    finish();
                }
            }
            finally
            {
                // Do this first so we are closed even if the call below throws.
                _closed = true;

                _user_output_stream.close();
            }
        }
    }



    @Override
    void writeIonVersionMarkerAsIs(SymbolTable systemSymtab)
        throws IOException
    {
        if (_user_depth != 0) {
            throw new IllegalStateException("IVM not on top-level");
        }
        super.startValue();
        _patch.startPatch(TID_RAW, _writer.position());
        _patch.patchValue(4);
        _writer.write(_Private_IonConstants.BINARY_VERSION_MARKER_1_0);
        _patch.endPatch();
        super.endValue();
    }

    @Override
    void writeLocalSymtab(SymbolTable symbols)
        throws IOException
    {
        // this method *should* be called when @_patch is a top-level value, but
        // we cannot be sure, so try to find the top-level anyway
        PatchedValues top;
        // find the parent
        for (top = _patch; top.getParent() != null; top = top.getParent()) {}
        super.startValue();
        top.injectSymbolTable(symbols, _patch.getParent() != null);
        super.endValue();
        super.writeLocalSymtab(symbols);
    }

    public final void stepIn(IonType containerType) throws IOException
    {
        int tid;
        switch (containerType)
        {
            case LIST:   tid = tidList;   break;
            case SEXP:   tid = tidSexp;   break;
            case STRUCT: tid = tidStruct; break;
            default:
                throw new IllegalArgumentException();
        }
        startValue(tid);
        _patch = _patch.addChild();
        _in_struct = (tid == tidStruct);
        ++_user_depth;
    }

    public final void stepOut() throws IOException
    {
        if (_patch.getParent() == null) {
            throw new IllegalStateException(IonMessages.CANNOT_STEP_OUT);
        }
        // container is over, getting out
        _patch = _patch.getParent();
        // now close current value
        closeValue();
        if (_patch.getParent() == null) {
            _in_struct = false;
            // we're on top-level
            if (_auto_flush) {
                flush();
            }
        } else {
            _in_struct = (_patch.getParent().getType() == _Private_IonConstants.tidStruct);
        }
        --_user_depth;
    }


    public void writeNull(IonType type) throws IOException
    {
        int tid;
        switch (type) {
        case NULL:      tid = _Private_IonConstants.tidNull;      break;
        case BOOL:      tid = _Private_IonConstants.tidBoolean;   break;
        case INT:       tid = _Private_IonConstants.tidPosInt;    break;
        case FLOAT:     tid = _Private_IonConstants.tidFloat;     break;
        case DECIMAL:   tid = _Private_IonConstants.tidDecimal;   break;
        case TIMESTAMP: tid = _Private_IonConstants.tidTimestamp; break;
        case SYMBOL:    tid = _Private_IonConstants.tidSymbol;    break;
        case STRING:    tid = _Private_IonConstants.tidString;    break;
        case BLOB:      tid = _Private_IonConstants.tidBlob;      break;
        case CLOB:      tid = _Private_IonConstants.tidClob;      break;
        case SEXP:      tid = _Private_IonConstants.tidSexp;      break;
        case LIST:      tid = _Private_IonConstants.tidList;      break;
        case STRUCT:    tid = _Private_IonConstants.tidStruct;    break;
        default:
            throw new IllegalArgumentException("Invalid type: " + type);
        }
        startValue(TID_RAW);
        _writer.write((tid << 4) | _Private_IonConstants.lnIsNull);
        _patch.patchValue(1);
        closeValue();
    }

    public void writeBool(boolean value) throws IOException
    {
        int ln = value ? _Private_IonConstants.lnBooleanTrue : _Private_IonConstants.lnBooleanFalse;
        startValue(TID_RAW);
        _writer.write((_Private_IonConstants.tidBoolean << 4) | ln);
        _patch.patchValue(1);
        closeValue();
    }
    public void writeInt(long value) throws IOException
    {
        int len;
        if (value < 0) {
            startValue(_Private_IonConstants.tidNegInt);
            len = _writer.writeUIntValue(-value);
        } else {
            startValue(_Private_IonConstants.tidPosInt);
            len = _writer.writeUIntValue(value);
        }
        _patch.patchValue(len);
        closeValue();
    }

    public void writeInt(BigInteger value) throws IOException
    {
        if (value == null) {
            writeNull(IonType.INT);
            return;
        }

        boolean     is_negative = (value.signum() < 0);
        BigInteger  positive = value;

        if (is_negative) {
            positive = value.negate();
        }

        int len = IonBinary.lenIonInt(positive);
        startValue(is_negative ? _Private_IonConstants.tidNegInt : _Private_IonConstants.tidPosInt);
        _writer.writeUIntValue(positive, len);
        _patch.patchValue(len);

        closeValue();
    }

    public void writeFloat(double value) throws IOException
    {
        int len = IonBinary.lenIonFloat(value);
        startValue(_Private_IonConstants.tidFloat); // int's are always less than varlen long
        len = _writer.writeFloatValue(value);
        _patch.patchValue(len);
        closeValue();
    }

    @Override
    public void writeDecimal(BigDecimal value) throws IOException
    {
        if (value == null) {
            writeNull(IonType.DECIMAL);
            return;
        }
        startValue(_Private_IonConstants.tidDecimal);
        int len = _writer.writeDecimalContent(value);
        _patch.patchValue(len);
        closeValue();
    }

    public void writeTimestamp(Timestamp value) throws IOException
    {
        if (value == null) {
            writeNull(IonType.TIMESTAMP);
            return;
        }
        startValue(_Private_IonConstants.tidTimestamp);
        int len = _writer.writeTimestamp(value);
        _patch.patchValue(len);
        closeValue();
    }

    public void writeString(String value) throws IOException
    {
        if (value == null) {
            writeNull(IonType.STRING);
            return;
        }
        startValue(_Private_IonConstants.tidString);
        int len = _writer.writeStringData(value);
        _patch.patchValue(len);
        closeValue();
    }

    @Override
    void writeSymbolAsIs(int symbolId) throws IOException
    {
        startValue(_Private_IonConstants.tidSymbol);
        int len = _writer.writeUIntValue(symbolId);
        _patch.patchValue(len);
        closeValue();
    }

    @Override
    public void writeSymbolAsIs(String value) throws IOException
    {
        if (value == null) {
            writeNull(IonType.SYMBOL);
            return;
        }
        int sid = add_symbol(value);
        writeSymbolAsIs(sid);
    }

    public void writeClob(byte[] value, int start, int len) throws IOException
    {
        if (value == null) {
            writeNull(IonType.CLOB);
            return;
        }
        if (start < 0 || len < 0 || start+len > value.length) {
            throw new IllegalArgumentException("the start and len must be contained in the byte array");
        }
        startValue(_Private_IonConstants.tidClob);
        _writer.write(value, start, len);
        _patch.patchValue(len);
        closeValue();
    }

    public void writeBlob(byte[] value, int start, int len) throws IOException
    {
        if (value == null) {
            writeNull(IonType.BLOB);
            return;
        }
        if (start < 0 || len < 0 || start+len > value.length) {
            throw new IllegalArgumentException("the start and len must be contained in the byte array");
        }
        startValue(_Private_IonConstants.tidBlob);
        _writer.write(value, start, len);
        _patch.patchValue(len);
        closeValue();
    }

    // just transfer the bytes into the current patch as 'proper' ion binary serialization
    public void writeRaw(byte[] value, int start, int len) throws IOException
    {
        startValue(TID_RAW);
        _writer.write(value, start, len);
        _patch.patchValue(len);
        closeValue();
    }

    public void writeBoolList(boolean[] values) throws IOException
    {
        stepIn(IonType.LIST);
        for (boolean b : values) {
            writeBool(b);
        }
        stepOut();
    }

    public void writeIntList(byte[] values) throws IOException
    {
        stepIn(IonType.LIST);
        for (byte b : values) {
            writeInt(b);
        }
        stepOut();
    }

    public void writeIntList(short[] values) throws IOException
    {
        stepIn(IonType.LIST);
        for (short s : values) {
            writeInt(s);
        }
        stepOut();
    }

    public void writeIntList(int[] values) throws IOException
    {
        stepIn(IonType.LIST);
        for (int i : values) {
            writeInt(i);
        }
        stepOut();
    }

    public void writeIntList(long[] values) throws IOException
    {
        stepIn(IonType.LIST);
        for (long l : values) {
            writeInt(l);
        }
        stepOut();
    }

    public void writeFloatList(float[] values) throws IOException
    {
        stepIn(IonType.LIST);
        for (float f : values) {
            writeFloat(f);
        }
        stepOut();
    }

    public void writeFloatList(double[] values) throws IOException
    {
        stepIn(IonType.LIST);
        for (double d : values) {
            writeFloat(d);
        }
        stepOut();
    }

    public void writeStringList(String[] values) throws IOException
    {
        stepIn(IonType.LIST);
        for (String s : values) {
            writeString(s);
        }
        stepOut();
    }

    // TODO make private after IonBinaryWriter is removed
    /**
     * Writes everything we've got into the output stream, performing all
     * necessary patches along the way.
     *
     * This implements {@link com.amazon.ion.IonBinaryWriter#writeBytes(OutputStream)}
     * via our subclass {@link IonWriterBinaryCompatibility.System}.
     */
    int writeBytes(OutputStream userstream) throws IOException {
        if (_patch.getParent() != null) {
            throw new IllegalStateException("Tried to flush while not on top-level");
        }

        try {
            BlockedByteInputStream datastream =
                new BlockedByteInputStream(_manager.buffer());
            int size = writeRecursive(datastream, userstream, _patch);
            return size;
        } finally {
            _patch.reset();
        }
    }

    int writeRecursive(BlockedByteInputStream datastream, OutputStream userstream, PatchedValues p) throws IOException {
        int totalSize = 0;
        for (int i = 0; i <= p._freePos; ++i) {
            int type = p._types[i];
            int pos = p._positions[i];
            int fnlen = (int)(p._lengths[i] >> 32);
            int vallen = (int)(p._lengths[i] & 0xFFFFFFFF);
            if (p.getParent() == null) {
                if (pos > totalSize) {
                    // write whatever data that we have in the datastream (eg external data)
                    datastream.writeTo(userstream, pos - totalSize);
                    totalSize = pos;
                }
                totalSize += fnlen + vallen;
            }
            // write member name
            if (fnlen > 0) {
                datastream.writeTo(userstream, fnlen);
            }
            switch (type) {
            case TID_ANNOTATION_PATCH:
                IonBinary.writeVarUInt(userstream, vallen);
                datastream.writeTo(userstream, vallen);
                break;

            case TID_SYMBOL_TABLE_PATCH:
                SymbolTable symtab = p._symtabs.remove();
                if (!symtab.isSystemTable()) {
                    byte[] symtabBytes = reverseEncode(1024, symtab);
                    userstream.write(symtabBytes);
                    totalSize += symtabBytes.length;
                }
                break;

            case TID_RAW:
                datastream.writeTo(userstream, vallen);
                break;

            default:
                // write type
                if (vallen >= _Private_IonConstants.lnIsVarLen) {
                    int typeByte = (type << 4) | _Private_IonConstants.lnIsVarLen;
                    userstream.write(typeByte);
                    IonBinary.writeVarUInt(userstream, vallen);
                } else {
                    int typeByte = (type << 4) | vallen;
                    userstream.write(typeByte);
                }
                switch (type) {
                case _Private_IonConstants.tidList:
                case _Private_IonConstants.tidSexp:
                case _Private_IonConstants.tidStruct:
                case _Private_IonConstants.tidTypedecl:
                    // write the container
                    assert p._children != null;
                    writeRecursive(datastream, userstream, p._children.remove());
                    break;

                default:
                    datastream.writeTo(userstream, vallen);
                }
            }
        }
        return totalSize;
    }

    static class CountingStream extends OutputStream
    {
        private final OutputStream _wrapped;
        private       int          _written;

        CountingStream(OutputStream userstream) {
            _wrapped = userstream;
        }

        public int getBytesWritten() {
            return _written;
        }

        @Override
        public void write(int b) throws IOException
        {
            _wrapped.write(b);
            _written++;
        }
        @Override
        public void write(byte[] bytes) throws IOException
        {
            _wrapped.write(bytes);
            _written += bytes.length;
        }
        @Override
        public void write(byte[] bytes, int off, int len) throws IOException
        {
            _wrapped.write(bytes, off, len);
            _written += len;
        }

    }

    protected int write_symbol_table(OutputStream userstream,
                                     SymbolTable symtab) throws IOException
    {
        CountingStream cs = new CountingStream(userstream);
        // TODO this is assuming the symtab needed here, broken for open content.
        IonWriterSystemBinary writer =
            new IonWriterSystemBinary(_default_system_symbol_table,
                                      cs,
                                      false /* autoflush */ ,
                                      false /* ensureInitialIvm */);
        symtab.writeTo(writer);
        writer.finish();
        int symtab_len = cs.getBytesWritten();
        return symtab_len;
    }

    protected int XXX_get_pending_length_with_no_symbol_tables()
    {
        int buffer_length = _manager.buffer().size();
        int patch_amount = 0;

        for (int patch_idx = 0; patch_idx < _patch_count; patch_idx ++) {
            // int vlen = _patch_list[patch_idx + IonBinaryWriter.POSITION_OFFSET];
            int vlen = _patch_lengths[patch_idx];
            if (vlen >= _Private_IonConstants.lnIsVarLen) {
                int ln = IonBinary.lenVarUInt(vlen);
                patch_amount += ln;
            }
        }

        int symbol_table_length = 0;

        int total_length = 0;
        total_length += buffer_length
                     +  patch_amount
                     +  symbol_table_length;

        return total_length;
    }
}