/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.apache.johnzon.core;

import javax.json.JsonException;
import javax.json.stream.JsonLocation;
import javax.json.stream.JsonParsingException;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;

//This class represents either the Json tokenizer and the Json parser.
public class JsonStreamParserImpl extends JohnzonJsonParserImpl implements JsonChars {
    private final boolean autoAdjust;

    //the main buffer where the stream will be buffered
    private final char[] buffer;

    //current parser position within the buffer
    //Initial MIN_VALUE will trigger buffer refill, normally bufferPos is >= -1
    //-1 would cause a re-read of the first character in the buffer (which is at zero index)
    private int bufferPos = Integer.MIN_VALUE;

    // performance optimisation to avoid subtraction on readNextChar
    private int bufferLeft = 0;

    //available character in the buffer. It might be <= "buffer.length".
    private int availableCharsInBuffer;

    //start and end position of values in the buffer
    //may cross boundaries, then value is in fallBackCopyBuffer
    private int startOfValueInBuffer = -1;
    private int endOfValueInBuffer = -1;

    private final Reader in;

    //do we read from a character stream or a byte stream
    //not used at the moment but maybe relevant in future to calculate the JsonLocation offset
    @SuppressWarnings("unused")
    private final BufferStrategy.BufferProvider<char[]> bufferProvider;
    private final BufferStrategy.BufferProvider<char[]> valueProvider;

    //max length for strings and numbers (max count of characters)
    private final int maxValueLength;

    //we use a byte here, because comparing bytes
    //is more efficient than comparing enums
    //Additionally we handle internally two more event: COMMA_EVENT and KEY_SEPARATOR_EVENT
    private byte previousEvent = -1;

    //this buffer is used to store current String or Number value in case that
    //within the value a buffer boundary is crossed or the string contains escaped characters
    private char[] fallBackCopyBuffer;
    private boolean releaseFallBackCopyBuffer = true;
    private int fallBackCopyBufferLength;
    // when boundaries of fallBackCopyBuffer have been reached
    private List<Buffer> previousFallBackCopyBuffers;

    // location (line, column, offset)
    // We try to calculate this efficiently so we do not just increment the values per char read
    // Instead we calculate the column and offset relative to the pastBufferReadCount and/or lastLineBreakPosition.
    private long currentLine = 1;
    private long lastLineBreakPosition;
    private long pastBufferReadCount;

    //cache (if current value is a number) integral state and the number itself if its only one digit    
    private boolean isCurrentNumberIntegral = true;
    private int currentIntegralNumber = Integer.MIN_VALUE; //for number from 0 - 9

    //maybe we want also cache BigDecimals
    //private BigDecimal currentBigDecimalNumber = null;

    //We need a stack if we want detect bad formatted Json do determine if we are within an array or not
    //example
    //     Streamparser sees: ],1  <-- we look from here
    //the 1 is only allowed if we are within an array
    //This can only be determined by build up a stack which tracks the trail of Json objects and arrays
    //This stack here is only needed for validating the above mentioned case, if we want to be lenient we can skip suing the stack.
    //Stack can cause out of memory issues when the nesting depth of a Json stream is too deep.
    private StructureElement currentStructureElement = null;

    private int arrayDepth = 0;
    private int objectDepth = 0;

    private boolean closed;

    //minimal stack implementation
    private static final class StructureElement {
        private final StructureElement previous;
        private final boolean isArray;

        StructureElement(final StructureElement previous, final boolean isArray) {
            super();
            this.previous = previous;
            this.isArray = isArray;
        }
    }

    private static final class Buffer {
        private char[] buffer;
        private int length;

        public Buffer(char[] buffer, int length) {
            this.buffer = buffer;
            this.length = length;
        }
    }

    //detect charset according to RFC 4627
    public JsonStreamParserImpl(final InputStream inputStream, final int maxStringLength,
                                final BufferStrategy.BufferProvider<char[]> bufferProvider, final BufferStrategy.BufferProvider<char[]> valueBuffer,
                                final boolean autoAdjust, final JsonProviderImpl provider) {

        this(inputStream, null, null, maxStringLength, bufferProvider, valueBuffer, autoAdjust, provider);
    }

