/*
 *  Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 *  WSO2 Inc. licenses this file to you 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 org.apache.synapse.aspects.flow.statistics.util;

import org.apache.synapse.MessageContext;
import org.apache.synapse.SynapseConstants;
import org.apache.synapse.aspects.flow.statistics.collectors.RuntimeStatisticCollector;
import org.apache.synapse.aspects.flow.statistics.data.raw.StatisticsLog;
import org.apache.synapse.aspects.flow.statistics.publishing.PublishingEvent;
import org.apache.synapse.aspects.flow.statistics.publishing.PublishingFlow;
import org.apache.synapse.aspects.flow.statistics.publishing.PublishingPayload;
import org.apache.synapse.aspects.flow.statistics.publishing.PublishingPayloadEvent;
import org.apache.synapse.commons.json.JsonUtil;
import org.apache.synapse.core.axis2.Axis2MessageContext;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * Provides various methods used for tracing data collections.
 */
public class TracingDataCollectionHelper {

	/**
	 * Extract payload from the synapse message context.
	 *
	 * @param messageContext synapse message context
	 * @return payload
	 */
	public static String collectPayload(MessageContext messageContext) {
		String payload = null;
		try {
			org.apache.axis2.context.MessageContext a2mc = ((Axis2MessageContext) messageContext).getAxis2MessageContext();
			if (JsonUtil.hasAJsonPayload(a2mc)) {
				payload = JsonUtil.jsonPayloadToString(a2mc);
			} else {
				payload = messageContext.getEnvelope().toString();
			}
		} catch (Exception e) {
			// SOAP envelop is not created yet
			payload = "NONE";
		}
		return payload;
	}

	/**
	 * Extract properties from the synapse message context.
	 *
	 * @param synCtx synapse message context
	 * @return property map
	 */
	public static Map<String, Object> extractContextProperties(MessageContext synCtx) {
		Set<String> propertySet = synCtx.getPropertyKeySet();
		Map<String, Object> propertyMap = new TreeMap<>();

		for (String property : propertySet) {
			Object propertyValue = synCtx.getProperty(property);
			propertyMap.put(property, propertyValue);
		}

		// Remove message-flow-tracer properties
		propertyMap.remove(SynapseConstants.STATISTICS_STACK);
		propertyMap.remove(StatisticsConstants.STAT_COLLECTOR_PROPERTY);

		return propertyMap;
	}

	/**
	 * Extract transport headers from the synapse message context.
	 *
	 * @param synCtx synapse message context
	 * @return transport headers map
	 */
	public static Map<String, Object> extractTransportProperties(MessageContext synCtx) {
		Map<String, Object> transportPropertyMap = new TreeMap<>();

		Axis2MessageContext axis2smc = (Axis2MessageContext) synCtx;
		org.apache.axis2.context.MessageContext axis2MessageCtx = axis2smc.getAxis2MessageContext();
		Object headers = axis2MessageCtx.getProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);

		if (headers != null && headers instanceof Map) {
			Map headersMap = (Map) headers;
			Set<String> axis2PropertySet = headersMap.keySet();
			for (String entry : axis2PropertySet) {
				transportPropertyMap.put(entry, headersMap.get(entry));
			}
		}

		// Adding important transport related properties
		if (axis2MessageCtx.getTo() != null) {
			transportPropertyMap.put(SynapseConstants.HEADER_TO, axis2MessageCtx.getTo().getAddress());
		}

		if (axis2MessageCtx.getFrom() != null) {
			transportPropertyMap.put(SynapseConstants.HEADER_FROM, axis2MessageCtx.getFrom().getAddress());
		}

		if (axis2MessageCtx.getWSAAction() != null) {
			transportPropertyMap.put("WSAction", axis2MessageCtx.getWSAAction());
		}

		if (axis2MessageCtx.getSoapAction() != null) {
			transportPropertyMap.put("SOAPAction", axis2MessageCtx.getSoapAction());
		}

		if (axis2MessageCtx.getReplyTo() != null) {
			transportPropertyMap.put(SynapseConstants.HEADER_REPLY_TO, axis2MessageCtx.getReplyTo().getAddress());
		}

		if (axis2MessageCtx.getMessageID() != null) {
			transportPropertyMap.put(SynapseConstants.HEADER_MESSAGE_ID, axis2MessageCtx.getMessageID());
		}

		// Remove unnecessary properties
		if (transportPropertyMap.get("Cookie") != null) {
			transportPropertyMap.remove("Cookie");
		}

