/*
 * Copyright 2006-2021 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.core.communication.routing.internal;

import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import de.rcenvironment.core.communication.common.NetworkGraph;
import de.rcenvironment.core.communication.common.NetworkGraphLink;
import de.rcenvironment.core.communication.common.InstanceNodeSessionId;
import de.rcenvironment.core.communication.model.NetworkResponse;
import de.rcenvironment.core.communication.model.NetworkRoutingInformation;
import de.rcenvironment.core.communication.protocol.MessageMetaData;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.incubator.GraphvizUtils;
import de.rcenvironment.core.utils.incubator.GraphvizUtils.DotFileBuilder;

/**
 * Provides tools to generate human readable string representations of routing components (such as {@link TopologyMap}) that are useful for
 * debugging and monitoring.
 * 
 * @author Phillip Kroll
 * @author Robert Mischke
 */
public final class NetworkFormatter {

    private static final String GRAPHVIZ_STYLE_KEY = "style";

    private static final String GRAPHVIZ_STYLE_VALUE_DASHED = "dashed";

    private static final String GRAPHVIZ_STYLE_VALUE_BOLD = "bold";

    private NetworkFormatter() {
        // no instance
    }

    /**
     * TODO krol_ph: Enter comment!
     * 
     * @param networkGraph The network Graph
     * @return A string representation for debugging/displaying.
     */
    public static String linkList(TopologyMap networkGraph) {
        return linkList(networkGraph.getAllLinks());
    }

    /**
     * Formats a collection of links to a string.
     * 
     * @param linkCollection The collection of links.
     * @return A string representation for debugging/displaying.
     */
    public static String linkList(Collection<TopologyLink> linkCollection) {
        String result = "";
        List<TopologyLink> linkList = new ArrayList<TopologyLink>(linkCollection);
        Collections.sort(linkList);

        for (TopologyLink link : linkList) {
            result += StringUtils.format("  %s --[%s]--> %s (Hash=%s)\n",
                link.getSource().getInstanceNodeSessionIdString(),
                link.getConnectionId(),
                link.getDestination().getInstanceNodeSessionIdString(),
                link.hashCode());
        }
        return result;
    }

    /**
     * Formats a list of nodes to a string.
     * 
     * @param networkGraph The network graph.
     * @return A string representation for debugging/displaying.
     */
    public static String nodeList(TopologyMap networkGraph) {
        String result = "";
        List<TopologyNode> networkNodes = new ArrayList<TopologyNode>(networkGraph.getNodes());
        Collections.sort(networkNodes);

        SimpleDateFormat timestampFormat = new SimpleDateFormat("yyyyMMdd-HHmmss.SSS");
        for (TopologyNode networkNode : networkNodes) {
            String nodeIdInfo = networkNode.getNodeIdentifier().getInstanceNodeSessionIdString();
            // mark local node
            if (nodeIdInfo.equals(networkGraph.getLocalNodeId().getInstanceNodeSessionIdString())) {
                nodeIdInfo += "*";
            }
            result +=
                StringUtils.format(
                    "  [%9$s] %1$s, --%4$s-> * --%5$s->, '%8$s', Seq=%2$s, Created=%3$tk:%3$tM:%3$tS, Conv=%6$s, Hash=%7$s\n", nodeIdInfo,
                    // TODO quick & dirty; improve
                    networkNode.getSequenceNumber() + " (" + timestampFormat.format(new Date(networkNode.getSequenceNumber()))
                        + ")",
                    networkNode.getCreatedTime(),
                    networkGraph.getPredecessors(networkNode).size(),
                    networkGraph.getSuccessors(networkNode).size(),
                    (networkNode.getLastGraphHashCode() == networkGraph.hashCode()),
                    networkNode.hashCode(),
                    networkNode.getDisplayName(),
                    networkNode.getIsWorkflowHost());
        }
        return result;
    }

    /**
     * TODO krol_ph: Enter comment!
     * 
     * @param networkGraph The network graph.
     * @return A string representation for debugging/displaying.
     */
    public static String graphMetaData(TopologyMap networkGraph) {
        return StringUtils.format("  Local Node Id: %s, Nodes: %s, Links: %s, Fully conv.: %s, Hash=%s\n",
            networkGraph.getLocalNodeId().getInstanceNodeSessionIdString(),
            networkGraph.getNodeCount(),
            networkGraph.getLinkCount(),
            networkGraph.hasSameTopologyHashesForAllNodes(),
            networkGraph.hashCode());
    }

