/*******************************************************************************
 * Copyright (c) 2013 EclipseSource.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Ralf Sternberg - initial implementation and API
 ******************************************************************************/
package com.eclipsesource.json;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;


class JsonParser {

  private static final int MIN_BUFFER_SIZE = 10;
  private static final int DEFAULT_BUFFER_SIZE = 1024;

  private final Reader reader;
  private final char[] buffer;
  private int bufferOffset;
  private int index;
  private int fill;
  private int line;
  private int lineOffset;
  private int current;
  private StringBuilder captureBuffer;
  private int captureStart;

  /*
   * |                      bufferOffset
   *                        v
   * [a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t]        < input
   *                       [l|m|n|o|p|q|r|s|t|?|?]    < buffer
   *                          ^               ^
   *                       |  index           fill
   */

  JsonParser( String string ) {
    this( new StringReader( string ),
          Math.max( MIN_BUFFER_SIZE, Math.min( DEFAULT_BUFFER_SIZE, string.length() ) ) );
  }

  JsonParser( Reader reader ) {
    this( reader, DEFAULT_BUFFER_SIZE );
  }

  JsonParser( Reader reader, int buffersize ) {
    this.reader = reader;
    buffer = new char[ buffersize ];
    line = 1;
    captureStart = -1;
  }

  JsonValue parse() throws IOException {
    read();
    skipWhiteSpace();
    JsonValue result = readValue();
    skipWhiteSpace();
    if( !isEndOfText() ) {
      throw error( "Unexpected character" );
    }
    return result;
  }

  private JsonValue readValue() throws IOException {
    switch( current ) {
    case 'n':
      return readNull();
    case 't':
      return readTrue();
    case 'f':
      return readFalse();
    case '"':
      return readString();
    case '[':
      return readArray();
    case '{':
      return readObject();
    case '-':
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      return readNumber();
    default:
      throw expected( "value" );
    }
  }

  private JsonArray readArray() throws IOException {
    read();
    JsonArray array = new JsonArray();
    skipWhiteSpace();
    if( readChar( ']' ) ) {
      return array;
    }
    do {
      skipWhiteSpace();
      array.add( readValue() );
      skipWhiteSpace();
    } while( readChar( ',' ) );
    if( !readChar( ']' ) ) {
      throw expected( "',' or ']'" );
    }
    return array;
  }

  private JsonObject readObject() throws IOException {
    read();
    JsonObject object = new JsonObject();
    skipWhiteSpace();
    if( readChar( '}' ) ) {
      return object;
    }
    do {
      skipWhiteSpace();
      String name = readName();
      skipWhiteSpace();
      if( !readChar( ':' ) ) {
        throw expected( "':'" );
      }
      skipWhiteSpace();
      object.add( name, readValue() );
      skipWhiteSpace();
    } while( readChar( ',' ) );
    if( !readChar( '}' ) ) {
      throw expected( "',' or '}'" );
    }
    return object;
  }

  private String readName() throws IOException {
    if( current != '"' ) {
      throw expected( "name" );
    }
    return readStringInternal();
  }

  private JsonValue readNull() throws IOException {
    read();
    readRequiredChar( 'u' );
    readRequiredChar( 'l' );
    readRequiredChar( 'l' );
    return JsonValue.NULL;
  }

  private JsonValue readTrue() throws IOException {
    read();
    readRequiredChar( 'r' );
    readRequiredChar( 'u' );
    readRequiredChar( 'e' );
    return JsonValue.TRUE;
  }

  private JsonValue readFalse() throws IOException {
    read();
    readRequiredChar( 'a' );
    readRequiredChar( 'l' );
    readRequiredChar( 's' );
    readRequiredChar( 'e' );
    return JsonValue.FALSE;
  }

  private void readRequiredChar( char ch ) throws IOException {
    if( !readChar( ch ) ) {
      throw expected( "'" + ch + "'" );
    }
  }

  private JsonValue readString() throws IOException {
    return new JsonString( readStringInternal() );
  }

  private String readStringInternal() throws IOException {
    read();
    startCapture();
    while( current != '"' ) {
      if( current == '\\' ) {
        pauseCapture();
        readEscape();
        startCapture();
      } else if( current < 0x20 ) {
        throw expected( "valid string character" );
      } else {
        read();
      }
    }
    String string = endCapture();
    read();
    return string;
  }

