/* ============
 * Orson Charts
 * ============
 * 
 * (C)opyright 2013, 2014, by Object Refinery Limited.
 * 
 * http://www.object-refinery.com/orsoncharts/index.html
 * 
 * JSON.simple
 * -----------
 * The code in this file originates from the JSON.simple project by 
 * FangYidong<fangyidong@yahoo.com.cn>:
 * 
 *     https://code.google.com/p/json-simple/
 *  
 * which is licensed under the Apache Software License version 2.0.  
 * 
 * It has been modified locally and repackaged under 
 * com.orsoncharts.util.json.* to avoid conflicts with any other version that
 * may be present on the classpath. 
 * 
 */

package com.orsoncharts.util.json.parser;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.orsoncharts.util.json.JSONArray;
import com.orsoncharts.util.json.JSONObject;


/**
 * Parser for JSON text. Please note that JSONParser is NOT thread-safe.
 */
public class JSONParser {
    
    public static final int S_INIT = 0;
    public static final int S_IN_FINISHED_VALUE = 1;//string,number,boolean,null,object,array
    public static final int S_IN_OBJECT = 2;
    public static final int S_IN_ARRAY = 3;
    public static final int S_PASSED_PAIR_KEY = 4;
    public static final int S_IN_PAIR_VALUE = 5;
    public static final int S_END = 6;
    public static final int S_IN_ERROR = -1;
    
    private LinkedList handlerStatusStack;
    private Yylex lexer = new Yylex((Reader)null);
    private Yytoken token = null;
    private int status = S_INIT;
    
    private int peekStatus(LinkedList statusStack){
        if (statusStack.size() == 0) {
            return -1;
        }
        Integer result = (Integer) statusStack.getFirst();
        return result.intValue();
    }
    
    /**
     * Reset the parser to the initial state without resetting the underlying 
     * reader.
     */
    public void reset(){
        token = null;
        status = S_INIT;
        handlerStatusStack = null;
    }
    
    /**
     * Reset the parser to the initial state with a new character reader.
     * 
     * @param in  the new character reader.
     */
    public void reset(Reader in){
        lexer.yyreset(in);
        reset();
    }
    
    /**
     * @return The position of the beginning of the current token.
     */
    public int getPosition(){
        return lexer.getPosition();
    }
    
    public Object parse(String s) throws ParseException {
        return parse(s, (ContainerFactory) null);
    }
    
    public Object parse(String s, ContainerFactory containerFactory) 
            throws ParseException {
        StringReader in = new StringReader(s);
        try {
            return parse(in, containerFactory);
        }
        catch(IOException ie){
            /*
             * Actually it will never happen.
             */
            throw new ParseException(-1, 
                    ParseException.ERROR_UNEXPECTED_EXCEPTION, ie);
        }
    }
    
    public Object parse(Reader in) throws IOException, ParseException {
        return parse(in, (ContainerFactory) null);
    }
    