    /**
     * TODO krol_ph: Enter comment!
     * 
     * @param networkGraph The network graph.
     * @return A string representation for debugging/displaying.
     */
    public static String summary(TopologyMap networkGraph) {
        return StringUtils.format("Topology Metadata:\n%sKnown Nodes:\n%sLinks:\n%s",
            NetworkFormatter.graphMetaData(networkGraph),
            NetworkFormatter.nodeList(networkGraph),
            NetworkFormatter.linkList(networkGraph));
    }

    /**
     * Renders the current network topology to a Graphviz "dot" file.
     * 
     * @param topologyMap the {@link TopologyMap} to render
     * @return the "dot" file script content
     */
    public static String topologyToGraphviz(TopologyMap topologyMap) {
        DotFileBuilder builder = GraphvizUtils.createDotFileBuilder("rce_network");
        List<TopologyNode> networkNodes = new ArrayList<TopologyNode>(topologyMap.getNodes());
        Map<String, String> backwardEdgeProperties = new HashMap<String, String>();
        backwardEdgeProperties.put(GRAPHVIZ_STYLE_KEY, GRAPHVIZ_STYLE_VALUE_DASHED);
        for (TopologyNode networkNode : networkNodes) {
            // TODO filter to only include reachable nodes
            String vertexId = networkNode.getNodeIdentifier().getInstanceNodeIdString();  // only use persistent part to avoid clutter
            String label = networkNode.getDisplayName();
            builder.addVertex(vertexId, label);
        }
        List<TopologyLink> linkList = new ArrayList<TopologyLink>(topologyMap.getAllLinks());
        for (TopologyLink link : linkList) {
            String edgeId = link.getConnectionId();
            String label = edgeId.substring(0, edgeId.indexOf('-'));
            if (label.endsWith("s")) {
                builder.addEdge(link.getSource().getInstanceNodeIdString(), link.getDestination().getInstanceNodeIdString(), label);
            } else {
                builder.addEdge(link.getSource().getInstanceNodeIdString(), link.getDestination().getInstanceNodeIdString(), label,
                    backwardEdgeProperties);
            }
        }
        // mark local node
        String localNodeId = topologyMap.getLocalNodeId().getInstanceNodeIdString();
        builder.addVertexProperty(localNodeId, GRAPHVIZ_STYLE_KEY, GRAPHVIZ_STYLE_VALUE_BOLD);
        // generate script
        return builder.getScriptContent();
    }

    /**
     * Formats message to string.
     * 
     * @param messageContent The message content.
     * @param metaData The meta data of the message.
     * @return A string representation for debugging/displaying.
     */
    public static String message(Serializable messageContent, Map<String, String> metaData) {
        MessageMetaData handler = MessageMetaData.wrap(metaData);
        return StringUtils.format("Src='%s', Dst='%s', Body='%s', HopC=%d, MsgId='%s', Trace='%s'",
            handler.getSender(),
            handler.getFinalRecipient(),
            messageContent.toString(),
            handler.getHopCount(),
            handler.getMessageId(),
            handler.getTrace());
    }

    /**
     * Formats LSA to string.
     * 
     * @param lsa The links state advertisement.
     * @return A string representation for debugging/displaying.
     */
    public static String lsa(LinkStateAdvertisement lsa) {
        return StringUtils.format("owner=%s, links=%s, seq=%s, type=%s, hash=%s\n%s",
            lsa.getOwner(),
            lsa.getLinks().size(),
            lsa.getSequenceNumber(),
            lsa.getReason(),
            lsa.getGraphHashCode(),
            linkList(lsa.getLinks()));
    }

    /**
     * Formats LSA cache to string.
     * 
     * @param lsaCache The LSA cache.
     * @return A string representation for debugging/displaying.
     */
    public static String lsaCache(LinkStateAdvertisementBatch lsaCache) {
        String result = StringUtils.format("size=%s\n", lsaCache.size());
        for (LinkStateAdvertisement lsa : lsaCache.values()) {
            result += lsa(lsa) + "\n";
        }
        return result;
    }

