package plot;

import java.awt.*;
import real.Formats;

/** 

The class RealGraphics is the basic class in all 
these floating point graphics routines.  
(*) It has a real coordinate system, 
initiated constructed with setCorners().
(*) Graphics drawing commands imitate PostScript.
(*) The basic class of things drawn is `PlotPath'.  
After one is instantiated,
we build it with methods `moveto', `lineto', etc.
and draw it with `stroke', `fill'.  Coordinate systems are
changed with commands `scale', `rotate', `translate', `zoom'.
There is no true graphics state, however; colours are usually
used in the call directly. 

*/

public class RealGraphics {
	double m[][];
	double t[];
	final static double INFTY = 32783.0;
	Color currentColour = Color.black;
	Color background = Color.white;
	Graphics gfx;
	int w, h;

	/** The variables w and h determine the canvas size in pixels. */

	public RealGraphics(Graphics gfx, int w, int h) {
		this.gfx = gfx;
		this.w = w;
		this.h = h;
		setBackground(Color.white);
		m = new double[2][2];
		t = new double[2];
		m[0][0] = 1.0; m[0][1] =  0.0;
		m[1][0] = 0.0; m[1][1] = -1.0; 
		t[0] = 0.0;
		t[1] = (double) h;
	}
	
	public void setBackground(Color c) {
		background = c;
	}
	
	public Color getBackground(Color c) {
		return(background);
	}
	
	public void setColor(Color c) {
		gfx.setColor(c);
		currentColour = c;
	}
	
	public Color getColor() {
		return(currentColour);
	}
	
	public void setBounds(int w, int h) {
		this.w = w; this.h = h;
	}
	
	public Graphics getGraphics() {
		return(gfx);
	}
	
	public void drawPolygon(Polygon p) {
		gfx.drawPolygon(p);
	}
	
	public void fillPolygon(Polygon p) {
		gfx.fillPolygon(p);
	}
	
	/** The point
	(llx, lly) becomes lower left, (urx, ury) upper right. */
	public void setCorners(double llx, double lly, 
			double urx, double ury) {
		
		m[0][0] = (double) w/(urx - llx);
		m[0][1] = 0.0;
		m[1][1] = - (double) h/(ury - lly);
		m[1][0] = 0.0;
		t[0] = - m[0][0]*llx;
		t[1] = - m[1][1]*ury;
	}

	/** This returns the floating point corners of the pixel at upper left. 
	This can be used to determine true pixel size, often important 
	for knowing how fine-grained to do plots at. */

	public double[][] pixel() {
		double b[][] = new double[4][];
		b[0] = toRealPoint(0, 0);
		b[1] = toRealPoint(1, 0);
		b[2] = toRealPoint(1, 1);
		b[3] = toRealPoint(0, 1);
		return(b);
	}
	
	/** Assumes rectangular coordinates. */
	public double[] pixelDimensions() {
		double P[] = toRealPoint(0, 0);
		double Q[] = toRealPoint(1, 1);
		Q[0] = Math.abs(Q[0] - P[0]); Q[1] = Math.abs(Q[1] - P[0]);
		return(Q);
	}

	/** Returns a pixel of width n, centered at P */
	public double[][] pixel(double P[], int n) {
		int p[] = toPoint(P);
		p[0] -= (n/2); p[1] -= (n/2);
		double b[][] = new double[4][];
		b[0] = toRealPoint(p);
		p[0] += n;
		b[1] = toRealPoint(p);
		p[1] += n;
		b[2] = toRealPoint(p);
		p[0] -= n;
		b[3] = toRealPoint(p);
		return(b);
	}

	/** Locates corners: ll, lr, ur, ul. */
	public double[][] getRealBounds() {
		double b[][] = new double[4][];
		b[0] = toRealPoint(0, h);
		b[1] = toRealPoint(w, h);
		b[2] = toRealPoint(w, 0);
		b[3] = toRealPoint(0, 0);
		return(b);
	}
	
	public int[] getBounds() {
		int b[] = new int[2];
		b[0] = w; b[1] = h;
		return(b);
	}
	