    /**
     * Parse JSON text into java object from the input source.
     *     
     * @param in  the input source.
     * @param containerFactory  use this factory to create your own JSON 
     *     object and JSON array containers.
     * @return Instance of the following:
     *     com.orsoncharts.util.json.simple.JSONObject,
     *     com.orsoncharts.util.json.simple.JSONArray,
     *     java.lang.String,
     *     java.lang.Number,
     *     java.lang.Boolean,
     *     null
     * 
     * @throws IOException if there is an I/O problem.
     * @throws ParseException if there is a parsing problem.
     */
    public Object parse(Reader in, ContainerFactory containerFactory) 
            throws IOException, ParseException{
        reset(in);
        LinkedList statusStack = new LinkedList();
        LinkedList valueStack = new LinkedList();
        
        try {
            do {
                nextToken();
                switch(status) {
                case S_INIT:
                    switch(token.type) {
                    case Yytoken.TYPE_VALUE:
                        status=S_IN_FINISHED_VALUE;
                        statusStack.addFirst(new Integer(status));
                        valueStack.addFirst(token.value);
                        break;
                    case Yytoken.TYPE_LEFT_BRACE:
                        status=S_IN_OBJECT;
                        statusStack.addFirst(new Integer(status));
                        valueStack.addFirst(createObjectContainer(containerFactory));
                        break;
                    case Yytoken.TYPE_LEFT_SQUARE:
                        status=S_IN_ARRAY;
                        statusStack.addFirst(new Integer(status));
                        valueStack.addFirst(createArrayContainer(containerFactory));
                        break;
                    default:
                        status=S_IN_ERROR;
                    }//inner switch
                    break;
                    
                case S_IN_FINISHED_VALUE:
                    if (token.type == Yytoken.TYPE_EOF) {
                        return valueStack.removeFirst();
                    } else {
                    throw new ParseException(getPosition(), 
                            ParseException.ERROR_UNEXPECTED_TOKEN, token);
                }
                    
                case S_IN_OBJECT:
                    switch(token.type) {
                    case Yytoken.TYPE_COMMA:
                        break;
                    case Yytoken.TYPE_VALUE:
                        if (token.value instanceof String) {
                            String key = (String) token.value;
                            valueStack.addFirst(key);
                            status = S_PASSED_PAIR_KEY;
                            statusStack.addFirst(new Integer(status));
                        }
                        else{
                            status = S_IN_ERROR;
                        }
                        break;
                    case Yytoken.TYPE_RIGHT_BRACE:
                        if (valueStack.size() > 1) {
                            statusStack.removeFirst();
                            valueStack.removeFirst();
                            status = peekStatus(statusStack);
                        }
                        else{
                            status = S_IN_FINISHED_VALUE;
                        }
                        break;
                    default:
                        status = S_IN_ERROR;
                        break;
                    }//inner switch
                    break;
                    
                case S_PASSED_PAIR_KEY:
                    switch(token.type) {
                    case Yytoken.TYPE_COLON:
                        break;
                    case Yytoken.TYPE_VALUE:
                        statusStack.removeFirst();
                        String key = (String) valueStack.removeFirst();
                        Map parent = (Map) valueStack.getFirst();
                        parent.put(key,token.value);
                        status = peekStatus(statusStack);
                        break;
                    case Yytoken.TYPE_LEFT_SQUARE:
                        statusStack.removeFirst();
                        key = (String) valueStack.removeFirst();
                        parent = (Map) valueStack.getFirst();
                        List newArray = createArrayContainer(containerFactory);
                        parent.put(key,newArray);
                        status = S_IN_ARRAY;
                        statusStack.addFirst(new Integer(status));
                        valueStack.addFirst(newArray);
                        break;
                    case Yytoken.TYPE_LEFT_BRACE:
                        statusStack.removeFirst();
                        key = (String) valueStack.removeFirst();
                        parent = (Map) valueStack.getFirst();
                        Map newObject = createObjectContainer(containerFactory);
                        parent.put(key, newObject);
                        status = S_IN_OBJECT;
                        statusStack.addFirst(new Integer(status));
                        valueStack.addFirst(newObject);
                        break;
                    default:
                        status = S_IN_ERROR;
                    }
                    break;
                    
                case S_IN_ARRAY:
                    switch(token.type) {
                    case Yytoken.TYPE_COMMA:
                        break;
                    case Yytoken.TYPE_VALUE:
                        List val = (List) valueStack.getFirst();
                        val.add(token.value);
                        break;
                    case Yytoken.TYPE_RIGHT_SQUARE:
                        if (valueStack.size() > 1) {
                            statusStack.removeFirst();
                            valueStack.removeFirst();
                            status = peekStatus(statusStack);
                        }
                        else {
                            status = S_IN_FINISHED_VALUE;
                        }
                        break;
                    case Yytoken.TYPE_LEFT_BRACE:
                        val = (List) valueStack.getFirst();
                        Map newObject = createObjectContainer(containerFactory);
                        val.add(newObject);
                        status = S_IN_OBJECT;
                        statusStack.addFirst(new Integer(status));
                        valueStack.addFirst(newObject);
                        break;
                    case Yytoken.TYPE_LEFT_SQUARE:
                        val = (List) valueStack.getFirst();
                        List newArray = createArrayContainer(containerFactory);
                        val.add(newArray);
                        status = S_IN_ARRAY;
                        statusStack.addFirst(new Integer(status));
                        valueStack.addFirst(newArray);
                        break;
                    default:
                        status = S_IN_ERROR;
                    }//inner switch
                    break;
                case S_IN_ERROR:
                    throw new ParseException(getPosition(), 
                            ParseException.ERROR_UNEXPECTED_TOKEN, token);
                }//switch
                if (status == S_IN_ERROR) {
                    throw new ParseException(getPosition(), 
                            ParseException.ERROR_UNEXPECTED_TOKEN, token);
                }
            } while (token.type != Yytoken.TYPE_EOF);
        }
        catch (IOException ie) {
            throw ie;
        }
        
        throw new ParseException(getPosition(), 
                ParseException.ERROR_UNEXPECTED_TOKEN, token);
    }
    