  private void readEscape() throws IOException {
    read();
    switch( current ) {
    case '"':
    case '/':
    case '\\':
      captureBuffer.append( (char)current );
      break;
    case 'b':
      captureBuffer.append( '\b' );
      break;
    case 'f':
      captureBuffer.append( '\f' );
      break;
    case 'n':
      captureBuffer.append( '\n' );
      break;
    case 'r':
      captureBuffer.append( '\r' );
      break;
    case 't':
      captureBuffer.append( '\t' );
      break;
    case 'u':
      char[] hexChars = new char[4];
      for( int i = 0; i < 4; i++ ) {
        read();
        if( !isHexDigit() ) {
          throw expected( "hexadecimal digit" );
        }
        hexChars[i] = (char)current;
      }
      captureBuffer.append( (char)Integer.parseInt( String.valueOf( hexChars ), 16 ) );
      break;
    default:
      throw expected( "valid escape sequence" );
    }
    read();
  }

  private JsonValue readNumber() throws IOException {
    startCapture();
    readChar( '-' );
    int firstDigit = current;
    if( !readDigit() ) {
      throw expected( "digit" );
    }
    if( firstDigit != '0' ) {
      while( readDigit() ) {
      }
    }
    readFraction();
    readExponent();
    return new JsonNumber( endCapture() );
  }

  private boolean readFraction() throws IOException {
    if( !readChar( '.' ) ) {
      return false;
    }
    if( !readDigit() ) {
      throw expected( "digit" );
    }
    while( readDigit() ) {
    }
    return true;
  }

  private boolean readExponent() throws IOException {
    if( !readChar( 'e' ) && !readChar( 'E' ) ) {
      return false;
    }
    if( !readChar( '+' ) ) {
      readChar( '-' );
    }
    if( !readDigit() ) {
      throw expected( "digit" );
    }
    while( readDigit() ) {
    }
    return true;
  }

  private boolean readChar( char ch ) throws IOException {
    if( current != ch ) {
      return false;
    }
    read();
    return true;
  }

  private boolean readDigit() throws IOException {
    if( !isDigit() ) {
      return false;
    }
    read();
    return true;
  }

  private void skipWhiteSpace() throws IOException {
    while( isWhiteSpace() ) {
      read();
    }
  }

  private void read() throws IOException {
    if( isEndOfText() ) {
      throw error( "Unexpected end of input" );
    }
    if( index == fill ) {
      if( captureStart != -1 ) {
        captureBuffer.append( buffer, captureStart, fill - captureStart );
        captureStart = 0;
      }
      bufferOffset += fill;
      fill = reader.read( buffer, 0, buffer.length );
      index = 0;
      if( fill == -1 ) {
        current = -1;
        return;
      }
    }
    if( current == '\n' ) {
      line++;
      lineOffset = bufferOffset + index;
    }
    current = buffer[index++];
  }

  private void startCapture() {
    if( captureBuffer == null ) {
      captureBuffer = new StringBuilder();
    }
    captureStart = index - 1;
  }

  private void pauseCapture() {
    int end = current == -1 ? index : index - 1;
    captureBuffer.append( buffer, captureStart, end - captureStart );
    captureStart = -1;
  }

  private String endCapture() {
    int end = current == -1 ? index : index - 1;
    String captured;
    if( captureBuffer.length() > 0 ) {
      captureBuffer.append( buffer, captureStart, end - captureStart );
      captured = captureBuffer.toString();
      captureBuffer.setLength( 0 );
    } else {
      captured = new String( buffer, captureStart, end - captureStart );
    }
    captureStart = -1;
    return captured;
  }

  private ParseException expected( String expected ) {
    if( isEndOfText() ) {
      return error( "Unexpected end of input" );
    }
    return error( "Expected " + expected );
  }

  private ParseException error( String message ) {
    int absIndex = bufferOffset + index;
    int column = absIndex - lineOffset;
    int offset = isEndOfText() ? absIndex : absIndex - 1;
    return new ParseException( message, offset, line, column - 1 );
  }

  private boolean isWhiteSpace() {
    return current == ' ' || current == '\t' || current == '\n' || current == '\r';
  }

  private boolean isDigit() {
    return current >= '0' && current <= '9';
  }

  private boolean isHexDigit() {
    return current >= '0' && current <= '9'
        || current >= 'a' && current <= 'f'
        || current >= 'A' && current <= 'F';
  }

  private boolean isEndOfText() {
    return current == -1;
  }

}