    //use charset provided
    public JsonStreamParserImpl(final InputStream inputStream, final Charset encoding, final int maxStringLength,
                                final BufferStrategy.BufferProvider<char[]> bufferProvider, final BufferStrategy.BufferProvider<char[]> valueBuffer,
                                final boolean autoAdjust, final JsonProviderImpl provider) {

        this(inputStream, null, encoding, maxStringLength, bufferProvider, valueBuffer, autoAdjust, provider);
    }

    public JsonStreamParserImpl(final Reader reader, final int maxStringLength, final BufferStrategy.BufferProvider<char[]> bufferProvider,
                                final BufferStrategy.BufferProvider<char[]> valueBuffer, final boolean autoAdjust, final JsonProviderImpl provider) {

        this(null, reader, null, maxStringLength, bufferProvider, valueBuffer, autoAdjust, provider);
    }

    private JsonStreamParserImpl(final InputStream inputStream, final Reader reader, final Charset encoding, final int maxStringLength,
                                 final BufferStrategy.BufferProvider<char[]> bufferProvider, final BufferStrategy.BufferProvider<char[]> valueBuffer,
                                 final boolean autoAdjust, final JsonProviderImpl provider) {

        super(provider);
        this.autoAdjust = autoAdjust;
        this.maxValueLength = maxStringLength <= 0 ? 8192 : maxStringLength;
        this.fallBackCopyBuffer = valueBuffer.newBuffer();
        this.buffer = bufferProvider.newBuffer();
        this.bufferProvider = bufferProvider;
        this.valueProvider = valueBuffer;

        if (fallBackCopyBuffer.length < maxStringLength) {
            throw cust("Size of value buffer cannot be smaller than maximum string length");
        }

        if (reader != null) {
            this.in = reader;
        } else if (encoding != null) { // always respect it
            this.in = new InputStreamReader(inputStream, encoding);
        } else { // should we log the usage is unexpected? (for perf we want to avoid the pushbackinputstream)
            this.in = new RFC4627AwareInputStreamReader(inputStream);
        }
    }

    //append a single char to the value buffer
    private void appendToCopyBuffer(final char c) {
        if (fallBackCopyBufferLength >= fallBackCopyBuffer.length - 1) {
            createNewFallBackCopyBuffer();
        }
        fallBackCopyBuffer[fallBackCopyBufferLength++] = c;
    }

    //copy content between "start" and "end" from buffer to value buffer 
    private void copyCurrentValue() {
        final int length = endOfValueInBuffer - startOfValueInBuffer;
        if (length > 0) {

            if (length > maxValueLength) {
                throw tmc();
            }

            if (fallBackCopyBufferLength >= fallBackCopyBuffer.length - length) { // not good at runtime but handled
                createNewFallBackCopyBuffer();
            }

            System.arraycopy(buffer, startOfValueInBuffer, fallBackCopyBuffer, fallBackCopyBufferLength, length);
            fallBackCopyBufferLength += length;
        }

        startOfValueInBuffer = endOfValueInBuffer = -1;
    }

    // Creates new fallBackCopyBuffer and stores the old instance in previousFallBackCopyBuffers,
    // this is much faster than resizing (recreating + copying) fallBackCopyBuffer
    private void createNewFallBackCopyBuffer() {
        if (!autoAdjust) {
            throw new ArrayIndexOutOfBoundsException("Buffer too small for such a long string");
        }

        if (previousFallBackCopyBuffers == null) {
            previousFallBackCopyBuffers = new LinkedList<>();
        }

        previousFallBackCopyBuffers.add(new Buffer(fallBackCopyBuffer, fallBackCopyBufferLength));
        fallBackCopyBuffer = valueProvider.newBuffer();
        fallBackCopyBufferLength = 0;
    }

    private void releasePreviousFallBackCopyBuffers() {
        if (previousFallBackCopyBuffers == null) {
            return;
        }

        previousFallBackCopyBuffers.forEach(it -> valueProvider.release(it.buffer));
        previousFallBackCopyBuffers = null;
    }


    @Override
    public final boolean hasNext() {
        if (currentStructureElement != null || previousEvent == 0) {
            return true;
        }
        if (previousEvent != END_ARRAY && previousEvent != END_OBJECT &&
                previousEvent != VALUE_STRING && previousEvent != VALUE_FALSE && previousEvent != VALUE_TRUE &&
                previousEvent != VALUE_NULL && previousEvent != VALUE_NUMBER) {
            if (bufferPos < 0) { // check we don't have an empty string to parse
                final char c = readNextChar();
                unreadChar();
                return c != EOF;
            }
            return true;
        }

        //detect garbage at the end of the file after last object or array is closed
        if (bufferPos < availableCharsInBuffer) {

            final char c = readNextNonWhitespaceChar(readNextChar());

            if (c == EOF) {
                return false;
            }

            if (bufferPos < availableCharsInBuffer) {
                throw uexc("EOF expected");
            }

        }

        return false;

    }

