/
com/planet_ink/coffee_mud/Abilities/Common/
com/planet_ink/coffee_mud/Abilities/Diseases/
com/planet_ink/coffee_mud/Abilities/Druid/
com/planet_ink/coffee_mud/Abilities/Fighter/
com/planet_ink/coffee_mud/Abilities/Languages/
com/planet_ink/coffee_mud/Abilities/Misc/
com/planet_ink/coffee_mud/Abilities/Prayers/
com/planet_ink/coffee_mud/Abilities/Properties/
com/planet_ink/coffee_mud/Abilities/Skills/
com/planet_ink/coffee_mud/Abilities/Songs/
com/planet_ink/coffee_mud/Abilities/Spells/
com/planet_ink/coffee_mud/Abilities/Thief/
com/planet_ink/coffee_mud/Abilities/Traps/
com/planet_ink/coffee_mud/Behaviors/
com/planet_ink/coffee_mud/CharClasses/interfaces/
com/planet_ink/coffee_mud/Commands/
com/planet_ink/coffee_mud/Commands/interfaces/
com/planet_ink/coffee_mud/Common/
com/planet_ink/coffee_mud/Common/interfaces/
com/planet_ink/coffee_mud/Exits/interfaces/
com/planet_ink/coffee_mud/Items/Armor/
com/planet_ink/coffee_mud/Items/Basic/
com/planet_ink/coffee_mud/Items/CompTech/
com/planet_ink/coffee_mud/Items/MiscMagic/
com/planet_ink/coffee_mud/Items/Weapons/
com/planet_ink/coffee_mud/Items/interfaces/
com/planet_ink/coffee_mud/Libraries/
com/planet_ink/coffee_mud/Libraries/interfaces/
com/planet_ink/coffee_mud/Locales/
com/planet_ink/coffee_mud/MOBS/
com/planet_ink/coffee_mud/Races/
com/planet_ink/coffee_mud/Races/interfaces/
com/planet_ink/coffee_mud/WebMacros/
com/planet_ink/coffee_mud/WebMacros/interfaces/
com/planet_ink/coffee_mud/core/
com/planet_ink/coffee_mud/core/collections/
com/planet_ink/coffee_mud/core/interfaces/
com/planet_ink/coffee_mud/core/intermud/
com/planet_ink/coffee_mud/core/intermud/i3/
com/planet_ink/coffee_web/server/
com/planet_ink/siplet/applet/
lib/
resources/factions/
resources/fakedb/
resources/progs/autoplayer/
resources/quests/holidays/
web/
web/admin.templates/
web/admin/grinder/
web/admin/images/
web/clan.templates/
web/pub.templates/
web/pub/images/mxp/
web/pub/sounds/
web/pub/textedit/
package com.planet_ink.siplet.support;

import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.TreeMap;
/*
   Copyright 2013-2016 Bo Zimmerman

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

	   http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
/**
 * A JSON parser.
 * Not much to say.  It can take a valid json string and generate a standing
 * object that represents the document string, and also generate a string
 * from such an object.
 * @author Bo Zimmerman
 *
 */
public class MiniJSON
{
	private enum ObjectParseState
	{
		INITIAL, NEEDKEY, GOTKEY, NEEDOBJECT, GOTOBJECT
	}

	private enum NumberParseState
	{
		INITIAL, NEEDN0DIGIT, HAVEDIGIT, NEEDDOT, NEEDDOTDIGIT, HAVEDOTDIGIT, HAVEE, HAVEEDIGIT
	}

	private enum ArrayParseState
	{
		INITIAL, EXPECTOBJECT, NEEDOBJECT, GOTOBJECT
	}

	/**
	 * The official definition of "null" for a JSON object
	 */
	public static final Object	NULL	= new Object();

	/**
	 * An official MiniJSON parsing exception. It means the document being parsed was malformed in some way.
	 * @author Bo Zimmerman
	 */
	public static class MJSONException extends Exception
	{
		private static final long	serialVersionUID	= -2651922052891126260L;

