/* Java distributed Mandelbrot viewer.  Extra credit implemented: 
   Can select different preset color schemes (via save/load) (3 pts)
   Can save/restore colormap files (5 pts)
   Has undo command (5 pts)
   Has redo command (2 pts)
   Takes command-line parameters (5 pts)
   Can zoom to arbitrary rectangular regions (5 pts)
   TOTAL:  25 pts
*/


import java.awt.*;
import java.util.*;
import java.io.*;
import java.awt.event.*;
import java.awt.image.*;


public final class Fractal extends Frame implements ActionListener {

  private static final String START_MESSAGE="Mandelbrot Viewer 1.0\nLeft-click or click-and drag to zoom; right-click to zoom out.\nSee the README for more documentation.";
  private static final String BACK="<-";
  private static final String FORWARD="->";
  private Button resetButton=new Button("Reset");
  private Button backButton=new Button(BACK);
  private Button forwardButton=new Button(FORWARD);
  private Mandelbrot mbrot;
  private MenuBar cBar;
  private static final String title="Mandelbrot viewer 1.0";
  
  public static void main(String[] args) {

    System.out.println(START_MESSAGE);
    
    int s=0;
    boolean colorflag=false;
    try{s=Integer.parseInt(args[0]); colorflag=true; if (s<1||s>65535) s=0;}
    catch(Exception e) {}
    
    Fractal f=new Fractal(s);
    f.setTitle(title);
    f.pack();
    f.validate();
    f.setVisible(true);
    f.setMenuBar(f.cBar);    //I have to add the menubar AFTER displaying the window, or
                             //else the frame will not appear until the window is resized.
    
    try {
      double a=0,b=0,c=0,d=0;
      if(args.length==5 || args.length==6) {
	if(args[0].equals("-zoom") && args.length==5) {
	  a=Double.valueOf(args[1]).doubleValue();
	  b=Double.valueOf(args[2]).doubleValue();
	  c=Double.valueOf(args[3]).doubleValue();
	  d=Double.valueOf(args[4]).doubleValue();
	}
	if(args[1].equals("-zoom") && args.length==6) {
	  a=Double.valueOf(args[2]).doubleValue();
	  b=Double.valueOf(args[3]).doubleValue();
	  c=Double.valueOf(args[4]).doubleValue();
	  d=Double.valueOf(args[5]).doubleValue();
	}
	if(a>=c || d>=b) throw new IOException();
	f.mbrot.zoomTo(a,b,c,d);
      }
    }
    catch(Exception e) {System.out.println("Invalid parameters.");}
  }
  
  Fractal(int s) {
    mbrot=new Mandelbrot(s);
    mbrot.dummy.addActionListener(this);  //The fractal generates events when changes occur

    setLayout(new BorderLayout());

    Panel p=new Panel();
    p.setLayout(new GridLayout(1,3));
    add("South", p);
    
    resetButton.addActionListener(this);
    backButton.addActionListener(this);
    forwardButton.addActionListener(this);
    backButton.setEnabled(false);
    forwardButton.setEnabled(false);
    p.add(backButton);
    p.add(resetButton);
    p.add(forwardButton);
    
    cBar=new MenuBar();
    Menu colorMenu=new Menu("Colors");
    MenuItem loadMap=new MenuItem("Load a colormap");
    MenuItem saveMap=new MenuItem("Save a colormap");
    MenuItem printParms=new MenuItem("Print parameters");
    Menu stateMenu=new Menu("State");

    cBar.add(colorMenu);
    cBar.add(stateMenu);
    colorMenu.add(loadMap);
    colorMenu.add(saveMap);
    stateMenu.add(printParms);
    loadMap.addActionListener(this);
    saveMap.addActionListener(this);
    printParms.addActionListener(this);

    add("Center", mbrot);
    
    addWindowListener(new WindowAdapter () {
      public void windowClosing(WindowEvent e) {
	System.exit(1);
      } 
    });
  }  
  