    private static boolean isAsciiDigit(final char value) {
        return value <= NINE && value >= ZERO;
    }

    //check if value is a valid hex digit and return the numeric value
    private int parseHexDigit(final char value) {

        if (isAsciiDigit(value)) {
            return value - 48;
        } else if (value <= 'f' && value >= 'a') {
            return (value) - 87;
        } else if ((value <= 'F' && value >= 'A')) {
            return (value) - 55;
        } else {
            throw uexc("Invalid hex character");
        }
    }

    private JsonLocation createLocation() {

        //we start with column = 1, so column is always >= 1
        //APi is not clear in this, but starting column with 1 is convenient
        long column = 1;
        long charOffset = 0;

        if (bufferPos >= -1) {

            charOffset = pastBufferReadCount + bufferPos + 1;
            column = lastLineBreakPosition == 0 ? charOffset + 1 : charOffset - lastLineBreakPosition;
        }

        //For now its unclear how to calculate offset for (byte) inputsream.
        //API says count bytes but thats dependent on encoding and not efficient
        //skip this for now, count always bytes and defer this until the JSR TCK arrives.

        return new JsonLocationImpl(currentLine, column, charOffset);
    }

    //read the next char from the stream and set/increment the bufferPos
    //will also refill buffer if necessary
    //if we are currently processing a value (string or number) and buffer 
    //refill is necessary copy the already read value part into the value buffer
    protected final char readNextChar() {

        if (bufferLeft == 0) {
            //fillbuffer

            //copy content from old buffer to valuebuffer
            //correct start end mark
            if (startOfValueInBuffer > -1 && endOfValueInBuffer == -1) {
                endOfValueInBuffer = availableCharsInBuffer;
                copyCurrentValue();

                startOfValueInBuffer = 0;
            }

            if (bufferPos >= -1) {
                pastBufferReadCount += availableCharsInBuffer;
            }

            try {
                availableCharsInBuffer = in.read(buffer, 0, buffer.length);
                if (availableCharsInBuffer <= 0) {
                    return EOF;
                }

            } catch (final IOException e) {
                close();
                throw uexio(e);
            }

            bufferPos = 0;
            bufferLeft = availableCharsInBuffer - 1;
            //end fillbuffer
        } else {

            //since JOHNZON-18 not longer necessary
            //prevent "bufferoverflow
            //if(bufferPos + 1 >= availableCharsInBuffer) {
            //    return EOF;
            //}

            bufferPos++;
            bufferLeft--;
        }

        return buffer[bufferPos];
    }

    //skip whitespaces
    //tracks location informations (line, column)
    //returns the first non whitespace character
    protected final char readNextNonWhitespaceChar(char c) {

        int dosCount = 0;

        while (c == SPACE || c == TAB || c == CR || c == EOL) {

            if (c == EOL) {
                currentLine++;
                lastLineBreakPosition = pastBufferReadCount + bufferPos;
            }

            //prevent DOS (denial of service) attack
            if (dosCount >= maxValueLength) {
                throw tmc();
            }
            dosCount++;

            //read next character
            c = readNextChar();

        }

        return c;
    }

    @Override
    public Event current() {
        if (previousEvent < 0 && hasNext()) {
            internalNext();
        }
        return previousEvent >= 0 && previousEvent < Event.values().length
                ? Event.values()[previousEvent]
                : null;
    }

    private void unreadChar() {
        bufferPos--;
        bufferLeft++;
    }