		/**
		 * Constructs a new exception with the given parse error
		 * @param string the parse error
		 */
		public MJSONException(String string)
		{
			super(string);
		}

		/**
		 * Constructs a new exception with the given parse error, and underlying cause
		 * @param string the parse error
		 * @param e an underlying cause of the parse error
		 */
		public MJSONException(String string, Exception e)
		{
			super(string, e);
		}
	}

	/**
	 * Given a normal string, this method will return a JSON-Safe
	 * string, which means escaped crlf, escaped tabs and backslashes, etc.
	 * @param value the unsafe string
	 * @return the JSON safe string
	 */
	public static String toJSONString(final String value)
	{
		final StringBuilder strBldr = new StringBuilder("");
		for(final char c : value.toCharArray())
		{
			switch (c)
			{
			case '\"':
			case '\\':
			case '/':
				strBldr.append('\\').append(c);
				break;
			case '\b':
				strBldr.append('\\').append('b');
				break;
			case '\f':
				strBldr.append('\\').append('f');
				break;
			case '\n':
				strBldr.append('\\').append('n');
				break;
			case '\r':
				strBldr.append('\\').append('r');
				break;
			case '\t':
				strBldr.append('\\').append('t');
				break;
			default:
				strBldr.append(c);
				break;
			}
		}
		return strBldr.toString();
	}

	/**
	 * An official JSON object.  Implemented as a Map, this class
	 * has numerous methods for accessing the internal keys and
	 * their mapped values in different ways, both raw, and checked.
	 * @author Bo Zimmerman
	 */
	public static class JSONObject extends TreeMap<String, Object>
	{
		private static final long	serialVersionUID	= 8390676973120915175L;

		/**
		 * Internal method that returns a raw value object, or throws
		 * an exception if the key is not found
		 * @param key the key to look for
		 * @return the raw Object the key is mapped to
		 * @throws MJSONException the key was not found
		 */
		private Object getCheckedObject(String key) throws MJSONException
		{
			if (!containsKey(key))
				throw new MJSONException("Key '" + key + "' not found");
			return get(key);
		}

		/**
		 * Returns a JSONObject mapped in THIS object by the given key.
		 * Throws an exception if anything goes wrong.
		 * @param key the key of the object
		 * @return the JSON Object mapped to by that key
		 * @throws MJSONException a missing key, or not a JSON Object
		 */
		public JSONObject getCheckedJSONObject(String key) throws MJSONException
		{
			final Object o = getCheckedObject(key);
			if (!(o instanceof JSONObject))
				throw new MJSONException("Key '" + key + "' is not a JSON object");
			return (JSONObject) o;
		}

		/**
		 * Returns a JSON Array mapped in this object by the given key.
		 * Throws an exception if anything goes wrong.
		 * @param key the key of the Array
		 * @return the JSON Array mapped to by that key
		 * @throws MJSONException a missing key, or not a JSON Array
		 */
		public Object[] getCheckedArray(String key) throws MJSONException
		{
			final Object o = getCheckedObject(key);
			if (!(o instanceof Object[]))
				throw new MJSONException("Key '" + key + "' is not an array");
			return (Object[]) o;
		}

		/**
		 * Returns a String mapped in this object by the given key.
		 * Throws an exception if anything goes wrong.
		 * @param key the key of the String
		 * @return the String mapped to by that key
		 * @throws MJSONException a missing key, or not a String
		 */
		public String getCheckedString(String key) throws MJSONException
		{
			final Object o = getCheckedObject(key);
			if (!(o instanceof String))
				throw new MJSONException("Key '" + key + "' is not a String");
			return (String) o;
		}