    /**
     * Formats a network rout to string.
     * 
     * @param networkRoute The network route.
     * @return A string representation for debugging/displaying.
     */
    public static String networkRoute(NetworkRoute networkRoute) {
        String result =
            StringUtils.format("length: %s, %s, time: %s ms", networkRoute.getPath().size(),
                networkRoute.getSource().getInstanceNodeSessionIdString(),
                networkRoute.getComputationalEffort());
        for (TopologyLink link : networkRoute.getPath()) {
            result += StringUtils.format(" --> %s", link.getDestination().getInstanceNodeSessionIdString());
        }
        return result;
    }

    /**
     * Formats network statistics to string.
     * 
     * @param networkStats The network statistics.
     * @return A string representation for debugging/displaying.
     */
    public static String networkStats(NetworkStats networkStats) {
        return StringUtils.format(
            "\nSuccessful communications: %s\n"
                + "Failed communications: %s\n\n"
                + "LSAs send:     %s\n"
                + "LSAs received: %s\n"
                + "LSAs rejected: %s\n\n"
                + "Max received hop count:     %s\n"
                + "Max time to live:           %s\n"
                + "Number of computed routes:  %s\n\n"
                + "Average hop count of send LSAs:     %s\n"
                + "Average hop count of received LSAs: %s\n"
                + "Average hop count of rejected LSAs: %s\n",

            networkStats.getSuccessfulCommunications(),
            networkStats.getFailedCommunications(),

            networkStats.getSentLSAs(),
            networkStats.getReceivedLSAs(),
            networkStats.getRejectedLSAs(),

            networkStats.getMaxReceivedHopCount(),
            networkStats.getMaxTimeToLive(),
            networkStats.getShortestPathComputations(),

            networkStats.averageHopCountOfSentLSAs(),
            networkStats.averageHopCountOfReceivedLSAs(),
            networkStats.averageHopCountOfRejectedLSAs());
    }

    /**
     * Formats network response to string.
     * 
     * @param networkResponse The network response.
     * @return A string representation for debugging/displaying.
     */
    public static String networkResponseToString(NetworkResponse networkResponse) {
        return StringUtils.format("id=%s, succ=%s, code=%s, header=%s",
            networkResponse.getRequestId(),
            networkResponse.isSuccess(),
            networkResponse.getResultCode(),
            networkResponse.accessRawMetaData().toString());
    }

    /**
     * Generates a human-readable representation of a {@link TopologyMap}'s contents.
     * 
     * @param topologyMap the {@link TopologyMap} to render
     * @param extendedInfo whether extended (ie, developer) information should be added
     * @return the formatted, multi-line string
     */
    public static String formatTopologyMap(TopologyMap topologyMap, boolean extendedInfo) {
        return StringUtils.format("Nodes:\n%sConnections:\n%s",
            NetworkFormatter.formatTopologyNodes(topologyMap, extendedInfo),
            NetworkFormatter.formatTopologyLinks(topologyMap, extendedInfo));
    }

    /**
     * Renders the current network topology to a simple console-ready "network information" output.
     * 
     * @param networkGraph the {@link TopologyMap} to render
     * @return the information text.
     */
    public static String networkGraphToConsoleInfo(NetworkGraph networkGraph) {
        StringBuilder buffer = new StringBuilder();
        List<InstanceNodeSessionId> networkNodes = new ArrayList<InstanceNodeSessionId>(networkGraph.getNodeIds());
        buffer.append("Reachable network nodes (" + networkNodes.size() + " total):\n");
        for (InstanceNodeSessionId nodeId : networkNodes) {
            buffer.append(StringUtils.format("  %s [%s]\n", nodeId.getAssociatedDisplayName(), nodeId.getInstanceNodeSessionIdString()));
        }
        buffer.append("Message channels (" + networkGraph.getLinkCount() + " total):\n");
        for (NetworkGraphLink link : networkGraph.getLinks()) {
            buffer.append(StringUtils.format("  %s--[%s]->[%s]\n", link.getSourceNodeId(), link.getLinkId(), link.getTargetNodeId()));
        }
        return buffer.toString();
    }

