
// JGTUtils.java
// Andrew Davison, ad@coe.psu.ac.th, Jan. 2023

/* Utilities for JGraphT (https://jgrapht.org/):

     * read in an unweighted or weighted (un)directed 
       graph from a text file holding an adj. list, 
       adj. matrix, or a GraphML file 
       (http://graphml.graphdrawing.org/)
      - the weighted methods include a 'W' in their names;
      - the weighted versions use my WeightedEdge class

     * write graph out to a text file, creating an adj. list, 
       adj. matrix, or a GraphML file 

     * draw using GraphViz (https://graphviz.org/)
        - must be installed separately from JGraphT

     * draw using JGraphX (https://github.com/jgraph/jgraphx)
        -its API is included with JGraphT
*/


import java.io.*;
import java.util.*;
import java.nio.file.*;
import java.awt.*;
import java.awt.image.*;
import javax.imageio.*;
import javax.swing.*;

import org.jgrapht.*;
import org.jgrapht.ext.*;
import org.jgrapht.graph.*;
import org.jgrapht.graph.builder.*;
import org.jgrapht.nio.*;
import org.jgrapht.nio.csv.*;
import org.jgrapht.nio.graphml.*;
import org.jgrapht.nio.dot.*;
import org.jgrapht.util.*;

// JGraphX API
import com.mxgraph.layout.*;
import com.mxgraph.layout.hierarchical.*;
import com.mxgraph.util.*;
import com.mxgraph.swing.mxGraphComponent;
import com.mxgraph.model.*;


public class JGTUtils
{
  // JFrame coordinates
  private static final int OFFSET = 40;

  private static int xCoord = 80;
  private static int yCoord = 80;
  

  public static Graph<String, DefaultEdge> 
                              makeSimple(String[][] edges)
  {
    Graph<String, DefaultEdge> g = new SimpleGraph<>(DefaultEdge.class);
    for (String[] edge : edges)
      Graphs.addEdgeWithVertices(g, edge[0], edge[1]);
    return g;
  } // makeSimple()


  public static Graph<String, DefaultEdge> makeDG(String[][] edges)
  {
    Graph<String, DefaultEdge> g = 
               new DefaultDirectedGraph<>(DefaultEdge.class);
    for (String[] edge : edges)
      Graphs.addEdgeWithVertices(g, edge[0], edge[1]);
    return g;
  } // makeDG()


  // ------------- read in graph from a text file -------------------


  public static Graph<String, DefaultEdge> readAdjList(
           String input, boolean isDirected)
  // make a graph from an (un)directed unweighted adjacency list 
  {
    Graph<String, DefaultEdge> g;
    if (isDirected)
      g = GraphTypeBuilder.directed().
          allowingMultipleEdges(true).
          allowingSelfLoops(true).
          weighted(false).
          edgeClass(DefaultEdge.class).
          vertexSupplier(SupplierUtil.createStringSupplier(1)).
          buildGraph();
    else
      g = GraphTypeBuilder.undirected().
          allowingMultipleEdges(true).
          allowingSelfLoops(true).
          weighted(false).
          edgeClass(DefaultEdge.class).
          vertexSupplier(SupplierUtil.createStringSupplier(1)).
          buildGraph();

    CSVImporter<String, DefaultEdge> importer = 
             new CSVImporter<>(CSVFormat.ADJACENCY_LIST, ' ');
    importer.setVertexFactory(id->id);
    importer.importGraph(g, new StringReader(input));
    return g;
  } /* readAdjList */