    @Override
    protected final Event internalNext() {
        //main entry, make decision how to handle the current character in the stream

        if (!hasNext()) {
            final char c = readNextChar();
            unreadChar();
            if (c != EOF) {
                throw uexc("No available event");
            }
            throw new NoSuchElementException();
        }

        if (previousEvent > 0 && currentStructureElement == null) {
            throw uexc("Unexpected end of structure");
        }

        final char c = readNextNonWhitespaceChar(readNextChar());

        if (c == COMMA_CHAR) {
            //last event must one of the following-> " ] } LITERAL
            if (previousEvent == KEY_SEPARATOR_EVENT || previousEvent == START_ARRAY
                    || previousEvent == START_OBJECT || previousEvent == COMMA_EVENT
                    || previousEvent == KEY_NAME) {
                throw uexc("Expected \" ] } LITERAL");
            }

            previousEvent = COMMA_EVENT;
            return internalNext();

        }

        if (c == KEY_SEPARATOR) {

            if (previousEvent != KEY_NAME) {
                throw uexc("A : can only follow a key name");
            }

            previousEvent = KEY_SEPARATOR_EVENT;
            return internalNext();

        }

        if (!isCurrentNumberIntegral) {
            isCurrentNumberIntegral = true;
        }
        //        if (currentBigDecimalNumber != null) {
        //            currentBigDecimalNumber = null;
        //        }
        if (currentIntegralNumber != Integer.MIN_VALUE) {
            currentIntegralNumber = Integer.MIN_VALUE;
        }

        releasePreviousFallBackCopyBuffers();
        if (fallBackCopyBufferLength != 0) {
            fallBackCopyBufferLength = 0;
        }

        startOfValueInBuffer = endOfValueInBuffer = -1;

        switch (c) {

            case START_OBJECT_CHAR:

                return handleStartObject();

            case END_OBJECT_CHAR:

                return handleEndObject();

            case START_ARRAY_CHAR:

                return handleStartArray();

            case END_ARRAY_CHAR:

                return handleEndArray();

            case QUOTE_CHAR:

                return handleQuote();

            case '0':
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
            case MINUS:
            case FALSE_F: // false
            case TRUE_T: // true
            case NULL_N: // null

                return handleLiteral();

            default:

                return defaultHandling(c);
        }
    }


    protected Event defaultHandling(char c) {
        if (c == EOF) {
            throw uexc("End of file hit too early");
        }
        throw uexc("Expected structural character or digit or 't' or 'n' or 'f' or '-'");
    }

    private Event handleStartObject() {

        //last event must one of the following-> : , [
        if (previousEvent > 0 && previousEvent != KEY_SEPARATOR_EVENT && previousEvent != START_ARRAY && previousEvent != COMMA_EVENT) {
            throw uexc("Expected : , [");
        }

        //push upon the stack
        if (currentStructureElement == null) {
            currentStructureElement = new StructureElement(null, false);
        } else {
            if (!currentStructureElement.isArray && previousEvent != KEY_SEPARATOR_EVENT) {
                throw uexc("Expected :");
            }
            final StructureElement localStructureElement = new StructureElement(currentStructureElement, false);
            currentStructureElement = localStructureElement;
        }

        objectDepth++;

        return EVT_MAP[previousEvent = START_OBJECT];

    }

    private Event handleEndObject() {

        //last event must one of the following-> " ] { } LITERAL
        if (previousEvent == START_ARRAY || previousEvent == COMMA_EVENT || previousEvent == KEY_NAME
                || previousEvent == KEY_SEPARATOR_EVENT || currentStructureElement == null) {
            throw uexc("Expected \" ] { } LITERAL");
        }

        if (currentStructureElement.isArray) {
            throw uexc("Expected : ]");
        }

        //pop from stack
        currentStructureElement = currentStructureElement.previous;

        objectDepth--;

        return EVT_MAP[previousEvent = END_OBJECT];
    }

    private Event handleStartArray() {

        //last event must one of the following-> : , [
        if (previousEvent > 0 && previousEvent != KEY_SEPARATOR_EVENT && previousEvent != START_ARRAY && previousEvent != COMMA_EVENT) {
            throw uexc("Expected : , [");
        }

        //push upon the stack
        if (currentStructureElement == null) {
            currentStructureElement = new StructureElement(null, true);
        } else {
            if (!currentStructureElement.isArray && previousEvent != KEY_SEPARATOR_EVENT) {
                throw uexc("Expected \"");
            }
            final StructureElement localStructureElement = new StructureElement(currentStructureElement, true);
            currentStructureElement = localStructureElement;
        }

        arrayDepth++;

        return EVT_MAP[previousEvent = START_ARRAY];
    }

