// Wendy Chien
// Project 4
// Recitation F11

import java.awt.*;
import java.lang.*;
import java.util.*;
import java.math.*;

public class Matrix3D {
  public float contents[][];
  
  //
  // Constructors
  //
  public Matrix3D() {                  // initialize with identity transform
    int i,j;

    contents = new float[4][4];
    for (i=0;i<4;i++) {
      for (j=0;j<4;j++) {
	if (i==j) 
	  contents[i][j] = 1;
	else
	  contents[i][j] = 0;
      }
    }

  }
   
  public Matrix3D(Matrix3D copy) {     // initialize with copy of source
    int i,j;
    
    contents = new float[4][4];
    for (i=0;i<4;i++) {
      for (j=0;j<4;j++) {
	contents[i][j] = copy.contents[i][j];
      }
    }
  }

  public Matrix3D(Raster r) {          // initialize with a mapping from canonical space to screen space
    int i,j;

    contents = new float[4][4];
    contents[0][0] = (float) r.width/2;
    contents[0][1] = (float) 0.0;
    contents[0][2] = (float) 0.0;
    contents[0][3] = (float) r.width/2;
    contents[1][0] = (float) 0.0;
    contents[1][1] = (float) r.height/2;
    contents[1][2] = (float) 0.0;
    contents[1][3] = (float) r.height/2;
    contents[2][0] = (float) 0.0;
    contents[2][1] = (float) 0.0;
    contents[2][2] = (float) 1.0;
    contents[2][3] = (float) 1.0;
    contents[3][0] = (float) 0.0;
    contents[3][1] = (float) 0.0;
    contents[3][2] = (float) 0.0;
    contents[3][3] = (float) 1.0;
    
    
  }

//
  // General interface methods
  //        
  public void set(int i, int j, float value) {          // set element [j][i] to value
    // be careful about what i and j are
    contents[i][j] = value;
  }

  public float get(int i, int j) {                      // return element [j][i]
    return (contents[i][j]);
  }

  public Vertex3D mpMultiply(Matrix3D m, Vertex3D p) {
    // Multiplies a matrix with a point.

    Vertex3D pt;
    float w;
    int i,k;

    pt = new Vertex3D();
    for (i=0;i<4;i++) {
      pt.x = p.x*m.contents[0][0] +
	     p.y*m.contents[0][1] +
	     p.z*m.contents[0][2] +
	     m.contents[0][3];
      pt.y = p.x*m.contents[1][0] +
	     p.y*m.contents[1][1] +
	     p.z*m.contents[1][2] +
	     m.contents[1][3];
      pt.z = p.x*m.contents[2][0] +
	     p.y*m.contents[2][1] +
	     p.z*m.contents[2][2] +
	     m.contents[2][3];
      w = p.x*m.contents[3][0] +
	  p.y*m.contents[3][1] +
	  p.z*m.contents[3][2] +
	  m.contents[3][3];

      if (w != 1.0) {
	pt.x = pt.x/w;
	pt.y = pt.y/w;
	pt.z = pt.z/w;
      }
	
    }
    return(pt);
  }
    
  public Matrix3D mmAdd(Matrix3D m1, Matrix3D m2) {
    Matrix3D sum;
    int i,j;

    sum = new Matrix3D();
    for(i=0;i<3;i++) {
      for(j=0;j<3;j++) {
	sum.contents[i][j] = m1.contents[i][j] + m2.contents[i][j];
      }
    }
    
    return(sum);

  } 

  //
  // 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 (i = 0; i < length; i++)
  //        out[start+i] = this * in[start+i]
  //
  public void transform(Vertex3D in[], Vertex3D out[], int start, int length) {
    int i,k;
   
    for (i=0;i<length;i++) {
      out[start+i] =  mpMultiply(this,in[start+i]);
    }

    // copying the points outside the subset directly from in[] 
    for (i=0;i<start;i++) {
      out[i] = in[i];
    }
  }