  public static Graph<String, WeightedEdge> readWAdjList(
           String input, boolean isDirected)
  // make a graph from an (un)directed WEIGHTED adjacency list;
  // uses my WeightedEdge class
  {
    Graph<String, WeightedEdge> g;
    if (isDirected)
      g = GraphTypeBuilder.directed().
          allowingMultipleEdges(true).
          allowingSelfLoops(true).
          weighted(true).
          edgeClass(WeightedEdge.class).
          vertexSupplier(SupplierUtil.createStringSupplier(1)).
          buildGraph();
    else
      g = GraphTypeBuilder.undirected().
          allowingMultipleEdges(true).
          allowingSelfLoops(true).
          weighted(true).
          edgeClass(WeightedEdge.class).
          vertexSupplier(SupplierUtil.createStringSupplier(1)).
          buildGraph();

    CSVImporter<String, WeightedEdge> importer = 
             new CSVImporter<>(CSVFormat.ADJACENCY_LIST, ' ');
    importer.setVertexFactory(id->id);
    importer.setParameter(CSVFormat.Parameter.EDGE_WEIGHTS, true);

    importer.importGraph(g, new StringReader(input));
    return g;
  } /* readWAdjList */


  public static Graph<String, DefaultEdge> readAdjMatrix(
                              String input, boolean isDirected)
  // make a graph from an (un)directed unweighted adjacency matrix 
  {
    Graph<String, DefaultEdge> g;
    if (isDirected) 
      g = new DirectedPseudograph<>(
                        SupplierUtil.createStringSupplier(1),
                        SupplierUtil.DEFAULT_EDGE_SUPPLIER, false);
    else
      g = new Pseudograph<>( SupplierUtil.createStringSupplier(1),
                             SupplierUtil.DEFAULT_EDGE_SUPPLIER, false);

    CSVImporter<String, DefaultEdge> importer = 
                  new CSVImporter<>(CSVFormat.MATRIX, ',');
    importer.setParameter(CSVFormat.Parameter.MATRIX_FORMAT_NODEID, true);
    importer.setVertexFactory(id->id);
    importer.setParameter(
         CSVFormat.Parameter.MATRIX_FORMAT_ZERO_WHEN_NO_EDGE, true);
    importer.importGraph(g, new StringReader(input));

    return g;
  } /* readAdjMatrix */



  public static Graph<String, WeightedEdge> readWAdjMatrix(
                        String input, boolean isDirected)
  // make a graph from an (un)directed WEIGHTED adjacency matrix;
  // uses my WeightedEdge class
  {
    Graph<String, WeightedEdge> g;
    if (isDirected) 
      g = new DirectedPseudograph<>(
                SupplierUtil.createStringSupplier(1),
                SupplierUtil.createSupplier(WeightedEdge.class), true);
    else
      g = new Pseudograph<>( SupplierUtil.createStringSupplier(1),
                SupplierUtil.createSupplier(WeightedEdge.class), true);

    CSVImporter<String, WeightedEdge> importer = 
                  new CSVImporter<>(CSVFormat.MATRIX, ',');
    importer.setParameter(CSVFormat.Parameter.MATRIX_FORMAT_NODEID, true);
    importer.setVertexFactory(id->id);
    importer.setParameter(
         CSVFormat.Parameter.MATRIX_FORMAT_ZERO_WHEN_NO_EDGE, true);
    importer.setParameter(CSVFormat.Parameter.EDGE_WEIGHTS, true);
    importer.importGraph(g, new StringReader(input));

    return g;
  } /* readWAdjMatrix */



  public static Graph<String, DefaultEdge> readGraphML(
          String input, boolean isDirected)
  // make a graph from an (un)directed unweighted GraphML file 
  {
    Graph<String, DefaultEdge> g;
    if (isDirected)
      g = GraphTypeBuilder.directed().
                    allowingMultipleEdges(true).
                    allowingSelfLoops(true).
                    weighted(false).
                    vertexSupplier(SupplierUtil.createStringSupplier(1)).
                    edgeClass(DefaultEdge.class).
                    buildGraph();
    else
      g = GraphTypeBuilder.undirected().
                    allowingMultipleEdges(true).
                    allowingSelfLoops(true).
                    weighted(false).
                    vertexSupplier(SupplierUtil.createStringSupplier(1)).
                    edgeClass(DefaultEdge.class).
                    buildGraph();

    GraphMLImporter<String, DefaultEdge> importer = new GraphMLImporter<>();
    importer.setVertexFactory(id->id);
    importer.importGraph(g, new StringReader(input));
    return g;
  } /* readGraphML */