    private Event handleEndArray() {

        //last event must one of the following-> [ ] } " LITERAL
        if (previousEvent == START_OBJECT || previousEvent == COMMA_EVENT || previousEvent == KEY_SEPARATOR_EVENT
                || currentStructureElement == null) {
            throw uexc("Expected [ ] } \" LITERAL");
        }

        if (!currentStructureElement.isArray) {
            throw uexc("Expected : }");
        }

        //pop from stack
        currentStructureElement = currentStructureElement.previous;

        arrayDepth--;

        return EVT_MAP[previousEvent = END_ARRAY];
    }

    @Override
    protected boolean isInArray() {
        return arrayDepth > 0;
    }


    @Override
    protected boolean isInObject() {
        return objectDepth > 0;
    }

    @Override
    protected BufferStrategy.BufferProvider<char[]> getCharArrayProvider() {
        return bufferProvider;
    }

    //read a string, gets called recursively
    //Handles escape/d characters
    //if string contains escape chars and/or cross buffer boundary then copy in the value buffer
    //if not then denote string start and end in startOfValueInBuffer and endOfValueInBuffer and read directly from buffer
    private void readString() {

        do {
            char n = readNextChar();
            //when first called n its first char after the starting quote
            //after that its the next character after the while loop below

            if (n == QUOTE_CHAR) {
                endOfValueInBuffer = startOfValueInBuffer = bufferPos; //->"" case
                return;
            } else if (n == EOL) {
                throw uexc("Unexpected linebreak");

            } else if (/* n >= '\u0000' && */ n <= '\u001F') {
                throw uexc("Unescaped control character");

            } else if (n == ESCAPE_CHAR) {

                n = readNextChar();

                //  \ u XXXX -> unicode char
                if (n == 'u') {
                    n = parseUnicodeHexChars();
                    appendToCopyBuffer(n);

                    // \\ -> \
                } else if (n == ESCAPE_CHAR) {
                    appendToCopyBuffer(n);

                    //another escape chars, for example \t
                } else {
                    appendToCopyBuffer(Strings.asEscapedChar(n));
                }

            } else {

                startOfValueInBuffer = bufferPos;
                endOfValueInBuffer = -1;

                while ((n = readNextChar()) > '\u001F' && n != ESCAPE_CHAR && n != EOL && n != QUOTE_CHAR) {
                    //read fast
                }

                endOfValueInBuffer = bufferPos;

                if (n == QUOTE_CHAR) {

                    if (fallBackCopyBufferLength > 0) {
                        copyCurrentValue();
                    } else {
                        if ((endOfValueInBuffer - startOfValueInBuffer) > maxValueLength) {
                            throw tmc();
                        }

                    }

                    return;
                } else if (n == EOL) {
                    throw uexc("Unexpected linebreak");

                } else if (n >= '\u0000' && n <= '\u001F') {
                    throw uexc("Unescaped control character");
                }

                copyCurrentValue();

                //current n is one of < '\u001F' -OR- ESCAPE_CHAR -OR- EOL -OR- QUOTE

                unreadChar(); //unread one char

            }
        } while (true);

        // before this do while(true) it was:
        //
        //recurse until string is terminated by a non escaped quote
        //readString();
        //
        //
        // but recursive = can't read big strings

    }


    //read the next four chars, check them and treat them as an single unicode char
    private char parseUnicodeHexChars() {
        // \u08Ac etc       
        return (char) (((parseHexDigit(readNextChar())) * 4096) + ((parseHexDigit(readNextChar())) * 256)
                + ((parseHexDigit(readNextChar())) * 16) + ((parseHexDigit(readNextChar()))));

    }

    private Event handleQuote() {

        //always the beginning quote of a key or value  

        //last event must one of the following-> : { [ ,
        if (previousEvent != -1 &&
                (previousEvent != KEY_SEPARATOR_EVENT &&
                 previousEvent != START_OBJECT &&
                 previousEvent != START_ARRAY  &&
                 previousEvent != COMMA_EVENT)) {
            throw uexc("Expected : { [ ,");
        }
        //starting quote already consumed
        readString();
        //end quote already consumed

        //make the decision if its an key or value
        if (previousEvent == KEY_SEPARATOR_EVENT) {
            //must be value

            if (currentStructureElement != null && currentStructureElement.isArray) {
                //not in array, only allowed within array
                throw uexc("Key value pair not allowed in an array");
            }

            return EVT_MAP[previousEvent = VALUE_STRING];

        } else { //Event is  START_OBJECT  OR START_ARRAY OR COMMA_EVENT
            //must be a key if we are in an object, if not its a value 

            if ((currentStructureElement != null && currentStructureElement.isArray) || currentStructureElement == null) {
                return EVT_MAP[previousEvent = VALUE_STRING];
            }

            return EVT_MAP[previousEvent = KEY_NAME];
        }

    }