  public void actionPerformed(ActionEvent e) {
    String s=e.getActionCommand();
    if(s.equals("Reset")) mbrot.reset();
    if(s.startsWith("Load")) {
      FileDialog filed=new FileDialog(this, "Load a colormap", FileDialog.LOAD);

      filed.setFilenameFilter(new FilenameFilter() {
	public boolean accept(File dir, String name) {
	  if(name.endsWith(".map")) return true;
	  return false;
	}
      });
      filed.show();
      if (filed.getDirectory()==null) return;         //Exit if the user cancelled.
      File f=new File(filed.getDirectory(),filed.getFile());
      mbrot.loadMap(f);
    }

    if(s.equals("Save a colormap")) {
      FileDialog filed=new FileDialog(this, "Save a colormap", FileDialog.SAVE);
      filed.show();
      if (filed.getDirectory()==null) return;         //Exit if the user cancelled.
      File f=new File(filed.getDirectory(),filed.getFile());
      mbrot.saveMap(f);
    }

    if(s.equals("stackschanged")) {
      backButton.setEnabled(true);
      forwardButton.setEnabled(false);
      //saveImage.setEnabled(false);
      mbrot.redoStack.removeAllElements();
    }
    
    if(s.equals(BACK)) {
      mbrot.grunt.stop();
      Box b=(Box)mbrot.undoStack.pop();
      mbrot.redoStack.push(mbrot.cBounds.clone());
      mbrot.cBounds=b;
      forwardButton.setEnabled(true);
      backButton.setEnabled(!mbrot.undoStack.empty());
      //saveImage.setEnabled(false);
      mbrot.isValid=false;
      mbrot.repaint();
    }

    if(s.equals(FORWARD)) {
      mbrot.grunt.stop();
      Box b=(Box)mbrot.redoStack.pop();
      mbrot.undoStack.push(mbrot.cBounds.clone());
      mbrot.cBounds=b;
      backButton.setEnabled(true);
      forwardButton.setEnabled(!mbrot.redoStack.empty());
      //saveImage.setEnabled(false);
      mbrot.isValid=false;
      mbrot.repaint();
    }

    if(s.equals("imagecomplete")) {
      //saveImage.setEnabled(true);
    }

    if(s.equals("Print parameters")) {
      mbrot.printParms();
    }
    
    /*    if(s.equals("Save the current image")) {
      FileDialog filed=new FileDialog(this, "Save an image", FileDialog.SAVE);
      filed.show();
      if (filed.getDirectory()==null) return;         //Exit if the user cancelled.
      File f=new File(filed.getDirectory(),filed.getFile());
      try{mbrot.saveImage(f);}
      catch(IOException e) {System.out.println("Error: could not write file.");}
      } */
    
  }
  
}








final class Mandelbrot extends Canvas {
  
  int[] currentArray;       //An array for the current image.
  Image currentImage;                 //The currently displayed image.
  int oldX=0, oldY=0;                 //the currrentImage size.
  boolean isValid=false;              //Is currentImage valid?

  Button dummy=new Button();  //Java does not, for some reason, allow a Canvas

  Box cBounds=new Box(-2, 2, 2, -2);        //Bounds on the complex plane.
  Box cStep=new Box();
  double currentX=0, currentY=0;
  int currentIteration=0;
  Complex point=new Complex(0,0);
  Complex current=new Complex(0,0);

  Stack undoStack=new Stack();
  Stack redoStack=new Stack();
 
  ThreadGroup myGroup=new ThreadGroup("Slavedrivers");
  SlaveDriver grunt=new SlaveDriver(myGroup, "grunt");

  private Color[] color;                          //The array of colors.

  private int MAX_STEPS;
  private int NUM_MACHINES=5;
  


  public Mandelbrot(int steps) {
    if(steps!=0) MAX_STEPS=steps;
    else MAX_STEPS=32;
    Random r=new Random();
    color=new Color[MAX_STEPS];
    color[0]=Color.black;
    for(int i=1; i<MAX_STEPS; i++)
      color[i]=new Color(Math.abs(r.nextInt())%254,Math.abs(r.nextInt()%254),Math.abs(r.nextInt())%254);
    
    FractalListener fl=new FractalListener();
    addMouseMotionListener(fl);
    addMouseListener(fl);
  }

  public Dimension getPreferredSize() {return new Dimension(200,200);}
  public Dimension getMinimumSize() {return new Dimension(25,25);}
  
  void reset() {
    grunt.stop();
    undoStack.push(cBounds.clone());
    cBounds.set(-2, 2, 2, -2);
    dummy.dispatchEvent(new ActionEvent(this,ActionEvent.ACTION_PERFORMED,"stackschanged"));
    isValid=false;
    repaint();
  }

