ONOS-4326: TopoRegions: Implement basic structure of response to 'topo2Start' event.
- this is WIP: still need to extract data from model cache.

Change-Id: I5ab843a1c352275a8da89964c886b660e3b8b616
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java
index be19389..df6dba9 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/Topo2Jsonifier.java
@@ -37,12 +37,19 @@
 import org.onosproject.ui.model.topo.UiDevice;
 import org.onosproject.ui.model.topo.UiHost;
 import org.onosproject.ui.model.topo.UiLink;
+import org.onosproject.ui.model.topo.UiNode;
 import org.onosproject.ui.model.topo.UiRegion;
 import org.onosproject.ui.model.topo.UiTopoLayout;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static org.onosproject.ui.model.topo.UiNode.LAYER_DEFAULT;
 
 /**
  * Facility for creating JSON messages to send to the topology view in the
@@ -50,6 +57,11 @@
  */
 class Topo2Jsonifier {
 
+    private static final String E_DEF_NOT_LAST =
+            "UiNode.LAYER_DEFAULT not last in layer list";
+    private static final String E_UNKNOWN_UI_NODE =
+            "Unknown subclass of UiNode: ";
+
     private final ObjectMapper mapper = new ObjectMapper();
 
     private ServiceDirectory directory;
@@ -87,7 +99,10 @@
         portStatsService = directory.get(PortStatisticsService.class);
         topologyService = directory.get(TopologyService.class);
         tunnelService = directory.get(TunnelService.class);
+    }
 