    //read a number
    //if a number cross buffer boundary then copy in the value buffer
    //if not then denote string start and end in startOfValueInBuffer and endOfValueInBuffer and read directly from buffer
    private void readNumber() {

        char c = buffer[bufferPos];

        //start can change on any read() if we cross buffer boundary
        startOfValueInBuffer = bufferPos;
        endOfValueInBuffer = -1;

        char y = EOF;

        //sum up the digit values 
        int cumulatedDigitValue = 0;
        while (isAsciiDigit(y = readNextChar())) {

            if (c == ZERO) {
                throw uexc("Leading zeros not allowed");
            }

            if (c == MINUS && cumulatedDigitValue == 48) {
                throw uexc("Leading zeros after minus not allowed");
            }

            cumulatedDigitValue += y;

        }

        if (c == MINUS && cumulatedDigitValue == 0) {

            throw uexc("Unexpected premature end of number");
        }

        if (y == DOT) {
            isCurrentNumberIntegral = false;
            cumulatedDigitValue = 0;
            while (isAsciiDigit(y = readNextChar())) {
                cumulatedDigitValue++;
            }

            if (cumulatedDigitValue == 0) {

                throw uexc("Unexpected premature end of number");
            }

        }

        if (y == EXP_LOWERCASE || y == EXP_UPPERCASE) {
            isCurrentNumberIntegral = false;

            y = readNextChar(); //+ or - or digit

            if (!isAsciiDigit(y) && y != MINUS && y != PLUS) {
                throw uexc("Expected DIGIT or + or -");
            }

            if (y == MINUS || y == PLUS) {
                y = readNextChar();
                if (!isAsciiDigit(y)) {
                    throw uexc("Unexpected premature end of number");
                }

            }

            while (isAsciiDigit(y = readNextChar())) {
                //no-op
            }

        }

        endOfValueInBuffer = y == EOF && endOfValueInBuffer < 0 ? -1 : bufferPos;

        if (y == COMMA_CHAR || y == END_ARRAY_CHAR || y == END_OBJECT_CHAR || y == EOL || y == SPACE || y == TAB || y == CR || y == EOF) {

            unreadChar();//unread one char

            //['-', DIGIT]
            if (isCurrentNumberIntegral && c == MINUS && cumulatedDigitValue >= 48 && cumulatedDigitValue <= 57) {

                currentIntegralNumber = -(cumulatedDigitValue - 48); //optimize -0 till -9
                return;
            }

            //[DIGIT]
            if (isCurrentNumberIntegral && c != MINUS && cumulatedDigitValue == 0) {

                currentIntegralNumber = (c - 48); //optimize 0 till 9
                return;
            }

            if (fallBackCopyBufferLength > 0) {

                //we crossed a buffer boundary, use value buffer
                copyCurrentValue();

            } else {
                if ((endOfValueInBuffer - startOfValueInBuffer) >= maxValueLength) {
                    throw tmc();
                }
            }

            return;

        }

        throw uexc("Unexpected premature end of number");

    }

    //handles false, true, null and numbers
    private Event handleLiteral() {

        //last event must one of the following-> : , [
        if (previousEvent != -1 && previousEvent != KEY_SEPARATOR_EVENT && previousEvent != START_ARRAY && previousEvent != COMMA_EVENT) {
            throw uexc("Expected : , [");
        }

        if (previousEvent == COMMA_EVENT && !currentStructureElement.isArray) {
            //only allowed within array
            throw uexc("Not in an array context");
        }

        char c = buffer[bufferPos];

        // probe literals
        switch (c) {
            case TRUE_T:

                if (readNextChar() != TRUE_R || readNextChar() != TRUE_U || readNextChar() != TRUE_E) {
                    throw uexc("Expected LITERAL: true");
                }
                return EVT_MAP[previousEvent = VALUE_TRUE];
            case FALSE_F:

                if (readNextChar() != FALSE_A || readNextChar() != FALSE_L || readNextChar() != FALSE_S || readNextChar() != FALSE_E) {
                    throw uexc("Expected LITERAL: false");
                }

                return EVT_MAP[previousEvent = VALUE_FALSE];

            case NULL_N:

                if (readNextChar() != NULL_U || readNextChar() != NULL_L || readNextChar() != NULL_L) {
                    throw uexc("Expected LITERAL: null");
                }
                return EVT_MAP[previousEvent = VALUE_NULL];

            default:
                readNumber();
                return EVT_MAP[previousEvent = VALUE_NUMBER];
        }

    }

