APXML = {};

(function(){

	var reservedApxmlTokens = ["agent_application_id",
	                           "agent_client_id",
	                           "apia_password",
	                           "apia_priority",
	                           "apia_process_group",
	                           "apia_source"];

	var apxmlPkgs = new JavaImporter(
			java.util,
			com.alarmpoint.integrationagent.util,
			com.thoughtworks.xstream,
			com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider
			);
	with(apxmlPkgs) {
		var xstream = new XStream(new PureJavaReflectionProvider());
		XStream.setupDefaultSecurity(xstream);
		xstream.allowTypesByWildcard(["com.alarmpoint.**", "java.util.*"]);
	}

	// private functions

	// The purpose of this function is to ease the quoting rules on parameters to APClient.bin for strings.
	// Basically, '"something with spaces"' or "\"something with spaces\"" aren't very nice.
	// This allows the user to do this: 'something with spaces' or "something with spaces".
	var booleanTest = /^('|")(true|false)(\1)$/i,
	    listOrObjTest = /^\[.+\]$|^\{.+\}$/,
	    quotedTest = /^".+"$/;
	function guessParameterType(parameter) {

		// First convert whatever we get into a js string.
		var result = String(parameter);

		// We assume the parameter true or false to be a string. To treat them as booleans if must get them quoted.
		// This is done because it is assumed more users want true and false to be treated as strings.
		var booleanResult = booleanTest.exec(parameter);
		if (booleanResult) {
			return booleanResult[2].toLowerCase();
		}

		// Check if we have a list or object.
		var listOrObjResult = listOrObjTest.exec(parameter);
		if (listOrObjResult) {
			try {
				// check if this is a valid JSON, not something else e.g. UUID
				JSON.parse(listOrObjResult[0]);
				return listOrObjResult[0];
			} catch (e) {
				// continue
			}
		}

		var quotedResult = quotedTest.exec(parameter);
		if (quotedResult) {
      // Make sure that we have a Javascript string to work with (ie, not a Java String object)
      var sanitizedResult = "" + quotedResult[0];
      // Get rid of all double-quotes, then surround the result with matched double-quotes (this is what apxmlTokenToJs() is expecting)
      sanitizedResult = "\"" + sanitizedResult.replace(/"/g,"") + "\"";
      return sanitizedResult;
    }

		// Ensure that the string can be serialized.
		return JSON.stringify(result);
	}

	function getEventProperties(evProps) {
		var addTokensLinkedHashMap;
		IALOG.debug("apxml.js: entering getEventProperties()... Attempting to extract event properties: {0}", evProps);
		with (apxmlPkgs) {
			// deserialize the additional tokens
			if (evProps != null && evProps != "") {
				addTokensLinkedHashMap = xstream.fromXML(evProps);
			} else {
				addTokensLinkedHashMap = new HashMap();
			}

			var obj = {},
				keyIterator = addTokensLinkedHashMap.keySet().iterator(),
				key;
			while (keyIterator.hasNext()) {
				key = keyIterator.next();
				obj[key] = String(addTokensLinkedHashMap.get(key));
			}
			return obj;
		}
	}

	// Convert each apxmlToken to a json string, then use JSON.parse to convert the string to an js object.
	function apxmlTokenToJs(apxmlToken) {
		var key = String(apxmlToken.getKey()).toLowerCase(),
			obj,
			value,
			str;

		if (key === 'additionaltokens') {
			obj = {};
			obj.additionalTokens = getEventProperties(apxmlToken.getValue());
		} else {
			value = guessParameterType(apxmlToken.getValue());
			str = "{\"" + key + "\":" + value + "}";
			try {
				IALOG.debug("Attempting to parse JSON: {0}", str);
				obj = JSON.parse(str);
			}
			catch (e) {
				IALOG.error("Exception caught trying to parse JSON: {0}", str);
				throw e;
			}
		}

		return obj;
	}

	// public functions
	APXML.toString = function(apxml) {
		var string = "",
		    tokenIterator = apxml.getTokens();
		while (tokenIterator.hasNext()) {
		    var token = tokenIterator.next();
		    string += "(" + token.getKey() + ", " + token.getValue() + ")";
		}
		return string;
	};

	APXML.dedup = function(apxml, filter) {
		var dedup = false;
		with(apxmlPkgs) {
			if (typeof filter !== 'undefined') {
				var recentlyReceived = EventDeduplicator.getInstance().recentOccurrenceCount(apxml, filter);
				if (recentlyReceived > 1)
				{
					dedup = true;
				}
			}
		}
		return dedup;
	};

	/**
	 *  Checks to see if this is a duplicate message, but does not increment the counter, in case the message
	 *  processing later fails.  To update the counter, you must call APXML.dedup() after processing succeeds.
	 */
	APXML.isDuplicateOccurrence = function(apxml, filter) {
		with(apxmlPkgs) {
			if (typeof filter !== 'undefined') {
				return (0 < EventDeduplicator.getInstance().peekRecentOccurrenceCount(apxml, filter));
			}
		}
		return false;
	};

	APXML.toEventJs = function(apxml, event, newKeys) {
		IALOG.debug("\tEntering APXML.toEventJs. APXML is:\n{0}", apxml);
		event.properties = {};

		// Allow a client to send a map that indicates which apxml tokens represent top level event elements.
		var	keys = {
				'conferences': 'xmconferences',
				'priority'   : 'xmpriority',
				'recipients' : 'recipients',
				'responses'  : 'xmresponses'
			};

		if (typeof newKeys === 'object') {
			for (var k in newKeys) {
				keys[k] = newKeys[k];
			}
		}

		for (var itr = apxml.getTokens(); itr.hasNext(); ) {
			var apxmlToken = itr.next(),
				apxmlTokenAsJs = apxmlTokenToJs(apxmlToken);

			// This for loop obscures what is going on here.
			// apxmlTokenAsJs will only have a single item in the map.
			// A for loop is used to get the key since it isn't known.
			apxmlTokenLoop:
			for (var key in apxmlTokenAsJs) {
				// Before we check the client provided map there are some special case tokens
				// these tokens are used as top level keys rather than as properties.
				switch (key) {
					case keys['conferences']:
						event.conferences = apxmlTokenAsJs[keys['conferences']];
					    continue apxmlTokenLoop;
					case keys['priority']:
						event.priority = apxmlTokenAsJs[keys['priority']];
					    continue apxmlTokenLoop;
					case keys['recipients']:
						event.recipients = apxmlTokenAsJs[keys['recipients']];
					    continue apxmlTokenLoop;
					case keys['responses']:
						event.responses = apxmlTokenAsJs[keys['responses']];
					    continue apxmlTokenLoop;
				}

				// Don't add internal apxml tokens to event object.
				for (var i in reservedApxmlTokens) {
					if (key === reservedApxmlTokens[i]) continue apxmlTokenLoop;
				}
				event.properties[key] = apxmlTokenAsJs[key];
			}
		}

		IALOG.debug("\tAPXML.toEventJs returning event object:\n{0}", JSON.stringify(event, null, 2));
		return event;
	};

	APXML.toResponseJs = function(apxml) {
		IALOG.debug("\tEntering APXML.toResponseJs. APXML is:\n{0}", apxml);
		var response = {};
		for (var itr = apxml.getTokens(); itr.hasNext(); ) {
			var apxmlToken = itr.next(),
				apxmlTokenAsJs = apxmlTokenToJs(apxmlToken);

			for (var key in apxmlTokenAsJs) {
				response[key] = apxmlTokenAsJs[key];
			}
		}

		IALOG.debug("\tAPXML.toResponseJs returning response object:\n{0}", JSON.stringify(response, null, 2));
		return response;
	};

	APXML.fromJsObject = function(data) {
		IALOG.debug("\tEntering APXML.fromJsObject. Received data:\n{0}", JSON.stringify(data) );
		var apxml = ServiceAPI.createAPXML();

		if (data instanceof Object) {
			Object.keys(data).forEach(function(el){
				apxml.setToken(el, JSON.stringify(data[el]));
			});
		}

		IALOG.debug("\tAPXML.fromJsObject returning apxml:\n{0}", apxml);
		return apxml;
	};
})();
