/*
 * Copyright 2014-2019 JKOOL, LLC.
 *
 * 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.
 */
package com.jkoolcloud.tnt4j.format;

import java.util.Collection;
import java.util.Date;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;

import com.jkoolcloud.tnt4j.config.Configurable;
import com.jkoolcloud.tnt4j.core.*;
import com.jkoolcloud.tnt4j.source.DefaultSourceFactory;
import com.jkoolcloud.tnt4j.source.Source;
import com.jkoolcloud.tnt4j.source.SourceType;
import com.jkoolcloud.tnt4j.tracker.TrackingActivity;
import com.jkoolcloud.tnt4j.tracker.TrackingEvent;
import com.jkoolcloud.tnt4j.utils.Useconds;
import com.jkoolcloud.tnt4j.utils.Utils;

/**
 * <p>
 * JSON implementation of {@link Formatter} interface provides default formatting of {@link TrackingActivity},
 * {@link TrackingEvent}, {@link Snapshot}, {@link Property} into JSON format.
 * </p>
 *
 *
 * @version $Revision: 22 $
 *
 * @see DefaultFormatter
 * @see TrackingActivity
 * @see TrackingEvent
 * @see Snapshot
 * @see Property
 */

public class JSONFormatter implements EventFormatter, Configurable, JSONLabels {
	private static final boolean NEWLINE_FORMAT = Boolean.getBoolean("tnt4j.formatter.json.newline");
	protected static final String EMPTY_STR = "";
	protected static final String EMPTY_PROP = "{}";
	private static final String DEF_OP_NAME = "log";

	protected static final String START = "{";
	protected static final String START_LINE = "{\n";
	protected static final String END = "}";
	protected static final String END_LINE = "\n}";
	protected static final String ATTR_END = ",";
	protected static final String ATTR_END_LINE = ",\n";
	protected static final String ATTR_SEP = ": ";
	protected static final String ARRAY_END = "]";
	protected static final String ARRAY_START = "[";
	protected static final String ARRAY_START_LINE = "[\n";

	private Map<String, ?> config = null;
	protected boolean newLineFormat = true;
	protected String defOpName = DEF_OP_NAME;
	protected SpecNumbersHandling specialNumbersHandling = SpecNumbersHandling.SUPPRESS;

	protected String START_JSON = START_LINE;
	protected String END_JSON = END_LINE;
	protected String ATTR_JSON = ATTR_END_LINE;
	protected String ARRAY_START_JSON = ARRAY_START_LINE;

	/**
	 * Create JSON formatter without newlines during formatting
	 *
	 */
	public JSONFormatter() {
		this(NEWLINE_FORMAT);
	}

	/**
	 * Create JSON formatter and conditionally format with newline
	 *
	 * @param newLine
	 *            apply newline formatting to JSON
	 */
	public JSONFormatter(boolean newLine) {
		newLineFormat = newLine;
		initTags();
	}

	private void initTags() {
		START_JSON = newLineFormat ? START_LINE : START;
		END_JSON = newLineFormat ? END_LINE : END;
		ATTR_JSON = newLineFormat ? ATTR_END_LINE : ATTR_END;
		ARRAY_START_JSON = newLineFormat ? ARRAY_START_LINE : ARRAY_START;
	}

	@Override
	public String format(Object obj, Object... args) {
		if (obj instanceof TrackingActivity) {
			return format((TrackingActivity) obj);
		} else if (obj instanceof TrackingEvent) {
			return format((TrackingEvent) obj);
		} else if (obj instanceof Snapshot) {
			return format((Snapshot) obj);
		} else if (obj instanceof Property) {
			return format((Property) obj);
		} else {
			StringBuilder jsonString = new StringBuilder(1024);
			jsonString.append(START_JSON);
			jsonString.append(JSON_TIME_USEC_LABEL).append(ATTR_SEP).append(Useconds.CURRENT.get()).append(ATTR_JSON);

			String msgText = StringEscapeUtils.escapeJson(Utils.format(Utils.toString(obj), args)); // escape double
																									// quote chars
			jsonString.append(JSON_MSG_TEXT_LABEL).append(ATTR_SEP);
			Utils.quote(msgText, jsonString);
			jsonString.append(END_JSON);
			return jsonString.toString();
		}
	}