		/**
		 * Returns a Long mapped in this object by the given key.
		 * Throws an exception if anything goes wrong.
		 * @param key the key of the Long
		 * @return the Long mapped to by that key
		 * @throws MJSONException a missing key, or not a Long
		 */
		public Long getCheckedLong(String key) throws MJSONException
		{
			final Object o = getCheckedObject(key);
			if (!(o instanceof Long))
				throw new MJSONException("Key '" + key + "' is not a long");
			return (Long) o;
		}

		/**
		 * Returns a Double mapped in this object by the given key.
		 * Throws an exception if anything goes wrong.
		 * @param key the key of the Long
		 * @return the Double mapped to by that key
		 * @throws MJSONException a missing key, or not a Double
		 */
		public Double getCheckedDouble(String key) throws MJSONException
		{
			final Object o = getCheckedObject(key);
			if (!(o instanceof Double))
				throw new MJSONException("Key '" + key + "' is not a double");
			return (Double) o;
		}

		/**
		 * Returns a Boolean mapped in this object by the given key.
		 * Throws an exception if anything goes wrong.
		 * @param key the key of the Long
		 * @return the Boolean mapped to by that key
		 * @throws MJSONException a missing key, or not a Boolean
		 */
		public Boolean getCheckedBoolean(String key) throws MJSONException
		{
			final Object o = getCheckedObject(key);
			if (!(o instanceof Boolean))
				throw new MJSONException("Key '" + key + "' is not a boolean");
			return (Boolean) o;
		}

		/**
		 * Returns a numeric value mapped in this object by the given key.
		 * Throws an exception if anything goes wrong.
		 * @param key the key of the Long
		 * @return the double value of the number mapped to by that key
		 * @throws MJSONException a missing key, or not a numeric value
		 */
		public double getCheckedNumber(String key) throws MJSONException
		{
			final Object o = getCheckedObject(key);
			if (o instanceof Double)
				return ((Double) o).doubleValue();
			if (o instanceof Long)
				return ((Long) o).doubleValue();
			throw new MJSONException("Key '" + key + "' is not a number");
		}

		/**
		 * Checks this object for the given key, and checks if it 
		 * is an official NULL or not.
		 * Throws an exception if the key is missing.
		 * @param key the key of the possible null
		 * @return true if the key maps to NULL or false otherwise
		 * @throws MJSONException the key was missing
		 */
		public boolean isCheckedNULL(String key) throws MJSONException
		{
			final Object o = getCheckedObject(key);
			return o == NULL;
		}

		/**
		 * Correctly appends the given thing to the given stringbuffer which
		 * is assumed to be in the middle of a JSON obect definition, right
		 * after the key and the colon:
		 * @param value the StringBuffer to append a value to
		 * @param obj the value to append -- a string, null, array, or number
		 */
		public static void appendJSONValue(final StringBuilder value, Object obj)
		{
			if (obj instanceof String)
			{
				value.append("\"").append(toJSONString((String)obj)).append("\"");
			}
			else 
			if (obj == NULL)
			{
				value.append("null");
			}
			else 
			if (obj instanceof Object[])
			{
				value.append("[");
				final Object[] array = (Object[]) obj;
				for (int i = 0; i < array.length; i++)
				{
					if (i > 0)
						value.append(",");
					appendJSONValue(value, array[i]);
				}
				value.append("]");
			}
			else
			if(obj != null)
			{
				value.append(obj.toString());
			}
		}

		/**
		 * Returns a full JSON document representation of this JSON object
		 */
		@Override
		public String toString()
		{
			final StringBuilder value = new StringBuilder("");
			value.append("{");
			for (final Iterator<String> k = keySet().iterator(); k.hasNext();)
			{
				final String keyVar = k.next();
				value.append("\"").append(toJSONString(keyVar)).append("\":");
				final Object obj = get(keyVar);
				appendJSONValue(value, obj);
				if (k.hasNext())
				{
					value.append(",");
				}
			}
			value.append("}");
			return value.toString();
		}

	}

