package org.teiid.resource.adapter.google.dataprotocol;

import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.LinkedHashMap;
import java.util.List;

import org.teiid.core.util.StringUtil;
import org.teiid.resource.adapter.google.common.SpreadsheetOperationException;

/**
 * Parsing google json is a little non-standard.  They assume a js binding, so array syntax, strings, and date are used.
 * This parser supports most of the customizations except for unquoted dictionary keys.
 * 
 * Assumes all numbers are properly represented by Double.
 */
public class GoogleJSONParser {

	private static final class ReaderCharSequence implements CharSequence {
		Reader r;
		int i = -1;

		@Override
		public CharSequence subSequence(int start, int end) {
			throw new UnsupportedOperationException();
		}

		@Override
		public int length() {
			return Integer.MAX_VALUE;
		}

		@Override
		public char charAt(int index) {
			if (index != ++i) {
				throw new IllegalStateException();
			}
			int result;
			try {
				result = r.read();
			} catch (IOException e) {
				throw new SpreadsheetOperationException(e);
			}
			if (result == -1) {
				throw new SpreadsheetOperationException("Read end of stream before the end of a string value");
			}
			return (char)result;
		}
	}

	private Calendar cal;
	private int[] parts = new int[7];
	private StringBuilder sb = new StringBuilder();
	private ReaderCharSequence charSequence = new ReaderCharSequence();
	
	public Object parseObject(Reader r, boolean wrapped) throws IOException {
		if (wrapped) {
			while (true) {
				int c = r.read();
				if (c == -1) {
					return null;
				}
				if (c == '(') {
					break;
				}
			}
		}
		return parseObject(new PushbackReader(r), skipWhitespace(r));
	}

	private Object parseObject(PushbackReader r, int c) throws IOException {
		switch (c) {
		case '{':
			LinkedHashMap<String, Object> map = new LinkedHashMap<String, Object>();
			while (true) {
				c = skipWhitespace(r);
				switch (c) {
				case '}': 
					return map;
				case ',':
					//this is lenient
					continue;
				}
				String s = parseString(r, c);
				c = skipWhitespace(r);
				if (c != ':') {
					throw new SpreadsheetOperationException("Expected : in object name value pair");
				}
				c = skipWhitespace(r);
				Object o = parseObject(r, c);
				map.put(s, o);
			}
		case '[':
			List<Object> array = new ArrayList<Object>();
			boolean seenComma = true;
			while (true) {
				c = skipWhitespace(r);
				switch (c) {
				case ',': //special handling for google arrays
					if (seenComma) {
						array.add(null);						
					}
					seenComma = true;
					break;
				case ']':
					return array;
				default:
					seenComma = false;
					Object o = parseObject(r, c);
					array.add(o);
				}
			}
		case -1:
			return null;
		case '"':
		case '\'':
			return parseString(r, c);
		default:
			return parseLiteral(r, (char)c);
		}
	}

	private Object parseLiteral(PushbackReader r, char c) throws IOException {
		sb.setLength(0);
		do {
			sb.append(c);
			int i = r.read();
			if (i == -1) {
				break;
			}
			c = (char)i;
		} while (!Character.isWhitespace(c) && c != ',' && c != ']' && c != '}');
		
		//date handling
		if (areEquals(sb, "new")) { //$NON-NLS-1$
			sb.setLength(0);
			int length = 0;
			Arrays.fill(parts, 0);
			for (int i = 0; ; i++) {
				if (i > 5) { //remove " Date("
					if (c == ',' || c == ')') {
						if (length > 6) {
							throw new SpreadsheetOperationException("Too many date fields");
						}
						parts[length++] = Integer.valueOf(sb.toString());
						if (c == ')') {
							break;
						}
						sb.setLength(0);
					} else {
						sb.append(c);
					}
				}
				int chr = r.read();
				if (chr == -1) {
					throw new SpreadsheetOperationException("Encountered end of stream in date value");
				}
				c = (char)chr;
			}
			if (length > 3) {
				Calendar calendar = getCalendar();
				calendar.set(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5]);
				Timestamp ts = new Timestamp(calendar.getTimeInMillis());
				ts.setNanos(parts[6]*1000000); //convert from millis to nanos
				return ts;
			}
			Calendar calendar = getCalendar();
			calendar.set(parts[0], parts[1], parts[2]);
			return new java.sql.Date(cal.getTimeInMillis());
		}
		if (!Character.isWhitespace(c)) { 
			r.unread(c); //the terminating character is still needed by the caller
			//TODO could hold this state so that a pushback reader is not needed
		}		
		if (areEquals(sb, "false")) { //$NON-NLS-1$
			return Boolean.FALSE;
		} else if (areEquals(sb, "true")) { //$NON-NLS-1$
			return Boolean.TRUE;
		} else if (areEquals(sb, "null")) { //$NON-NLS-1$
			return null;
		}
		return Double.valueOf(sb.toString());
	}
	
	private boolean areEquals(CharSequence cs, CharSequence cs1) {
		if (cs.length() != cs1.length()) {
			return false;
		}
		for (int i = 0; i < cs.length(); i++) {
			if (cs.charAt(i) != cs1.charAt(i)) {
				return false;
			}
		}
		return true;
	}
	
	Calendar getCalendar() {
		if (cal == null) {
			cal = Calendar.getInstance();
		}
		cal.clear();
		return cal;
	}
	
	public void setCalendar(Calendar cal) {
		this.cal = cal;
	}
	
	private String parseString(final Reader r, int quoteChar) {
		if (quoteChar != '"' && quoteChar != '\'') {
    		throw new IllegalStateException();
		}
		charSequence.i = -1;
		charSequence.r = r;	
		sb.setLength(0);
		return StringUtil.unescape(charSequence, quoteChar, false, sb);
	}

	private int skipWhitespace(Reader r) throws IOException {
		while (true) {
			int c = r.read();
			if (c == -1) {
				return c;
			}
			if (!Character.isWhitespace((char)c)) {
				return c;
			}
		}
	}
	
}