    /**
     * Renders the current network topology to a Graphviz "dot" file.
     * 
     * @param networkGraph the {@link TopologyMap} to render
     * @param markSpanningTree TODO
     * @return the "dot" file script content
     */
    public static String networkGraphToGraphviz(NetworkGraph networkGraph, boolean markSpanningTree) {
        DotFileBuilder builder = GraphvizUtils.createDotFileBuilder("rce_network");
        List<InstanceNodeSessionId> networkNodes = new ArrayList<InstanceNodeSessionId>(networkGraph.getNodeIds());

        // prepare spanning tree data if requested, or set an empty placeholder
        Set<NetworkGraphLink> spanningTreeLinks;
        if (markSpanningTree) {
            NetworkRoutingInformation routingInformation = networkGraph.getRoutingInformation();
            if (routingInformation == null) {
                throw new IllegalArgumentException("Spanning tree requested for a graph without routing information");
            }
            spanningTreeLinks = routingInformation.getSpanningTreeLinks();
        } else {
            // placeholder
            spanningTreeLinks = new HashSet<NetworkGraphLink>();
        }

        // add vertices
        for (InstanceNodeSessionId nodeId : networkNodes) {
            String vertexId = nodeId.getInstanceNodeIdString(); // only use the persistent part to avoid clutter 
            String label = nodeId.getAssociatedDisplayName(); // TODO improve
            builder.addVertex(vertexId, label);
        }

        // add edges
        List<NetworkGraphLink> linkList = new ArrayList<NetworkGraphLink>(networkGraph.getLinks());
        // TODO move style handling into GraphViz builder? (requires adding edge ids) - misc_ro
        StringBuilder styleBuilder = new StringBuilder();
        for (NetworkGraphLink link : linkList) {
            String edgeId = link.getLinkId();
            String label = edgeId.substring(0, edgeId.indexOf('-'));
            Map<String, String> edgeProperties = new HashMap<String, String>();
            if (label.endsWith("r")) {
                styleBuilder.append(GRAPHVIZ_STYLE_VALUE_DASHED);
            }
            if (spanningTreeLinks.contains(link)) {
                if (styleBuilder.length() != 0) {
                    styleBuilder.append(",");
                }
                styleBuilder.append(GRAPHVIZ_STYLE_VALUE_BOLD);
            }
            edgeProperties.put(GRAPHVIZ_STYLE_KEY, styleBuilder.toString());
            styleBuilder.setLength(0);
            builder.addEdge(link.getSourceNodeId().getInstanceNodeIdString(), link.getTargetNodeId().getInstanceNodeIdString(), label,
                edgeProperties);
        }

        // mark local node
        String localNodeId = networkGraph.getLocalNodeId().getInstanceNodeIdString();
        builder.addVertexProperty(localNodeId, GRAPHVIZ_STYLE_KEY, GRAPHVIZ_STYLE_VALUE_BOLD);

        // generate script
        return builder.getScriptContent();
    }

    private static String formatTopologyNodes(TopologyMap topologyMap, boolean extendedInfo) {
        InstanceNodeSessionId localNodeId = topologyMap.getLocalNodeId();
        Set<InstanceNodeSessionId> reachableNodes = topologyMap.getIdsOfReachableNodes(false);
        List<TopologyNode> topologyNodes = new ArrayList<TopologyNode>(topologyMap.getNodes());

        StringBuilder result = new StringBuilder();

        Collections.sort(topologyNodes);
        for (TopologyNode topologyNode : topologyNodes) {
            // do not show unreachable nodes (at least, not by default)
            InstanceNodeSessionId nodeId = topologyNode.getNodeIdentifier();
            if (!reachableNodes.contains(nodeId)) {
                continue;
            }
            // mark local node
            String markers = "";
            if (nodeId.equals(localNodeId)) {
                markers += " <self>";
            }
            result.append(
                StringUtils.format("  %s%s\n",
                    nodeId,
                    markers));
        }
        return result.toString();
    }

    private static String formatTopologyLinks(TopologyMap topologyMap, boolean extendedInfo) {
        StringBuilder result = new StringBuilder();
        List<TopologyLink> linkList = new ArrayList<TopologyLink>(topologyMap.getAllLinks());
        Collections.sort(linkList);

        for (TopologyLink link : linkList) {
            result.append(StringUtils.format("  %s --[%s]--> %s\n",
                link.getSource(),
                link.getConnectionId(),
                link.getDestination()));
        }
        return result.toString();
    }

}
