import Point3D;
import Raster;
import java.lang.*;
import java.io.*;

public class Matrix3D {

  // variables

  float[] a = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

  // constructors

  public Matrix3D() {
    // initialize with identity transform
    loadIdentity();
  }

  public Matrix3D(Matrix3D copy) {
    // initialize with copy of source
    for (int i = 0; i < 16; i++) {
      a[i] = copy.gt(i);
    }
  }

  public Matrix3D(Raster r) {
    // initialize with a mapping from canonical space to screen space
    loadIdentity();
    scale(r.width / 2, r.height / -2, 0);    
    translate(1, -1, 0);
  }

  // methods
        
  public void set(int i, int j, float value) {
    // set element [j][i] to value
    a[4 * j + i] = value;
  }

  public float get(int i, int j) {
    // return element [j][i]
    return a[4 * j + i];
  }
  
  public float gt(int i) {
    // return element in ith index of array
    return a[i];
  }

  public Point3D applyPt(Point3D pt) {
    // applies pt to this and returns pt'
    float xx = pt.x * a[0] + pt.y * a[1] + pt.z * a[2] + a[3];
    float yy = pt.x * a[4] + pt.y * a[5] + pt.z * a[6] + a[7];
    float zz = pt.x * a[8] + pt.y * a[9] + pt.z * a[10] + a[11];
    float scale = pt.x * a[12] + pt.y * a[13] + pt.z * a[14] + a[15];
    return new Point3D(xx / scale, yy / scale, zz / scale);
  }

  public void transform(Point3D in[], Point3D out[], int start, int length) {
    // Transform points from the in array to the out array
    // using the current matrix. The subset of points transformed
    // begins at the start index and has the specified length
    for (int i = 0; i < length; i++) {
      out[start + i] = applyPt(in[start + i]);
    }
  }
  
  public final void compose(Matrix3D s) {
    // this = this * s
    float[] b = a;

    a[0] = b[0] * s.gt(0) + b[1] * s.gt(4) + b[2] * s.gt(8)
      + b[3] * s.gt(12);
    a[1] = b[0] * s.gt(1) + b[1] * s.gt(5) + b[2] * s.gt(9)
      + b[3] * s.gt(13);
    a[2] = b[0] * s.gt(2) + b[1] * s.gt(6) + b[2] * s.gt(10)
      + b[3] * s.gt(14);
    a[3] = b[0] * s.gt(3) + b[1] * s.gt(7) + b[2] * s.gt(11)
      + b[3] * s.gt(15);

    a[4] = b[4] * s.gt(0) + b[5] * s.gt(4) + b[6] * s.gt(8)
      + b[7] * s.gt(12);
    a[5] = b[4] * s.gt(1) + b[5] * s.gt(5) + b[6] * s.gt(9)
      + b[7] * s.gt(13);
    a[6] = b[4] * s.gt(2) + b[5] * s.gt(6) + b[6] * s.gt(10)
      + b[7] * s.gt(14);
    a[7] = b[4] * s.gt(3) + b[5] * s.gt(7) + b[6] * s.gt(11)
      + b[7] * s.gt(15);

    a[8] = b[8] * s.gt(0) + b[9] * s.gt(4) + b[10] * s.gt(8)
      + b[11] * s.gt(12);
    a[9] = b[8] * s.gt(1) + b[9] * s.gt(5) + b[10] * s.gt(9)
      + b[11] * s.gt(13);
    a[10] = b[8] * s.gt(2) + b[9] * s.gt(6) + b[10] * s.gt(10)
      + b[11] * s.gt(14);
    a[11] = b[8] * s.gt(3) + b[9] * s.gt(7) + b[10] * s.gt(11)
      + b[11] * s.gt(15);

    a[12] = b[12] * s.gt(0) + b[13] * s.gt(4) + b[14] * s.gt(8)
      + b[15] * s.gt(12);
    a[13] = b[12] * s.gt(1) + b[13] * s.gt(5) + b[14] * s.gt(9)
      + b[15] * s.gt(13);
    a[14] = b[12] * s.gt(2) + b[13] * s.gt(6) + b[14] * s.gt(10)
      + b[15] * s.gt(14);
    a[15] = b[12] * s.gt(3) + b[13] * s.gt(7) + b[14] * s.gt(11)
      + b[15] * s.gt(15);
  }

  public void loadIdentity() {
    // this = identity
    for (int i = 0; i < 16; i++) {
      if ((i == 0) || (i == 5) || (i == 10) || (i == 15))
	a[i] = 1;
      else a[i] = 0;
    }
  }

  public void translate(float tx, float ty, float tz) {
    // this = this * t
    Matrix3D trn = new Matrix3D();
    trn.set(3, 0, tx);
    trn.set(3, 1, ty);
    trn.set(3, 2, tz);
    compose(trn);
  }

