package eu.dnetlib.utils;

import java.text.FieldPosition;
import java.text.Format;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class JsonFormat extends Format {
	private class JsonPair {
		private final String name;
		private final Object value;
		
		public JsonPair(final String name, final Object value) {
			this.name = name;
			this.value = value;
		}
		
		public String getName() {
			return name;
		}
		
		public Object getValue() {
			return value;
		}
	}
	
	private static final long serialVersionUID = 1L;

	@Override
	@SuppressWarnings("unchecked")
	public StringBuffer format(final Object object, final StringBuffer toAppendTo, final FieldPosition position) {
		if (object == null)
			toAppendTo.append("null");
		else if (object instanceof Boolean)
			toAppendTo.append(object);
		else if (object instanceof List<?>)
			formatJsonArray((List<Object>) object, toAppendTo);
		else if (object instanceof Number)
			formatJsonNumber((Number) object, toAppendTo);
		else if (object instanceof Map<?, ?>)
			formatJsonObject((Map<String, Object>) object, toAppendTo);
		else if (object instanceof String)
			formatJsonString((String) object, toAppendTo);
		else
			throw new IllegalArgumentException();
		return toAppendTo;
	}

	@Override
	public Object parseObject(final String source, final ParsePosition position) {
		if (position.getIndex() >= source.length()) { // premature end of source
			position.setErrorIndex(position.getIndex());
			return null;
		}
		if (source.startsWith("null", position.getIndex())) {
			position.setIndex(position.getIndex() + 4);
			return null;
		}
		if (source.startsWith("true", position.getIndex())) {
			position.setIndex(position.getIndex() + 4);
			return Boolean.TRUE;
		}
		if (source.startsWith("false", position.getIndex())) {
			position.setIndex(position.getIndex() + 5);
			return Boolean.FALSE;
		}
		final int beginIndex = position.getIndex();
		Object value = null;
		switch (source.charAt(position.getIndex())) {
		case '[':
			value = parseJsonArray(source, position);
			break;
		case '{':
			value = parseJsonObject(source, position);
			break;
		case '"':
			value = parseJsonString(source, position);
			break;
		default:
			value = parseJsonNumber(source, position);
		}
		if (position.getErrorIndex() != -1)
			position.setIndex(beginIndex);
		return value;
	}

	private void formatJsonArray(final List<Object> array, final StringBuffer toAppendTo) {
		toAppendTo.append('[');
		for (Object object : array) {
			format(object, toAppendTo, null);
			toAppendTo.append(',');
		}
		if (array.size() > 0)
			toAppendTo.setLength(toAppendTo.length() - 1);
		toAppendTo.append(']');
	}

	private void formatJsonNumber(final Number number, final StringBuffer toAppendTo) {
		toAppendTo.append(number);
	}
	
	private void formatJsonObject(final Map<String, Object> object, final StringBuffer toAppendTo) {
		toAppendTo.append('{');
		for (Map.Entry<?, ?> pair : object.entrySet()) {
			if (pair.getKey() instanceof String)
				formatJsonString((String) pair.getKey(), toAppendTo);
			else
				throw new IllegalArgumentException();
			toAppendTo.append(':');
			format(pair.getValue(), toAppendTo, null);
			toAppendTo.append(',');
		}
		if (object.size() > 0)
			toAppendTo.setLength(toAppendTo.length() - 1);
		toAppendTo.append('}');
	}

	private void formatJsonString(final String string, final StringBuffer toAppendTo) {
		toAppendTo.append('"');
		for (int i = 0; i < string.length(); i++) {
			switch (string.charAt(i)) {
			case '"':
				toAppendTo.append("\\\"");
				break;
			case '\\':
				toAppendTo.append("\\\\");
				break;
			case '\b':
				toAppendTo.append("\\b");
				break;
			case '\f':
				toAppendTo.append("\\f");
				break;
			case '\n':
				toAppendTo.append("\\n");
				break;
			case '\r':
				toAppendTo.append("\\r");
				break;
			case '\t':
				toAppendTo.append("\\t");
				break;
			default:
				toAppendTo.append(string.charAt(i));
			}
		}
		toAppendTo.append('"');
	}

	private List<Object> parseJsonArray(final String source, final ParsePosition position) {
		if ((position.getIndex() >= source.length()) || (source.charAt(position.getIndex()) != '[')) { // premature end of source or no opening bracket found
			position.setErrorIndex(position.getIndex());
			return null;
		}
		position.setIndex(position.getIndex() + 1);
		final List<Object> array = new ArrayList<Object>();
		if ((position.getIndex() < source.length()) && (source.charAt(position.getIndex()) != ']')) { // next character is not closing bracket; elements exist
			parseJsonElements(source, position, array);
			if (position.getErrorIndex() != -1)
				return null;
		}
		if ((position.getIndex() >= source.length()) || (source.charAt(position.getIndex()) != ']')) { // premature end of source or no closing bracket found
			position.setErrorIndex(position.getIndex());
			return null;
		}
		position.setIndex(position.getIndex() + 1);
		return array;
	}
	
	private void parseJsonElements(final String source, final ParsePosition position, final List<Object> elements) {
		final Object value = parseObject(source, position);
		if (position.getErrorIndex() != -1)
			return;
		elements.add(value);
		if ((position.getIndex() < source.length()) && (source.charAt(position.getIndex()) == ',')) { // next character is a comma; more elements 
			position.setIndex(position.getIndex() + 1);
			parseJsonElements(source, position, elements);
				if (position.getErrorIndex() != -1) // no more elements found
					return;
		}
	}
	
	private Number parseJsonNumber(final String source, final ParsePosition position) {
		if (position.getIndex() >= source.length()) { // premature end of input
			position.setErrorIndex(position.getIndex());
			return null;
		}
		return NumberFormat.getInstance().parse(source, position);
	}
	
	private Map<String, Object> parseJsonObject(final String source, final ParsePosition position) {
		if ((position.getIndex() >= source.length()) || (source.charAt(position.getIndex()) != '{')) { // premature end of source or no opening brace found
			position.setErrorIndex(position.getIndex());
			return null;
		}
		position.setIndex(position.getIndex() + 1);
		final Map<String, Object> object = new HashMap<String, Object>();
		if ((position.getIndex() < source.length()) && (source.charAt(position.getIndex()) != '}')) { // next character is not closing brace; members exist
			parseJsonMembers(source, position, object);
			if (position.getErrorIndex() != -1)
				return null;
		}
		if ((position.getIndex() >= source.length()) || (source.charAt(position.getIndex()) != '}')) { // premature end of source or no closing brace found
			position.setErrorIndex(position.getIndex());
			return null;
		}
		position.setIndex(position.getIndex() + 1);
		return object;
	}
	
	private void parseJsonMembers(final String source, final ParsePosition position, final Map<String, Object> members) {
		final JsonPair pair = parseJsonPair(source, position);
		if (position.getErrorIndex() != -1) // no pair found
			return;
		members.put(pair.getName(), pair.getValue());
		if ((position.getIndex() < source.length()) && (source.charAt(position.getIndex()) == ',')) { // next character is a comma; more members follow
			position.setIndex(position.getIndex() + 1);
			parseJsonMembers(source, position, members);
			if (position.getErrorIndex() != -1) // no more members found
				return;
		}
	}
	
	private JsonPair parseJsonPair(final String source, final ParsePosition position) {
		final String name = parseJsonString(source, position);
		if (position.getErrorIndex() != -1) // no name found
			return null;
		if ((position.getIndex() >= source.length()) || (source.charAt(position.getIndex()) != ':')) { // premature end of input or no colon found
			position.setErrorIndex(position.getIndex());
			return null;
		}
		position.setIndex(position.getIndex() + 1);
		final Object value = parseObject(source, position);
		if (position.getErrorIndex() != -1) // no value found
			return null;
		return new JsonPair(name, value);
	}
	
	private String parseJsonString(final String source, final ParsePosition position) {
		if ((position.getIndex() >= source.length()) || (source.charAt(position.getIndex()) != '"')) { // premature end of source or no opening quote found
			position.setErrorIndex(position.getIndex());
			return null;
		}
		final StringBuilder string = new StringBuilder();
		for (int i = position.getIndex() + 1; i < source.length(); i++) {
			switch (source.charAt(i)) {
			case '"': // closing quote found; JSON string ended
				position.setIndex(i + 1);
				return string.toString();
			case '\\': // escape character
				if (++i == source.length()) { // no next character found
					position.setErrorIndex(i);
					return null;
				}
				switch(source.charAt(i)) {
				case '"': // quote
					string.append('"');
					break;
				case '\\': // backslash
					string.append('\\');
					break;
				case 'b': // backspace
					string.append('\b');
					break;
				case 'f': // form feed
					string.append('\f');
					break;
				case 'n': // newline
					string.append('\n');
					break;
				case 'r': // carriage return
					string.append('\n');
					break;
				case 't': // horizontal tab
					string.append('\t');
					break;
				default: // invalid escape character
					position.setErrorIndex(i);
					return null;
				}
				break;
			default:
				string.append(source.charAt(i));
			}
		} // source ended without finding closing quote
		position.setErrorIndex(source.length());
		return null;
	}
}