  public static Graph<String, WeightedEdge> readWGraphML(
          String input, boolean isDirected)
  // make a graph from an (un)directed WEIGHTED GraphML file;
  // uses my WeightedEdge class
  {
    Graph<String, WeightedEdge> g;
    if (isDirected)
      g = GraphTypeBuilder.directed().
                    allowingMultipleEdges(true).
                    allowingSelfLoops(true).
                    weighted(true).
                    vertexSupplier(SupplierUtil.createStringSupplier(1)).
                    edgeClass(WeightedEdge.class).
                    buildGraph();
    else
      g = GraphTypeBuilder.undirected().
                    allowingMultipleEdges(true).
                    allowingSelfLoops(true).
                    weighted(true).
                    vertexSupplier(SupplierUtil.createStringSupplier(1)).
                    edgeClass(WeightedEdge.class).
                    buildGraph();

    GraphMLImporter<String, WeightedEdge> importer = new GraphMLImporter<>();
    importer.setVertexFactory(id->id);
    importer.importGraph(g, new StringReader(input));
    return g;
  } /* readWGraphML */




  public static String loadText(String fnm)
  /* Return all lines as a single string but with
     comment lines removed
  */
  { 
    File file = new File(fnm);
    if (!file.isFile()) {
      System.out.println(fnm + " is not a file");
      return null;
    }

    StringBuilder sb = new StringBuilder();
    try {
      System.out.println("Loading " + fnm);
      BufferedReader br = new BufferedReader( new FileReader(fnm));
      String line;
      char ch;
      while((line = br.readLine()) != null) {
        line = line.trim();
        if (line.length() == 0)  // blank line
          continue;
        if (line.startsWith("//"))   // comment
          continue;
        else {
          sb.append(line.replaceAll("\\s+", " ").trim() + "\n");
        }

      }
      br.close();
      return sb.toString();
    } 
    catch (IOException e) 
    { System.out.println("Unable to load " + fnm);  }

    return null;
  }  // end of loadText()



  // ------------- write graph out to a text file -------------------

  public static <E> String writeAdjList(
                          Graph<String, E> g)
  {
    CSVExporter<String, E> exporter =
      new CSVExporter<>(CSVFormat.ADJACENCY_LIST, ' ');
    exporter.setVertexIdProvider(v->v);  // use vertex labels
    if (g.getType().isWeighted())
      exporter.setParameter(CSVFormat.Parameter.EDGE_WEIGHTS, true);
    StringWriter w = new StringWriter();
    exporter.exportGraph(g, w);
    return w.toString();
  } /* writeAdjList */



  public static <E> String writeAdjMatrix(Graph<String, E> g)
  {
    CSVExporter<String, E> exporter =
      new CSVExporter<>(CSVFormat.MATRIX, ',');
    exporter.setParameter(
       CSVFormat.Parameter.MATRIX_FORMAT_ZERO_WHEN_NO_EDGE, true);
    exporter.setVertexIdProvider(v->v);  // use vertex labels
    exporter.setParameter(CSVFormat.Parameter.MATRIX_FORMAT_NODEID, true);
    if (g.getType().isWeighted())
      exporter.setParameter(CSVFormat.Parameter.EDGE_WEIGHTS, true);

    StringWriter w = new StringWriter();
    exporter.exportGraph(g, w);
    return w.toString();
  }  // end of writeAdjMatrix()



  public static <E> String writeGraphML(Graph<String, E> g)
  {
    GraphMLExporter<String, E> exporter = 
               new GraphMLExporter<>();
    exporter.setEdgeIdProvider(new IntegerIdProvider<>());
    if (g.getType().isWeighted())
      exporter.setExportEdgeWeights(true);
    exporter.setVertexIdProvider(v->v); // use vertex labels
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    exporter.exportGraph(g, os);
    try {
      return new String(os.toByteArray(), "UTF-8");
    }
    catch(UnsupportedEncodingException e)
    {  System.out.println(e); }
    return null;
  }  // end of writeGraphML()