  //Takes upper-left and lower-right coordinates and zooms to them
  void zoomTo(double a, double b, double c, double d) {
    grunt.stop();
    undoStack.push(cBounds.clone());
    cBounds.set(a,b,c,d);
    dummy.dispatchEvent(new ActionEvent(this,ActionEvent.ACTION_PERFORMED,"stackschanged"));
    isValid=false;
    repaint();
  }    

  void saveMap(File f) {         //Save the colormap
    PrintWriter pw;
    try{pw=new PrintWriter(new FileWriter(f));}
    catch (Exception e) {System.out.println("ERROR: could not construct file writer");return;}
    for(int i=0; i<color.length; i++)
      pw.println(color[i].getRed()+" "+color[i].getGreen()+" "+color[i].getBlue());
    pw.close();
  }
  
  /*  void saveImage(File f) throws IOException {
    FileOutputStream writer=new FileOutputStream(f);
    writer.write(10);
    writer.write(5);
    writer.write(1);
    writer.write(8);
    writer.write(0);
    writer.write(0);
    int x=currentImage.getHeight(null);
    int y=currentImage.getWidth(null);
    writer.write(x%256);
    writer.write(x-x%256);
    writer.write(y%256);
    writer.write(y-y%256);
    writer.write(250);
    writer.write(0);
    writer.write(250);
    writer.write(0);
    for(int i=1; i<=49; i++) 
      writer.write(0);
    writer.write(1);
    int scan;
    if(x%2==0)
      scan=x;
    else
      scan=x+1;
    writer.write(scan%256);
    writer.write(scan-scan%256);
    for(int i=1; i<=60; i++) 
      writer.write(0);
    for(int j=0; j<=
    
    }*/
  
  void loadMap(File f){         //Load the colormap
    BufferedReader br;
    try{br=new BufferedReader(new FileReader(f));}
    catch (Exception e) {System.out.println("ERROR: could not construct file reader");return;}
    
    Color[] tempcolor=new Color[257];
    StringTokenizer t;
    String temps="";
    Color c;
    int c1,c2,c3,i;
    for( i=0; i<256; i++) {
      try {temps=br.readLine();}
      catch(Exception e) {System.out.println("ERROR: file contains no data");return;}
      t=new StringTokenizer(temps);
      if(t.countTokens()<3) {System.out.println("ERROR: line "+i+" is invalid format");return;}
      try{
	c1=Integer.parseInt(t.nextToken());
	c2=Integer.parseInt(t.nextToken());
	c3=Integer.parseInt(t.nextToken());
	c=new Color(c1,c2,c3);
      }
      catch(Exception e) {System.out.println("ERROR: parse error on line "+i);return;}
      tempcolor[i]=c;
      try{if(!br.ready()) break;}
      catch(Exception e) {System.out.println("ERROR: "+e);return;}
      
    }
    if(grunt.isAlive()) grunt.stop();    //Stop any currently rendering threads.
    color=new Color[i+1];
    for(int j=0; j<=i; j++) 
      color[j]=tempcolor[j];
    MAX_STEPS=i;
    isValid=false;
    repaint();
  }

  void printParms() {
    System.out.println("Center: ("+(cBounds.x2+cBounds.x1)/2+", "+(cBounds.y2+cBounds.y1)/2+
		       ")");
    System.out.println("Zoom factor: "+16/cBounds.getArea()+"x");
    System.out.println("Upper-left coordinates: ("+cBounds.x1+", "+cBounds.y1+")");
    System.out.println("Lower-right coordinates: ("+cBounds.x2+", "+cBounds.y2+")");
    System.out.println();
  }

  
  public void paint(Graphics g) {
    //   System.out.println("paint called "+g);
    //    System.out.println(grunt.activeCount()+" threads running.");
    if(isValid) {
      if(oldX==getSize().width && oldY==getSize().height)
	g.drawImage(currentImage,0,0,null);
      else isValid=false;
    }
    if(!isValid) {
      if(grunt.isAlive()) grunt.stop();           //Stop the old thread
      oldX=getSize().width;                     //Switch the parameters
      oldY=getSize().height;
      cStep.set((cBounds.x2-cBounds.x1)/oldX, 0, 0, (cBounds.y2-cBounds.y1)/oldY);
      currentX=cBounds.x1;
      currentY=cBounds.y1;
      currentArray=new int[oldX*oldY];
      grunt=new SlaveDriver(myGroup, "grunt p");
      //      System.out.println("new thread created");
      grunt.start();                            //Start a new thread.
      //      System.out.println("new thread started: "+grunt.isAlive());
    }
    
  }