+    // for unit testing
+    Topo2Jsonifier() {
     }
 
     private ObjectNode objectNode() {
@@ -155,47 +170,104 @@
      * Returns a JSON representation of the region to display in the topology
      * view.
      *
-     * @param region the region to transform to JSON
+     * @param region     the region to transform to JSON
+     * @param subRegions the subregions within this region
      * @return a JSON representation of the data
      */
-    ObjectNode region(UiRegion region) {
+    ObjectNode region(UiRegion region, Set<UiRegion> subRegions) {
         ObjectNode payload = objectNode();
-
         if (region == null) {
             payload.put("note", "no-region");
             return payload;
         }
+        payload.put("id", region.idAsString());
+        payload.set("subregions", jsonSubRegions(subRegions));
 
-        payload.put("id", region.id().toString());
+        List<String> layerTags = region.layerOrder();
+        List<Set<UiNode>> splitDevices = splitByLayer(layerTags, region.devices());
+        List<Set<UiNode>> splitHosts = splitByLayer(layerTags, region.hosts());
+        Set<UiLink> links = region.links();
 
-        ArrayNode layerOrder = arrayNode();
-        payload.set("layerOrder", layerOrder);
-        region.layerOrder().forEach(layerOrder::add);
-
-        ArrayNode devices = arrayNode();
-        payload.set("devices", devices);
-        for (UiDevice device : region.devices()) {
-            devices.add(json(device));
-        }
-
-        ArrayNode hosts = arrayNode();
-        payload.set("hosts", hosts);
-        for (UiHost host : region.hosts()) {
-            hosts.add(json(host));
-        }
-
-        ArrayNode links = arrayNode();
-        payload.set("links", links);
-        for (UiLink link : region.links()) {
-            links.add(json(link));
-        }
+        payload.set("devices", jsonGrouped(splitDevices));
+        payload.set("hosts", jsonGrouped(splitHosts));
+        payload.set("links", jsonLinks(links));
+        payload.set("layerOrder", jsonStrings(layerTags));
 
         return payload;
     }
 
+    private ArrayNode jsonSubRegions(Set<UiRegion> subregions) {
+        ArrayNode kids = arrayNode();
+        if (subregions != null) {
+            subregions.forEach(s -> kids.add(jsonClosedRegion(s)));
+        }
+        return kids;
+    }
+
+    private ArrayNode jsonStrings(List<String> strings) {
+        ArrayNode array = arrayNode();
+        strings.forEach(array::add);
+        return array;
+    }
+
+    private ArrayNode jsonLinks(Set<UiLink> links) {
+        ArrayNode result = arrayNode();
+        links.forEach(lnk -> result.add(json(lnk)));
+        return result;
+    }
+
+    private ArrayNode jsonGrouped(List<Set<UiNode>> groupedNodes) {
+        ArrayNode result = arrayNode();
+        groupedNodes.forEach(g -> {
+            ArrayNode subset = arrayNode();
+            g.forEach(n -> subset.add(json(n)));
+            result.add(subset);
+        });
+        return result;
+    }
+
+    /**
+     * Returns a JSON payload that encapsulates the devices, hosts, links that
+     * do not belong to any region.
+     *
+     * @param oDevices  orphan devices
+     * @param oHosts    orphan hosts
+     * @param oLinks    orphan links
+     * @param layerTags layer tags
+     * @return a JSON representation of the data
+     */
+    ObjectNode orphans(Set<UiDevice> oDevices, Set<UiHost> oHosts,
+                       Set<UiLink> oLinks, List<String> layerTags) {
+
+        ObjectNode payload = objectNode();
+
+        List<Set<UiNode>> splitDevices = splitByLayer(layerTags, oDevices);
+        List<Set<UiNode>> splitHosts = splitByLayer(layerTags, oHosts);
+
+        payload.set("devices", jsonGrouped(splitDevices));
+        payload.set("hosts", jsonGrouped(splitHosts));
+        payload.set("links", jsonLinks(oLinks));
+        payload.set("layerOrder", jsonStrings(layerTags));
+
+        return payload;
+    }
+
+    private ObjectNode json(UiNode node) {
+        if (node instanceof UiRegion) {
+            return jsonClosedRegion((UiRegion) node);
+        }
+        if (node instanceof UiDevice) {
+            return json((UiDevice) node);
+        }
+        if (node instanceof UiHost) {
+            return json((UiHost) node);
+        }
+        throw new IllegalStateException(E_UNKNOWN_UI_NODE + node.getClass());
+    }
+
     private ObjectNode json(UiDevice device) {
         ObjectNode node = objectNode()
-                .put("id", device.id().toString())
+                .put("id", device.idAsString())
                 .put("type", device.type())
                 .put("online", device.isOnline())
                 .put("master", device.master().toString())
@@ -216,7 +288,7 @@
 
     private ObjectNode json(UiHost host) {
         return objectNode()
-                .put("id", host.id().toString())
+                .put("id", host.idAsString())
                 .put("layer", host.layer());
         // TODO: complete host details
     }
@@ -224,9 +296,101 @@
 
     private ObjectNode json(UiLink link) {
         return objectNode()
-                .put("id", link.id().toString());
+                .put("id", link.idAsString());
         // TODO: complete link details
     }
 
 
+    private ObjectNode jsonClosedRegion(UiRegion region) {
+        return objectNode()
+                .put("id", region.idAsString());
+        // TODO: complete closed-region details
+    }
+
+
+    /**
+     * Returns a JSON array representation of a list of regions. Note that the
+     * information about each region is limited to what needs to be used to
+     * show the regions as nodes on the view.
+     *
+     * @param regions the regions
+     * @return a JSON representation of the minimal region information
+     */
+    public ArrayNode closedRegions(Set<UiRegion> regions) {
+        ArrayNode array = arrayNode();
+        for (UiRegion r : regions) {
+            array.add(jsonClosedRegion(r));
+        }
+        return array;
+    }
+
+    /**
+     * Returns a JSON array representation of a list of devices.
+     *
+     * @param devices the devices
+     * @return a JSON representation of the devices
+     */
+    public ArrayNode devices(Set<UiDevice> devices) {
+        ArrayNode array = arrayNode();
+        for (UiDevice device : devices) {
+            array.add(json(device));
+        }
+        return array;
+    }
+
+    /**
+     * Returns a JSON array representation of a list of hosts.
+     *
+     * @param hosts the hosts
+     * @return a JSON representation of the hosts
+     */
+    public ArrayNode hosts(Set<UiHost> hosts) {
+        ArrayNode array = arrayNode();
+        for (UiHost host : hosts) {
+            array.add(json(host));
+        }
+        return array;
+    }
+
+    /**
+     * Returns a JSON array representation of a list of links.
+     *
+     * @param links the links
+     * @return a JSON representation of the links
+     */
+    public ArrayNode links(Set<UiLink> links) {
+        ArrayNode array = arrayNode();
+        for (UiLink link : links) {
+            array.add(json(link));
+        }
+        return array;
+    }
+
+    // package-private for unit testing
+    List<Set<UiNode>> splitByLayer(List<String> layerTags,
+                                   Set<? extends UiNode> nodes) {
+        final int nLayers = layerTags.size();
+        if (!layerTags.get(nLayers - 1).equals(LAYER_DEFAULT)) {
+            throw new IllegalArgumentException(E_DEF_NOT_LAST);
+        }
+
+        List<Set<UiNode>> splitList = new ArrayList<>(layerTags.size());
+        Map<String, Set<UiNode>> byLayer = new HashMap<>(layerTags.size());
+
+        for (String tag : layerTags) {
+            Set<UiNode> set = new HashSet<>();
+            byLayer.put(tag, set);
+            splitList.add(set);
+        }
+
+        for (UiNode n : nodes) {
+            String which = n.layer();
+            if (!layerTags.contains(which)) {
+                which = LAYER_DEFAULT;
+            }
+            byLayer.get(which).add(n);
+        }
+
+        return splitList;
+    }
 }