  public Vertex3D transform(Vertex3D pt) {
    Vertex3D p;
    
    p = mpMultiply(this,pt);
    
    return p;
  }

  /*
  public Vertex3D transform(Vertex3D vt) {
    Point3D p, pt;
    Vertex3D vertex;

    pt = new Point3D(vt.x,vt.y,vt.z);
    p = mpMultiply(this,pt);
    vertex = new Vertex3D(p.x,p.y,p.z);

    return vertex;
  }
  */

  public Point3D normalTransform(Point3D n) {
    float mat[][];
    Point3D norm;
 
    mat = new float[3][3];
    mat[0][0] = (contents[1][1]*contents[2][2]-contents[1][2]*contents[2][1])/(contents[0][0]*contents[1][1]*contents[2][2]-contents[0][0]*contents[1][2]*contents[2][1]-contents[1][0]*contents[0][1]*contents[2][2]+contents[1][0]*contents[0][2]*contents[2][1]+contents[2][0]*contents[0][1]*contents[1][2]-contents[2][0]*contents[0][2]*contents[1][1]);
    mat[0][1] = -1*(contents[1][0]*contents[2][2]-contents[1][2]*contents[2][0])/(contents[0][0]*contents[1][1]*contents[2][2]-contents[0][0]*contents[1][2]*contents[2][1]-contents[1][0]*contents[0][1]*contents[2][2]+contents[1][0]*contents[0][2]*contents[2][1]+contents[2][0]*contents[0][1]*contents[1][2]-contents[2][0]*contents[0][2]*contents[1][1]);
    mat[0][2] = (contents[1][0]*contents[2][1]-contents[1][1]*contents[2][0])/(contents[0][0]*contents[1][1]*contents[2][2]-contents[0][0]*contents[1][2]*contents[2][1]-contents[1][0]*contents[0][1]*contents[2][2]+contents[1][0]*contents[0][2]*contents[2][1]+contents[2][0]*contents[0][1]*contents[1][2]-contents[2][0]*contents[0][2]*contents[1][1]);
    mat[1][0] = -1*(contents[0][1]*contents[2][2]-contents[0][2]*contents[2][1])/(contents[0][0]*contents[1][1]*contents[2][2]-contents[0][0]*contents[1][2]*contents[2][1]-contents[1][0]*contents[0][1]*contents[2][2]+contents[1][0]*contents[0][2]*contents[2][1]+contents[2][0]*contents[0][1]*contents[1][2]-contents[2][0]*contents[0][2]*contents[1][1]); 
    mat[1][1] = (contents[0][0]*contents[2][2]-contents[0][2]*contents[2][0])/(contents[0][0]*contents[1][1]*contents[2][2]-contents[0][0]*contents[1][2]*contents[2][1]-contents[1][0]*contents[0][1]*contents[2][2]+contents[1][0]*contents[0][2]*contents[2][1]+contents[2][0]*contents[0][1]*contents[1][2]-contents[2][0]*contents[0][2]*contents[1][1]);
    mat[1][2] = -1*(contents[0][0]*contents[2][1]-contents[0][1]*contents[2][0])/(contents[0][0]*contents[1][1]*contents[2][2]-contents[0][0]*contents[1][2]*contents[2][1]-contents[1][0]*contents[0][1]*contents[2][2]+contents[1][0]*contents[0][2]*contents[2][1]+contents[2][0]*contents[0][1]*contents[1][2]-contents[2][0]*contents[0][2]*contents[1][1]);
    mat[2][0] = (contents[0][1]*contents[1][2]-contents[0][2]*contents[1][1])/(contents[0][0]*contents[1][1]*contents[2][2]-contents[0][0]*contents[1][2]*contents[2][1]-contents[1][0]*contents[0][1]*contents[2][2]+contents[1][0]*contents[0][2]*contents[2][1]+contents[2][0]*contents[0][1]*contents[1][2]-contents[2][0]*contents[0][2]*contents[1][1]);
    mat[2][1] = -1*(contents[0][0]*contents[1][2]-contents[0][2]*contents[1][0])/(contents[0][0]*contents[1][1]*contents[2][2]-contents[0][0]*contents[1][2]*contents[2][1]-contents[1][0]*contents[0][1]*contents[2][2]+contents[1][0]*contents[0][2]*contents[2][1]+contents[2][0]*contents[0][1]*contents[1][2]-contents[2][0]*contents[0][2]*contents[1][1]);
    mat[2][2] = (contents[0][0]*contents[1][1]-contents[0][1]*contents[1][0])/(contents[0][0]*contents[1][1]*contents[2][2]-contents[0][0]*contents[1][2]*contents[2][1]-contents[1][0]*contents[0][1]*contents[2][2]+contents[1][0]*contents[0][2]*contents[2][1]+contents[2][0]*contents[0][1]*contents[1][2]-contents[2][0]*contents[0][2]*contents[1][1]);
    
    norm = new Point3D();
    norm.x = mat[0][0]*n.x + mat[0][1]*n.y + mat[0][2]*n.z;
    norm.y = mat[1][0]*n.x + mat[1][1]*n.y + mat[1][2]*n.z;
    norm.z = mat[2][0]*n.x + mat[2][1]*n.y + mat[2][2]*n.z;

    //norm.x = mat[0][0] + mat[0][1] + mat[0][2];
    //norm.y = mat[1][0] + mat[1][1] + mat[1][2];
    //norm.z = mat[2][0] + mat[2][1] + mat[2][2];

    return norm;
  }