    class FractalListener extends MouseAdapter implements MouseMotionListener{ //Add a mouse listener for zooming.
      Point p=new Point();
      Point lastStart=new Point();         //Used to keep track of the last box drawn.
      Point lastEnd=new Point();

      public void mouseMoved(MouseEvent e) {}
      
      public void mouseDragged(MouseEvent e) {
	Graphics g=getGraphics();  //get a graphics context;
	g.setColor(Color.black);
	g.setXORMode(Color.white);

	g.drawRect(lastStart.x, lastStart.y, lastEnd.x-lastStart.x, lastEnd.y-lastStart.y);    //Erase the old rectangle.
	if(p.x<e.getX())
	  if(p.y<e.getY()) {
	    g.drawRect(p.x,p.y, e.getX()-p.x, e.getY()-p.y);
	    lastStart.setLocation(p.x,p.y);
	    lastEnd.setLocation(e.getX(), e.getY());
	  }
	  else {
	    g.drawRect(p.x,e.getY(), e.getX()-p.x, p.y-e.getY());
	    lastStart.setLocation(p.x, e.getY());
lastEnd.setLocation(e.getX(), p.y);
	  }
	else
	  if(p.y<e.getY()) {
	    g.drawRect(e.getX(),p.y, p.x-e.getX(), e.getY()-p.y);
	    lastStart.setLocation(e.getX(), p.y);
lastEnd.setLocation(p.x, e.getY());
	  }
	  else {
	    g.drawRect(e.getX(),e.getY(), p.x-e.getX(), p.y-e.getY());
	    lastStart.setLocation(e.getX(),e.getY());
	    lastEnd.setLocation(p.x,p.y);
	  }
	g.dispose();
      }

      public void mousePressed(MouseEvent e) {
	p.setLocation(e.getX(),e.getY());
	lastStart.setLocation(p);
	lastEnd.setLocation(p);
      }

      public void mouseReleased(MouseEvent e) {
	int d=(p.x-e.getX())*(p.x-e.getX())+(p.y-e.getY())*(p.y-e.getY());
	if(d>2) {
	  grunt.stop();
	  undoStack.push(cBounds.clone());
	  dummy.dispatchEvent(new ActionEvent(this,ActionEvent.ACTION_PERFORMED,"stackschanged"));
	  if(p.x<e.getX()) {cBounds.x2=cBounds.x1+e.getX()*cStep.x1; cBounds.x1+=p.x*cStep.x1;}
	  else {cBounds.x2=cBounds.x1+p.x*cStep.x1; cBounds.x1+=e.getX()*cStep.x1;}
	  if(p.y<e.getY()) {cBounds.y2=cBounds.y1+e.getY()*cStep.y2; cBounds.y1+=p.y*cStep.y2;}
	  else {cBounds.y2=cBounds.y1+p.y*cStep.y2; cBounds.y1+=e.getY()*cStep.y2;}
	  isValid=false;
	  repaint();
	}
      }