	/** Paints a blank canvas onto the offscreen image. */
	public void clear() {
		gfx.setColor(background);
		gfx.fillRect(0, 0, w, h);
		gfx.setColor(currentColour);
	}
	
	/** Returns pixel with coordinates P. 
	Coordinates can be be infinity but not NaN. */
	public int[] toPoint(double p[]) {
		return(toPoint(p[0], p[1]));
	}
	
	/** Returns pixel with coordinates (x, y). 
	Coordinates can be be infinity but not NaN. */
	public int[] toPoint(double x, double y) {
		double x0 = m[0][0]*x + m[0][1]*y + t[0];
		double y0 = m[1][0]*x + m[1][1]*y + t[1];
		// System.out.println("x, y = " + x + ", " + y);
		// System.out.println("x0, y0 = " + x0 + ", " + y0);
		int q[] = new int[2];
		// never let it wrap around
		if (-INFTY < x0 && x0 < INFTY) {
			q[0] = (int) Math.round(x0);
		} else if (x0 <= -INFTY || x0 == Double.NEGATIVE_INFINITY) {
			q[0] = (int) -INFTY;
		} else {
			q[0] = (int) INFTY;
		}
		if (-INFTY < y0 && y0 < INFTY) {
			q[1] = (int) Math.round(y0);
		} else if (y0 <= -INFTY || y0 == Double.NEGATIVE_INFINITY) {
			q[1] = (int) -INFTY;
		} else {
			q[1] = (int) INFTY;
		}
		// System.out.println("x1, y1 = " + q[0] + ", " + q[1]);
		return(q);
	}

	public int[] toRelativePoint(double x, double y) {
		double x0 = m[0][0]*x + m[0][1]*y;
		double y0 = m[1][0]*x + m[1][1]*y;
		int q[] = new int[2];
		// never let it wrap around
		if (-INFTY < x0 && x0 < INFTY) {
			q[0] = (int) Math.round(x0);
		} else if (x0 <= -INFTY || x0 == Double.NEGATIVE_INFINITY) {
			q[0] = (int) -INFTY;
		} else {
			q[0] = (int) INFTY;
		}
		if (-INFTY < y0 && y0 < INFTY) {
			q[1] = (int) Math.round(y0);
		} else if (y0 <= -INFTY || y0 == Double.NEGATIVE_INFINITY) {
			q[1] = (int) -INFTY;
		} else {
			q[1] = (int) INFTY;
		}
		return(q);
	}

	public int[] toRelativePoint(double x[]) {
		return(toRelativePoint(x[0], x[1]));
	}

	/** Pixels to floating point coordinates. */
	public double[] toRealPoint(int p[]) {
		return(toRealPoint(p[0], p[1]));
	}

	/** Pixels to floating point coordinates. */
	// pixels to coords
	// p = m*q + t, q = m^{-1}(p - t)
	public double[] toRealPoint(int x, int y) {
		double x0 = (double) x - t[0];
		double y0 = (double) y - t[1];
		double d = m[0][0]*m[1][1] - m[0][1]*m[1][0];
		double x1 =  m[1][1]*x0 - m[0][1]*y0;
		double y1 = -m[1][0]*x0 + m[0][0]*y0;
		x1 /= d;
		y1 /= d;
		double q[] = new double[2];
		q[0] = x1; q[1] = y1;
		return(q);
	}

	/** Real coordinates to pixels, but in real numbers. */
	double[] toFinePoint(double p[]) {
		double x0 = m[0][0]*p[0] + m[0][1]*p[1] + t[0];
		double y0 = m[1][0]*p[0] + m[1][1]*p[1] + t[1];
		double q[] = new double[2];
		q[0] = x0; q[1] = y0;
		return(q);
	}
	
	/** Real coordinates to pixels, but in real numbers. */
	double[] toFinePoint(double x, double y) {
		double x0 = m[0][0]*x + m[0][1]*y + t[0];
		double y0 = m[1][0]*x + m[1][1]*y + t[1];
		double q[] = new double[2];
		q[0] = x0; q[1] = y0;
		return(q);
	}