    @Override
    public String getString() {
        if (previousEvent == KEY_NAME || previousEvent == VALUE_STRING || previousEvent == VALUE_NUMBER) {
            combinePreviousFallbackBuffersToCurrent();

            //if there a content in the value buffer read from them, if not use main buffer
            return fallBackCopyBufferLength > 0 ? new String(fallBackCopyBuffer, 0, fallBackCopyBufferLength) : new String(buffer,
                    startOfValueInBuffer, endOfValueInBuffer - startOfValueInBuffer);
        } else {
            throw new IllegalStateException(EVT_MAP[previousEvent] + " doesn't support getString()");
        }
    }

    // Combines all old stored fallback buffers into the current fallback buffer again so we have a char[] to easily access
    // Releases all previous fallback buffers while doing so
    private void combinePreviousFallbackBuffersToCurrent() {
        if (previousFallBackCopyBuffers == null) {
            return;
        }

        int newSize = previousFallBackCopyBuffers.stream().mapToInt(it -> it.length).sum() + fallBackCopyBufferLength;
        char[] newBuffer = new char[newSize];

        int index = 0;
        for (Buffer buffer : previousFallBackCopyBuffers) {
            System.arraycopy(buffer.buffer, 0, newBuffer, index, buffer.length);
            index += buffer.length;
        }

        System.arraycopy(fallBackCopyBuffer, 0, newBuffer, index, fallBackCopyBufferLength);
        index += fallBackCopyBufferLength;

        releasePreviousFallBackCopyBuffers();
        if (releaseFallBackCopyBuffer) {
            valueProvider.release(fallBackCopyBuffer);
            releaseFallBackCopyBuffer = false;
        }

        fallBackCopyBuffer = newBuffer;
        fallBackCopyBufferLength = index;
    }

    @Override
    public boolean isIntegralNumber() {

        if (previousEvent != VALUE_NUMBER) {
            throw new IllegalStateException(EVT_MAP[previousEvent] + " doesn't support isIntegralNumber()");
        } else {
            return isCurrentNumberIntegral;
        }
    }

    @Override
    public boolean isNotTooLong() {
        return (endOfValueInBuffer - startOfValueInBuffer) < 19;
    }

    @Override
    public int getInt() {
        if (previousEvent != VALUE_NUMBER) {
            throw new IllegalStateException(EVT_MAP[previousEvent] + " doesn't support getInt()");
        } else if (isCurrentNumberIntegral && currentIntegralNumber != Integer.MIN_VALUE) {
            return currentIntegralNumber;
        } else if (isCurrentNumberIntegral) {
            combinePreviousFallbackBuffersToCurrent();
            //if there a content in the value buffer read from them, if not use main buffer
            final Integer retVal = fallBackCopyBufferLength > 0 ? parseIntegerFromChars(fallBackCopyBuffer, 0, fallBackCopyBufferLength)
                    : parseIntegerFromChars(buffer, startOfValueInBuffer, endOfValueInBuffer);
            if (retVal == null) {
                return getBigDecimal().intValue();
            } else {
                return retVal.intValue();
            }
        } else {
            return getBigDecimal().intValue();
        }
    }

    @Override
    public long getLong() {
        if (previousEvent != VALUE_NUMBER) {
            throw new IllegalStateException(EVT_MAP[previousEvent] + " doesn't support getLong()");
        } else if (isCurrentNumberIntegral && currentIntegralNumber != Integer.MIN_VALUE) {
            return currentIntegralNumber;
        } else if (isCurrentNumberIntegral) {
            combinePreviousFallbackBuffersToCurrent();
            //if there a content in the value buffer read from them, if not use main buffer
            final Long retVal = fallBackCopyBufferLength > 0 ? parseLongFromChars(fallBackCopyBuffer, 0, fallBackCopyBufferLength)
                    : parseLongFromChars(buffer, startOfValueInBuffer, endOfValueInBuffer);
            if (retVal == null) {
                return getBigDecimal().longValue();
            } else {
                return retVal.longValue();
            }
        } else {
            return getBigDecimal().longValue();
        }

    }

