
// SpaceViewer.java
// Andrew Davison, May 2023, ad@coe.psu.ac.th

/* A JPanel for viewing *SMALL* tuple spaces.
   The top two thirds of the panel consists of two
   columns of ten boxes. The left column displays
   query tuples (string copies of jSpace's TemplateFields).
   The right column shows the tuples currently in the space.

   When a query or tuple is placed in a box, the thread name
   that called the tuple operation is displayed next to it.

   When a query is matched to a tuple, a colored line is drawn
   between them. If the query will remove the tuple, then the line
   is red. If the query will only copy the tuple then the line
   is blue.

   Below the two columns is an area showing the 
   current states of up to 8 threads which are using the tuple
   space. If they are all BLOCKED, that's a good indication
   of deadlock.

   SpaceViewer is used by my VisSpace class, which implements
   its tuple space operations in the same way as jSpace's 
   SequentialSpace class.

  Usage:
    > compile.bat SpaceViewer.java

*/

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.*;


public class SpaceViewer extends JPanel implements Runnable
{
  // panel dimensions
  private static final int PWIDTH = 850;  
  private static final int PHEIGHT = 550;  

  private static final int DELAY = 500;  // ms

  private static final int NUM_BOXES = 10; 
  private static final int BOX_WIDTH = 250;
  private static final int BOX_HEIGHT = 30;

  private static final int LEFT_OFFSET = 150;

  // link colors
  private static final Color GET_COLOR = Color.RED;
  private static final Color QUERY_COLOR = Color.BLUE;

  private static final int MAX_DRAW_THREADS = 8;  


  private boolean isRunning = true;
  private Font font;

  private Point[] queryBoxes, tupleBoxes;  // box positions
  private String[] queryIDs, tupleIDs;     // thread IDs
  private String[] queryStrs, tupleStrs;   
             // query/tuple strings that are drawn inside the boxes

  private ArrayList<Point> links;
      /* The Point object for each link does NOT store coordinates.
         It stores two indicies, one for the query box, one for
         the tuple box, which are used at the two ends of the link.
      */
  private ArrayList<Color> linkCols;   // link colors
      // red denotes get/removal; blue means query/copying

  private TreeMap<String,String> threadsInfo;
      // maps a thread's name to the thread's current state



  public SpaceViewer()
  {
    setBackground(Color.WHITE);
    font = new Font("Verdana", Font.PLAIN, 14);

    initBox();
    queryIDs = new String[NUM_BOXES];
    queryStrs = new String[NUM_BOXES];

    tupleIDs = new String[NUM_BOXES];
    tupleStrs = new String[NUM_BOXES];

    links = new ArrayList<>();
    linkCols = new ArrayList<>();

    threadsInfo = new TreeMap<>();

    new Thread(this).start();
  }  // end of SpaceViewer()


  private void initBox()
  // calculate box positions
  {
    queryBoxes = new Point[NUM_BOXES];
    tupleBoxes = new Point[NUM_BOXES];

    // two columns
    int x = LEFT_OFFSET;
    int y = 50;
    for (int i=0; i < NUM_BOXES; i++) {
      queryBoxes[i] = new Point(x, y);    // on the left
      tupleBoxes[i] = new Point(x+BOX_WIDTH+50, y);  // on the right
      y += BOX_HEIGHT;
    }
  }  // end of initBox()

   
  public Dimension getPreferredSize()
  {   return new Dimension(PWIDTH, PHEIGHT); }


  public void run() 
  // animation loop, with DELAY controlling its speed
  {
    Thread.currentThread().setName("SpaceViewer");

    while (isRunning) {
      updateThreads();
      repaint();
      SpaceUtils.pause(DELAY);
    }
  }  // end of run()


  public void stopRunning()
  {  isRunning = false;  }


  private void updateThreads()
  // update the thread names and their states
  {
    threadsInfo.clear();
    ArrayList<Pair<String,String>> infoList = SpaceUtils.listThreads();
      // the two strings in a pair are a thread name and its state
    for (Pair<String,String> p : infoList) {
      String threadName = p.getX();
      String threadState = p.getY();
      threadsInfo.put(threadName, threadState);
    }
  }  // end of updateThreads()


  public void paintComponent(Graphics g)
  {
    super.paintComponent(g); 
    Graphics2D g2d = (Graphics2D)g;

    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
    g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
                           RenderingHints.VALUE_RENDER_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, 
                         RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
    g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
                         RenderingHints.VALUE_FRACTIONALMETRICS_ON);

    g2d.setFont(font);
    g2d.setColor(Color.BLACK);