	/**
	 * Parse either an Long, or Double object from the doc buffer
	 * @param doc the full JSON document
	 * @param index one dimensional array containing current index into the doc
	 * @return either an Long or a Double
	 * @throws MJSONException any parsing errors
	 */
	private Object parseNumber(char[] doc, int[] index) throws MJSONException
	{
		final int numStart = index[0];
		NumberParseState state = NumberParseState.INITIAL;
		while (index[0] < doc.length)
		{
			final char c = doc[index[0]];
			switch (state)
			{
			case INITIAL:
				if (c == '0')
					state = NumberParseState.NEEDDOT;
				else
				if(c=='-')
					state = NumberParseState.NEEDN0DIGIT;
				else
				if(Character.isDigit(c))
					state = NumberParseState.HAVEDIGIT;
				else
					throw new MJSONException("Expected digit at " + index[0]);
				break;
			case NEEDN0DIGIT:
				if (c == '0')
					throw new MJSONException("Expected digit at " + index[0]);
				else
				if(Character.isDigit(c))
					state = NumberParseState.HAVEDIGIT;
				else
					throw new MJSONException("Expected digit at " + index[0]);
				break;
			case HAVEDIGIT:
				if (c == '.')
					state = NumberParseState.NEEDDOTDIGIT;
				else
				if((c=='E')||(c=='e'))
					state = NumberParseState.HAVEE;
				else
				if(Character.isDigit(c))
					state = NumberParseState.HAVEDIGIT;
				else
				{
					index[0]--;
					return Long.valueOf(new String(doc, numStart, index[0] - numStart + 1));
				}
				break;
			case NEEDDOT:
				if (c == '.')
					state = NumberParseState.NEEDDOTDIGIT;
				else
				{
					index[0]--;
					return Long.valueOf(new String(doc, numStart, index[0] - numStart + 1));
				}
				break;
			case NEEDDOTDIGIT:
				if (Character.isDigit(c))
					state = NumberParseState.HAVEDOTDIGIT;
				else
					throw new MJSONException("Expected digit at " + index[0]);
				break;
			case HAVEDOTDIGIT:
				if (Character.isDigit(c))
					state = NumberParseState.HAVEDOTDIGIT;
				else
				if((c=='e')||(c=='E'))
					state = NumberParseState.HAVEE;
				else
				{
					index[0]--;
					return Double.valueOf(new String(doc, numStart, index[0] - numStart + 1));
				}
				break;
			case HAVEE:
				if (c == '0')
					throw new MJSONException("Expected non-zero digit at " + index[0]);
				else
				if(Character.isDigit(c)||(c=='+')||(c=='-'))
					state = NumberParseState.HAVEEDIGIT;
				else
					throw new MJSONException("Expected +- or non-zero digit at " + index[0]);
				break;
			case HAVEEDIGIT:
				if (!Character.isDigit(c))
				{
					index[0]--;
					return Double.valueOf(new BigDecimal(new String(doc, numStart, index[0] - numStart + 1)).doubleValue());
				}
				break;
			}
			index[0]++;
		}
		throw new MJSONException("Unexpected end of number at" + index[0]);
	}

	/**
	 * Given a char array, and index into it, returns the byte value of the 1 hex
	 * digits at the indexed point of the char array.
	 * @param doc the json doc containing a hex number
	 * @param index the index into that json doc where the hex number begins
	 * @return the byte value of the 1 digit hex number
	 * @throws MJSONException a parse error meaning it wasn't a hex number at all
	 */
	private byte getByteFromHex(char[] doc, int index) throws MJSONException
	{
		final char c = doc[index];
		if ((c >= '0') && (c <= '9'))
			return (byte) (c - '0');
		if ((c >= 'a') && (c <= 'f'))
			return (byte) (10 + (c - 'a'));
		if ((c >= 'A') && (c <= 'F'))
			return (byte) (10 + (c - 'A'));
		throw new MJSONException("Illegal hex digit at " + index);
	}