  public final void compose(Matrix3D src) {                        // this = this * src
    int i,j;
    Matrix3D temp;
    
    temp = new Matrix3D();
    for (i=0;i<4;i++) {
      for (j=0;j<4;j++) {
	temp.contents[i][j] = contents[i][0]*src.contents[0][j] +
	                      contents[i][1]*src.contents[1][j] +
	                      contents[i][2]*src.contents[2][j] +
                              contents[i][3]*src.contents[3][j];
      }
    }
    
    for (i=0;i<4;i++) {
      for (j=0;j<4;j++) {
	contents[i][j] = temp.contents[i][j];
      }
    }

  }

  public void loadIdentity() {                                     // this = identity
    int i,j;

    for (i=0;i<4;i++) {
      for (j=0;j<4;j++) {
	if (i==j) 
	  contents[i][j] = 1;
	else
	  contents[i][j] = 0;
      }
    }
  }

  public void translate(float tx, float ty, float tz) {            // this = this * t   
    Matrix3D trans;

    trans = new Matrix3D();
    trans.set(0,3,tx);
    trans.set(1,3,ty);
    trans.set(2,3,tz);

    compose(trans);

  }

  public void scale(float sx, float sy, float sz) {                // this = this * scale
    Matrix3D scle;

    scle = new Matrix3D();
    scle.set(0,0,sx);
    scle.set(1,1,sy);
    scle.set(2,2,sz);

    compose(scle);
    
  }

  public void skew(float kxy, float kxz, float kyz) {              // this = this * skew
    Matrix3D skw;
    
    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, sym, skw, ident;
    Point3D a;
    
    a = new Point3D(ax,ay,az);
    a = normalize(a);
    ax = a.x;
    ay = a.y;
    az = a.z;

    sym = new Matrix3D();
    sym.set(0,0,ax*ax); 
    sym.set(0,1,ax*ay); 
    sym.set(1,0,ax*ay); 
    sym.set(0,2,ax*az); 
    sym.set(2,0,ax*az); 
    sym.set(1,1,ay*ay); 
    sym.set(1,2,ay*az); 
    sym.set(2,1,ay*az); 
    sym.set(2,2,az*az); 
    sym.scale(1-(float)Math.cos(angle),1-(float)Math.cos(angle),1-(float)Math.cos(angle));

    skw = new Matrix3D();
    skw.set(0,0,0);
    skw.set(0,1,-1*az); 
    skw.set(0,2,ay); 
    skw.set(1,0,az); 
    skw.set(1,1,0);
    skw.set(1,2,-1*ax); 
    skw.set(2,0,-1*ay); 
    skw.set(2,1,ax); 
    skw.set(2,2,0);
    skw.scale((float)Math.sin(angle),(float)Math.sin(angle),(float)Math.sin(angle));

    ident = new Matrix3D();
    ident.scale((float)Math.cos(angle),(float)Math.cos(angle),(float)Math.cos(angle));
    
    rot = mmAdd(sym,mmAdd(skw,ident));

    compose(rot);

  }
    
