/*
 * $Id: JSONParser.java,v 1.1 2006/04/15 14:10:48 platform Exp $
 * Created on 2006-4-15
 */
package org.json.simple.parser;

import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

import rejava.io.Reader;
import rejava.io.StringReader;

/**
 * Parser for JSON text. Please note that JSONParser is NOT thread-safe.
 * 
 * @author FangYidong<fangyidong@yahoo.com.cn>
 */
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 final Yylex lexer = new Yylex((Reader) null);
	private Yytoken token = null;
	private int status = JSONParser.S_INIT;

	private int peekStatus(final LinkedList<?> statusStack) {
		if (statusStack.size() == 0) {
			return -1;
		}
		final Integer status = (Integer) statusStack.getFirst();
		return status.intValue();
	}

	/**
	 * Reset the parser to the initial state without resetting the underlying
	 * reader.
	 * 
	 */
	public void reset() {
		this.token = null;
		this.status = JSONParser.S_INIT;
		this.handlerStatusStack = null;
	}

	/**
	 * Reset the parser to the initial state with a new character reader.
	 * 
	 * @param in
	 *            - The new character reader.
	 * @throws IOException
	 * @throws ParseException
	 */
	public void reset(final Reader in) {
		this.lexer.yyreset(in);
		this.reset();
	}

	/**
	 * @return The position of the beginning of the current token.
	 */
	public int getPosition() {
		return this.lexer.getPosition();
	}

	public Object parse(final String s) throws ParseException {
		return this.parse(s, (ContainerFactory) null);
	}

	public Object parse(final String s, final ContainerFactory containerFactory)
			throws ParseException {
		final StringReader in = new StringReader(s);
		try {
			return this.parse(in, containerFactory);
		} catch (final IOException ie) {
			/*
			 * Actually it will never happen.
			 */
			throw new ParseException(-1, ParseException.ERROR_UNEXPECTED_EXCEPTION, ie);
		}
	}

	public Object parse(final Reader in) throws IOException, ParseException {
		return this.parse(in, (ContainerFactory) null);
	}

	/**
	 * Parse JSON text into java object from the input source.
	 * 
	 * @param in
	 * @param containerFactory
	 *            - Use this factory to createyour own JSON object and JSON
	 *            array containers.
	 * @return Instance of the following: org.json.simple.JSONObject,
	 *         org.json.simple.JSONArray, java.lang.String, java.lang.Number,
	 *         java.lang.Boolean, null
	 * 
	 * @throws IOException
	 * @throws ParseException
	 */
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public Object parse(final Reader in, final ContainerFactory containerFactory)
			throws IOException, ParseException {
		this.reset(in);
		final LinkedList statusStack = new LinkedList();
		final LinkedList valueStack = new LinkedList();

		try {
			do {
				this.nextToken();
				switch (this.status) {
				case S_INIT:
					switch (this.token.type) {
					case Yytoken.TYPE_VALUE:
						this.status = JSONParser.S_IN_FINISHED_VALUE;
						statusStack.addFirst(new Integer(this.status));
						valueStack.addFirst(this.token.value);
						break;
					case Yytoken.TYPE_LEFT_BRACE:
						this.status = JSONParser.S_IN_OBJECT;
						statusStack.addFirst(new Integer(this.status));
						valueStack.addFirst(this.createObjectContainer(containerFactory));
						break;
					case Yytoken.TYPE_LEFT_SQUARE:
						this.status = JSONParser.S_IN_ARRAY;
						statusStack.addFirst(new Integer(this.status));
						valueStack.addFirst(this.createArrayContainer(containerFactory));
						break;
					default:
						this.status = JSONParser.S_IN_ERROR;
					}// inner switch
					break;

				case S_IN_FINISHED_VALUE:
					if (this.token.type == Yytoken.TYPE_EOF) {
						return valueStack.removeFirst();
					} else {
						throw new ParseException(this.getPosition(),
								ParseException.ERROR_UNEXPECTED_TOKEN, this.token);
					}

				case S_IN_OBJECT:
					switch (this.token.type) {
					case Yytoken.TYPE_COMMA:
						break;
					case Yytoken.TYPE_VALUE:
						if (this.token.value instanceof String) {
							final String key = (String) this.token.value;
							valueStack.addFirst(key);
							this.status = JSONParser.S_PASSED_PAIR_KEY;
							statusStack.addFirst(new Integer(this.status));
						} else {
							this.status = JSONParser.S_IN_ERROR;
						}
						break;
					case Yytoken.TYPE_RIGHT_BRACE:
						if (valueStack.size() > 1) {
							statusStack.removeFirst();
							valueStack.removeFirst();
							this.status = this.peekStatus(statusStack);
						} else {
							this.status = JSONParser.S_IN_FINISHED_VALUE;
						}
						break;
					default:
						this.status = JSONParser.S_IN_ERROR;
						break;
					}// inner switch
					break;

				case S_PASSED_PAIR_KEY:
					switch (this.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, this.token.value);
						this.status = this.peekStatus(statusStack);
						break;
					case Yytoken.TYPE_LEFT_SQUARE:
						statusStack.removeFirst();
						key = (String) valueStack.removeFirst();
						parent = (Map) valueStack.getFirst();
						final List newArray = this.createArrayContainer(containerFactory);
						parent.put(key, newArray);
						this.status = JSONParser.S_IN_ARRAY;
						statusStack.addFirst(new Integer(this.status));
						valueStack.addFirst(newArray);
						break;
					case Yytoken.TYPE_LEFT_BRACE:
						statusStack.removeFirst();
						key = (String) valueStack.removeFirst();
						parent = (Map) valueStack.getFirst();
						final Map newObject = this.createObjectContainer(containerFactory);
						parent.put(key, newObject);
						this.status = JSONParser.S_IN_OBJECT;
						statusStack.addFirst(new Integer(this.status));
						valueStack.addFirst(newObject);
						break;
					default:
						this.status = JSONParser.S_IN_ERROR;
					}
					break;

				case S_IN_ARRAY:
					switch (this.token.type) {
					case Yytoken.TYPE_COMMA:
						break;
					case Yytoken.TYPE_VALUE:
						List val = (List) valueStack.getFirst();
						val.add(this.token.value);
						break;
					case Yytoken.TYPE_RIGHT_SQUARE:
						if (valueStack.size() > 1) {
							statusStack.removeFirst();
							valueStack.removeFirst();
							this.status = this.peekStatus(statusStack);
						} else {
							this.status = JSONParser.S_IN_FINISHED_VALUE;
						}
						break;
					case Yytoken.TYPE_LEFT_BRACE:
						val = (List) valueStack.getFirst();
						final Map newObject = this.createObjectContainer(containerFactory);
						val.add(newObject);
						this.status = JSONParser.S_IN_OBJECT;
						statusStack.addFirst(new Integer(this.status));
						valueStack.addFirst(newObject);
						break;
					case Yytoken.TYPE_LEFT_SQUARE:
						val = (List) valueStack.getFirst();
						final List newArray = this.createArrayContainer(containerFactory);
						val.add(newArray);
						this.status = JSONParser.S_IN_ARRAY;
						statusStack.addFirst(new Integer(this.status));
						valueStack.addFirst(newArray);
						break;
					default:
						this.status = JSONParser.S_IN_ERROR;
					}// inner switch
					break;
				case S_IN_ERROR:
					throw new ParseException(this.getPosition(),
							ParseException.ERROR_UNEXPECTED_TOKEN, this.token);
				}// switch
				if (this.status == JSONParser.S_IN_ERROR) {
					throw new ParseException(this.getPosition(),
							ParseException.ERROR_UNEXPECTED_TOKEN, this.token);
				}
			} while (this.token.type != Yytoken.TYPE_EOF);
		} catch (final IOException ie) {
			throw ie;
		}

		throw new ParseException(this.getPosition(), ParseException.ERROR_UNEXPECTED_TOKEN,
				this.token);
	}

	private void nextToken() throws ParseException, IOException {
		this.token = this.lexer.yylex();
		if (this.token == null) {
			this.token = new Yytoken(Yytoken.TYPE_EOF, null);
		}
	}

	private Map<?, ?> createObjectContainer(final ContainerFactory containerFactory) {
		if (containerFactory == null) {
			return new JSONObject();
		}
		final Map<?, ?> m = containerFactory.createObjectContainer();

		if (m == null) {
			return new JSONObject();
		}
		return m;
	}

	private List<?> createArrayContainer(final ContainerFactory containerFactory) {
		if (containerFactory == null) {
			return new JSONArray();
		}
		final List<?> l = containerFactory.creatArrayContainer();

		if (l == null) {
			return new JSONArray();
		}
		return l;
	}

	public void parse(final String s, final ContentHandler contentHandler) throws ParseException {
		this.parse(s, contentHandler, false);
	}

	public void parse(final String s, final ContentHandler contentHandler, final boolean isResume)
			throws ParseException {
		final StringReader in = new StringReader(s);
		try {
			this.parse(in, contentHandler, isResume);
		} catch (final IOException ie) {
			/*
			 * Actually it will never happen.
			 */
			throw new ParseException(-1, ParseException.ERROR_UNEXPECTED_EXCEPTION, ie);
		}
	}

	public void parse(final Reader in, final ContentHandler contentHandler) throws IOException,
			ParseException {
		this.parse(in, contentHandler, false);
	}

	/**
	 * Stream processing of JSON text.
	 * 
	 * @see ContentHandler
	 * 
	 * @param in
	 * @param contentHandler
	 * @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
	 * @throws ParseException
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public void parse(final Reader in, final ContentHandler contentHandler, boolean isResume)
			throws IOException, ParseException {
		if (!isResume) {
			this.reset(in);
			this.handlerStatusStack = new LinkedList<>();
		} else {
			if (this.handlerStatusStack == null) {
				isResume = false;
				this.reset(in);
				this.handlerStatusStack = new LinkedList<>();
			}
		}

		final LinkedList statusStack = this.handlerStatusStack;

		try {
			do {
				switch (this.status) {
				case S_INIT:
					contentHandler.startJSON();
					this.nextToken();
					switch (this.token.type) {
					case Yytoken.TYPE_VALUE:
						this.status = JSONParser.S_IN_FINISHED_VALUE;
						statusStack.addFirst(new Integer(this.status));
						if (!contentHandler.primitive(this.token.value)) {
							return;
						}
						break;
					case Yytoken.TYPE_LEFT_BRACE:
						this.status = JSONParser.S_IN_OBJECT;
						statusStack.addFirst(new Integer(this.status));
						if (!contentHandler.startObject()) {
							return;
						}
						break;
					case Yytoken.TYPE_LEFT_SQUARE:
						this.status = JSONParser.S_IN_ARRAY;
						statusStack.addFirst(new Integer(this.status));
						if (!contentHandler.startArray()) {
							return;
						}
						break;
					default:
						this.status = JSONParser.S_IN_ERROR;
					}// inner switch
					break;

				case S_IN_FINISHED_VALUE:
					this.nextToken();
					if (this.token.type == Yytoken.TYPE_EOF) {
						contentHandler.endJSON();
						this.status = JSONParser.S_END;
						return;
					} else {
						this.status = JSONParser.S_IN_ERROR;
						throw new ParseException(this.getPosition(),
								ParseException.ERROR_UNEXPECTED_TOKEN, this.token);
					}

				case S_IN_OBJECT:
					this.nextToken();
					switch (this.token.type) {
					case Yytoken.TYPE_COMMA:
						break;
					case Yytoken.TYPE_VALUE:
						if (this.token.value instanceof String) {
							final String key = (String) this.token.value;
							this.status = JSONParser.S_PASSED_PAIR_KEY;
							statusStack.addFirst(new Integer(this.status));
							if (!contentHandler.startObjectEntry(key)) {
								return;
							}
						} else {
							this.status = JSONParser.S_IN_ERROR;
						}
						break;
					case Yytoken.TYPE_RIGHT_BRACE:
						if (statusStack.size() > 1) {
							statusStack.removeFirst();
							this.status = this.peekStatus(statusStack);
						} else {
							this.status = JSONParser.S_IN_FINISHED_VALUE;
						}
						if (!contentHandler.endObject()) {
							return;
						}
						break;
					default:
						this.status = JSONParser.S_IN_ERROR;
						break;
					}// inner switch
					break;

				case S_PASSED_PAIR_KEY:
					this.nextToken();
					switch (this.token.type) {
					case Yytoken.TYPE_COLON:
						break;
					case Yytoken.TYPE_VALUE:
						statusStack.removeFirst();
						this.status = this.peekStatus(statusStack);
						if (!contentHandler.primitive(this.token.value)) {
							return;
						}
						if (!contentHandler.endObjectEntry()) {
							return;
						}
						break;
					case Yytoken.TYPE_LEFT_SQUARE:
						statusStack.removeFirst();
						statusStack.addFirst(new Integer(JSONParser.S_IN_PAIR_VALUE));
						this.status = JSONParser.S_IN_ARRAY;
						statusStack.addFirst(new Integer(this.status));
						if (!contentHandler.startArray()) {
							return;
						}
						break;
					case Yytoken.TYPE_LEFT_BRACE:
						statusStack.removeFirst();
						statusStack.addFirst(new Integer(JSONParser.S_IN_PAIR_VALUE));
						this.status = JSONParser.S_IN_OBJECT;
						statusStack.addFirst(new Integer(this.status));
						if (!contentHandler.startObject()) {
							return;
						}
						break;
					default:
						this.status = JSONParser.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();
					this.status = this.peekStatus(statusStack);
					if (!contentHandler.endObjectEntry()) {
						return;
					}
					break;

				case S_IN_ARRAY:
					this.nextToken();
					switch (this.token.type) {
					case Yytoken.TYPE_COMMA:
						break;
					case Yytoken.TYPE_VALUE:
						if (!contentHandler.primitive(this.token.value)) {
							return;
						}
						break;
					case Yytoken.TYPE_RIGHT_SQUARE:
						if (statusStack.size() > 1) {
							statusStack.removeFirst();
							this.status = this.peekStatus(statusStack);
						} else {
							this.status = JSONParser.S_IN_FINISHED_VALUE;
						}
						if (!contentHandler.endArray()) {
							return;
						}
						break;
					case Yytoken.TYPE_LEFT_BRACE:
						this.status = JSONParser.S_IN_OBJECT;
						statusStack.addFirst(new Integer(this.status));
						if (!contentHandler.startObject()) {
							return;
						}
						break;
					case Yytoken.TYPE_LEFT_SQUARE:
						this.status = JSONParser.S_IN_ARRAY;
						statusStack.addFirst(new Integer(this.status));
						if (!contentHandler.startArray()) {
							return;
						}
						break;
					default:
						this.status = JSONParser.S_IN_ERROR;
					}// inner switch
					break;

				case S_END:
					return;

				case S_IN_ERROR:
					throw new ParseException(this.getPosition(),
							ParseException.ERROR_UNEXPECTED_TOKEN, this.token);
				}// switch
				if (this.status == JSONParser.S_IN_ERROR) {
					throw new ParseException(this.getPosition(),
							ParseException.ERROR_UNEXPECTED_TOKEN, this.token);
				}
			} while (this.token.type != Yytoken.TYPE_EOF);
		} catch (final IOException ie) {
			this.status = JSONParser.S_IN_ERROR;
			throw ie;
		} catch (final ParseException pe) {
			this.status = JSONParser.S_IN_ERROR;
			throw pe;
		} catch (final RuntimeException re) {
			this.status = JSONParser.S_IN_ERROR;
			throw re;
		} catch (final Error e) {
			this.status = JSONParser.S_IN_ERROR;
			throw e;
		}

		this.status = JSONParser.S_IN_ERROR;
		throw new ParseException(this.getPosition(), ParseException.ERROR_UNEXPECTED_TOKEN,
				this.token);
	}
}