	/** Returns true if visible on the screen.
	Returns false if x or y is NaN. */ 
	// since all comparisons with NaN are false
	public boolean isVisible(double x, double y) {
		int p[] = toPoint(x, y);
		if (0 <= p[0] && p[0] < w 
			&& 0 <= p[1] && p[1] < h) {
			return(true);
		}
		else {
			return(false);
		}
	}

	/** Returns true if visible on the screen.
	Returns false if x or y is NaN. */ 
	public boolean isVisible(double p[]) {
		return(isVisible(p[0], p[1]));
	}

	/** Returns true if visible on the screen. */ 
	public boolean isVisible(int p[]) {
		// doesn't use xmin etc.
		if (0 <= p[0] && p[0] < w 
			&& 0 <= p[1] && p[1] < h) {
			return(true);
		}
		else {
			return(false);
		}
	}

	/** Follows PostScript model, but using column vectors.
		All of these postmultiply 
		the CTM by the relevant matrix. */ 

	public void scale(double a, double b) {
		m[0][0] *= a;
		m[1][0] *= a;
		m[0][1] *= b;
		m[1][1] *= b;
	}

	/** Follows PostScript model. */
	public void translate(double a, double b) {
		t[0] += m[0][0]*a + m[0][1]*b;
		t[1] += m[1][0]*a + m[1][1]*b;
	}
	
	/** Follows PostScript model. */
	// multiply on the right by rotation matrix
	public void rotate(double theta) {
		double c = Math.cos(theta);
		double s = Math.sin(theta);
		
		double x00 =  m[0][0]*c + m[0][1]*s;
		double x01 = -m[0][0]*s + m[0][1]*c;
		double x10 =  m[1][0]*c + m[1][1]*s;
		double x11 = -m[1][0]*s + m[1][1]*c;
		m[0][0] = x00;
		m[0][1] = x01;
		m[1][0] = x10;
		m[1][1] = x11;
	}

	// z = magnification, t = new origin
	public void zoom(double z, double t[]) {
		Point s;
		double r[];

		// calculate new matrix
		scale(z, z);
		r = toRealPoint(w/2, h/2);
		translate(r[0] - t[0], r[1] - t[1]);
	}

	public void zoom(double mx, double my, double t[]) {
		Point s;
		double r[];

		// calculate new matrix
		scale(mx, my);
		r = toRealPoint(w/2, h/2);
		translate(r[0] - t[0], r[1] - t[1]);
	}
	
	public void setFont(Font f) {
		gfx.setFont(f);
	}
	
	
	public void drawString(String s, double Q[], Color c) {
		Color tmp = gfx.getColor();
		gfx.setColor(c);
		int P[] = toPoint(Q);
		gfx.drawString(s, P[0], P[1]);
		gfx.setColor(tmp);
	}
	
	/** Uses float coordinates. */
	public void drawString(String s, double x, double y, Color c) {
		Color tmp = gfx.getColor();
		gfx.setColor(c);
		int P[] = toPoint(x, y);
		gfx.drawString(s, P[0], P[1]);
		gfx.setColor(tmp);
	}
	
	/** Uses float coordinates. */
	public void drawString(String s, double Q[]) {
		int P[] = toPoint(Q);
		gfx.drawString(s, P[0], P[1]);
	}
	
	/** Uses float coordinates. */
	public void drawString(String s, double x, double y) {
		int P[] = toPoint(x, y);
		gfx.drawString(s, P[0], P[1]);
	}
	
	public String toString() {
		StringBuffer sb = new StringBuffer();
		sb.append(Formats.toString(m[0][0], 6, 8));
		sb.append(" ");
		sb.append(Formats.toString(m[0][1], 6, 8));
		sb.append(" \n");
		sb.append(Formats.toString(m[1][0], 6, 8));
		sb.append(" ");
		sb.append(Formats.toString(m[1][1], 6, 8));
		sb.append(" \n");
		sb.append(Formats.toString(t[0], 6, 8));
		sb.append(" ");
		sb.append(Formats.toString(t[1], 6, 8));
		sb.append(" \n");
		return(new String(sb));
	}
	

}