    private void nextToken() throws ParseException, IOException {
        token = lexer.yylex();
        if (token == null) {
            token = new Yytoken(Yytoken.TYPE_EOF, null);
        }
    }
    
    private Map createObjectContainer(ContainerFactory containerFactory) {
        if (containerFactory == null) {
            return new JSONObject();
        }
        Map m = containerFactory.createObjectContainer();
        if (m == null) {
            return new JSONObject();
        }
        return m;
    }
    
    private List createArrayContainer(ContainerFactory containerFactory) {
        if (containerFactory == null) {
            return new JSONArray();
        }
        List l = containerFactory.creatArrayContainer();
        if (l == null) {
            return new JSONArray();
        }
        return l;
    }
    
    public void parse(String s, ContentHandler contentHandler) 
            throws ParseException {
        parse(s, contentHandler, false);
    }
    
    public void parse(String s, ContentHandler contentHandler, boolean isResume)
            throws ParseException{
        StringReader in = new StringReader(s);
        try{
            parse(in, contentHandler, isResume);
        }
        catch(IOException ie) {
            /*
             * Actually it will never happen.
             */
            throw new ParseException(-1, 
                    ParseException.ERROR_UNEXPECTED_EXCEPTION, ie);
        }
    }
    
    public void parse(Reader in, ContentHandler contentHandler) 
            throws IOException, ParseException {
        parse(in, contentHandler, false);
    }
    