    @Override
    public boolean isFitLong() { // not exact but good enough for most cases
        if (!isCurrentNumberIntegral) {
            return false;
        }

        // no buffer overflow - assumes a buffer can hold a long
        // + length <= since max long is 9223372036854775807 and min is -9223372036854775808
        final int len = endOfValueInBuffer - startOfValueInBuffer;
        return fallBackCopyBufferLength <= 0 && len > 0 && len <= 18;
    }

    @Override
    public BigDecimal getBigDecimal() {
        if (previousEvent != VALUE_NUMBER) {
            throw new IllegalStateException(EVT_MAP[previousEvent] + " doesn't support getBigDecimal()");
            //        } else if (currentBigDecimalNumber != null) {
            //            return currentBigDecimalNumber;
        } else if (isCurrentNumberIntegral && currentIntegralNumber != Integer.MIN_VALUE) {
            return new BigDecimal(currentIntegralNumber);
        }

        combinePreviousFallbackBuffersToCurrent();
        //if there a content in the value buffer read from them, if not use main buffer
        return (/*currentBigDecimalNumber = */fallBackCopyBufferLength > 0 ? new BigDecimal(fallBackCopyBuffer, 0,
                fallBackCopyBufferLength) : new BigDecimal(buffer, startOfValueInBuffer, (endOfValueInBuffer - startOfValueInBuffer)));
    }

    @Override
    public JsonLocation getLocation() {
        return createLocation();
    }

    @Override
    public void close() {
        if (closed) {
            return;
        }

        bufferProvider.release(buffer);
        if (releaseFallBackCopyBuffer) {
            valueProvider.release(fallBackCopyBuffer);
        }
        releasePreviousFallBackCopyBuffers();

        try {
            in.close();
        } catch (final IOException e) {
            throw new JsonException("Unexpected IO exception " + e.getMessage(), e);
        } finally {
            closed = true;
        }
    }

    //parse a char[] to long while checking overflow
    //if overflowed return null
    //no additional checks since we are sure here that there are no non digits in the array
    private static Long parseLongFromChars(final char[] chars, final int start, final int end) {

        long retVal = 0;
        final boolean negative = chars[start] == MINUS;
        for (int i = negative ? start + 1 : start; i < end; i++) {
            final long tmp = retVal * 10 + (chars[i] - ZERO);
            if (tmp < retVal) { //check overflow
                return null;
            } else {
                retVal = tmp;
            }
        }

        return negative ? -retVal : retVal;
    }

    //parse a char[] to int while checking overflow
    //if overflowed return null
    //no additional checks since we are sure here that there are no non digits in the array
    private static Integer parseIntegerFromChars(final char[] chars, final int start, final int end) {

        int retVal = 0;
        final boolean negative = chars[start] == MINUS;
        for (int i = negative ? start + 1 : start; i < end; i++) {
            final int tmp = retVal * 10 + (chars[i] - ZERO);
            if (tmp < retVal) { //check overflow
                return null;
            } else {
                retVal = tmp;
            }
        }

        return negative ? -retVal : retVal;
    }

    private JsonParsingException uexc(final char c, final String message) {
        final JsonLocation location = createLocation();
        return new JsonParsingException("Unexpected character '" + c + "' (Codepoint: " + String.valueOf(c).codePointAt(0) + ") on "
                + location + ". Reason is [[" + message + "]]", location);
    }

    private JsonParsingException uexc(final String message) {
        final char c = bufferPos < 0 ? 0 : buffer[bufferPos];
        return uexc(c, message);
    }

    private JsonParsingException tmc() {
        final JsonLocation location = createLocation();
        return new JsonParsingException("Too many characters. Maximum string/number length of " + maxValueLength + " exceeded on "
                + location + ". Maybe increase org.apache.johnzon.max-string-length in jsonp factory properties or system properties.", location);
    }

    private JsonParsingException uexio(final IOException e) {
        final JsonLocation location = createLocation();
        return new JsonParsingException("Unexpected IO exception on " + location, e, location);
    }

    private JsonParsingException cust(final String message) {
        final JsonLocation location = createLocation();
        return new JsonParsingException("General exception on " + location + ". Reason is [[" + message + "]]", location);
    }

}