content: /*
   * Copyright (C) 2010 Google Inc.
   *
   * Licensed 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 com.airbnb.lottie.parser.moshi;
  
  import androidx.annotation.Nullable;
  
  import java.io.EOFException;
  import java.io.IOException;
  
  import okio.Buffer;
  import okio.BufferedSource;
  import okio.ByteString;
  
  final class JsonUtf8Reader extends JsonReader {
    private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10;
  
    private static final ByteString SINGLE_QUOTE_OR_SLASH = ByteString.encodeUtf8("'\\");
    private static final ByteString DOUBLE_QUOTE_OR_SLASH = ByteString.encodeUtf8("\"\\");
    private static final ByteString UNQUOTED_STRING_TERMINALS
        = ByteString.encodeUtf8("{}[]:, \n\t\r\f/\\;#=");
    private static final ByteString LINEFEED_OR_CARRIAGE_RETURN = ByteString.encodeUtf8("\n\r");
    private static final ByteString CLOSING_BLOCK_COMMENT = ByteString.encodeUtf8("*/");
  
    private static final int PEEKED_NONE = 0;
    private static final int PEEKED_BEGIN_OBJECT = 1;
    private static final int PEEKED_END_OBJECT = 2;
    private static final int PEEKED_BEGIN_ARRAY = 3;
    private static final int PEEKED_END_ARRAY = 4;
    private static final int PEEKED_TRUE = 5;
    private static final int PEEKED_FALSE = 6;
    private static final int PEEKED_NULL = 7;
    private static final int PEEKED_SINGLE_QUOTED = 8;
    private static final int PEEKED_DOUBLE_QUOTED = 9;
    private static final int PEEKED_UNQUOTED = 10;
    /**
     * When this is returned, the string value is stored in peekedString.
     */
    private static final int PEEKED_BUFFERED = 11;
    private static final int PEEKED_SINGLE_QUOTED_NAME = 12;
    private static final int PEEKED_DOUBLE_QUOTED_NAME = 13;
    private static final int PEEKED_UNQUOTED_NAME = 14;
    private static final int PEEKED_BUFFERED_NAME = 15;
    /**
     * When this is returned, the integer value is stored in peekedLong.
     */
    private static final int PEEKED_LONG = 16;
    private static final int PEEKED_NUMBER = 17;
    private static final int PEEKED_EOF = 18;
  
    /* State machine when parsing numbers */
    private static final int NUMBER_CHAR_NONE = 0;
    private static final int NUMBER_CHAR_SIGN = 1;
    private static final int NUMBER_CHAR_DIGIT = 2;
    private static final int NUMBER_CHAR_DECIMAL = 3;
    private static final int NUMBER_CHAR_FRACTION_DIGIT = 4;
    private static final int NUMBER_CHAR_EXP_E = 5;
    private static final int NUMBER_CHAR_EXP_SIGN = 6;
    private static final int NUMBER_CHAR_EXP_DIGIT = 7;
  
    /**
     * The input JSON.
     */
    private final BufferedSource source;
    private final Buffer buffer;
  
    private int peeked = PEEKED_NONE;
  
    /**
     * A peeked value that was composed entirely of digits with an optional
     * leading dash. Positive values may not have a leading 0.
     */
    private long peekedLong;
  
    /**
     * The number of characters in a peeked number literal.
     */
    private int peekedNumberLength;
  
    /**
     * A peeked string that should be parsed on the next double, long or string.
     * This is populated before a numeric value is parsed and used if that parsing
     * fails.
     */
    private @Nullable
    String peekedString;
  
    JsonUtf8Reader(BufferedSource source) {
      if (source == null) {
        throw new NullPointerException("source == null");
      }
      this.source = source;
      // Don't use source.getBuffer(). Because android studio use old version okio instead of your own okio.
      this.buffer = source.buffer();
      pushScope(JsonScope.EMPTY_DOCUMENT);
    }
  
  
    @Override public void beginArray() throws IOException {
      int p = peeked;
      if (p == PEEKED_NONE) {
        p = doPeek();
      }
      if (p == PEEKED_BEGIN_ARRAY) {
        pushScope(JsonScope.EMPTY_ARRAY);
        pathIndices[stackSize - 1] = 0;
        peeked = PEEKED_NONE;
      } else {
        throw new JsonDataException("Expected BEGIN_ARRAY but was " + peek()
            + " at path " + getPath());
      }
    }
  
    @Override public void endArray() throws IOException {
      int p = peeked;
      if (p == PEEKED_NONE) {
        p = doPeek();
      }
      if (p == PEEKED_END_ARRAY) {
        stackSize--;
        pathIndices[stackSize - 1]++;
        peeked = PEEKED_NONE;
      } else {
        throw new JsonDataException("Expected END_ARRAY but was " + peek()
            + " at path " + getPath());
      }
    }
  
    @Override public void beginObject() throws IOException {
      int p = peeked;
      if (p == PEEKED_NONE) {
        p = doPeek();
      }
      if (p == PEEKED_BEGIN_OBJECT) {
        pushScope(JsonScope.EMPTY_OBJECT);
        peeked = PEEKED_NONE;
      } else {
        throw new JsonDataException("Expected BEGIN_OBJECT but was " + peek()
            + " at path " + getPath());
      }
    }
  
    @Override public void endObject() throws IOException {
      int p = peeked;
      if (p == PEEKED_NONE) {
        p = doPeek();
      }
      if (p == PEEKED_END_OBJECT) {
        stackSize--;
        pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
        pathIndices[stackSize - 1]++;
        peeked = PEEKED_NONE;
      } else {
        throw new JsonDataException("Expected END_OBJECT but was " + peek()
            + " at path " + getPath());
      }
    }
  
    @Override public boolean hasNext() throws IOException {
      int p = peeked;
      if (p == PEEKED_NONE) {
        p = doPeek();
      }
      return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY && p != PEEKED_EOF;
    }
  
    @Override public Token peek() throws IOException {
      int p = peeked;
      if (p == PEEKED_NONE) {
        p = doPeek();
      }
  
      switch (p) {
        case PEEKED_BEGIN_OBJECT:
          return Token.BEGIN_OBJECT;
        case PEEKED_END_OBJECT:
          return Token.END_OBJECT;
        case PEEKED_BEGIN_ARRAY:
          return Token.BEGIN_ARRAY;
        case PEEKED_END_ARRAY:
          return Token.END_ARRAY;
        case PEEKED_SINGLE_QUOTED_NAME:
        case PEEKED_DOUBLE_QUOTED_NAME:
        case PEEKED_UNQUOTED_NAME:
        case PEEKED_BUFFERED_NAME:
          return Token.NAME;
        case PEEKED_TRUE:
        case PEEKED_FALSE:
          return Token.BOOLEAN;
        case PEEKED_NULL:
          return Token.NULL;
        case PEEKED_SINGLE_QUOTED:
        case PEEKED_DOUBLE_QUOTED:
        case PEEKED_UNQUOTED:
        case PEEKED_BUFFERED:
          return Token.STRING;
        case PEEKED_LONG:
        case PEEKED_NUMBER:
          return Token.NUMBER;
        case PEEKED_EOF:
          return Token.END_DOCUMENT;
        default:
          throw new AssertionError();
      }
    }
  
    private int doPeek() throws IOException {
      int peekStack = scopes[stackSize - 1];
      if (peekStack == JsonScope.EMPTY_ARRAY) {
        scopes[stackSize - 1] = JsonScope.NONEMPTY_ARRAY;
      } else if (peekStack == JsonScope.NONEMPTY_ARRAY) {
        // Look for a comma before the next element.
        int c = nextNonWhitespace(true);
        buffer.readByte(); // consume ']' or ','.
        switch (c) {
          case ']':
            return peeked = PEEKED_END_ARRAY;
          case ';':
            checkLenient(); // fall-through
          case ',':
            break;
          default:
            throw syntaxError("Unterminated array");
        }
      } else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) {
        scopes[stackSize - 1] = JsonScope.DANGLING_NAME;
        // Look for a comma before the next element.
        if (peekStack == JsonScope.NONEMPTY_OBJECT) {
          int c = nextNonWhitespace(true);
          buffer.readByte(); // Consume '}' or ','.
          switch (c) {
            case '}':
              return peeked = PEEKED_END_OBJECT;
            case ';':
              checkLenient(); // fall-through
            case ',':
              break;
            default:
              throw syntaxError("Unterminated object");
          }
        }
        int c = nextNonWhitespace(true);
        switch (c) {
          case '"':
            buffer.readByte(); // consume the '\"'.
            return peeked = PEEKED_DOUBLE_QUOTED_NAME;
          case '\'':
            buffer.readByte(); // consume the '\''.
            checkLenient();
            return peeked = PEEKED_SINGLE_QUOTED_NAME;
          case '}':
            if (peekStack != JsonScope.NONEMPTY_OBJECT) {
              buffer.readByte(); // consume the '}'.
              return peeked = PEEKED_END_OBJECT;
            } else {
              throw syntaxError("Expected name");
            }
          default:
            checkLenient();
            if (isLiteral((char) c)) {
              return peeked = PEEKED_UNQUOTED_NAME;
            } else {
              throw syntaxError("Expected name");
            }
        }
      } else if (peekStack == JsonScope.DANGLING_NAME) {
        scopes[stackSize - 1] = JsonScope.NONEMPTY_OBJECT;
        // Look for a colon before the value.
        int c = nextNonWhitespace(true);
        buffer.readByte(); // Consume ':'.
        switch (c) {
          case ':':
            break;
          case '=':
            checkLenient();
            if (source.request(1) && buffer.getByte(0) == '>') {
              buffer.readByte(); // Consume '>'.
            }
            break;
          default:
            throw syntaxError("Expected ':'");
        }
      } else if (peekStack == JsonScope.EMPTY_DOCUMENT) {
        scopes[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT;
      } else if (peekStack == JsonScope.NONEMPTY_DOCUMENT) {
        int c = nextNonWhitespace(false);
        if (c == -1) {
          return peeked = PEEKED_EOF;
        } else {
          checkLenient();
        }
      } else if (peekStack == JsonScope.CLOSED) {
        throw new IllegalStateException("JsonReader is closed");
      }
  
      int c = nextNonWhitespace(true);
      switch (c) {
        case ']':
          if (peekStack == JsonScope.EMPTY_ARRAY) {
            buffer.readByte(); // Consume ']'.
            return peeked = PEEKED_END_ARRAY;
          }
          // fall-through to handle ",]"
        case ';':
        case ',':
          // In lenient mode, a 0-length literal in an array means 'null'.
          if (peekStack == JsonScope.EMPTY_ARRAY || peekStack == JsonScope.NONEMPTY_ARRAY) {
            checkLenient();
            return peeked = PEEKED_NULL;
          } else {
            throw syntaxError("Unexpected value");
          }
        case '\'':
          checkLenient();
          buffer.readByte(); // Consume '\''.
          return peeked = PEEKED_SINGLE_QUOTED;
        case '"':
          buffer.readByte(); // Consume '\"'.
          return peeked = PEEKED_DOUBLE_QUOTED;
        case '[':
          buffer.readByte(); // Consume '['.
          return peeked = PEEKED_BEGIN_ARRAY;
        case '{':
          buffer.readByte(); // Consume '{'.
          return peeked = PEEKED_BEGIN_OBJECT;
        default:
      }
  
      int result = peekKeyword();
      if (result != PEEKED_NONE) {
        return result;
      }
  
      result = peekNumber();
      if (result != PEEKED_NONE) {
        return result;
      }
  
      if (!isLiteral(buffer.getByte(0))) {
        throw syntaxError("Expected value");
      }
  
      checkLenient();
      return peeked = PEEKED_UNQUOTED;
    }
  
    private int peekKeyword() throws IOException {
      // Figure out which keyword we're matching against by its first character.
      byte c = buffer.getByte(0);
      String keyword;
      String keywordUpper;
      int peeking;
      if (c == 't' || c == 'T') {
        keyword = "true";
        keywordUpper = "TRUE";
        peeking = PEEKED_TRUE;
      } else if (c == 'f' || c == 'F') {
        keyword = "false";
        keywordUpper = "FALSE";
        peeking = PEEKED_FALSE;
      } else if (c == 'n' || c == 'N') {
        keyword = "null";
        keywordUpper = "NULL";
        peeking = PEEKED_NULL;
      } else {
        return PEEKED_NONE;
      }
  
      // Confirm that chars [1..length) match the keyword.
      int length = keyword.length();
      for (int i = 1; i < length; i++) {
        if (!source.request(i + 1)) {
          return PEEKED_NONE;
        }
        c = buffer.getByte(i);
        if (c != keyword.charAt(i) && c != keywordUpper.charAt(i)) {
          return PEEKED_NONE;
        }
      }
  
      if (source.request(length + 1) && isLiteral(buffer.getByte(length))) {
        return PEEKED_NONE; // Don't match trues, falsey or nullsoft!
      }
  
      // We've found the keyword followed either by EOF or by a non-literal character.
      buffer.skip(length);
      return peeked = peeking;
    }
  
    private int peekNumber() throws IOException {
      long value = 0; // Negative to accommodate Long.MIN_VALUE more easily.
      boolean negative = false;
      boolean fitsInLong = true;
      int last = NUMBER_CHAR_NONE;
  
      int i = 0;
  
      charactersOfNumber:
      for (; true; i++) {
        if (!source.request(i + 1)) {
          break;
        }
  
        byte c = buffer.getByte(i);
        switch (c) {
          case '-':
            if (last == NUMBER_CHAR_NONE) {
              negative = true;
              last = NUMBER_CHAR_SIGN;
              continue;
            } else if (last == NUMBER_CHAR_EXP_E) {
              last = NUMBER_CHAR_EXP_SIGN;
              continue;
            }
            return PEEKED_NONE;
  
          case '+':
            if (last == NUMBER_CHAR_EXP_E) {
              last = NUMBER_CHAR_EXP_SIGN;
              continue;
            }
            return PEEKED_NONE;
  
          case 'e':
          case 'E':
            if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT) {
              last = NUMBER_CHAR_EXP_E;
              continue;
            }
            return PEEKED_NONE;
  
          case '.':
            if (last == NUMBER_CHAR_DIGIT) {
              last = NUMBER_CHAR_DECIMAL;
              continue;
            }
            return PEEKED_NONE;
  
          default:
            if (c < '0' || c > '9') {
              if (!isLiteral(c)) {
                break charactersOfNumber;
              }
              return PEEKED_NONE;
            }
            if (last == NUMBER_CHAR_SIGN || last == NUMBER_CHAR_NONE) {
              value = -(c - '0');
              last = NUMBER_CHAR_DIGIT;
            } else if (last == NUMBER_CHAR_DIGIT) {
              if (value == 0) {
                return PEEKED_NONE; // Leading '0' prefix is not allowed (since it could be octal).
              }
              long newValue = value * 10 - (c - '0');
              fitsInLong &= value > MIN_INCOMPLETE_INTEGER
                  || (value == MIN_INCOMPLETE_INTEGER && newValue < value);
              value = newValue;
            } else if (last == NUMBER_CHAR_DECIMAL) {
              last = NUMBER_CHAR_FRACTION_DIGIT;
            } else if (last == NUMBER_CHAR_EXP_E || last == NUMBER_CHAR_EXP_SIGN) {
              last = NUMBER_CHAR_EXP_DIGIT;
            }
        }
      }
  
      // We've read a complete number. Decide if it's a PEEKED_LONG or a PEEKED_NUMBER.
      if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative)
          && (value != 0 || !negative)) {
        peekedLong = negative ? value : -value;
        buffer.skip(i);
        return peeked = PEEKED_LONG;
      } else if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT
          || last == NUMBER_CHAR_EXP_DIGIT) {
        peekedNumberLength = i;
        return peeked = PEEKED_NUMBER;
      } else {
        return PEEKED_NONE;
      }
    }
  
    private boolean isLiteral(int c) throws IOException {
      switch (c) {
        case '/':
        case '\\':
        case ';':
        case '#':
        case '=':
          checkLenient(); // fall-through
        case '{':
        case '}':
        case '[':
        case ']':
        case ':':
        case ',':
        case ' ':
        case '\t':
        case '\f':
        case '\r':
        case '\n':
          return false;
        default:
          return true;
      }
    }
  
    @Override public String nextName() throws IOException {
      int p = peeked;
      if (p == PEEKED_NONE) {
        p = doPeek();
      }
      String result;
      if (p == PEEKED_UNQUOTED_NAME) {
        result = nextUnquotedValue();
      } else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
        result = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH);
      } else if (p == PEEKED_SINGLE_QUOTED_NAME) {
        result = nextQuotedValue(SINGLE_QUOTE_OR_SLASH);
      } else if (p == PEEKED_BUFFERED_NAME) {
        result = peekedString;
      } else {
        throw new JsonDataException("Expected a name but was " + peek() + " at path " + getPath());
      }
      peeked = PEEKED_NONE;
      pathNames[stackSize - 1] = result;
      return result;
    }
  
    @Override public int selectName(Options options) throws IOException {
      int p = peeked;
      if (p == PEEKED_NONE) {
        p = doPeek();
      }
      if (p < PEEKED_SINGLE_QUOTED_NAME || p > PEEKED_BUFFERED_NAME) {
        return -1;
      }
      if (p == PEEKED_BUFFERED_NAME) {
        return findName(peekedString, options);
      }
  
      int result = source.select(options.doubleQuoteSuffix);
      if (result != -1) {
        peeked = PEEKED_NONE;
        pathNames[stackSize - 1] = options.strings[result];
  
        return result;
      }
  
      // The next name may be unnecessary escaped. Save the last recorded path name, so that we
      // can restore the peek state in case we fail to find a match.
      String lastPathName = pathNames[stackSize - 1];
  
      String nextName = nextName();
      result = findName(nextName, options);
  
      if (result == -1) {
        peeked = PEEKED_BUFFERED_NAME;
        peekedString = nextName;
        // We can't push the path further, make it seem like nothing happened.
        pathNames[stackSize - 1] = lastPathName;
      }
  
      return result;
    }
  
    @Override public void skipName() throws IOException {
      if (failOnUnknown) {
        throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
      }
      int p = peeked;
      if (p == PEEKED_NONE) {
        p = doPeek();
      }
      if (p == PEEKED_UNQUOTED_NAME) {
        skipUnquotedValue();
      } else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
        skipQuotedValue(DOUBLE_QUOTE_OR_SLASH);
      } else if (p == PEEKED_SINGLE_QUOTED_NAME) {
        skipQuotedValue(SINGLE_QUOTE_OR_SLASH);
      } else if (p != PEEKED_BUFFERED_NAME) {
        throw new JsonDataException("Expected a name but was " + peek() + " at path " + getPath());
      }
      peeked = PEEKED_NONE;
      pathNames[stackSize - 1] = "null";
    }
  
    /**
     * If {@code name} is in {@code options} this consumes it and returns its index.
     * Otherwise this returns -1 and no name is consumed.
     */
    private int findName(String name, Options options) {
      for (int i = 0, size = options.strings.length; i < size; i++) {
        if (name.equals(options.strings[i])) {
          peeked = PEEKED_NONE;
          pathNames[stackSize - 1] = name;
  
          return i;
        }
      }
      return -1;
    }
  
    @Override public String nextString() throws IOException {
      int p = peeked;
      if (p == PEEKED_NONE) {
        p = doPeek();
      }
      String result;
      if (p == PEEKED_UNQUOTED) {
        result = nextUnquotedValue();
      } else if (p == PEEKED_DOUBLE_QUOTED) {
        result = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH);
      } else if (p == PEEKED_SINGLE_QUOTED) {
        result = nextQuotedValue(SINGLE_QUOTE_OR_SLASH);
      } else if (p == PEEKED_BUFFERED) {
        result = peekedString;
        peekedString = null;
      } else if (p == PEEKED_LONG) {
        result = Long.toString(peekedLong);
      } else if (p == PEEKED_NUMBER) {
        result = buffer.readUtf8(peekedNumberLength);
      } else {
        throw new JsonDataException("Expected a string but was " + peek() + " at path " + getPath());
      }
      peeked = PEEKED_NONE;
      pathIndices[stackSize - 1]++;
      return result;
    }
  
    @Override public boolean nextBoolean() throws IOException {
      int p = peeked;
      if (p == PEEKED_NONE) {
        p = doPeek();
      }
      if (p == PEEKED_TRUE) {
        peeked = PEEKED_NONE;
        pathIndices[stackSize - 1]++;
        return true;
      } else if (p == PEEKED_FALSE) {
        peeked = PEEKED_NONE;
        pathIndices[stackSize - 1]++;
        return false;
      }
      throw new JsonDataException("Expected a boolean but was " + peek() + " at path " + getPath());
    }
  
    @Override public double nextDouble() throws IOException {
      int p = peeked;
      if (p == PEEKED_NONE) {
        p = doPeek();
      }
  
      if (p == PEEKED_LONG) {
        peeked = PEEKED_NONE;
        pathIndices[stackSize - 1]++;
        return (double) peekedLong;
      }
  
      if (p == PEEKED_NUMBER) {
        peekedString = buffer.readUtf8(peekedNumberLength);
      } else if (p == PEEKED_DOUBLE_QUOTED) {
        peekedString = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH);
      } else if (p == PEEKED_SINGLE_QUOTED) {
        peekedString = nextQuotedValue(SINGLE_QUOTE_OR_SLASH);
      } else if (p == PEEKED_UNQUOTED) {
        peekedString = nextUnquotedValue();
      } else if (p != PEEKED_BUFFERED) {
        throw new JsonDataException("Expected a double but was " + peek() + " at path " + getPath());
      }
  
      peeked = PEEKED_BUFFERED;
      double result;
      try {
        result = Double.parseDouble(peekedString);
      } catch (NumberFormatException e) {
        throw new JsonDataException("Expected a double but was " + peekedString
            + " at path " + getPath());
      }
      if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) {
        throw new JsonEncodingException("JSON forbids NaN and infinities: " + result
            + " at path " + getPath());
      }
      peekedString = null;
      peeked = PEEKED_NONE;
      pathIndices[stackSize - 1]++;
      return result;
    }
  
    /**
     * Returns the string up to but not including {@code quote}, unescaping any character escape
     * sequences encountered along the way. The opening quote should have already been read. This
     * consumes the closing quote, but does not include it in the returned string.
     *
     * @throws IOException if any unicode escape sequences are malformed.
     */
    private String nextQuotedValue(ByteString runTerminator) throws IOException {
      StringBuilder builder = null;
      while (true) {
        long index = source.indexOfElement(runTerminator);
        if (index == -1L) {
          throw syntaxError("Unterminated string");
        }
  
        // If we've got an escape character, we're going to need a string builder.
        if (buffer.getByte(index) == '\\') {
          if (builder == null) {
            builder = new StringBuilder();
          }
          builder.append(buffer.readUtf8(index));
          buffer.readByte(); // '\'
          builder.append(readEscapeCharacter());
          continue;
        }
  
        // If it isn't the escape character, it's the quote. Return the string.
        if (builder == null) {
          String result = buffer.readUtf8(index);
          buffer.readByte(); // Consume the quote character.
          return result;
        } else {
          builder.append(buffer.readUtf8(index));
          buffer.readByte(); // Consume the quote character.
          return builder.toString();
        }
      }
    }
  
    /**
     * Returns an unquoted value as a string.
     */
    private String nextUnquotedValue() throws IOException {
      long i = source.indexOfElement(UNQUOTED_STRING_TERMINALS);
      return i != -1 ? buffer.readUtf8(i) : buffer.readUtf8();
    }
  
    private void skipQuotedValue(ByteString runTerminator) throws IOException {
      while (true) {
        long index = source.indexOfElement(runTerminator);
        if (index == -1L) {
          throw syntaxError("Unterminated string");
        }
  
        if (buffer.getByte(index) == '\\') {
          buffer.skip(index + 1);
          readEscapeCharacter();
        } else {
          buffer.skip(index + 1);
          return;
        }
      }
    }
  
    private void skipUnquotedValue() throws IOException {
      long i = source.indexOfElement(UNQUOTED_STRING_TERMINALS);
      buffer.skip(i != -1L ? i : buffer.size());
    }
  
    @Override public int nextInt() throws IOException {
      int p = peeked;
      if (p == PEEKED_NONE) {
        p = doPeek();
      }
  
      int result;
      if (p == PEEKED_LONG) {
        result = (int) peekedLong;
        if (peekedLong != result) { // Make sure no precision was lost casting to 'int'.
          throw new JsonDataException("Expected an int but was " + peekedLong
              + " at path " + getPath());
        }
        peeked = PEEKED_NONE;
        pathIndices[stackSize - 1]++;
        return result;
      }
  
      if (p == PEEKED_NUMBER) {
        peekedString = buffer.readUtf8(peekedNumberLength);
      } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_SINGLE_QUOTED) {
        peekedString = p == PEEKED_DOUBLE_QUOTED
            ? nextQuotedValue(DOUBLE_QUOTE_OR_SLASH)
            : nextQuotedValue(SINGLE_QUOTE_OR_SLASH);
        try {
          result = Integer.parseInt(peekedString);
          peeked = PEEKED_NONE;
          pathIndices[stackSize - 1]++;
          return result;
        } catch (NumberFormatException ignored) {
          // Fall back to parse as a double below.
        }
      } else if (p != PEEKED_BUFFERED) {
        throw new JsonDataException("Expected an int but was " + peek() + " at path " + getPath());
      }
  
      peeked = PEEKED_BUFFERED;
      double asDouble;
      try {
        asDouble = Double.parseDouble(peekedString);
      } catch (NumberFormatException e) {
        throw new JsonDataException("Expected an int but was " + peekedString
            + " at path " + getPath());
      }
      result = (int) asDouble;
      if (result != asDouble) { // Make sure no precision was lost casting to 'int'.
        throw new JsonDataException("Expected an int but was " + peekedString
            + " at path " + getPath());
      }
      peekedString = null;
      peeked = PEEKED_NONE;
      pathIndices[stackSize - 1]++;
      return result;
    }
  
    @Override public void close() throws IOException {
      peeked = PEEKED_NONE;
      scopes[0] = JsonScope.CLOSED;
      stackSize = 1;
      buffer.clear();
      source.close();
    }
  
    @Override public void skipValue() throws IOException {
      if (failOnUnknown) {
        throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
      }
      int count = 0;
      do {
        int p = peeked;
        if (p == PEEKED_NONE) {
          p = doPeek();
        }
  
        if (p == PEEKED_BEGIN_ARRAY) {
          pushScope(JsonScope.EMPTY_ARRAY);
          count++;
        } else if (p == PEEKED_BEGIN_OBJECT) {
          pushScope(JsonScope.EMPTY_OBJECT);
          count++;
        } else if (p == PEEKED_END_ARRAY) {
          count--;
          if (count < 0) {
            throw new JsonDataException(
                "Expected a value but was " + peek() + " at path " + getPath());
          }
          stackSize--;
        } else if (p == PEEKED_END_OBJECT) {
          count--;
          if (count < 0) {
            throw new JsonDataException(
                "Expected a value but was " + peek() + " at path " + getPath());
          }
          stackSize--;
        } else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) {
          skipUnquotedValue();
        } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) {
          skipQuotedValue(DOUBLE_QUOTE_OR_SLASH);
        } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) {
          skipQuotedValue(SINGLE_QUOTE_OR_SLASH);
        } else if (p == PEEKED_NUMBER) {
          buffer.skip(peekedNumberLength);
        } else if (p == PEEKED_EOF) {
          throw new JsonDataException(
              "Expected a value but was " + peek() + " at path " + getPath());
        }
        peeked = PEEKED_NONE;
      } while (count != 0);
  
      pathIndices[stackSize - 1]++;
      pathNames[stackSize - 1] = "null";
    }
  
    /**
     * Returns the next character in the stream that is neither whitespace nor a
     * part of a comment. When this returns, the returned character is always at
     * {@code buffer.getByte(0)}.
     */
    private int nextNonWhitespace(boolean throwOnEof) throws IOException {
      /*
       * This code uses ugly local variables 'p' and 'l' representing the 'pos'
       * and 'limit' fields respectively. Using locals rather than fields saves
       * a few field reads for each whitespace character in a pretty-printed
       * document, resulting in a 5% speedup. We need to flush 'p' to its field
       * before any (potentially indirect) call to fillBuffer() and reread both
       * 'p' and 'l' after any (potentially indirect) call to the same method.
       */
      int p = 0;
      while (source.request(p + 1)) {
        int c = buffer.getByte(p++);
        if (c == '\n' || c == ' ' || c == '\r' || c == '\t') {
          continue;
        }
  
        buffer.skip(p - 1);
        if (c == '/') {
          if (!source.request(2)) {
            return c;
          }
  
          checkLenient();
          byte peek = buffer.getByte(1);
          switch (peek) {
            case '*':
              // skip a /* c-style comment */
              buffer.readByte(); // '/'
              buffer.readByte(); // '*'
              if (!skipToEndOfBlockComment()) {
                throw syntaxError("Unterminated comment");
              }
              p = 0;
              continue;
  
            case '/':
              // skip a // end-of-line comment
              buffer.readByte(); // '/'
              buffer.readByte(); // '/'
              skipToEndOfLine();
              p = 0;
              continue;
  
            default:
              return c;
          }
        } else if (c == '#') {
          // Skip a # hash end-of-line comment. The JSON RFC doesn't specify this behaviour, but it's
          // required to parse existing documents.
          checkLenient();
          skipToEndOfLine();
          p = 0;
        } else {
          return c;
        }
      }
      if (throwOnEof) {
        throw new EOFException("End of input");
      } else {
        return -1;
      }
    }
  
    private void checkLenient() throws IOException {
      if (!lenient) {
        throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON");
      }
    }
  
    /**
     * Advances the position until after the next newline character. If the line
     * is terminated by "\r\n", the '\n' must be consumed as whitespace by the
     * caller.
     */
    private void skipToEndOfLine() throws IOException {
      long index = source.indexOfElement(LINEFEED_OR_CARRIAGE_RETURN);
      buffer.skip(index != -1 ? index + 1 : buffer.size());
    }
  
    /**
     * Skips through the next closing block comment.
     */
    private boolean skipToEndOfBlockComment() throws IOException {
      long index = source.indexOf(CLOSING_BLOCK_COMMENT);
      boolean found = index != -1;
      buffer.skip(found ? index + CLOSING_BLOCK_COMMENT.size() : buffer.size());
      return found;
    }
  
  
    @Override public String toString() {
      return "JsonReader(" + source + ")";
    }
  
    /**
     * Unescapes the character identified by the character or characters that immediately follow a
     * backslash. The backslash '\' should have already been read. This supports both unicode escapes
     * "u000A" and two-character escapes "\n".
     *
     * @throws IOException if any unicode escape sequences are malformed.
     */
    private char readEscapeCharacter() throws IOException {
      if (!source.request(1)) {
        throw syntaxError("Unterminated escape sequence");
      }
  
      byte escaped = buffer.readByte();
      switch (escaped) {
        case 'u':
          if (!source.request(4)) {
            throw new EOFException("Unterminated escape sequence at path " + getPath());
          }
          // Equivalent to Integer.parseInt(stringPool.get(buffer, pos, 4), 16);
          char result = 0;
          for (int i = 0, end = i + 4; i < end; i++) {
            byte c = buffer.getByte(i);
            result <<= 4;
            if (c >= '0' && c <= '9') {
              result += (c - '0');
            } else if (c >= 'a' && c <= 'f') {
              result += (c - 'a' + 10);
            } else if (c >= 'A' && c <= 'F') {
              result += (c - 'A' + 10);
            } else {
              throw syntaxError("\\u" + buffer.readUtf8(4));
            }
          }
          buffer.skip(4);
          return result;
  
        case 't':
          return '\t';
  
        case 'b':
          return '\b';
  
        case 'n':
          return '\n';
  
        case 'r':
          return '\r';
  
        case 'f':
          return '\f';
  
        case '\n':
        case '\'':
        case '"':
        case '\\':
        case '/':
          return (char) escaped;
  
        default:
          if (!lenient) {
            throw syntaxError("Invalid escape sequence: \\" + (char) escaped);
          }
          return (char) escaped;
      }
    }
  
  }