	/**
	 * Given a json document char array, and an index into it, parses a string at 
	 * the indexed point of the char array and returns its value.
	 * @param doc the json doc containing the string
	 * @param index the index into that json doc where the string begins
	 * @return the value of the found string
	 * @throws MJSONException a parse exception, meaning no string was there
	 */
	private String parseString(char[] doc, int[] index) throws MJSONException
	{
		final StringBuilder value=new StringBuilder("");
		if (doc[index[0]] != '\"')
		{
			throw new MJSONException("Expectged quote at: " + doc[index[0]]);
		}
		index[0]++;
		while (index[0] < doc.length)
		{
			final char c = doc[index[0]];
			if (c == '\"')
				return value.toString();
			else
			if(c=='\\')
			{
				if (index[0] == doc.length - 1)
					throw new MJSONException("Unfinished escape");
				index[0]++;
				switch (doc[index[0]])
				{
				case '\"':
				case '\\':
				case '/':
					value.append(doc[index[0]]);
					break;
				case 'b':
					value.append('\b'); 
					break;
				case 'f':
					value.append('\f');
					break;
				case 'n':
					value.append('\n'); 
					break;
				case 'r':
					value.append('\r'); 
					break;
				case 't':
					value.append('\t'); 
					break;
				case 'u':
				{
					if (index[0] >= doc.length - 5)
						throw new MJSONException("Unfinished unicode escape at " + index[0]);
					final byte[] hexBuf = new byte[4];
					hexBuf[0] = getByteFromHex(doc, ++index[0]);
					hexBuf[1] = getByteFromHex(doc, ++index[0]);
					hexBuf[2] = getByteFromHex(doc, ++index[0]);
					hexBuf[3] = getByteFromHex(doc, ++index[0]);
					try
					{
						value.append(new String(hexBuf, "Cp1251"));
					}
					catch (final UnsupportedEncodingException e)
					{
						throw new MJSONException("Illegal character at" + index[0], e);
					}
					break;
				}
				default:
					throw new MJSONException("Illegal escape character: " + doc[index[0]]);
				}
			}
			else
				value.append(c);
			index[0]++;
		}
		throw new MJSONException("Unfinished string at " + index[0]);
	}

	/**
	 * Given a json document char array, and an index into it, parses an array at 
	 * the indexed point of the char array and returns its value object.
	 * @param doc the json doc containing the array
	 * @param index the index into that json doc where the array begins
	 * @return the value object of the found array
	 * @throws MJSONException a parse exception, meaning no array was there
	 */
	private Object[] parseArray(char[] doc, int[] index) throws MJSONException
	{
		ArrayParseState state = ArrayParseState.INITIAL;
		final List<Object> finalSet = new ArrayList<Object>();
		while (index[0] < doc.length)
		{
			final char c = doc[index[0]];
			if (!Character.isWhitespace(c))
			{
				switch (state)
				{
				case INITIAL:
					if (c == '[')
						state = ArrayParseState.NEEDOBJECT;
					else
						throw new MJSONException("Expected String at " + index[0]);
					break;
				case EXPECTOBJECT:
					finalSet.add(parseElement(doc, index));
					state = ArrayParseState.GOTOBJECT;
					break;
				case NEEDOBJECT:
					if (c == ']')
						return finalSet.toArray(new Object[0]);
					else
					{
						finalSet.add(parseElement(doc, index));
						state = ArrayParseState.GOTOBJECT;
					}
					break;
				case GOTOBJECT:
					if (c == ']')
						return finalSet.toArray(new Object[0]);
					else
					if(c==',')
						state = ArrayParseState.EXPECTOBJECT;
					else
						throw new MJSONException("Expected ] or , at " + index[0]);
					break;
				}
			}
			index[0]++;
		}
		throw new MJSONException("Expected ] at " + index[0]);
	}