  public static void saveText(String fnm, String s)
  {
    System.out.println("Writing to " + fnm);
    try {
      BufferedWriter bw = new BufferedWriter(new FileWriter(fnm));
      bw.write(s);
      bw.close();
    }
    catch(IOException e)
    {  System.out.println("Unable to write to " + fnm);  }
  }  // end of saveText()


  // -------------------- draw using GraphViz ---------------


  public static <E> void displayAsDOT(Graph<String, E> g)
  {  displayAsDOT("DOT Graph", g);   }



  public static <E> void displayAsDOT(String title, Graph<String, E> g)
  // display a (weighted) graph in DOT format
  {
    String dotStr = JGTUtils.writeDOT(g);
    String dotFnm = JGTUtils.makeTemp(".dot");
    JGTUtils.saveText(dotFnm, dotStr);

    String pngFnm = JGTUtils.makeTemp(".png");
    int exitCode = JGTUtils.drawGraph(dotFnm, pngFnm);
    if (exitCode == 0) {
      ImageViewer.view(title, xCoord, yCoord, pngFnm);
      xCoord += OFFSET;
      yCoord += OFFSET;
    }
  }  // displayAsDOT()


  public static <E> void writeDOTImage(Graph<String, E> g, String pngFnm)
  // save a PNG of a (weighted) graph in DOT format
  {
    String dotStr = JGTUtils.writeDOT(g);
    String dotFnm = JGTUtils.makeTemp(".dot");
    JGTUtils.saveText(dotFnm, dotStr);

    int exitCode = JGTUtils.drawGraph(dotFnm, pngFnm);
    if (exitCode != 0)
      System.out.println("Unable to save image to " + pngFnm);
  }  // writeDOTImage()



  public static <E> String writeDOT(Graph<String, E> g)
  // write a (weighted) graph in DOT format.
  {
    try {
      DOTExporter<String, E> exporter =
               new DOTExporter<>(v -> v.replace('.', '_'));

      exporter.setGraphAttributeProvider(() -> {
        Map<String, Attribute> map = new LinkedHashMap<>();
        map.put("rankdir", DefaultAttribute.createAttribute("LR")); // left-to-right
        // map.put("oneblock", DefaultAttribute.createAttribute("true"));
                          // one circle will be used by circo renderer 
      return map;
      });

      exporter.setVertexAttributeProvider((v) -> {
        Map<String, Attribute> map = new LinkedHashMap<>();
        map.put("label", DefaultAttribute.createAttribute(v));
        map.put("style", DefaultAttribute.createAttribute("filled"));
        map.put("fillcolor", DefaultAttribute.createAttribute("#C3D9FF"));
                                      // same color as JGraphX
        return map;
      });

      if (g.getType().isWeighted())  // label the edges
        exporter.setEdgeAttributeProvider(e -> {
          Map<String, Attribute> map = new LinkedHashMap<>();
          map.put("label", DefaultAttribute.createAttribute(e.toString()));
          return map;
        });

      Writer sw = new StringWriter();
      exporter.exportGraph(g, sw);
      return sw.toString();
    }
    catch(ExportException e)
    {  System.out.println("Unable to render: " + e);  }
    return null;
  } /* writeDOT */



  public static int drawGraph(String dotFnm, String pngFnm)
  /* Draw a graph using GraphViz, saving it in pngFnm
  */
  {
    System.out.println("Drawing graph using GraphViz dot");
    ProcessBuilder processBuilder = new ProcessBuilder();
    // processBuilder.command("circo", "-Tpng", dotFnm, "-o", pngFnm);
    processBuilder.command("dot", "-Tpng", dotFnm, "-o", pngFnm);  
    try {
      Process process = processBuilder.start();
      return process.waitFor();
    } 
    catch (IOException e) 
    {  System.out.println(e);
       System.out.println("-- Use displayAsMX() or download GraphViz");
    }
    catch (InterruptedException e) 
    {  System.out.println(e);  }
    return 1;
  }  // end of drawGraph