	/**
	 * Format a given {@link TrackingEvent} into JSON format
	 *
	 * @param event
	 *            tracking event instance to be formatted
	 * @see TrackingEvent
	 */
	@Override
	public String format(TrackingEvent event) {
		StringBuilder jsonString = new StringBuilder(1024);

		jsonString.append(START_JSON);
		if (!Utils.isEmpty(event.getGUID())) {
			jsonString.append(JSON_GUID_LABEL).append(ATTR_SEP);
			Utils.quote(event.getGUID(), jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(event.getTrackingId())) {
			jsonString.append(JSON_TRACK_ID_LABEL).append(ATTR_SEP);
			Utils.quote(event.getTrackingId(), jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(event.getSignature())) {
			jsonString.append(JSON_TRACK_SIGN_LABEL).append(ATTR_SEP);
			Utils.quote(event.getSignature(), jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(event.getParentId())) {
			jsonString.append(JSON_PARENT_TRACK_ID_LABEL).append(ATTR_SEP);
			Utils.quote(event.getParentId(), jsonString).append(ATTR_JSON);
		}
		jsonString.append(JSON_SOURCE_LABEL).append(ATTR_SEP);
		Utils.quote(StringEscapeUtils.escapeJson(event.getSource().getName()), jsonString).append(ATTR_JSON);
		String ssn = getSSN(event.getSource());
		if (!Utils.isEmpty(ssn)) {
			String escaped = StringEscapeUtils.escapeJson(ssn); // escape double quote chars
			jsonString.append(JSON_SOURCE_SSN_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}
		jsonString.append(JSON_SOURCE_FQN_LABEL).append(ATTR_SEP);
		Utils.quote(StringEscapeUtils.escapeJson(event.getSource().getFQName()), jsonString).append(ATTR_JSON);
		if (!Utils.isEmpty(event.getSource().getUrl())) {
			String escaped = StringEscapeUtils.escapeJson(event.getSource().getUrl()); // escape double quote chars
			jsonString.append(JSON_SOURCE_URL_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}
		if (event.get2(TrackingEvent.OBJ_ONE) != null) {
			// we have a relation
			jsonString.append(JSON_RELATE_TYPE_LABEL).append(ATTR_SEP);
			Utils.quote(event.get2Type(), jsonString).append(ATTR_JSON);
			jsonString.append(JSON_RELATE_FQN_A_LABEL).append(ATTR_SEP);
			Utils.quote(event.get2(TrackingEvent.OBJ_ONE).getFQName(), jsonString).append(ATTR_JSON);
			jsonString.append(JSON_RELATE_FQN_B_LABEL).append(ATTR_SEP);
			Utils.quote(event.get2(TrackingEvent.OBJ_TWO).getFQName(), jsonString).append(ATTR_JSON);
		}
		jsonString.append(JSON_SEVERITY_LABEL).append(ATTR_SEP);
		Utils.quote(event.getSeverity(), jsonString).append(ATTR_JSON);
		jsonString.append(JSON_SEVERITY_NO_LABEL).append(ATTR_SEP).append(event.getSeverity().ordinal())
				.append(ATTR_JSON);
		jsonString.append(JSON_TYPE_LABEL).append(ATTR_SEP);
		Utils.quote(event.getOperation().getType(), jsonString).append(ATTR_JSON);
		jsonString.append(JSON_TYPE_NO_LABEL).append(ATTR_SEP).append(event.getOperation().getType().ordinal())
				.append(ATTR_JSON);
		jsonString.append(JSON_PID_LABEL).append(ATTR_SEP).append(event.getOperation().getPID()).append(ATTR_JSON);
		jsonString.append(JSON_TID_LABEL).append(ATTR_SEP).append(event.getOperation().getTID()).append(ATTR_JSON);
		jsonString.append(JSON_COMP_CODE_LABEL).append(ATTR_SEP);
		Utils.quote(event.getOperation().getCompCode(), jsonString).append(ATTR_JSON);
		jsonString.append(JSON_COMP_CODE_NO_LABEL).append(ATTR_SEP).append(event.getOperation().getCompCode().ordinal())
				.append(ATTR_JSON);
		jsonString.append(JSON_REASON_CODE_LABEL).append(ATTR_SEP).append(event.getOperation().getReasonCode())
				.append(ATTR_JSON);
		jsonString.append(JSON_TTL_SEC_LABEL).append(ATTR_SEP).append(event.getTTL()).append(ATTR_JSON);

		if (!Utils.isEmpty(event.getLocation())) {
			String escaped = StringEscapeUtils.escapeJson(event.getLocation()); // escape double quote chars
			jsonString.append(JSON_LOCATION_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(event.getOperation().getResolvedName())) {
			String escaped = StringEscapeUtils.escapeJson(event.getOperation().getResolvedName()); // escape double
																									// quote chars
			jsonString.append(JSON_OPERATION_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(event.getOperation().getResource())) {
			String escaped = StringEscapeUtils.escapeJson(event.getOperation().getResource()); // escape double quote
																								// chars
			jsonString.append(JSON_RESOURCE_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(event.getOperation().getUser())) {
			String escaped = StringEscapeUtils.escapeJson(event.getOperation().getUser()); // escape double quote chars
			jsonString.append(JSON_USER_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}
		jsonString.append(JSON_TIME_USEC_LABEL).append(ATTR_SEP).append(Useconds.CURRENT.get()).append(ATTR_JSON);
		if (event.getOperation().getStartTime() != null) {
			jsonString.append(JSON_START_TIME_USEC_LABEL).append(ATTR_SEP)
					.append(event.getOperation().getStartTime().getTimeUsec()).append(ATTR_JSON);
		}
		if (event.getOperation().getEndTime() != null) {
			jsonString.append(JSON_END_TIME_USEC_LABEL).append(ATTR_SEP)
					.append(event.getOperation().getEndTime().getTimeUsec()).append(ATTR_JSON);
			jsonString.append(JSON_ELAPSED_TIME_USEC_LABEL).append(ATTR_SEP)
					.append(event.getOperation().getElapsedTimeUsec()).append(ATTR_JSON);
			if (event.getOperation().getWaitTimeUsec() > 0) {
				jsonString.append(JSON_WAIT_TIME_USEC_LABEL).append(ATTR_SEP)
						.append(event.getOperation().getWaitTimeUsec()).append(ATTR_JSON);
			}
			if (event.getMessageAge() > 0) {
				jsonString.append(JSON_MSG_AGE_USEC_LABEL).append(ATTR_SEP).append(event.getMessageAge())
						.append(ATTR_JSON);
			}
		}
		int snapCount = event.getOperation().getSnapshotCount();
		int propCount = event.getOperation().getPropertyCount();
		jsonString.append(JSON_SNAPSHOT_COUNT_LABEL).append(ATTR_SEP).append(snapCount).append(ATTR_JSON);
		jsonString.append(JSON_PROPERTY_COUNT_LABEL).append(ATTR_SEP).append(propCount).append(ATTR_JSON);
		jsonString.append(JSON_MSG_SIZE_LABEL).append(ATTR_SEP).append(event.getSize()).append(ATTR_JSON);
		jsonString.append(JSON_MSG_MIME_LABEL).append(ATTR_SEP);
		Utils.quote(event.getMimeType(), jsonString).append(ATTR_JSON);
		jsonString.append(JSON_MSG_ENC_LABEL).append(ATTR_SEP);
		Utils.quote(event.getEncoding(), jsonString).append(ATTR_JSON);
		jsonString.append(JSON_MSG_CHARSET_LABEL).append(ATTR_SEP);
		Utils.quote(event.getCharset(), jsonString);

		String msgText = event.getMessage();
		if (!Utils.isEmpty(msgText)) {
			jsonString.append(ATTR_JSON);
			msgText = StringEscapeUtils.escapeJson(msgText); // escape double quote chars
			jsonString.append(JSON_MSG_TEXT_LABEL).append(ATTR_SEP);
			Utils.quote(msgText, jsonString);
		}

		String exStr = event.getOperation().getExceptionString();
		if (!Utils.isEmpty(exStr)) {
			jsonString.append(ATTR_JSON);
			String excText = StringEscapeUtils.escapeJson(exStr); // escape double quote chars
			jsonString.append(JSON_EXCEPTION_LABEL).append(ATTR_SEP);
			Utils.quote(excText, jsonString);
		}
		if (!Utils.isEmpty(event.getCorrelator())) {
			jsonString.append(ATTR_JSON);
			jsonString.append(JSON_CORR_ID_LABEL).append(ATTR_SEP).append(ARRAY_START_JSON)
					.append(itemsToJSON(event.getCorrelator())).append(ARRAY_END);
		}
		if (!Utils.isEmpty(event.getTag())) {
			jsonString.append(ATTR_JSON);
			jsonString.append(JSON_MSG_TAG_LABEL).append(ATTR_SEP).append(ARRAY_START_JSON)
					.append(itemsToJSON(event.getTag())).append(ARRAY_END);
		}
		if (propCount > 0) {
			jsonString.append(ATTR_JSON);
			jsonString.append(JSON_PROPERTIES_LABEL).append(ATTR_SEP).append(ARRAY_START_JSON)
					.append(itemsToJSON(event.getOperation().getProperties())).append(ARRAY_END);
		}
		if (snapCount > 0) {
			jsonString.append(ATTR_JSON);
			jsonString.append(JSON_SNAPSHOTS_LABEL).append(ATTR_SEP).append(ARRAY_START_JSON)
					.append(itemsToJSON(event.getOperation().getSnapshots())).append(ARRAY_END);
		}
		jsonString.append(END_JSON);
		return jsonString.toString();
	}

	/**
	 * Format a given {@link TrackingActivity} into JSON format
	 *
	 * @param activity
	 *            tracking activity instance to be formatted
	 * @see TrackingActivity
	 */
	@Override
	public String format(TrackingActivity activity) {
		StringBuilder jsonString = new StringBuilder(1024);

		jsonString.append(START_JSON);
		if (!Utils.isEmpty(activity.getGUID())) {
			jsonString.append(JSON_GUID_LABEL).append(ATTR_SEP);
			Utils.quote(activity.getGUID(), jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(activity.getTrackingId())) {
			jsonString.append(JSON_TRACK_ID_LABEL).append(ATTR_SEP);
			Utils.quote(activity.getTrackingId(), jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(activity.getSignature())) {
			jsonString.append(JSON_TRACK_SIGN_LABEL).append(ATTR_SEP);
			Utils.quote(activity.getSignature(), jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(activity.getParentId())) {
			jsonString.append(JSON_PARENT_TRACK_ID_LABEL).append(ATTR_SEP);
			Utils.quote(activity.getParentId(), jsonString).append(ATTR_JSON);
		}
		jsonString.append(JSON_SOURCE_LABEL).append(ATTR_SEP);
		Utils.quote(StringEscapeUtils.escapeJson(activity.getSource().getName()), jsonString).append(ATTR_JSON);
		String ssn = getSSN(activity.getSource());
		if (!Utils.isEmpty(ssn)) {
			String escaped = StringEscapeUtils.escapeJson(ssn); // escape double quote chars
			jsonString.append(JSON_SOURCE_SSN_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}
		jsonString.append(JSON_SOURCE_FQN_LABEL).append(ATTR_SEP);
		Utils.quote(StringEscapeUtils.escapeJson(activity.getSource().getFQName()), jsonString).append(ATTR_JSON);

		if (!Utils.isEmpty(activity.getSource().getUrl())) {
			String escaped = StringEscapeUtils.escapeJson(activity.getSource().getUrl()); // escape double quote chars
			jsonString.append(JSON_SOURCE_URL_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}
		jsonString.append(JSON_STATUS_LABEL).append(ATTR_SEP);
		Utils.quote(activity.getStatus(), jsonString).append(ATTR_JSON);
		jsonString.append(JSON_SEVERITY_LABEL).append(ATTR_SEP);
		Utils.quote(activity.getSeverity(), jsonString).append(ATTR_JSON);
		jsonString.append(JSON_SEVERITY_NO_LABEL).append(ATTR_SEP).append(activity.getSeverity().ordinal())
				.append(ATTR_JSON);
		jsonString.append(JSON_TYPE_LABEL).append(ATTR_SEP);
		Utils.quote(activity.getType(), jsonString).append(ATTR_JSON);
		jsonString.append(JSON_TYPE_NO_LABEL).append(ATTR_SEP).append(activity.getType().ordinal()).append(ATTR_JSON);
		jsonString.append(JSON_PID_LABEL).append(ATTR_SEP).append(activity.getPID()).append(ATTR_JSON);
		jsonString.append(JSON_TID_LABEL).append(ATTR_SEP).append(activity.getTID()).append(ATTR_JSON);
		jsonString.append(JSON_COMP_CODE_LABEL).append(ATTR_SEP);
		Utils.quote(activity.getCompCode(), jsonString).append(ATTR_JSON);
		jsonString.append(JSON_COMP_CODE_NO_LABEL).append(ATTR_SEP).append(activity.getCompCode().ordinal())
				.append(ATTR_JSON);
		jsonString.append(JSON_REASON_CODE_LABEL).append(ATTR_SEP).append(activity.getReasonCode()).append(ATTR_JSON);
		jsonString.append(JSON_TTL_SEC_LABEL).append(ATTR_SEP).append(activity.getTTL()).append(ATTR_JSON);
		if (!Utils.isEmpty(activity.getLocation())) {
			String escaped = StringEscapeUtils.escapeJson(activity.getLocation()); // escape double quote chars
			jsonString.append(JSON_LOCATION_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(activity.getResolvedName())) {
			String escaped = StringEscapeUtils.escapeJson(activity.getResolvedName()); // escape double quote chars
			jsonString.append(JSON_OPERATION_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(activity.getResource())) {
			String escaped = StringEscapeUtils.escapeJson(activity.getResource()); // escape double quote chars
			jsonString.append(JSON_RESOURCE_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(activity.getSource().getUser())) {
			String escaped = StringEscapeUtils.escapeJson(activity.getSource().getUser()); // escape double quote chars
			jsonString.append(JSON_USER_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}

		jsonString.append(JSON_TIME_USEC_LABEL).append(ATTR_SEP).append(Useconds.CURRENT.get()).append(ATTR_JSON);
		if (activity.getStartTime() != null) {
			jsonString.append(JSON_START_TIME_USEC_LABEL).append(ATTR_SEP).append(activity.getStartTime().getTimeUsec())
					.append(ATTR_JSON);
		}
		if (activity.getEndTime() != null) {
			jsonString.append(JSON_END_TIME_USEC_LABEL).append(ATTR_SEP).append(activity.getEndTime().getTimeUsec())
					.append(ATTR_JSON);
			jsonString.append(JSON_ELAPSED_TIME_USEC_LABEL).append(ATTR_SEP).append(activity.getElapsedTimeUsec())
					.append(ATTR_JSON);
			if (activity.getWaitTimeUsec() > 0) {
				jsonString.append(JSON_WAIT_TIME_USEC_LABEL).append(ATTR_SEP).append(activity.getWaitTimeUsec())
						.append(ATTR_JSON);
			}
		}
		jsonString.append(JSON_ID_COUNT_LABEL).append(ATTR_SEP).append(activity.getIdCount()).append(ATTR_JSON);
		jsonString.append(JSON_SNAPSHOT_COUNT_LABEL).append(ATTR_SEP).append(activity.getSnapshotCount())
				.append(ATTR_JSON);
		jsonString.append(JSON_PROPERTY_COUNT_LABEL).append(ATTR_SEP).append(activity.getPropertyCount());

		String exStr = activity.getExceptionString();
		if (!Utils.isEmpty(exStr)) {
			jsonString.append(ATTR_JSON);
			String excText = StringEscapeUtils.escapeJson(exStr); // escape double quote chars
			jsonString.append(JSON_EXCEPTION_LABEL).append(ATTR_SEP);
			Utils.quote(excText, jsonString);
		}
		if (!Utils.isEmpty(activity.getCorrelator())) {
			jsonString.append(ATTR_JSON);
			jsonString.append(JSON_CORR_ID_LABEL).append(ATTR_SEP).append(ARRAY_START_JSON)
					.append(itemsToJSON(activity.getCorrelator())).append(ARRAY_END);
		}
		if (activity.getIdCount() > 0) {
			jsonString.append(ATTR_JSON);
			jsonString.append(JSON_ID_SET_LABEL).append(ATTR_SEP).append(ARRAY_START_JSON)
					.append(itemsToJSON(activity.getIds())).append(ARRAY_END);
		}
		if (activity.getPropertyCount() > 0) {
			jsonString.append(ATTR_JSON);
			jsonString.append(JSON_PROPERTIES_LABEL).append(ATTR_SEP).append(ARRAY_START_JSON)
					.append(itemsToJSON(activity.getProperties())).append(ARRAY_END);
		}
		if (activity.getSnapshotCount() > 0) {
			jsonString.append(ATTR_JSON);
			jsonString.append(JSON_SNAPSHOTS_LABEL).append(ATTR_SEP).append(ARRAY_START_JSON)
					.append(itemsToJSON(activity.getSnapshots())).append(ARRAY_END);
		}
		jsonString.append(END_JSON);
		return jsonString.toString();
	}

	/**
	 * Format a given {@link Snapshot} into JSON format
	 *
	 * @param snap
	 *            snapshot object to be formatted into JSON
	 * @see Snapshot
	 */
	@Override
	public String format(Snapshot snap) {
		StringBuilder jsonString = new StringBuilder(1024);
		jsonString.append(START_JSON);

		if (!Utils.isEmpty(snap.getGUID())) {
			jsonString.append(JSON_GUID_LABEL).append(ATTR_SEP);
			Utils.quote(snap.getGUID(), jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(snap.getTrackingId())) {
			jsonString.append(JSON_TRACK_ID_LABEL).append(ATTR_SEP);
			Utils.quote(snap.getTrackingId(), jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(snap.getSignature())) {
			jsonString.append(JSON_TRACK_SIGN_LABEL).append(ATTR_SEP);
			Utils.quote(snap.getSignature(), jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(snap.getParentId())) {
			jsonString.append(JSON_PARENT_TRACK_ID_LABEL).append(ATTR_SEP);
			Utils.quote(snap.getParentId(), jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(snap.getId())) {
			String escaped = StringEscapeUtils.escapeJson(snap.getId()); // escape double quote chars
			jsonString.append(JSON_FQN_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(snap.getCategory())) {
			jsonString.append(JSON_CATEGORY_LABEL).append(ATTR_SEP);
			Utils.quote(snap.getCategory(), jsonString).append(ATTR_JSON);
		}
		if (!Utils.isEmpty(snap.getName())) {
			String escaped = StringEscapeUtils.escapeJson(snap.getName()); // escape double quote chars
			jsonString.append(JSON_NAME_LABEL).append(ATTR_SEP);
			Utils.quote(escaped, jsonString).append(ATTR_JSON);
		}
		jsonString.append(JSON_COUNT_LABEL).append(ATTR_SEP).append(snap.size()).append(ATTR_JSON);
		jsonString.append(JSON_TIME_USEC_LABEL).append(ATTR_SEP).append(snap.getTimeStamp().getTimeUsec()).append(ATTR_JSON);
		jsonString.append(JSON_TTL_SEC_LABEL).append(ATTR_SEP).append(snap.getTTL()).append(ATTR_JSON);

		Source source = snap.getSource();
		if (source != null) {
			jsonString.append(JSON_SOURCE_LABEL).append(ATTR_SEP);
			Utils.quote(StringEscapeUtils.escapeJson(source.getName()), jsonString).append(ATTR_JSON);
			String ssn = getSSN(source);
			if (!Utils.isEmpty(ssn)) {
				String escaped = StringEscapeUtils.escapeJson(ssn); // escape double quote chars
				jsonString.append(JSON_SOURCE_SSN_LABEL).append(ATTR_SEP);
				Utils.quote(escaped, jsonString).append(ATTR_JSON);
			}
			jsonString.append(JSON_SOURCE_FQN_LABEL).append(ATTR_SEP);
			Utils.quote(StringEscapeUtils.escapeJson(source.getFQName()), jsonString).append(ATTR_JSON);
			if (!Utils.isEmpty(source.getUrl())) {
				String escaped = StringEscapeUtils.escapeJson(source.getUrl()); // escape double quote chars
				jsonString.append(JSON_SOURCE_URL_LABEL).append(ATTR_SEP);
				Utils.quote(escaped, jsonString).append(ATTR_JSON);
			}
		}
		if (snap.getSeverity().ordinal() > OpLevel.NONE.ordinal()) {
			jsonString.append(JSON_SEVERITY_LABEL).append(ATTR_SEP);
			Utils.quote(snap.getSeverity(), jsonString).append(ATTR_JSON);
			jsonString.append(JSON_SEVERITY_NO_LABEL).append(ATTR_SEP).append(snap.getSeverity().ordinal()).append(ATTR_JSON);
		}
		jsonString.append(JSON_TYPE_LABEL).append(ATTR_SEP);
		Utils.quote(snap.getType(), jsonString).append(ATTR_JSON);
		jsonString.append(JSON_TYPE_NO_LABEL).append(ATTR_SEP).append(snap.getType().ordinal());
		if (snap.size() > 0) {
			jsonString.append(ATTR_JSON);
			jsonString.append(JSON_PROPERTIES_LABEL).append(ATTR_SEP).append(ARRAY_START_JSON)
					.append(itemsToJSON(snap.getProperties())).append(ARRAY_END);
		}
		jsonString.append(END_JSON);
		return jsonString.toString();
	}

	/**
	 * Format a given {@link Property} into JSON format.
	 * <p>
	 * If property is transient {@link com.jkoolcloud.tnt4j.core.Property#isTransient()}, empty string
	 * {@value #EMPTY_STR} is returned.
	 * <p>
	 * Empty string {@value #EMPTY_STR} is returned when {@code specialNumbersHandling} is set to
	 * {@link SpecNumbersHandling#SUPPRESS} and property value is {@link Double} or {@link Float} containing
	 * {@code 'Infinity'} or {@code 'NaN'} value
	 *
	 * @param prop
	 *            property object to be formatted into JSON
	 * @return formatted property as a JSON string, or empty string {@value #EMPTY_STR} if property is {@code null},
	 *         transient or having special numeric value
	 * @see Property
	 */
	public String format(Property prop) {
		if (prop == null || prop.isTransient()) {
			return EMPTY_STR;
		}

		Object value = prop.getValue();

		if (isSpecialSuppress(value)) {
			return EMPTY_STR;
		}

		StringBuilder jsonString = new StringBuilder(256);
		jsonString.append(START_JSON);
		jsonString.append(JSON_NAME_LABEL).append(ATTR_SEP);
		Utils.quote(StringEscapeUtils.escapeJson(prop.getKey()), jsonString).append(ATTR_JSON);
		jsonString.append(JSON_TYPE_LABEL).append(ATTR_SEP);
		Utils.quote(prop.getDataType(), jsonString).append(ATTR_JSON);
		if (prop.getValueType() != null && !prop.getValueType().equalsIgnoreCase(ValueTypes.VALUE_TYPE_NONE)) {
			jsonString.append(JSON_VALUE_TYPE_LABEL).append(ATTR_SEP);
			Utils.quote(prop.getValueType(), jsonString).append(ATTR_JSON);
		}
		jsonString.append(JSON_VALUE_LABEL).append(ATTR_SEP);
		if (isNoNeedToQuote(value)) {
			jsonString.append(propValueToString(value));
		} else {
			Utils.quote(StringEscapeUtils.escapeJson(propValueToString(value)), jsonString);
		}
		jsonString.append(END_JSON);
		return jsonString.toString();
	}

	/**
	 * Converts property value to string representation specific for JKool.
	 * <ul>
	 * <li>{@link Date} value is serialized as number using {@link java.util.Date#getTime()}</li>
	 * <li>{@link com.jkoolcloud.tnt4j.core.UsecTimestamp} value is serialized as number using
	 * {@link com.jkoolcloud.tnt4j.core.UsecTimestamp#getTimeUsec()}</li>
	 * <li>all other cases are serialized using {@link Utils#toString(Object)}</li>
	 * </ul>
	 *
	 * @param propValue
	 *            property value to convert
	 * @return string representation of property value
	 *
	 * @see java.util.Date#getTime()
	 * @see com.jkoolcloud.tnt4j.core.UsecTimestamp#getTimeUsec()
	 * @see com.jkoolcloud.tnt4j.utils.Utils#toString(Object)
	 */
	protected static String propValueToString(Object propValue) {
		if (propValue instanceof Date) {
			return Long.toString(((Date) propValue).getTime());
		} else if (propValue instanceof UsecTimestamp) {
			return Long.toString(((UsecTimestamp) propValue).getTimeUsec());
		} else {
			return Utils.toString(propValue);
		}
	}

	/**
	 * Checks whether provided {@code value} can be un-quoted in produced JSON.
	 *
	 * @param value
	 *            value to check
	 * @return {@code true} if value is one of: {@code null}, boolean, number, date, timestamp, {@code false} -
	 *         otherwise
	 *
	 * @see #isSpecialSuppress(Object)
	 */
	protected boolean isNoNeedToQuote(Object value) {
		return value == null || value instanceof Boolean || value instanceof Date || value instanceof UsecTimestamp
				|| (value instanceof Number && !isSpecialEnquote(value));
	}

	/**
	 * Checks whether provided {@code value} is special numeric value and if formatter is configured to suppress these
	 * values.
	 *
	 * @param value
	 *            value to check
	 * @return {@code true} if value is special and should be suppressed, {@code false} - otherwise
	 *
	 * @see #isSpecial(Object)
	 */
	protected boolean isSpecialSuppress(Object value) {
		return specialNumbersHandling == SpecNumbersHandling.SUPPRESS && isSpecial(value);
	}

	/**
	 * Checks whether provided {@code value} is special numeric value and if formatter is configured to enquote these
	 * values.
	 *
	 * @param value
	 *            value to check
	 * @return {@code true} if value is special and should be enquoted, {@code false} - otherwise
	 */
	protected boolean isSpecialEnquote(Object value) {
		return specialNumbersHandling == SpecNumbersHandling.ENQUOTE && isSpecial(value);
	}

	private static boolean isSpecial(Object value) {
		if (value instanceof Number) {
			return Utils.isSpecialNumberValue((Number) value);
		}

		return false;
	}

	@Override
	public String format(long ttl, Source source, OpLevel level, String msg, Object... args) {
		StringBuilder jsonString = new StringBuilder(1024);
		jsonString.append(START_JSON);
		jsonString.append(JSON_SEVERITY_LABEL).append(ATTR_SEP);
		Utils.quote(level, jsonString).append(ATTR_JSON);
		jsonString.append(JSON_SEVERITY_NO_LABEL).append(ATTR_SEP).append(level.ordinal()).append(ATTR_JSON);
		jsonString.append(JSON_TYPE_LABEL).append(ATTR_SEP);
		Utils.quote(OpType.LOG, jsonString).append(ATTR_JSON);
		jsonString.append(JSON_TYPE_NO_LABEL).append(ATTR_SEP).append(OpType.LOG.ordinal()).append(ATTR_JSON);

		jsonString.append(JSON_PID_LABEL).append(ATTR_SEP).append(Utils.getVMPID()).append(ATTR_JSON);
		jsonString.append(JSON_TID_LABEL).append(ATTR_SEP).append(Thread.currentThread().getId()).append(ATTR_JSON);

		String usrName = StringEscapeUtils.escapeJson(
				source == null ? DefaultSourceFactory.getInstance().getRootSource().getUser() : source.getUser());
		jsonString.append(JSON_USER_LABEL).append(ATTR_SEP);
		Utils.quote(usrName, jsonString).append(ATTR_JSON);
		jsonString.append(JSON_TTL_SEC_LABEL).append(ATTR_SEP).append(ttl).append(ATTR_JSON);
		jsonString.append(JSON_TIME_USEC_LABEL).append(ATTR_SEP).append(Useconds.CURRENT.get()).append(ATTR_JSON);
		jsonString.append(JSON_OPERATION_LABEL).append(ATTR_SEP);
		Utils.quote(defOpName, jsonString).append(ATTR_JSON);

		if (source != null) {
			jsonString.append(JSON_SOURCE_LABEL).append(ATTR_SEP);
			Utils.quote(StringEscapeUtils.escapeJson(source.getName()), jsonString).append(ATTR_JSON);
			String ssn = getSSN(source);
			if (!Utils.isEmpty(ssn)) {
				String escaped = StringEscapeUtils.escapeJson(ssn); // escape double quote chars
				jsonString.append(JSON_SOURCE_SSN_LABEL).append(ATTR_SEP);
				Utils.quote(escaped, jsonString).append(ATTR_JSON);
			}
			jsonString.append(JSON_SOURCE_FQN_LABEL).append(ATTR_SEP);
			Utils.quote(StringEscapeUtils.escapeJson(source.getFQName()), jsonString);
			if (!Utils.isEmpty(source.getUrl())) {
				jsonString.append(ATTR_JSON);
				String escaped = StringEscapeUtils.escapeJson(source.getUrl()); // escape double quote chars
				jsonString.append(JSON_SOURCE_URL_LABEL).append(ATTR_SEP);
				Utils.quote(escaped, jsonString);
			}
			Source geoloc = source.getSource(SourceType.GEOADDR);
			if (geoloc != null) {
				jsonString.append(ATTR_JSON);
				jsonString.append(JSON_LOCATION_LABEL).append(ATTR_SEP);
				Utils.quote(geoloc.getName(), jsonString);
			}
		}
		if (!Utils.isEmpty(msg)) {
			String msgText = Utils.format(msg, args);
			msgText = StringEscapeUtils.escapeJson(msgText); // escape double quote chars
			jsonString.append(ATTR_JSON);
			jsonString.append(JSON_MSG_TEXT_LABEL).append(ATTR_SEP);
			Utils.quote(msgText, jsonString);
		}
		Throwable ex = Utils.getThrowable(args);
		if (ex != null) {
			jsonString.append(ATTR_JSON);
			String excText = StringEscapeUtils.escapeJson(ex.toString()); // escape double quote chars
			jsonString.append(JSON_EXCEPTION_LABEL).append(ATTR_SEP);
			Utils.quote(excText, jsonString);
		}
		jsonString.append(END_JSON);
		return jsonString.toString();
	}

	/**
	 * Builds string reforestation of provided activity entity {@code items} collection.
	 *
	 * @param items
	 *            collection of activity entity items
	 * @return string representation of activity enmity items collection
	 */
	protected String itemsToJSON(Collection<?> items) {
		if (items == null) {
			return EMPTY_STR;
		}
		StringBuilder jsonString = new StringBuilder(2048);
		for (Object item : items) {
			String itemJSON;
			if (item instanceof TrackingEvent) {
				itemJSON = format((TrackingEvent) item);
			} else if (item instanceof TrackingActivity) {
				itemJSON = format((TrackingActivity) item);
			} else if (item instanceof Snapshot) {
				itemJSON = format((Snapshot) item);
			} else if (item instanceof Property) {
				itemJSON = format((Property) item);
			} else {
				itemJSON = Utils.quote(StringEscapeUtils.escapeJson(Utils.toString(item))); // escape double quote chars
			}

			if (StringUtils.isNotEmpty(itemJSON)) {
				addDelimiterOnDemand(jsonString, ATTR_JSON);
				jsonString.append(itemJSON);
			}
		}
		return jsonString.toString();
	}

	private static void addDelimiterOnDemand(StringBuilder json, String delimiter) {
		if (StringUtils.isEmpty(json) || StringUtils.isEmpty(delimiter)) {
			return;
		}

		if (!StringUtils.endsWith(json, delimiter)) {
			json.append(delimiter);
		}
	}

	@Override
	public Map<String, ?> getConfiguration() {
		return config;
	}

	@Override
	public void setConfiguration(Map<String, ?> settings) {
		config = settings;
		newLineFormat = Utils.getBoolean("Newline", settings, newLineFormat);
		defOpName = Utils.getString("OpName", settings, defOpName);
		String specNumbers = Utils.getString("SpecNumbersHandling", settings, SpecNumbersHandling.SUPPRESS.name());
		try {
			specialNumbersHandling = SpecNumbersHandling.valueOf(specNumbers.toUpperCase());
		} catch (IllegalArgumentException exc) {
			specialNumbersHandling = SpecNumbersHandling.SUPPRESS;
		}
		initTags();
	}

	/**
	 * Builds string representation of provided {@code source}.
	 *
	 * @param source
	 *            source instance
	 * @return string representation of source
	 */
	protected static String getSSN(Source source) {
		String ssn = source.getSSN();
		return Utils.isEmpty(ssn) ? source.getSourceFactory().getSSN() : ssn;
	}

	/**
	 * Enumeration of special numbers values handling techniques used by this formatter.
	 */
	public enum SpecNumbersHandling {
		/**
		 * Suppress properties having special numeric value.
		 */
		SUPPRESS,
		/**
		 * Enquote special numeric value.
		 */
		ENQUOTE,
		/**
		 * Maintain value as is.
		 */
		MAINTAIN,
	}

}