import java.applet.*;
import java.awt.*;
import java.io.*;
import java.net.*;
import Vertex3D;
import Triangle;
import Matrix4x4;
import MatrixStack;
import Light;
import Surface;
import Luxo;
import Box;
import Ball;

/*
    LuxoApplet provides only user interface elements.
    All od the interesting stuff is done in the
    LuxoCanvas Class.
*/
public class LuxoApplet extends Applet {
  Panel viewerPanel, buttonPanel;
  LuxoCanvas luxoViewer;
  Button button[];

  // These are the button labels. The index into this array
  // determines the number passed to LuxoCanvas.processButton()
  String bLabel[ ] = {
    "Reset",
    "Sky Cam",
    "Mom Cam",
    "Jr Cam",
    "Mom @ Jr",
    "Mom @ Ball",
    "Jr @ Mom",
    "Jr @ Ball",
    "Move Jr N",
    "Move Jr S",
    "Move Jr E",
    "Move Jr W",
    "User 1",
    "User 2",
    "User 3",
    "User 4"
  };


  // Here is where the user interface is created
  public void init() {
    int width = size().width;
    int height = size().height;
    button = new Button[bLabel.length];

    setLayout(new BorderLayout(10, 10));
    setBackground(new Color(128, 96, 128));

    viewerPanel = new Panel();
    luxoViewer = new LuxoCanvas(this, width - 10, height - 120);
    add(viewerPanel, BorderLayout.NORTH);
    viewerPanel.add(luxoViewer, null);

    buttonPanel = new Panel();
    add(buttonPanel, BorderLayout.SOUTH);
    buttonPanel.setLayout(new GridLayout(4, 4, 2, 2));
    for (int i = 0; i < bLabel.length; i++) {
      button[i] = new Button(bLabel[i]);
      button[i].setBackground(new Color(192, 160, 192));
      buttonPanel.add(button[i]);
    }

    luxoViewer.init();
  }

  // Here is where button presses are handled
  public boolean action(Event e, Object o) {
    if (e.target instanceof Button) {
      for (int i = 0; i < bLabel.length; i++) {
        if (bLabel[i].equals(o)) {
          luxoViewer.processButton(i);
          break;
        }
      }
      return true;
    } else {
      return false;
    }
  }

  // makes a nice 5 pixel border around
  // the LuxoCanvas and buttonPanel
  public Insets insets() {
    return new Insets(5,5,5,5);
  }
}



class LuxoCanvas extends Canvas {
    final int CHUNKSIZE = 500;          // initial array size for vertices and triangles
    final int SKYCOLOR = 0xffd0e0ff;
    Raster raster;
    Image screen;
    MatrixStack view;
    MatrixStack model;
    Vertex3D worldList[], screenList[];   // temporary storage for transformed vertices
    Luxo mom, jr;
    Vertex3D momEye, jrEye;
    Box floor;
    Ball ball;
    Light lightList[];
    int lights;

    Applet parent;

    // these variables are used to set up the viewing,
    // projection, and screen-space mapping matrices
    // several of these could have been Vertex3D Objects
    // but they really don't need some of the extra stuff
    // that a Vertex3D has, like normals and colors,
    // so I didn't decided to declare them as floats
    float eyex, eyey, eyez;
    float lookatx, lookaty, lookatz;
    float upx, upy, upz;
    float fov;
    int width, height;

    // the constructor set the size of the rendering, creates
    // a raster, and fills it with the background color.
    public LuxoCanvas(Applet p, int w, int h) {
      super();
      parent = p;
      setSize(w, h);
      raster = new Raster(w, h);
      raster.fill(SKYCOLOR);
      screen = raster.toImage( );
      width = w;
      height = h;
    }

