/**
 * Fractal viewer for JDK 1.0
 * Copyright © 1998,1999 @author Marko Mäkelä (msmakela@cc.hut.fi).
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

import java.applet.Applet;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Event;

/** The fractal image */
public final class Fractal10 extends Canvas implements Runnable {
  /** Dimensions of the image */
  private Dimension dimensions;

  /** Pixel cache */
  private byte[][] pixels;

  /** The image */
  private Image image;

  /** Graphics context for the image (@see image) */
  private Graphics ig;

  /** Graphics context for the Canvas */
  private Graphics graphics = null;

  /** Status message */
  private String status = "";

  /** Colors used for the image */
  private Color[] colors;

  /** The computing thread */
  private volatile Thread thread;

  /** Flag: should the computing thread restart */
  private volatile boolean threadRestart = false;

  /** Maximum number of iterations */
  private int maxIter;

  /** The applet that receives the status messages */
  private Applet applet;

  /** Lower and upper bounds of the t and x coordinates as fixed-point numbers
   *  (for speeding up internal calculations)
   */
  long lt, ut, lx, ux;
  /** The y coordinate as a fixed-point number */
  long y;
  /** The initial iteration coordinates as fixed-point numbers */
  long it, ix, iy;

  /** Same coordinates as doubles (used by the class interface) */
  double d_lt, d_ut, d_lx, d_ux, d_y, d_it, d_ix, d_iy;

  /** Flag: has the fractal been zoomed? */
  boolean zoomed = false;

  /** Type of fractal to be calculated */
  int type = MANDEL;
  static final int MANDEL = 1;
  static final int JULIA = 2;
  static final int BIASEDMANDEL = 3;

  /** The constructor
   * @param applet	Applet for receiving the status messages
   */
  public Fractal10 (Applet applet) {
    this.applet = applet;

    // Initialize the colors
    colors = new Color[255];
    for (int c = 1; c < 255; c++)
      colors[c - 1] = new Color ((c << 4) & 0xFF | (c >> 4),
				 (c << 3) & 0xFF, 0xFE ^ c);
    colors[254] = Color.black;
  }

  /** flag: is the zoom area being chosen? */
  private boolean zooming = false;
  /** first point in zoom area */
  private Point first;
  /** second point in zoom area */
  private Point second;

  /** Called when the mouse pointer is moved inside the window
   * @param event	the event
   * @param x		the x coordinate of the pointer
   * @param y		the y coordinate of the pointer
   * @return		true if the event was processed
   */
  public boolean mouseMove (Event event, int x, int y) {
    applet.showStatus (toCoordinates (new Point (x, y)));
    return true;
  }

  /** Called when the mouse pointer exits the window
   * @param event	the event
   * @param x		the x coordinate of the pointer
   * @param y		the y coordinate of the pointer
   * @return		true if the event was processed
   */
  public boolean mouseExit (Event event, int x, int y) {
    applet.showStatus (status);
    return true;
  }

  /** Called when the mouse button is pressed inside the window
   * @param event	the event
   * @param x		the x coordinate of the pointer
   * @param y		the y coordinate of the pointer
   * @return		true if the event was processed
   */
  public boolean mouseDown (Event event, int x, int y) {
    first = second = new Point (event.x, event.y);
    zooming = true;
    return true;
  }

  /** Called when the mouse is dragged inside the window
   * @param event	the event
   * @param x		the x coordinate of the pointer
   * @param y		the y coordinate of the pointer
   * @return		true if the event was processed
   */
  public boolean mouseDrag (Event event, int x, int y) {
    drawRect (first, second);
    second = new Point (event.x, event.y);
    drawRect (first, second);
    applet.showStatus (toCoordinates (first) + "-" +
		       toCoordinates (second));
    return true;
  }

  /** Called when the mouse button is released inside the window
   * @param event	the event
   * @param x		the x coordinate of the pointer
   * @param y		the y coordinate of the pointer
   * @return		true if the event was processed
   */
  public boolean mouseUp (Event event, int x, int y) {
    if (dimensions == null)
      dimensions = size ();
    long dt = (ut - lt) / dimensions.width;
    long dx = (ux - lx) / dimensions.height;

    long new_lt = lt + dt * first.x, new_ut = lt + dt * x;
    long new_lx = lx + dx * first.y, new_ux = lx + dx * y;

    lt = limit (new_lt); ut = limit (new_ut);
    lx = limit (new_lx); ux = limit (new_ux);

    d_lt = (double) lt / (1 << 29); d_ut = (double) ut / (1 << 29);
    d_lx = (double) lx / (1 << 29); d_ux = (double) ux / (1 << 29);
    zoomed = true;
    zooming = false;

    restartThread ();
    return true;
  }