  public void scale(float sx, float sy, float sz) {
    // this = this * scale
    Matrix3D scl = new Matrix3D();
    scl.set(0, 0, sx);
    scl.set(1, 1, sy);
    scl.set(2, 2, sz);
    compose(scl);
  }

  public void skew(float kxy, float kxz, float kyz) {
    // this = this * skew
    Matrix3D skw = new Matrix3D();
    skw.set(0, 1, kxy);
    skw.set(0, 2, kxz);
    skw.set(1, 2, kyz);
    compose(skw);
  }

  public void rotate(float ax, float ay, float az, float angle) {
    // this = this * rotate
    Matrix3D rot = new Matrix3D();
    float sinangle = new Double(java.lang.Math.sin(new Float(angle).doubleValue())).floatValue();
    float cosangle = new Double(java.lang.Math.cos(new Float(angle).doubleValue())).floatValue();
    rot.set(0, 0, ax * ax * (1 - cosangle) + cosangle);
    rot.set(1, 0, ax * ay * (1 - cosangle) - az * sinangle);
    rot.set(2, 0, ax * az * (1 - cosangle) + ay * sinangle);
    rot.set(0, 1, ax * ay * (1 - cosangle) + az * sinangle);
    rot.set(1, 1, ay * ay * (1 - cosangle) + cosangle);
    rot.set(2, 1, ay * az * (1 - cosangle) - ax * sinangle);
    rot.set(0, 2, ax * az * (1 - cosangle) - ay * sinangle);
    rot.set(1, 2, ay * az * (1 - cosangle) + ax * sinangle);
    rot.set(2, 2, az * az * (1 - cosangle) + cosangle);
    compose(rot);
  }

  public void lookAt(float eyex, float eyey, float eyez,
		     float atx,  float aty,  float atz,
		     float upx,  float upy,  float upz) {
    // this = this * lookat
    float nrm;
    nrm = (float) java.lang.Math.sqrt((atx - eyex) * (atx - eyex) +
				      (aty - eyey) * (aty - eyey) +
				      (atz - eyez) * (atz - eyez));
    float nx = (atx - eyex) / nrm;
    float ny = (aty - eyey) / nrm;
    float nz = (atz - eyez) / nrm;
    float ux = ny * upz - nz * upy;
    float uy = nz * upx - nx * upz;
    float uz = nx * upy - ny * upx;
    nrm = (float) java.lang.Math.sqrt(ux * ux + uy * uy + uz * uz);
    ux = ux / nrm;
    uy = uy / nrm;
    uz = uz / nrm;
    float vx = uy * nz - uz * ny;
    float vy = uz * nx - ux - nz;
    float vz = ux * ny - uy * nx;
    Matrix3D look = new Matrix3D();
    look.set(0, 0, ux);
    look.set(1, 0, uy);
    look.set(2, 0, uz);
    look.set(0, 1, vx);
    look.set(1, 1, vy);
    look.set(2, 1, vz);
    look.set(0, 2, nx);
    look.set(1, 2, ny);
    look.set(2, 2, nz);
    compose(look);
    translate(-eyex, -eyey, -eyez);
  }

  // Assume the following projection transformations
  // transform points into the canonical viewing space

  public void perspective(float left, float right,               
			  float bottom, float top,
			  float near, float far) {
    // this = this * pers
    Matrix3D pers = new Matrix3D();
    pers.set(0, 0, (2 * near) / (right - left));
    pers.set(2, 0, (right + left) / (left - right));
    pers.set(1, 1, (2 * near) / (bottom - top));
    pers.set(2, 1, (bottom + top) / (top - bottom));
    pers.set(2, 2, (far + near) / (far - near));
    pers.set(3, 2, (-2 * far * near) / (far - near));
    pers.set(2, 3, 1);
    pers.set(3, 3, 0);
    compose(pers);
  }

  public void orthographic(float left, float right,               
			   float bottom, float top,
			   float near, float far) {
    // this = this * orth
    Matrix3D orth = new Matrix3D();
    orth.set(0, 0, 2 / (right - left));
    orth.set(3, 0, (right + left) / (left - right));
    orth.set(1, 1, 2 / (bottom - top));
    orth.set(3, 1, (bottom + top) / (top - bottom));
    orth.set(2, 2, 2 / (far - near));
    orth.set(3, 2, (far + near) / (near - far));
    compose(orth);
  }

  public String toString() {
    String out = "";
    out = out + "[" + a[0] + " " + a[1] + " " + a[2] + " " + a[3] +
      " " + a[4] + " " + a[5] + " " + a[6] + " " + a[3] +
      " " + a[8] + " " + a[9] + " " + a[10] + " " + a[11] +
      " " + a[12] + " " + a[13] + " " + a[14] + " " + a[15] + "]";
    return out;
  }
  
} // end Matrix3D