    // the init method resets the whole LuxoCanvas
    public void init( ) {
        // First, we create and initialize temporary storage for transformed
        // vertices. Here is a brief explanation of how these arrays get used.
        // Modeling coordinates of objects are transformed onto the worldList
        // where they are lit. These world coordinates then get transformed to
        // the screenList where they are rasterized. You can actually do this
        // trnsformation in one step, however, the method used here is more
        // flexible if you want to do some neat things later on (like shadow
        // maps).
        worldList = new Vertex3D[CHUNKSIZE];
        screenList = new Vertex3D[CHUNKSIZE];
        for (int i = 0; i < CHUNKSIZE; i++) {
            worldList[i] = new Vertex3D();
            screenList[i] = new Vertex3D();
        }

        // set default values for the viewing parameters
        eyex = 0;        eyey = -60;        eyez = 24;
        lookatx = 0;     lookaty = 0;     lookatz = 6;
        upx = 0;         upy = 0;         upz = 1;
        fov = 35;                           // in degrees

        // Set up two light sources, one ambient, one directional.
        // We'll get to this stuff later in class. Don't mess with
        // them at this point.
        lightList = new Light[10];
        lights = 0;
        lightList[lights++] = new Light(Light.AMBIENT, 0, 0, 0, 1.0f, 1.0f, 1.0f);
        lightList[lights++] = new Light(Light.DIRECTIONAL, 0.5f, 1.0f, -0.5f, 1.0f, 1.0f, 1.0f);

        // We next set up to matrix transform stacks.
        //
        // The view matrixStack will be used to map from world to screen
        // coordinates. It looks at the width and height of the Raster
        // to compute the mapping from canonical to screen coordinates
        view = new MatrixStack(raster);
        float t = (float) Math.sin(Math.PI*(fov/2)/180);
        float s = (t*height)/width;

        // Here we set the projection (from eye to canonical space)
        view.frustum(-t, t, -s, s, -1, -500);

        // Make a copy of the composed mapping from eye to screen space
        view.push();

        // Set up a viewing matrix (hint: a camera)
        view.lookAt(eyex, eyey, eyez, lookatx, lookaty, lookatz, upx, upy, upz);

        // The model matrixStack will be used to map a model's local
        // coordinates into world coordinates, there are a lot of different
        // "local" coordinate systems used, so this matrixStack gets a lot
        // of use
        model = new MatrixStack();

        // Next we create our four models.
        // The lamps each have four components, each with their own
        // vertex lists, triangle lists, and local coordinate systems.
        // The other two models, the floor and the ball have only one
        // component.
        mom = new Luxo(new Surface(0.4f, 0.4f, 1.0f, 0.5f, 0.5f, 0.0f, 100.0f));
        jr = new Luxo(new Surface(1.0f, 0.2f, 0.2f, 0.5f, 0.5f, 0.0f, 100.0f));
        floor = new Box(-24.0f, 24.0f, -24.0f, 24.0f, -1.0f, 0.0f,
                        new Surface(0.8f, 0.7f, 0.5f, 0.8f, 0.2f, 0.0f, 100.0f));
        ball = new Ball(new Surface(1.0f, 0.0f, 1.0f, 0.5f, 0.5f, 0.0f, 100.0f));

        // Reproportion Mom to make Jr model.
        // Here we use the model matrixStack to modify the vertex list
        // of Jr. There are other ways of doing this. For instance,
        // we could have declared just one lamp model and reproportion
        // and recolor it everytime we render. I've decided to keep things
        // conceptually, simple here. Each object has it's own model.
        // This code simply rescales the body and neck of the Jr model
        // so that it will have more childlike proportions.
        model.scale(0.5f, 1.0f, 1.0f);
        Vertex3D vList[] = jr.getVertices(Luxo.BODY);
        model.transform(vList, vList, vList.length);
        vList = jr.getVertices(Luxo.NECK);
        model.transform(vList, vList, vList.length);
        model.loadIdentity();

        // Now that all of the models are set up, call the
        // method that renders the whole scene.
        DrawScene();
        repaint();
    }

    public void paint(Graphics g) {
        g.drawImage(screen, 0, 0, this);
    }

    public void update(Graphics g) {
        paint(g);
    }

    float old_x, old_y;

    public boolean mouseDown(Event e, int x, int y) {
        // You will need to make extensive changes to this code
        // to implement the various camera navigation modes
        if (e.metaDown()) {
            parent.showStatus("Resetting model matrix");
            long time = System.currentTimeMillis();
            for (int i = 0; i <= 10; i++) {
              model.loadIdentity();
              // rotate the world around the up axis
              model.rotate(upx, upy, upz, (float)(i*Math.PI/5));
              DrawScene();
              update(this.getGraphics());
            }
            time = System.currentTimeMillis() - time;
            parent.showStatus("Time = "+(time/10)+" ms per frame");
        }
        old_x = x;
        old_y = y;
	      return true;
    }

    public boolean mouseDrag(Event e, int x, int y) {
        // You will need to make extensive changes to this code
        // to implement the various camera navigation modes
        if (e.metaDown()) {
            parent.showStatus("Resetting model matrix");
        } else {
            float theta = (float) (((x - old_x) * Math.PI) / width);
            old_x = x;
            // rotate the world around the up axis
            model.rotate(upx, upy, upz, theta);
            DrawScene();
            repaint();
        }
        return true;
    }

    // The renderObject() method does all of the work of rendering objects.
    // You will probably not need to modify this method, though you may if
    // you wish.
    private void renderObject(MatrixStack m, Vertex3D vList[], Triangle tList[]) {
        int verts = vList.length;
        int tris = tList.length;
        // transform the model's coordinates to the world-coodinate system
        // using MatrixStack m. Set the triangle's vertex list to these
        // coordinates, then light the triangles.
        m.transform(vList, worldList, verts);
        Triangle.setVertexList(worldList);
        for (int i = 0; i < tris; i++) {
          tList[i].Illuminate(lightList, lights);
        }

        // Next we transform our world space coordinates to screen space.
        // The we tell the triangles to use these vertices to draw themselves.
        view.transform(worldList, screenList, verts);
        Triangle.setVertexList(screenList);
        for (int i = 0; i < tris; i++) {
          tList[i].Draw(raster);
        }
    }