  /** limit a coordinate value in reasonable bounds */
  private final static long limit (long l) {
    if (l < -MAX)
      return -MAX;
    else if (l > MAX)
      return MAX;
    else
      return l;
  }

  /** bounds for the t and x coordinates */
  private final static long MAX = 2 << 29;

  /** Draw an XOR rectangle */
  private void drawRect (Point p1, Point p2) {
    if (graphics != null) {
      graphics.setXORMode (colors[0]);
      graphics.drawRect (p1.x < p2.x ? p1.x : p2.x,
			 p1.y < p2.y ? p1.y : p2.y,
			 p1.x > p2.x ? p1.x - p2.x : p2.x - p1.x,
			 p1.y > p2.y ? p1.y - p2.y : p2.y - p1.y);
    }
  }

  /** Convert pixel coordinates to a string
   * @param p	the point representing the pixel coordinates in the canvas
   * @return	a string representing the coordinates
   */
  private String toCoordinates (Point p) {
    if (dimensions == null)
      dimensions = size ();
    long t = lt + (ut - lt) / dimensions.width * p.x;
    long x = lx + (ux - lx) / dimensions.height * p.y;

    return "(" + (double) t / (1 << 29) + "," + (double) x / (1 << 29) + ")";
  }

  /** Main function of the calculation thread */
  public synchronized void run () {
    /* Initialize the dimensions */
    dimensions = size ();
    /* Free up the old image */
    if (image != null)
      image.flush ();
    image = null; pixels = null;
    if (dimensions.width <= 0 || dimensions.height <= 0)
      return; /* cannot draw a zero-size image */

    /* Clear the restart flag */
    threadRestart = false;

    /** Get the graphics context */
    if (graphics == null)
      graphics = getGraphics ();

    pixels = new byte[dimensions.width][dimensions.height];
    image = createImage (dimensions.width, dimensions.height);
    ig = image.getGraphics ();
    ig.setColor (colors[0]);
    ig.fillRect (0, 0, dimensions.width, dimensions.height);

    for (int i = 64; i > 0 && !threadRestart; i >>= 1) {
      applet.showStatus (status = "Computing grid " + i);
      calculateImage (i);
    }

    if (threadRestart)
      run ();
    else
      applet.showStatus (status = "Computing completed");
  }