  public Point3D normalize(Point3D p) {
    float length;
    Point3D pt;
    
    pt = new Point3D();
    length = (float) Math.sqrt(p.x*p.x + p.y*p.y + p.z*p.z);

    if (length == 0)
      System.out.println("length is 0");
    pt.x = p.x/length;
    pt.y = p.y/length;
    pt.z = p.z/length;
    
    return (pt);
  }

  public Point3D cross(Point3D p1, Point3D p2) {
    Point3D p;
    float xval,yval,zval;

    xval = p1.y*p2.z - p1.z*p2.y;
    yval = p1.z*p2.x - p1.x*p2.z;
    zval = p1.x*p2.y - p1.y*p2.x;

    p = new Point3D(xval,yval,zval);

    return(p);
  }

  public float dot(Point3D p1, Point3D p2) {
    float xval,yval,zval;

    xval = p1.x*p2.x;
    yval = p1.y*p2.y;
    zval = p1.z*p2.z;
    
    return(xval+yval+zval);
  }

  public void lookAt(float eyex, float eyey, float eyez,
		     float atx,  float aty,  float atz,
                           float upx,  float upy,  float upz) {          // this = this * lookat
    // Note:  This does not handle the case where the lookat vector is parallel to the 
    //        up vector nor does it handle the case where the eye is at an inifinite 
    //        distance away.
  
    Point3D l,r,u;
    Point3D eye, at, up;
    Matrix3D m;
    int i;

    l = new Point3D(atx-eyex,aty-eyey,atz-eyez);
    
    up = new Point3D(upx,upy,upz);
    r = cross(l,up);
    if ((r.x == 0) && (r.y == 0) && (r.z == 0)) {
      // up vector and lookat vector are parallel--special case      
    }
    u = cross(r,l);

    l = normalize(l);
    r = normalize(r);
    u = normalize(u);

    eye = new Point3D(eyex,eyey,eyez);
    
    m = new Matrix3D();
    m.set(0,0,r.x);
    m.set(0,1,r.y);
    m.set(0,2,r.z);
    m.set(1,0,u.x);
    m.set(1,1,u.y);
    m.set(1,2,u.z);
    m.set(2,0,-1*l.x);
    m.set(2,1,-1*l.y);
    m.set(2,2,-1*l.z);
    
    m.set(0,3,-1*dot(r,eye));
    m.set(1,3,-1*dot(u,eye));
    m.set(2,3,dot(l,eye));

    compose(m);
  }
  
  //
  // Assume the following projection transformations
  // transform points into the canonical viewing space
  //
  public void perspective(float left, float right,                // this = this * persp
			  float bottom, float top,
		       float near, float far) {
    Matrix3D persp;

    persp = new Matrix3D();
    persp.set(0,0,(2*near)/(right-left));
    persp.set(0,2,-1*(right+left)/(right-left));
    persp.set(1,1,(2*near)/(bottom-top));
    persp.set(1,2,-1*(bottom+top)/(bottom-top));
    persp.set(2,3,(-2*far*near)/(far-near));
    persp.set(2,2,(far+near)/(far-near));
    persp.set(3,2,1);
    persp.set(3,3,0);

    compose(persp);
  }

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

  public void printOut() {
    int i,j;
    String s = "";

    System.out.println("Matrix: ");
    for(i=0;i<4;i++) {
      for(j=0;j<4;j++) {
	s = s+contents[i][j]+"\t";
      }
      s = s+"\n";
    }
    System.out.println(s);
  }

}