	/**
	 * Given a json document char array, and an index into it, parses a value object at 
	 * the indexed point of the char array and returns its value object.  A value object
	 * may be anything from a string, array, a JSON object, boolean, null, or a number.
	 * @param doc the json doc containing the value 
	 * @param index the index into that json doc where the value begins
	 * @return the value object of the found value
	 * @throws MJSONException a parse exception, meaning no recognized value was there
	 */
	private Object parseElement(char[] doc, int[] index) throws MJSONException
	{
		switch (doc[index[0]])
		{
		case '\"':
			return parseString(doc, index);
		case '[':
			return parseArray(doc, index);
		case '{':
			return parseObject(doc, index);
		case '-':
		case '0':
		case '1':
		case '2':
		case '3':
		case '4':
		case '5':
		case '6':
		case '7':
		case '8':
		case '9':
			return parseNumber(doc, index);
		case 't':
			if ((index[0] < doc.length - 5) && (new String(doc, index[0], 4).equals("true")))
			{
				index[0] += 3;
				return Boolean.TRUE;
			}
			throw new MJSONException("Invalid true at " + index[0]);
		case 'f':
			if ((index[0] < doc.length - 6) && (new String(doc, index[0], 5).equals("false")))
			{
				index[0] += 4;
				return Boolean.FALSE;
			}
			throw new MJSONException("Invalid false at " + index[0]);
		case 'n':
			if ((index[0] < doc.length - 5) && (new String(doc, index[0], 4).equals("null")))
			{
				index[0] += 3;
				return NULL;
			}
			throw new MJSONException("Invalid null at " + index[0]);
		default:
			throw new MJSONException("Unknown character at " + index[0]);
		}
	}

	/**
	 * Given a json document char array, and an index into it, parses a JSON object at 
	 * the indexed point of the char array and returns it as a mapped JSON object.
	 * @param doc the json doc containing the JSON object
	 * @param index the index into that json doc where the JSON object begins
	 * @return the value object of the found JSON object
	 * @throws MJSONException a parse exception, meaning no JSON object was there
	 */
	private JSONObject parseObject(char[] doc, int[] index) throws MJSONException
	{
		final JSONObject map = new JSONObject();
		String key = null;
		ObjectParseState state = ObjectParseState.INITIAL;
		while (index[0] < doc.length)
		{
			final char c = doc[index[0]];
			if (!Character.isWhitespace(c))
			{
				switch (state)
				{
				case INITIAL:
					if (c == '{')
						state = ObjectParseState.NEEDKEY;
					else
						throw new MJSONException("Expected Key/String at " + index[0]);
					break;
				case NEEDKEY:
					if (c == '\"')
					{
						key = parseString(doc, index);
						state = ObjectParseState.GOTKEY;
					}
					else
					if(c=='}')
						return map;
					else
						throw new MJSONException("Expected Key/String at " + index[0]);
					break;
				case GOTKEY:
					if (c == ':')
						state = ObjectParseState.NEEDOBJECT;
					else
						throw new MJSONException("Expected Colon at " + index[0]);
					break;
				case NEEDOBJECT:
					map.put(key, parseElement(doc, index));
					state = ObjectParseState.GOTOBJECT;
					break;
				case GOTOBJECT:
					if (c == ',')
						state = ObjectParseState.NEEDKEY;
					else
					if(c=='}')
						return map;
					else
						throw new MJSONException("Expected } or , at " + index[0]);
					break;
				}
			}
			index[0]++;
		}
		throw new MJSONException("Expected } at " + index[0]);
	}

	/**
	 * Given a string containing a JSON object, this method will parse it
	 * into a mapped JSONObject object recursively.
	 * @param doc the JSON document that contains a top-level JSON object
	 * @return the JSON object at the top level
	 * @throws MJSONException the parse error
	 */
	public JSONObject parseObject(String doc) throws MJSONException
	{
		try
		{
			return parseObject(doc.toCharArray(), new int[] { 0 });
		}
		catch (final MJSONException e)
		{
			throw e;
		}
		catch (final Exception e)
		{
			throw new MJSONException("Internal error", e);
		}
	}
}