    /**
     * Stream processing of JSON text.
     * 
     * @see ContentHandler
     * 
     * @param in  the input.
     * @param contentHandler  the content handler.
     * @param isResume  indicates if it continues previous parsing operation.
     *     If set to true, resume parsing the old stream, and parameter 'in' 
     *     will be ignored.  If this method is called for the first time in 
     *     this instance, isResume will be ignored.
     * 
     * @throws IOException if there is an I/O problem.
     * @throws ParseException if there is a parsing problem.
     */
    public void parse(Reader in, ContentHandler contentHandler, 
            boolean isResume) throws IOException, ParseException {
        if (!isResume) {
            reset(in);
            handlerStatusStack = new LinkedList();
        }
        else{
            if (handlerStatusStack == null) {
                isResume = false;
                reset(in);
                handlerStatusStack = new LinkedList();
            }
        }
        
        LinkedList statusStack = handlerStatusStack;    
        
        try {
            do {
                switch(status) {
                case S_INIT:
                    contentHandler.startJSON();
                    nextToken();
                    switch(token.type) {
                    case Yytoken.TYPE_VALUE:
                        status=S_IN_FINISHED_VALUE;
                        statusStack.addFirst(new Integer(status));
                        if (!contentHandler.primitive(token.value)) {
                            return;
                        }
                        break;
                    case Yytoken.TYPE_LEFT_BRACE:
                        status = S_IN_OBJECT;
                        statusStack.addFirst(new Integer(status));
                        if (!contentHandler.startObject()) {
                            return;
                        }
                        break;
                    case Yytoken.TYPE_LEFT_SQUARE:
                        status = S_IN_ARRAY;
                        statusStack.addFirst(new Integer(status));
                        if (!contentHandler.startArray()) {
                            return;
                        }
                        break;
                    default:
                        status = S_IN_ERROR;
                    }//inner switch
                    break;
                    
                case S_IN_FINISHED_VALUE:
                    nextToken();
                    if (token.type == Yytoken.TYPE_EOF) {
                        contentHandler.endJSON();
                        status = S_END;
                        return;
                    }
                    else {
                        status = S_IN_ERROR;
                        throw new ParseException(getPosition(), 
                                ParseException.ERROR_UNEXPECTED_TOKEN, token);
                    }
            
                case S_IN_OBJECT:
                    nextToken();
                    switch(token.type) {
                    case Yytoken.TYPE_COMMA:
                        break;
                    case Yytoken.TYPE_VALUE:
                        if (token.value instanceof String) {
                            String key = (String) token.value;
                            status = S_PASSED_PAIR_KEY;
                            statusStack.addFirst(new Integer(status));
                            if (!contentHandler.startObjectEntry(key)) {
                                return;
                            }
                        }
                        else {
                            status = S_IN_ERROR;
                        }
                        break;
                    case Yytoken.TYPE_RIGHT_BRACE:
                        if (statusStack.size() > 1) {
                            statusStack.removeFirst();
                            status = peekStatus(statusStack);
                        }
                        else{
                            status = S_IN_FINISHED_VALUE;
                        }
                        if (!contentHandler.endObject()) {
                            return;
                        }
                        break;
                    default:
                        status = S_IN_ERROR;
                        break;
                    }//inner switch
                    break;
                    
                case S_PASSED_PAIR_KEY:
                    nextToken();
                    switch(token.type) {
                    case Yytoken.TYPE_COLON:
                        break;
                    case Yytoken.TYPE_VALUE:
                        statusStack.removeFirst();
                        status = peekStatus(statusStack);
                        if (!contentHandler.primitive(token.value)) {
                            return;
                        }
                        if (!contentHandler.endObjectEntry()) {
                            return;
                        }
                        break;
                    case Yytoken.TYPE_LEFT_SQUARE:
                        statusStack.removeFirst();
                        statusStack.addFirst(new Integer(S_IN_PAIR_VALUE));
                        status = S_IN_ARRAY;
                        statusStack.addFirst(new Integer(status));
                        if (!contentHandler.startArray()) {
                            return;
                        }
                        break;
                    case Yytoken.TYPE_LEFT_BRACE:
                        statusStack.removeFirst();
                        statusStack.addFirst(new Integer(S_IN_PAIR_VALUE));
                        status = S_IN_OBJECT;
                        statusStack.addFirst(new Integer(status));
                        if (!contentHandler.startObject()) {
                            return;
                        }
                        break;
                    default:
                        status = S_IN_ERROR;
                    }
                    break;
                
                case S_IN_PAIR_VALUE:
                    /*
                     * S_IN_PAIR_VALUE is just a marker to indicate the end of 
                     * an object entry, it doesn't proccess any token,
                     * therefore delay consuming token until next round.
                     */
                    statusStack.removeFirst();
                    status = peekStatus(statusStack);
                    if (!contentHandler.endObjectEntry()) {
                        return;
                    }
                    break;
                    
                case S_IN_ARRAY:
                    nextToken();
                    switch(token.type){
                    case Yytoken.TYPE_COMMA:
                        break;
                    case Yytoken.TYPE_VALUE:
                        if (!contentHandler.primitive(token.value)) {
                            return;
                        }
                        break;
                    case Yytoken.TYPE_RIGHT_SQUARE:
                        if (statusStack.size() > 1) {
                            statusStack.removeFirst();
                            status = peekStatus(statusStack);
                        }
                        else{
                            status = S_IN_FINISHED_VALUE;
                        }
                        if (!contentHandler.endArray()) {
                            return;
                        }
                        break;
                    case Yytoken.TYPE_LEFT_BRACE:
                        status = S_IN_OBJECT;
                        statusStack.addFirst(new Integer(status));
                        if (!contentHandler.startObject()) {
                            return;
                        }
                        break;
                    case Yytoken.TYPE_LEFT_SQUARE:
                        status = S_IN_ARRAY;
                        statusStack.addFirst(new Integer(status));
                        if (!contentHandler.startArray()) {
                            return;
                        }
                        break;
                    default:
                        status = S_IN_ERROR;
                    }//inner switch
                    break;
                    
                case S_END:
                    return;
                    
                case S_IN_ERROR:
                    throw new ParseException(getPosition(), 
                            ParseException.ERROR_UNEXPECTED_TOKEN, token);
                }//switch
                if (status == S_IN_ERROR) {
                    throw new ParseException(getPosition(), 
                            ParseException.ERROR_UNEXPECTED_TOKEN, token);
                }
            } while(token.type != Yytoken.TYPE_EOF);
        }
        catch (IOException ie) {
            status = S_IN_ERROR;
            throw ie;
        }
        catch (ParseException pe) {
            status = S_IN_ERROR;
            throw pe;
        }
        catch (RuntimeException re) {
            status = S_IN_ERROR;
            throw re;
        }
        catch (Error e) {
            status = S_IN_ERROR;
            throw e;
        }
        
        status = S_IN_ERROR;
        throw new ParseException(getPosition(), 
                ParseException.ERROR_UNEXPECTED_TOKEN, token);
    }
}