  private static String makeTemp(String ext)
  // create a temporary file with the given extension,
  // and return the file name
  {
	try {
      File f = File.createTempFile("temp", ext);
      f.deleteOnExit();   // Delete when JVM exits
      return f.getAbsolutePath();
	}
	catch (IOException e) 
    {  System.out.println("Could not create temp file with extension " + ext);  }
    return null;
  }  // end of writeToTemp()


  // -------------------- draw using JGraphX ---------------


  public static <E> void displayAsMX(Graph<String, E> g)
  {  displayAsMX("GraphX Viewer", g);  }


  public static <E> void displayAsMX(String title, Graph<String, E> g)
  {
    JGraphXAdapter<String, E> graphAdapter = buildAdapter(g);

    System.out.println("Drawing graph using GraphX");
    JFrame f = new JFrame(title);

    mxGraphComponent gComp = new mxGraphComponent(graphAdapter);
    gComp.setEnabled(false);   // not editable
    gComp.zoom(1.5);

    gComp.setBorder(BorderFactory.createEmptyBorder(20, //top
                20, //left
                40, //bottom
                30) //right
    );
    gComp.getViewport().setBackground(Color.WHITE);
    gComp.setBackground(Color.WHITE);

    f.add(gComp);
    f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    f.pack();
    f.setResizable(false);
    f.setLocation(xCoord, yCoord);
    xCoord += OFFSET;
    yCoord += OFFSET;
    f.setVisible(true);
  } // end of displayAsMX()


  private static <E> JGraphXAdapter<String, E> buildAdapter(
                                      Graph<String, E> g)
  // (weighted) (un)directed graph
  {
    JGraphXAdapter<String, E> graphAdapter =
                                         new JGraphXAdapter<>(g);
    Map<String, Object> vStyle = graphAdapter.getStylesheet().
                                       getDefaultVertexStyle();
    //  System.out.println(vStyle);
    vStyle.put("shape", "ellipse");  // use an ellipse shape for nodes

    // change shape of vertex from ellipse to circle
    Map<String, mxICell> vcells = graphAdapter.getVertexToCellMap();
   //System.out.println(vcells);
    for(mxICell cell : vcells.values()) {
      mxGeometry geom = cell.getGeometry();
      geom.setWidth(geom.getHeight());
    }

    Map<String, Object> eStyle = graphAdapter.getStylesheet().
                                       getDefaultEdgeStyle();
    // System.out.println(eStyle);
    eStyle.put("fontColor", "black");
    if (!g.getType().isDirected())
      eStyle.put("endArrow", "none");   // no arrows

    if (!g.getType().isWeighted()) {
      Map<E, mxICell> ecells = graphAdapter.getEdgeToCellMap();
      // System.out.println(ecells);
      for(mxICell cell : ecells.values()) {
        // System.out.println(cell);
        cell.setValue("");  // no edge labels
      }
    }

    // mxGraphLayout layout = new mxCircleLayout(graphAdapter);
    mxGraphLayout layout = new mxHierarchicalLayout(graphAdapter,
                                                SwingConstants.WEST);

    layout.execute(graphAdapter.getDefaultParent());

    return graphAdapter;
  }  // end of buildAdapter()


  public static <E> void writeMXImage(Graph<String, E> g, String pngFnm)
  // save a PNG file of a (weighted) graph as an MX image
  {
    JGraphXAdapter<String, E> graphAdapter = buildAdapter(g);

    BufferedImage im =  mxCellRenderer.createBufferedImage(
                graphAdapter, null, 1.5, Color.WHITE, true, null);
                     // scale, bg, antialias, clip
    saveImage(im, pngFnm);
  }  // end of writeMXImage()


  private static void saveImage(BufferedImage im, String fnm)
  {
    try {
      File f = new File(fnm);
      ImageIO.write(im, "png", f);
      System.out.println("Saved image to " + fnm);
    }
    catch (IOException e) {
      System.out.println("Unable to save image to " + fnm);
    }
  }  // end of save()

}  // end of JGTUtils class