/* Stax2 API extension for Streaming Api for Xml processing (StAX).
 *
 * Copyright (c) 2006- Tatu Saloranta, tatu.saloranta@iki.fi
 *
 * Licensed under the License specified in the file LICENSE which is
 * included with the source code.
 * You may not use this file except in compliance with the License.
 *
 * 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.codehaus.stax2.ri;

import javax.xml.stream.XMLStreamConstants;

public final class Stax2Util
    implements XMLStreamConstants
{
    private Stax2Util() { } // no instantiation

    /**
     * Method that converts given standard Stax event type into
     * textual representation.
     */
    public static String eventTypeDesc(int type)
    {
        switch (type) {
        case START_ELEMENT:
            return "START_ELEMENT";
        case END_ELEMENT:
            return "END_ELEMENT";
        case START_DOCUMENT:
            return "START_DOCUMENT";
        case END_DOCUMENT:
            return "END_DOCUMENT";

        case CHARACTERS:
            return "CHARACTERS";
        case CDATA:
            return "CDATA";
        case SPACE:
            return "SPACE";

        case COMMENT:
            return "COMMENT";
        case PROCESSING_INSTRUCTION:
            return "PROCESSING_INSTRUCTION";
        case DTD:
            return "DTD";
        case ENTITY_REFERENCE:
            return "ENTITY_REFERENCE";
        }
        return "["+type+"]";
    }                                                                                
    /**
     * Method called to trim leading and/or trailing space that given
     * lexical value has.
     *
     * @return Trimmed value if <code>lexical</code> had at least one
     *   non-space character; null otherwise
     */
    public static String trimSpaces(String lexical)
    {
        int end = lexical.length();
        int start = 0;

        while (true) {
            if (start >= end) {
                return null;
            }
            if (!_isSpace(lexical.charAt(start))) {
                break;
            }
            ++start;
        }
        // No trailing space? Either original String as is, or just trim leading
        --end;
        if (!_isSpace(lexical.charAt(end))) {
            return (start == 0) ? lexical : lexical.substring(start);
        }

        // Otherwise, at least some trailing ws...
        while (--end > start && _isSpace(lexical.charAt(end))) { }

        return lexical.substring(start, end+1);
    }

    /**
     *<p>
     * Note that it is assumed that any "weird" white space
     * (xml 1.1 LSEP and NEL) have been replaced by canonical
     * alternatives (linefeed for element content, regular space
     * for attributes)
     */
    private final static boolean _isSpace(char c)
    {
        return ((int) c) <= 0x0020;
    }

    /**
     * Helper class used to simplify text gathering while keeping
     * at as efficient as possible.
     */
    public final static class TextBuffer
    {
        private String mText = null;

        /* !!! JDK 1.5: when we can upgrade to Java 5, can convert
         *  to using <code>StringBuilder</code> instead.
         */
        private StringBuffer mBuilder = null;

        public TextBuffer() { }

        public void reset()
        {
            mText = null;
            mBuilder = null;
        }

        public void append(String text)
        {
            int len = text.length();
            if (len > 0) {
                // Any prior text?
                if (mText != null) {
                    mBuilder = new StringBuffer(mText.length() + len);
                    mBuilder.append(mText);
                    mText = null;
                }
                if (mBuilder != null) {
                    mBuilder.append(text);
                } else {
                    mText = text;
                }
            }
        }

        public String get()
        {
            if (mText != null) {
                return mText;
            }
            if (mBuilder != null) {
                return mBuilder.toString();
            }
            return "";
        }

        public boolean isEmpty() { return (mText == null) && (mBuilder == null); }
    }

    /**
     * Helper class for efficiently reading and aggregating variable length
     * byte content.
     */
    public final static class ByteAggregator
    {
        private final static byte[] NO_BYTES = new byte[0];

        /**
         * Size of the first block we will allocate.
         */
        private final static int INITIAL_BLOCK_SIZE = 500;

        /**
         * Maximum block size we will use for individual non-aggregated
         * blocks. Let's limit to using 256k chunks.
         */
        //private final static int MAX_BLOCK_SIZE = (1 << 18);

        final static int DEFAULT_BLOCK_ARRAY_SIZE = 100;

        private byte[][] mBlocks;

        private int mBlockCount;

        private int mTotalLen;

        /**
         * Reusable byte buffer block; we retain biggest one from
         * {@link #mBlocks} after aggregation.
         */
        private byte[] mSpareBlock;

        public ByteAggregator() { }

        /**
         * Method called to initialize aggregation process.
         *
         * @return Block that can be used to read in content
         */
        public byte[] startAggregation()
        {
            mTotalLen = 0;
            mBlockCount = 0;
            byte[] result = mSpareBlock;
            if (result == null) {
                result = new byte[INITIAL_BLOCK_SIZE];
            } else {
                mSpareBlock = null;
            }
            return result;
        }

        /**
         * Method used to add bufferful of data to the aggregator, and
         * get another buffer to read more data into. Returned buffer
         * is generally as big as or bigger than the given buffer, to try
         * to improve performance for larger aggregations.
         *
         * @return Buffer in which to read additional data
         */
        public byte[] addFullBlock(byte[] block)
        {
            int blockLen = block.length;

            if (mBlocks == null) {
                mBlocks = new byte[DEFAULT_BLOCK_ARRAY_SIZE][];
            } else {
                int oldLen = mBlocks.length;
                if (mBlockCount >= oldLen) {
                    byte[][] old = mBlocks;
                    mBlocks = new byte[oldLen + oldLen][];
                    System.arraycopy(old, 0, mBlocks, 0, oldLen);
                }
            }
            mBlocks[mBlockCount] = block;
            ++mBlockCount;
            mTotalLen += blockLen;

            /* Let's allocate block that's half the total size, except
             * never smaller than twice the initial block size.
             * The idea is just to grow with reasonable rate, to optimize
             * between minimal number of chunks and minimal amount of
             * wasted space.
             */
            int newSize = Math.max((mTotalLen >> 1), (INITIAL_BLOCK_SIZE + INITIAL_BLOCK_SIZE));
            return new byte[newSize];
        }

        /**
         * Method called when results are finalized and we can get the
         * full aggregated result buffer to return to the caller
         */
        public byte[] aggregateAll(byte[] lastBlock, int lastLen)
        {
            int totalLen = mTotalLen + lastLen;

            if (totalLen == 0) { // quick check: nothing aggregated?
                return NO_BYTES;
            }
            
            byte[] result = new byte[totalLen];
            int offset = 0;

            if (mBlocks != null) {
                for (int i = 0; i < mBlockCount; ++i) {
                    byte[] block = mBlocks[i];
                    int len = block.length;
                    System.arraycopy(block, 0, result, offset, len);
                    offset += len;
                }
            }
            System.arraycopy(lastBlock, 0, result, offset, lastLen);
            // can reuse the last block: should be the biggest one we've handed
            mSpareBlock = lastBlock;
            offset += lastLen;
            if (offset != totalLen) { // just a sanity check
                throw new RuntimeException("Internal error: total len assumed to be "+totalLen+", copied "+offset+" bytes");
            }
            return result;
        }
    }
}