    private float toRadians(float degrees) {
      return (float) (degrees/180.0f * Math.PI);
    }

    // I wrote this little helper method to aid in drawing the
    // Mom luxo lamp. It limit's the model's articulations to
    // the allowed degrees of freedom. If you don't find it
    // helpful, you do not have to use it.
    private Vertex3D DrawMom(float a1, float a2, float a3, float a4) {
        model.rotate(0.0f, 0.0f, 1.0f, toRadians(a1));
        renderObject(model, mom.getVertices(Luxo.BASE), mom.getTriangles(Luxo.BASE));
        model.translate(0.0f, 0.0f, 2.5f);
        model.rotate(0.0f, 1.0f, 0.0f, toRadians(a2));
        renderObject(model, mom.getVertices(Luxo.BODY), mom.getTriangles(Luxo.BODY));
        model.translate(12.0f, 0.0f, 0.0f);
        model.rotate(0.0f, 1.0f, 0.0f, toRadians(a3));
        renderObject(model, mom.getVertices(Luxo.NECK), mom.getTriangles(Luxo.NECK));
        model.translate(12.0f, 0.0f, 0.0f);
        model.rotate(0.0f, 1.0f, 0.0f, toRadians(a4));
        renderObject(model, mom.getVertices(Luxo.HEAD), mom.getTriangles(Luxo.HEAD));
        // Hum... Why do I continue transforming points
        // after I've already drawn everything?
        model.translate(0.0f, 0.0f, -1.0f);
        return model.transform(new Vertex3D(0f, 0f, 0f));
    }

    // This helper method to aids in drawing the Jr. luxo lamp.
    // It limit's the model's articulations to the allowed
    // degrees of freedom. Just like above.
    private Vertex3D DrawJr(float a1, float a2, float a3, float a4) {
        model.scale(0.75f, 0.75f, 0.75f);
        model.rotate(0.0f, 0.0f, 1.0f, toRadians(a1));
        renderObject(model, jr.getVertices(Luxo.BASE), jr.getTriangles(Luxo.BASE));
        model.translate(0.0f, 0.0f, 2.5f);
        model.rotate(0.0f, 1.0f, 0.0f, toRadians(a2));
        renderObject(model,  jr.getVertices(Luxo.BODY), jr.getTriangles(Luxo.BODY));
        model.translate(6.0f, 0.0f, 0.0f);
        model.rotate(0.0f, 1.0f, 0.0f, toRadians(a3));
        renderObject(model, jr.getVertices(Luxo.NECK), jr.getTriangles(Luxo.NECK));
        model.translate(6.0f, 0.0f, 0.0f);
        model.rotate(0.0f, 1.0f, 0.0f, toRadians(a4));
        renderObject(model, jr.getVertices(Luxo.HEAD), jr.getTriangles(Luxo.HEAD));
        // Opps... I did it again, I played with your heart
        model.translate(1.0f, 0.0f, -1.0f);
        return model.transform(new Vertex3D(0f, 0f, 0f));
    }

    // Okay, you'll need to completely rewrite this method.
    void DrawScene() {
        // set the background color and reset the depth buffer
        raster.fill(SKYCOLOR);
        raster.resetz();

        // Draw the table, its frame is the same as the world frame.
        // In other words, it is already in world coordinates.
        renderObject(model, floor.getVertices(1), floor.getTriangles(1));

        // Save the world. Then add a ball.
        // The ball, has it's own coordinate frame, that, by default,
        // sits on the plane z = 0.
        model.push();
        model.translate(3.0f, 6.0f, 0.0f);
        renderObject(model, ball.getVertices(1), ball.getTriangles(1));
        model.pop();

        // Save the world. Then add Mom, whos frame also sits at z = 0.
        model.push();
        model.translate(-10.0f, 0.0f, 0.0f);
        momEye = DrawMom(0.0f, -120.0f, 120.0f, -60.0f);
        model.pop();

        // Save the world. Then add Jr, who frame sits, you guessed it, at 0.
        model.push();
        model.translate(6.0f, 0.0f, 0.0f);
        jrEye = DrawJr(180.0f, -135.0f, 90.0f, -60.0f);
        model.pop();

        // Make the raster into and Image.
        screen = raster.toImage( );
    }

    // Here's where you handle all of the button presses
    // from the button Panel
    public void processButton(int i) {
      switch (i) {
        case 0:                 // Reset
          model.loadIdentity();
          DrawScene();
          repaint();
          break;
        default:
          parent.showStatus("Button "+i+" was pressed");
          break;
      }
    }
}
