
// MandelSpace.java
// Yohan Liyanage, 2008
// Andrew Davison, ad@coe.psu.ac.th, May 2023

/* Compute a Mandelbrot image. 

   NUM_WORKERS workers are created, and 
   MandelSpace generates numRenders tuples of the form:
      ("RENDER", <start line> <no. of lines>)

   A worker gets one of these at random and 
   generates lines of pixels for the image.
   These are added to the space as tuples of the form:
       ("PIC", <worker id>, <start line> <no. of lines>,
                <2D pixel array>)

   The worker keeps doing this until there are no more
   RENDER tuples to process.

   The PIC tuples are read in by MandelSpace and 
   used to create the image.

   Once numRender PIC tuples have been stuck together, MandelSpace
   issues a DIE tuple, which tells the workers to terminate. 

   Based on code from:
      JavaSpaces Principles, Patterns, and Practice
      Eric Freeman, Susanne Hupfer, and Ken Arnold
      Addison Wesley, 1999
      ch.6, Sec. 6.1.1., p.155

   Usage:
    > compile.bat MandelSpace.java
    > run.bat MandelSpace

    Part of the application made up of:
       * MandelSpace.java
       * MandelWorker.java

  Typical Execution:
    Creating 5 workers
    Placed 114 render tasks in space
    Tell workers to terminate
    Time taken:  445ms
  or
    Creating 6 workers
    Placed 114 render tasks in space
    Tell workers to terminate
    Time taken:  405ms

  The sequential version takes about 2000 ms.

  Other version: 
    * CalcPiSeq.java
*/

import javax.swing.*;
import java.awt.*;

import org.jspace.*;


public class MandelSpace extends JFrame
{
  private static final int NUM_WORKERS = 6;  // or try 5

  private static final int MIN_LINES = 3;   
    // min no. of lines per task (>= 1)

  private static final int MAX_LINES = 10;   
      // max no. of lines per task (>= MIN_LINES)

  // dimensions of window
  private static final int WIDTH = 600;
  private static final int HEIGHT = 600;

  // used to determine no. of lines used in a calculation
  private int yMid = HEIGHT/2;

  // image and its graphic
  private Image mandelIm;
  private Graphics mandelGraphic;

  private boolean isFinished = false;

  private Space space;


  public MandelSpace() 
  {
    super("Mandelbrot Rendering...");
    setSize(WIDTH, HEIGHT);
    setResizable(false);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setVisible(true);

    // create image for drawing the Mandelbrot set
    mandelIm = createImage(WIDTH, HEIGHT);
    mandelGraphic = mandelIm.getGraphics();
    mandelGraphic.setColor(Color.BLACK);
    mandelGraphic.fillRect(0, 0, WIDTH, HEIGHT);

    space = new SequentialSpace();
    // space = new VisSpace(10);
    PrintAreas pas = new PrintAreas(NUM_WORKERS);

    System.out.println("Creating " + NUM_WORKERS + " workers");
    for (int i=0; i < NUM_WORKERS; i++)
        new Thread( new MandelWorker(i, WIDTH, HEIGHT,
                                     pas, space), 
                                     "W-" + i).start();
    repaint();

    new Thread(() -> render()).start();
        /* render in a separate thread, so it doesn't impact
           the drawing done by Swing */
  }  // end of MandelSpace()



  public void paint(Graphics g)
  // draw the current version of the Mandelbrot image;
  // this may only be partially completed
  {
    g.drawImage(mandelIm, 0, 0, null);
    if (isFinished)
      setTitle("Mandelbrot Completed");
  } /* paint */


  private void render()
  /* Divide the rendering into group of lines.
     Each group is represented by a tuple of the form:
        ("RENDER", <start line> <no. of lines>)

     numRenders of these are put into the space.

     Each worker contains a copy of the rendering region
     (x1,y1) to (x2, y2) and calcMandelbrot(), which
     is enough information for it to generate pixels
     for these lines.
    
     When a worker has finished its rendering, the data is
     put into the space as the tuple:
        ("PIC", <worker id>, <start line> <no. of lines>,
                <2D pixel array>)

     After render() has read numRenders PIC tuples, the
     image is finished, and it puts a DIE tuple into the
     space to terminate the workers. 

  */
  { long startTime = System.currentTimeMillis();

    try {
      int numRenders = 0;
      int numLines = 0;
      for (int i = 0; i < HEIGHT; i += numLines) {
        double h = calcLinesHeight(i);
        numLines = (int) ((MAX_LINES - MIN_LINES)*h + MIN_LINES);
        space.put("RENDER", i, numLines);
        numRenders++;
      }
      System.out.println("Placed " + numRenders + 
                               " render tasks in space");
	  
      // collect the worker's pictures
      while (numRenders > 0) {
        Object[] picObj = space.get(
               new ActualField("PIC"), 
               new FormalField(Integer.class),  // id
               new FormalField(Integer.class),  // start line 
               new FormalField(Integer.class),  // num lines
               new FormalField(Object.class));  // pixels array
        int id = (int)picObj[1];
        int i = (int)picObj[2];
        int nLines = (int)picObj[3];
        int[][] pixels = (int[][])picObj[4];
	      
        //System.out.println("Using W-" + id + " pic for " +
        //          i + ", " + nLines);
        updateImage(pixels, i, nLines);
        numRenders--;
      }
	  
      System.out.println("Tell workers to terminate");
      space.put("DIE");
    }
    catch (InterruptedException e) 
    {  System.out.println(e);  }

    isFinished = true;

    long elapsedTime = System.currentTimeMillis() - startTime;
    System.out.println("Time taken:  " + elapsedTime + "ms");
  } /* render */



  private double calcLinesHeight(int i)
  {  return Math.abs((double) (i - yMid)) / yMid;   }


  private void updateImage(int[][] pixels, int start, int numLines)
  /* add the pixel colors to the image starting
     from line == start, and for numLines
  */
  {
    int y = 0;   // first line of pixels array
    for (int line = start; y < numLines; y++, line++)
      for (int x = 0; x < WIDTH; x++) {
        int pixel = pixels[x][y];
        Color c = getPixelColor(pixel);
        mandelGraphic.setColor(c);
        mandelGraphic.fillRect(x, line, 1, 1);
      }

    repaint();   // show the ongoing rendering
  } /* updateImage */


  private Color getPixelColor(int pixel)
  // map a pixel to a mix of red and blue
  {
    Color c;
    int val = pixel % 101;   // 0-100
    if (val == 100)
      c = new Color(0, 0, 0);  // black
    else if (val > 90)
      c = new Color(val*2, 0, (val - 90)*25);
    else if (val > 80)
      c = new Color(val*2, 0, 0);
    else if (val > 60)
      c = new Color(val*3, 0, val);
    else if (val > 20)
      c = new Color(val*4, 0, val*2);
    else if (val > 10)
      c = new Color(val*5, 0, val*10);
    else
      c = new Color(0, 0, val*20);
    return c;
  } /* getPixelColor */



  // ---------------------------------------

  public static void main(String[] args)
  {  new MandelSpace();  } 


}  // end of MandelSpace class