    drawBoxes(g2d);
    drawLinks(g2d);
    drawThreads(g2d);
  } // end of paintComponent()


  private void drawBoxes(Graphics2D g2d)
  // draw the queries and tuples boxes
  {
    centerText(g2d, "Queries", LEFT_OFFSET + BOX_WIDTH/2, 30);
    centerText(g2d, "Tuples",  LEFT_OFFSET + 50 + 3*BOX_WIDTH/2, 30);

    Stroke st = g2d.getStroke();
    float[] dashPattern = {2f, 4f};   // opaque, transparent
    Stroke stroke1 = new BasicStroke(2f, BasicStroke.CAP_BUTT,
        BasicStroke.JOIN_MITER, 1.0f, dashPattern, 2.0f);
    g2d.setStroke(stroke1);

    for (int i=0; i < NUM_BOXES; i++) {
      // draw query box
      g2d.setColor(Color.GRAY);
      int x = queryBoxes[i].x;
      int y = queryBoxes[i].y;
      g2d.drawRect( x, y, BOX_WIDTH, BOX_HEIGHT);

      // draw the thread name associated with this query
      if (queryIDs[i] != null)
        rightAlignText(g2d, queryIDs[i], x-10, y+BOX_HEIGHT/2);

      // draw the query string inside its box
      g2d.setColor(Color.BLACK);
      if (queryStrs[i] != null)
        centerText(g2d, queryStrs[i], x+BOX_WIDTH/2, y+BOX_HEIGHT/2);

      // draw tuple box
      g2d.setColor(Color.GRAY);
      x = tupleBoxes[i].x;
      y = tupleBoxes[i].y;
      g2d.drawRect( x, y, BOX_WIDTH, BOX_HEIGHT);

      // draw the thread name associated with this tuple
      if (tupleIDs[i] != null)
        leftAlignText(g2d, tupleIDs[i], 
                            x+BOX_WIDTH+10, y+BOX_HEIGHT/2);

      // draw the tuple string inside its box
      g2d.setColor(Color.BLACK);
      if (tupleStrs[i] != null)
        centerText(g2d, tupleStrs[i], x+BOX_WIDTH/2, y+BOX_HEIGHT/2);
    }

    g2d.setColor(Color.BLACK);
    g2d.setStroke(st);
  }  // end of drawBoxes()


  private void drawLinks(Graphics2D g2d)
  /* draw the links (colored lines) that represent the
     matches between queries and tuples
  */
  {
    Stroke st = g2d.getStroke();
    g2d.setStroke(new BasicStroke(5));

    for (int i=0; i < links.size(); i++) {
      Point link = links.get(i);
      Point qBox = queryBoxes[link.x];
      Point tBox = tupleBoxes[link.y];
      g2d.setColor(linkCols.get(i));
      g2d.drawLine(qBox.x+BOX_WIDTH, qBox.y+BOX_HEIGHT/2, 
                   tBox.x, tBox.y+BOX_HEIGHT/2);
    }

    g2d.setStroke(st);
    g2d.setColor(Color.BLACK);
  }  // end of drawLinks()


  private void centerText(Graphics2D g2d, String s, int xc, int yc)
  // draw string s centered at (xc,yc)
  {
    FontMetrics fm = g2d.getFontMetrics();
    int x = xc - fm.stringWidth(s)/2;
    int y = yc - fm.getHeight()/2 + fm.getAscent();
    g2d.drawString(s, x, y);
  }  // end of centerText()


  private void leftAlignText(Graphics2D g2d, String s, int xl, int yl)
  // draw string s left aligned and y-centered at (xl,yl)
  {
    FontMetrics fm = g2d.getFontMetrics();
    int x = xl;
    int y = yl - fm.getHeight()/2 + fm.getAscent();
    g2d.drawString(s, x, y);
  }  // end of leftAlignText()


  private void rightAlignText(Graphics2D g2d, String s, int xr, int yr)
  // draw string s right aligned and y-centered at (xr,yr)
  {
    FontMetrics fm = g2d.getFontMetrics();
    int x = xr - fm.stringWidth(s);
    int y = yr - fm.getHeight()/2 + fm.getAscent();
    g2d.drawString(s, x, y);
  }  // end of rightAlignText()


  private void drawThreads(Graphics2D g2d)
  /* draw the thread name:state info in two columns
     below the query and tuple box columns
  */
  {
    int numThreads = threadsInfo.size();
    if (numThreads > MAX_DRAW_THREADS)
      numThreads = MAX_DRAW_THREADS;

    g2d.setColor(Color.BLACK);
    int x = LEFT_OFFSET;
    int y = 400;
    int i = 0;
    for (Map.Entry<String,String> entry : threadsInfo.entrySet()) {
      String s = entry.getKey() + ": " + entry.getValue();
      if (i < MAX_DRAW_THREADS/2)
        g2d.drawString(s, x, y);    // left column
      else 
        g2d.drawString(s, x+BOX_WIDTH+50, y);   // right column
      i++;
      y += BOX_HEIGHT;

      if (i == MAX_DRAW_THREADS/2)
        y = 400;     // start second column
      else if (i == MAX_DRAW_THREADS)
        break;
    }

    if (MAX_DRAW_THREADS < threadsInfo.size()) {
      int numLeft = threadsInfo.size() - MAX_DRAW_THREADS;
      g2d.drawString(numLeft + " thread states not shown", 
                                   x+BOX_WIDTH+50, y);
    }
  }  // end of drawThreads()



  // query and tuple rendering ---------
  /* these methods are used by VisSpace to 
     update the query, tuple, and links info

     If there are too many queries and/or tuples to be
     drawn, then these operations only print
     warning messages to stdout.
  */


  public void addQuery(String id, String s)
  {
    int pos = findEmptyBox(queryStrs);
    if (pos == -1) {
      System.out.println("No query display box left for " + 
                                s + " from " + id);
      return;
    }
    queryIDs[pos] = id;
    queryStrs[pos] = s;
    repaint();
  }  // end of addQuery()


  public void addTuple(String id, String s)
  {
    int pos = findEmptyBox(tupleStrs);
    if (pos == -1) {
      System.out.println("No tuple display box left for " + 
                                s + " from " + id);
      return;
    }
    tupleIDs[pos] = id;
    tupleStrs[pos] = s;
    repaint();
  }  // end of addTuple()


  private int findEmptyBox(String[] arr)
  {
    for (int i=0; i < NUM_BOXES; i++) {
      if (arr[i] == null)
        return i;
    }
    return -1;
  }  // end of findEmptyBox()



  public void linkQueryTuple(String query, 
                                String tuple, boolean isGet)
  /* link a given query and tuple together.
     If isGet is true, then the link will be red to
     indicate that the tuple  will be removed. Otherwise, it
     will be blue to signify that the tuple is only being read.
  */
  {
    int qpos = findMatchingBox(query, queryStrs);
    if (qpos == -1) {
      System.out.println("No query display box assigned to " + query);
      return;
    }

    int tpos = findMatchingBox(tuple, tupleStrs);
    if (tpos == -1) {
      System.out.println("No tuple display box assigned to " + tuple);
      return;
    }

    // System.out.println("Query " + query + 
    //             " linked to tuple " + tuple);
    links.add( new Point(qpos, tpos));
    linkCols.add( (isGet ? GET_COLOR : QUERY_COLOR));
    repaint();
  }  // end of linkQueryTuple()



  public void removeLink(String query, String tuple)
  {
    int qpos = findMatchingBox(query, queryStrs);
    if (qpos == -1) {
  //   System.out.println("No linked query box found for " + query);
      return;
    }

    int tpos = findMatchingBox(tuple, tupleStrs);
    if (tpos == -1) {
   //   System.out.println("No linked tuple box found for " + tuple);
      return;
    }

    // System.out.println("Removing link " + query + " === " + tuple);
    Point link = new Point(qpos, tpos);
    int pos = 0;
    while(pos < links.size()) {
      if (links.get(pos).equals(link))
        break;
      pos++;
    }

    if (pos == links.size()) {
      System.out.println("No link found for " + query + " === " + tuple);
    }
    else {
      links.remove(pos);
      linkCols.remove(pos);
      repaint();
    }
  }  // end of removeLink()


  public void removeQuery(String s)
  {
    int pos = findMatchingBox(s, queryStrs);
    if (pos == -1) {
      return;
    }
    // System.out.println("Removing query " + s);
    queryIDs[pos] = null;
    queryStrs[pos] = null;
    repaint();
  }  // end of removeQuery()


  public void removeTuple(String s)
  {
    int pos = findMatchingBox(s, tupleStrs);
    if (pos == -1) {
      return;
    }
    // System.out.println("Removing tuple " + s);
    tupleIDs[pos] = null;
    tupleStrs[pos] = null;
    repaint();
  }  // end of removeTuple()


  private int findMatchingBox(String s, String[] arr)
  {
    for (int i=0; i < NUM_BOXES; i++) {
      if (s.equals(arr[i]))
        return i;
    }
    return -1;
  }  // end of findEmptyBox()


  // -----------------------------------------
  // static invocation


  public static SpaceViewer view(String title)
  {
    JFrame f = new JFrame(((title == null) ? 
                                 "Space Viewer" : title));

    Container c = f.getContentPane();
    final SpaceViewer sv = new SpaceViewer();
    c.add(sv, BorderLayout.CENTER);

    // f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);  
                                       // main program keeps running
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  
                                       // terminates program

    f.addWindowListener( new WindowAdapter() {
      public void windowClosing(WindowEvent e) 
      {  sv.stopRunning(); }  // stop the redrawing (animation)
    });

    f.pack();
    f.setResizable(false);
  //  f.setLocationRelativeTo(null);  // center the window 
    f.setVisible(true);

    return sv;
  }  // end of view()


}  // end of SpaceViewer class