      public void mouseClicked(MouseEvent e) {
	grunt.stop();       //Stop the thread
	undoStack.push(cBounds.clone());
	if((e.getModifiers() & InputEvent.BUTTON1_MASK)==InputEvent.BUTTON1_MASK) {  //Left button clicked
	  double xsize=(cBounds.x2-cBounds.x1)/4;
	  double ysize=(cBounds.y1-cBounds.y2)/4;
	  cBounds.x2=(cBounds.x1+e.getX()*cStep.x1)+xsize;
	  cBounds.x1=(cBounds.x1+e.getX()*cStep.x1)-xsize;
	  cBounds.y2=(cBounds.y1+e.getY()*cStep.y2)-ysize;
	  cBounds.y1=(cBounds.y1+e.getY()*cStep.y2)+ysize;
	  isValid=false;
	  dummy.dispatchEvent((AWTEvent)new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "stackschanged"));
	  repaint();
	}
	else if((e.getModifiers() & InputEvent.BUTTON3_MASK)==InputEvent.BUTTON3_MASK) {  //Right button clicked
	  double xsize=(cBounds.x2-cBounds.x1);
	  double ysize=(cBounds.y1-cBounds.y2);
	  cBounds.x2=(cBounds.x1+e.getX()*cStep.x1)+xsize;
	  cBounds.x1=(cBounds.x1+e.getX()*cStep.x1)-xsize;
	  cBounds.y2=(cBounds.y1+e.getY()*cStep.y2)-ysize;
	  cBounds.y1=(cBounds.y1+e.getY()*cStep.y2)+ysize;
	  isValid=false;
	  dispatchEvent(new ActionEvent(this,ActionEvent.ACTION_PERFORMED,"stackschanged"));
	  repaint();
	}
      }
    }



  
  class SlaveDriver extends Thread {
    Graphics g;
    public void rel() {g.dispose();}

    private int checksum() {
      return (int)(cBounds.x1+10*cBounds.y1+50*cBounds.x2+100*cBounds.y2+500*getSize().width+1000*getSize().height);
    }
    
    public SlaveDriver(ThreadGroup g, String s) {
      super(g,s);
    }

    //The fractal rendering code, including bounds tracing and symmetry checking (later).
    public void run() {
      int check=checksum();
      g=getGraphics();

      for(int i=0; i<oldY; i++) {
	for(int j=0; j<oldX; j++) {
	  point.set(currentX,currentY);
	  current.set(currentX, currentY);
	  for(currentIteration=1; currentIteration<MAX_STEPS; currentIteration++) {
	    current.square().add(point);
	    if(!current.lessThan(2)) break;
	  }
	  if(currentIteration==MAX_STEPS) {
	    g.setColor(color[0]);
	    try{g.fillRect(j,i,1,1);}
	    catch(Exception e) {
	      System.out.println("Damn these Java thread bugs.  Reset now.  See JDC bug report #4031751");
	      stop();
	    }
	    currentArray[i*oldX+j]=color[0].getRGB();
	  }
	  else {
	    g.setColor(color[currentIteration]);
	    try{g.fillRect(j,i,1,1);}
	    catch(Exception e) {
	      System.out.println("Damn these Java thread bugs.  Reset now.  See JDC bug report #4031751");
	      stop();
	    }
	    currentArray[i*oldX+j]=color[currentIteration].getRGB();
	  }
	  currentX+=cStep.x1;
	}
	currentY+=cStep.y2;
	currentX=cBounds.x1;
      }


      currentImage=createImage(new MemoryImageSource(oldX,oldY,currentArray,0,oldX));
      dummy.dispatchEvent((AWTEvent)new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "imagecomplete"));                //This line notifies the main program that drawing is complete.
      if(check==checksum()) isValid=true;
    }
    
  }
  
  
}



//We make this class final to improve performance, since it is used extensively in computations.
final class Complex {
  private double re, im, retemp;
  
  Complex(double a, double b) {
    re=a;
    im=b;
  }
  
  void set(double a, double b) {
    re=a;
    im=b;
  }

  Complex square() {
    retemp=re*re-im*im;
    im=2*re*im;
    re=retemp;
    return this;
  }

  Complex add(Complex other) {
    re=re+other.re;
    im=im+other.im;
    return this;
  }

  boolean lessThan(Complex other) {
    return (re*re+im*im)<(other.re*other.re+other.im*other.im);
  }

  boolean lessThan(double d) {
    return (re*re+im*im)<(d*d);
  }
}

//This class keeps our bounding rectangle
class Box implements Cloneable {
  public double x1,y1,x2,y2;     // (x,y) initial and (x,y) final.
  
  public Object clone() {
    return new Box(x1,y1,x2,y2);
  }

  public void set(double a, double b, double c, double d) {
    x1=a;
    y1=b;
    x2=c;
    y2=d;
  }

  public double getArea() {
    return Math.abs((x2-x1)*(y2-y1));
  }

  public Box() {
    x1=y1=x2=y2=0;
  }

  public Box(int a, int b, int c, int d) {
    x1=a;
    y1=b;
    x2=c;
    y2=d;
  }

  public Box(double a, double b, double c, double d) {
    x1=a;
    y1=b;
    x2=c;
    y2=d;
  }
}

//This class is our to-do list for the server processes
class JobQueue {
  private Stack s;
  
  public synchronized Object get() {
    while(s.isEmpty()) {
      try{wait();}
      catch(InterruptedException e) {}
    }
    return s.pop();
  }

  public synchronized void put(Object obj) {
    s.push(obj);
    notify();
  }
  
  public synchronized void putAll(Object[] obj) {
    for(int i=0; i<obj.length; i++)
      s.push(obj[i]);
    notifyAll();
  }

  public synchronized void clearAll() {
    s.removeAllElements();
  }

}