  /** Calculate the image
   * @param step	specifies the meta-pixel size (step×step pixels)
   */
  private void calculateImage (int step) {
    long t, dt = (ut - lt) / dimensions.width * step;
    long x, dx = (ux - lx) / dimensions.height * step;
    int h, w;

    for (w = 0, t = lt; w < dimensions.width; w += step, t += dt) {
      for (h = 0, x = lx; h < dimensions.height; h += step, x += dx) {
	// Calculate uncalculated pixels
	if (pixels[w][h] == 0) {
	  // Monitor the restart flag
	  if (threadRestart)
	    return;
	  int i = 0;
	  long jt = 0, jx = 0, jy = 0;
	  switch (type) {
	  case JULIA:
	    jt = t; jx = x; jy = y;
	    while (i < maxIter) {
	      long jt2 = jt * jt, jx2 = jx * jx, jy2 = jy * jy;
	      if ((jt2 >> 29) + (jx2 >> 29) + (jy2 >> 29) >= ((long) 4 << 29))
		break;

	      i++;
	      jx = ((jx * jt) >> 28) + ix;
	      jy = ((jy * jt) >> 28) + iy;
	      jt = ((jt2 - jx2 - jy2) >> 29) + it;
	    }
	    break;
	  case BIASEDMANDEL:
	    jt = it; jx = ix; jy = iy;
	  case MANDEL:
	  default:
	    while (i < maxIter) {
	      long jt2 = jt * jt, jx2 = jx * jx, jy2 = jy * jy;
	      if ((jt2 >> 29) + (jx2 >> 29) + (jy2 >> 29) >= ((long) 4 << 29))
		break;

	      i++;
	      jx = ((jx * jt) >> 28) + x;
	      jy = ((jy * jt) >> 28) + y;
	      jt = ((jt2 - jx2 - jy2) >> 29) + t;
	    }
	    break;
	  }

	  pixels[w][h] = (byte) (i < maxIter ? (1 + (i % 254)) : 255);
	  ig.setColor (colors[(0xFF & pixels[w][h]) - 1]);
	  ig.fillRect (w, h, step, step);
	}
      }
    }

    if (step > 1) {
      /* Interpolate the pixel data. 
       * ···a··b···
       * ··········
       * ··········
       * c··d××e··f
       * ···××××···
       * ···××××···
       * g··h××i··j
       * ··········
       * ··········
       * ···k··l···
       *
       * If all pixels labeled a..l have equal value, the pixels marked with
       * × probably also have the same value.
       */

      for (w = step; w < dimensions.width - 2 * step; w += step) {
	for (h = step; h < dimensions.height - 2 * step; h += step) {
	  byte p = pixels[w][h];				// d
	  if (p == pixels[w + 2 * step]	[h + step]	&&	// j
	      p == pixels[w + step]	[h + 2 * step]	&&	// l

	      p == pixels[w + 2 * step]	[h]		&&	// f
	      p == pixels[w]		[h + 2 * step]	&&	// k

	      p == pixels[w + step]	[h - step]	&&	// b
	      p == pixels[w - step]	[h + step]	&&	// g
	      p == pixels[w + step]	[h + step]	&&	// i

	      p == pixels[w]		[h - step]	&&	// a
	      p == pixels[w - step]	[h]		&&	// c

	      p == pixels[w + step]	[h]		&&	// e
	      p == pixels[w]		[h + step])		// h
	    for (int ww = w + step; --ww >= w; )
	      for (int hh = h + step; --hh >= h; )
		pixels[ww][hh] = p;
	}
      }
    }

    // Redraw the image
    repaint ();
  }

  /** Update the image
   * @param g	the graphics context
   */
  public void update (Graphics g) {
    paint (g);
  }

  /** Paint the image
   * @param g	the graphics context
   */
  public void paint (Graphics g) {
    if (g != null && image != null) {
      g.drawImage (image, 0, 0, this);
      if (zooming)
	drawRect (first, second);
    }
  }

  /** Set the parameters of the fractal
   * @param lt		lower bound of t coordinate
   * @param ut		upper bound of t coordinate
   * @param lx		lower bound of x coordinate
   * @param ux		upper bound of x coordinate
   * @param y		y coordinate
   * @param it		initial iteration t coordinate
   * @param ix		initial iteration x coordinate
   * @param iy		initial iteration y coordinate
   * @param mi		maximum number of iterations
   * @param type	type of fractal to calculate
   */
  public void setParameters (double lt, double ut,
			     double lx, double ux,
			     double y,
			     double it, double ix,
			     double iy,
			     int mi, int type) {
    // Set the parameters
    this.lt = (long) ((1 << 29) * (d_lt = lt));
    this.ut = (long) ((1 << 29) * (d_ut = ut));
    this.lx = (long) ((1 << 29) * (d_lx = lx));
    this.ux = (long) ((1 << 29) * (d_ux = ux));
    this.y = (long) ((1 << 29) * (d_y = y));
    this.it = (long) ((1 << 29) * (d_it = it));
    this.ix = (long) ((1 << 29) * (d_ix = ix));
    this.iy = (long) ((1 << 29) * (d_iy = iy));
    maxIter = mi;
    this.type = type;

    // Restart the calculations
    restartThread ();
  }

  /** Restart the calculations */
  public void restartThread () {
    threadRestart = true;
    if (thread == null || !thread.isAlive ())
      (thread = new Thread (this)).start ();
  }

  /** Get the lower limit for the t coordinate */
  public double getLT () { return d_lt; }
  /** Get the upper limit for the t coordinate */
  public double getUT () { return d_ut; }
  /** Get the lower limit for the x coordinate */
  public double getLX () { return d_lx; }
  /** Get the lower limit for the x coordinate */
  public double getUX () { return d_ux; }

  /** Determine whether the coordinates have changed. */
  public boolean coordinatesChanged () {
    boolean z = zoomed; zoomed = false; return z;
  }
}