		return transportPropertyMap;
	}

	public static PublishingFlow createPublishingFlow(List<StatisticsLog> messageFlowLogs) {

		// Data structure using to serialize thr statistic data while publishing.
		PublishingFlow publishingFlow = new PublishingFlow();

		// Payload map which contains all the payloads of the message flow.
		Map<String, PublishingPayload> payloadMap = new HashMap<>();

		// Constants
		final String REFER = "#REFER:";
		final String BEFORE = "before-";
		final String AFTER = "after-";
		final Integer BEFORE_PAYLOAD = 8; // 8th attribute setting @PublishingEvent
		final Integer AFTER_PAYLOAD = 9; // 9th attribute setting @PublishingEvent


		String entryPoint = messageFlowLogs.get(0).getComponentName();
		String flowId = messageFlowLogs.get(0).getMessageFlowId();
		Integer entrypointHashcode = messageFlowLogs.get(0).getHashCode();

		boolean isTracingEnabledForFlow = messageFlowLogs.get(0).isTracingEnabled();

		for (int index = 0; index < messageFlowLogs.size(); index++) {
			StatisticsLog currentStatLog = messageFlowLogs.get(index);
            if (currentStatLog == null) {
                continue;
            }

			// Add each event to Publishing Flow
			publishingFlow.addEvent(new PublishingEvent(flowId, index, currentStatLog, entryPoint, entrypointHashcode));

			// Skip the rest of things, if message tracing is disabled
			if (!RuntimeStatisticCollector.isCollectingPayloads()) {
				continue;
			}

			// Skip flow is tracing is not enabled for the flow (from UI)
			if (!isTracingEnabledForFlow) {
				continue;
			}

			// Update children's immediateParent index
			List<Integer> childrenOfCurrent = currentStatLog.getChildren();
			for(Integer child:childrenOfCurrent) {
				messageFlowLogs.get(child).setImmediateParent(currentStatLog.getCurrentIndex());
			}

			if (currentStatLog.getBeforePayload() != null && currentStatLog.getAfterPayload() == null) {
				currentStatLog.setAfterPayload(currentStatLog.getBeforePayload());
			}

			if (currentStatLog.getBeforePayload() == null) {
				int parentIndex = currentStatLog.getImmediateParent();
				StatisticsLog parentStatLog = messageFlowLogs.get(parentIndex);

				if (parentStatLog.getAfterPayload().startsWith(REFER)) {
					// Parent also referring to after-payload
					currentStatLog.setBeforePayload(parentStatLog.getAfterPayload());
					currentStatLog.setAfterPayload(parentStatLog.getAfterPayload());

					String referringIndex = parentStatLog.getAfterPayload().split(":")[1];

					payloadMap.get(AFTER + referringIndex)
					               .addEvent(new PublishingPayloadEvent(index, BEFORE_PAYLOAD));
					payloadMap.get(AFTER + referringIndex)
					               .addEvent(new PublishingPayloadEvent(index, AFTER_PAYLOAD));

				} else {
					// Create a new after-payload reference
					currentStatLog.setBeforePayload(REFER + parentIndex);
					currentStatLog.setAfterPayload(REFER + parentIndex);

					payloadMap.get(AFTER + parentIndex)
					               .addEvent(new PublishingPayloadEvent(index, BEFORE_PAYLOAD));
					payloadMap.get(AFTER + parentIndex)
					               .addEvent(new PublishingPayloadEvent(index, AFTER_PAYLOAD));
				}

			} else {

				// For content altering components
				PublishingPayload publishingPayloadBefore = new PublishingPayload();
				publishingPayloadBefore.setPayload(currentStatLog.getBeforePayload());
				publishingPayloadBefore.addEvent(new PublishingPayloadEvent(index, BEFORE_PAYLOAD));
				payloadMap.put(BEFORE + index, publishingPayloadBefore);

				PublishingPayload publishingPayloadAfter = new PublishingPayload();
				publishingPayloadAfter.setPayload(currentStatLog.getAfterPayload());
				publishingPayloadAfter.addEvent(new PublishingPayloadEvent(index, AFTER_PAYLOAD));
				payloadMap.put(AFTER + index, publishingPayloadAfter);

			}

		}

		publishingFlow.setMessageFlowId(flowId);
		// Move all payloads to publishingFlow object
		publishingFlow.setPayloads(payloadMap.values());

		return publishingFlow;
	}
}
