//

import activeTable.*;
import render.*;
import java.awt.*;
import java.util.*;

public class ActiveTableApplet extends ActiveTableDisplay
{
   int N = 6;
   Doll doll[] = new Doll[N];
   boolean enableDolls = false;
   Geometry objectAtCamera;
   Material rgb[] = new Material[3];
   double FL = 1.8;

   boolean isRunningScript = false;
   Action actions[], action;

   double V(int i) { evalArgs(action); return action.a[i]; }
   void setV(int i, double value) { action.a[i] = value; }
   int I(int i) { return (int)(V(i)); }

   int actionI = 0;

   int nActors = 0, actorI = 0, actors[] = new int[16];
   int nSync = 1;

   public void runScript(double time) {
      boolean isDone = true;
      for (int i = actionI ; actions[i] != null ; i++) {
         action = actions[i];
 
         switch (action.type) {
         case Action.ACTOR:
	    if (V(0) == Script.EVERYONE) {
               nActors = N;
	       for (actorI = 0 ; actorI < N ; actorI++)
	          actors[actorI] = actorI;
	    }
	    else {
               nActors = action.n;
	       for (actorI = 0 ; actorI < nActors ; actorI++)
	          actors[actorI] = (int)V(actorI);
            }
            break;
         case Action.CAMERA_TO:
            isDone &= cameraTo(time);
            break;
         case Action.FACE:
	    for (actorI = 0 ; actorI < nActors ; actorI++)
               if (actors[actorI] != I(0))
                  isDone &= face(actors[actorI], I(0));
            break;
         case Action.LEFT_ARM:
	    for (actorI = 0 ; actorI < nActors ; actorI++)
	       isDone &= leftArm(actors[actorI], V(0) * Math.PI / 180);
            break;
         case Action.LOOK_AT:
	    for (actorI = 0 ; actorI < nActors ; actorI++)
               if (actors[actorI] != I(0))
                  isDone &= lookAt(actors[actorI], I(0));
            break;
         case Action.MOVE_BY:
	    for (actorI = 0 ; actorI < nActors ; actorI++)
               isDone &= moveBy(actors[actorI], V(0)/100, V(1)/100);
            break;
         case Action.MOVE_TO:
	    for (actorI = 0 ; actorI < nActors ; actorI++)
               isDone &= moveTo(actors[actorI], V(0)/100, V(1)/100);
            break;
         case Action.PAUSE:
	    isDone &= pause(time);
            break;
         case Action.RIGHT_ARM:
	    for (actorI = 0 ; actorI < nActors ; actorI++)
	       isDone &= rightArm(actors[actorI], V(0) * Math.PI / 180);
            break;
         case Action.SAY:
            if (nActors >= 0) {
               speaker = actors[0];
               speech = action.s;
	       thought = "";
            }
            break;
         case Action.SYNC:
            if (isDone) {
               actionI = i+1;
               nSync++;
            }
            return;
         case Action.THINK:
            if (nActors >= 0) {
               thinker = actors[0];
               thought = action.s;
               speech = "";
            }
            break;
         }
      }
   }

   Geometry getTarget(int actor, int key) {
      Geometry target = null;
      switch (key) {
      case Script.CAMERA:
         return objectAtCamera;
      case Script.NOTHING:
         break;
      case Script.NEAREST:
         double dLo = 10000;
         for (int j = 0 ; j < N ; j++)
            if (j != actor) {
               double u=dollX(actor)-dollX(j), v=dollZ(actor)-dollZ(j), d=u*u+v*v;
               if (d < dLo) {
                  dLo = d;
                  target = doll[j];
               }
            }
         break;
      default:
	 if (key != actor)
            return doll[key];
      }
      return target;
   }

   // DISPLAY SPEECH AND THOUGHT BUBBLES

   int speaker = 0, thinker = 0;
   String speech = "", thought = "";

   double v[] = {0,0,0};
   public void drawOverlay(Graphics g) {
      if (speech.length() > 0) {
	 if (speaker == Script.NARRATOR)
            TextBubble.draw(g, TextBubble.NARRATION, W, W/2, H/2, speech, isTextMode);
         else {
            getSpeakerLocation(doll[speaker].head, v);
            int x = (int)v[0], y = (int)v[1];
            TextBubble.draw(g, TextBubble.SPEECH , W, x, y, speech , isTextMode);
         }
      }
      if (thought.length() > 0) {
         getSpeakerLocation(doll[thinker].head, v);
         int x = (int)v[0], y = (int)v[1];
         TextBubble.draw(g, TextBubble.THOUGHT, W, x, y, thought, isTextMode);
      }
   }

   void getSpeakerLocation(Geometry object, double v[]) {
      Matrix m = new Matrix();
      m.copy(object.globalMatrix);
      m.postMultiply(renderer.getCamera());
      m.translate(0,1.5,0);
      renderer.xf(m, m.get(0,3),m.get(1,3),m.get(2,3),1, v);
      renderer.projectPoint(v);
   }

   // INITIALIZATION (CALLED ONCE)

   public void initialize() {

      actions = Script.parse(getParameter("script"));

      // WE WILL SIMULATE 10 VEHICLES

      setMaxVehicles(N);    // THIS IS USED ONLY WHEN SIMULATING
      connect();
      N = getMaxVehicles(); // SO THAT THIS WILL WORK WITH THE REAL TABLE

      // SET VEHICLES INITIAL POSITION/DIRECTION

      for (int i = 0 ; i < N ; i++) {
         VehicleInfo vi = getVehicleInfo(i);
         vi.x = (i + .5 - .5 * N) / N;
         vi.y = .5 * vi.x;
         vi.theta = 0;
         setVehicleInfo(i, vi);
      }

      // CREATE EVERYTHING

      super.initialize();
      setFOV(.4);
      //setFOV(.6);
      setFL(FL);
      renderer.headsUp(true);

      for (int j = 0 ; j < 3 ; j++) {
         double r = j==0?1:.1, g = j==1?1:.1, b = j==2?1:.1;
         rgb[j] = new Material();
         rgb[j].setAmbient(.2*r,.2*g,.2*b);
         rgb[j].setDiffuse(.8*r,.8*g,.8*b);
         rgb[j].setSpecular(1,1,1,10);
      }

      // SET VEHICLES FIRST TARGET POSITION/DIRECTION

      for (int i = 0 ; i < N ; i++) {
         VehicleInfo vi = getVehicleInfo(i);
         vi.x = .9 * (i + .5 - .5 * N) / N;
         vi.y = .25 - Math.abs(vi.x);
         vi.theta = (i < N/2 ? -Math.PI/4 : -3*Math.PI/4);
      }

      objectAtCamera = new Geometry();
   }

   void addDolls() {
      for (int i = 0 ; i < N ; i++) {
         world.add(doll[i] = new Doll());
	 int j = i % 3;
         doll[i].body.setMaterial(rgb[i%3]);
         doll[i].setSkin((1.*N-i)/(1.*N));
         doll[i].setHeight(2+2.*(N-1-i)/N);
         doll[i].setScale(.05);
         doll[i].setGazeWeight(1);
	 if (i % 2 == 1)
	    doll[i].setGirl();
	 switch (i % 6){
	 case 0: doll[i].setSkin(Doll.gold); break;
	 case 1: doll[i].setSkin(.6); break;
	 case 2: doll[i].setSkin(Doll.silver); break;
	 case 3: doll[i].setSkin(Doll.copper); break;
	 case 4: doll[i].setSkin(1); break;
	 case 5: doll[i].setSkin(Doll.violet); break;
	 }
      }
   }

   double dTime = 1, oldTime = 0;
   public void animate(double time) {
      dTime = time - oldTime;
      oldTime = time;

      if (enableDolls && doll[0] == null)
         addDolls();

      if (isRunningScript)
         runScript(time);

      double cx = renderer.getCamera().get(0,0);
      double cz = renderer.getCamera().get(2,0);
      push();
         translate(FL * cz, 0, FL * cx);
         transform(objectAtCamera);
      pop();

      for (int i = 0 ; i < N ; i++) {
         VehicleInfo vi = getVehicleInfo(i);

         if (doll[i] != null) {

            if (doll[i].facingObject != null)
               vi.theta = doll[i].getFacingAngle(doll[i].facingObject);
            setVehicleInfo(i, vi);

            doll[i].setAltitude(ActiveTableDisplay.Y_TOP);

            Matrix m = vehicle[i].matrix;
            doll[i].setPosition(m.get(0,3),m.get(2,3));

            m = vehicle[i].child[0].matrix;
            doll[i].setDirection(Math.atan2(m.get(0,2),m.get(0,0)));

            doll[i].animate(time);
         }
	 else
            setVehicleInfo(i, vi);
      }
      super.animate(time);
   }

   double dollX(int i) { return doll[i].matrix.get(0,3); }
   double dollZ(int i) { return doll[i].matrix.get(2,3); }

   boolean isTextMode = false;
   public boolean keyUp(Event e, int key) {
      if (isTextMode) {
         switch (key) {
            case '\'':
               isTextMode = false;
               break;
            case 8:
               if (speech.length() > 0)
                  speech = speech.substring(0, speech.length()-1);
               break;
            default:
               speech += (char)key;
               break;
         }
         return true;
      }
      switch (key) {
      case 'd':
         enableDolls = true;
         return true;
      case 's':
         enableDolls = true;
         actionI = 0;
         isRunningScript = true;
         return true;
      case 'c':
         for (int j = 0 ; j < N ; j++)
            doll[j].setGaze(objectAtCamera);
               break;
      case '`':
         isTextMode = true;
         return true;
      }
      return super.keyUp(e, key);
   }

   double xyz[] = new double[3];
   double tableX = 0, tableZ = 0;
   int selectI = -1, selectMode = 0;
   boolean dragged;
   public boolean mouseDown(Event e, int x, int y) {
      dragged = false;
      mx = x;
      my = y;
      Geometry g = getGeometry(x,y);
      selectMode = selectI = -1;
      if (g != null) {
         if (table.contains(g)) {
            getPoint(x,y,xyz);
            tableX = xyz[0];
            tableZ = xyz[2];
         }
         else
            for (int i = 0 ; i < N ; i++)
               if (vehicle[i].contains(g) || doll[i]!=null && doll[i].contains(g)) {
                  selectI = i;
                  selectMode = vehicle[i].contains(g) ? 0 :
                               doll[i].head.contains(g) ? 2 : 1;
                  return true;
               }
      }
      return super.mouseDown(e,x,y);
   }

   int mx, my;
   public boolean mouseDrag(Event e, int x, int y) {
      if (selectI >= 0) {
	 if (! dragged) {
	    dragged = true;
	    if (my - y > Math.abs(mx - x)) {
	       // PICK UP THE ACTOR
	    }
	 }
         return true;
      }
      return super.mouseDrag(e, x, y);
   }
   public boolean mouseUp(Event e, int x, int y) {
      if (selectMode == 0) {
         if (getPoint(x,y,xyz))
            moveTo(selectI, xyz[0], xyz[2]);
         return true;
      }
      if (selectI > 0) {
	 if (y >= H) {
	    if (selectMode == 1)
	       face(selectI, Script.CAMERA);
            else
	       lookAt(selectI, Script.CAMERA);
	    return true;
	 }
         Geometry g = getGeometry(x,y);
         for (int j = 0 ; j < N ; j++)
	    if (vehicle[j].contains(g) || doll[j].contains(g))
	       if (selectMode == 1)
	          face(selectI, j);
               else
	          lookAt(selectI, j);
         return true;
      }
      return super.mouseUp(e, x, y);
   }
   public boolean mouseMove(Event e, int x, int y) {
      isTextMode = false;
      return super.mouseMove(e, x, y);
   }

   boolean pause(double time) {
      if (nSync > V(2)) {
         setV(1, time);
         setV(2, nSync);
      }
      return time - V(1) >= V(0);
   }

   boolean cameraTo(double time) {
      double theta = -V(0) * Math.PI / 180;
      double phi   =  V(1) * Math.PI / 180;
      double timeOut = Math.max(.001, V(2));
      if (nSync > V(6)) {
	 Matrix camera = renderer.getCamera();
	 double theta0 = Math.atan2(camera.get(0,2),camera.get(0,0));
	 double phi0   = Math.asin(camera.get(2,1));
         setV(3, theta0);
         setV(4, phi0);
         setV(5, time);
         setV(6, nSync);
      }
      double t = Math.min(1, (time - V(5)) / timeOut);
      renderer.setCamera(lerp(t, V(3), theta), lerp(t, V(4), phi));
      return t>.99;
   }

   boolean lookAt(int i, int j) {
      return doll[i].setGaze(getTarget(i, j));
   }

   boolean face(int i, int j) {
      doll[i].setFacing(getTarget(i, j));
      double theta = doll[i].getFacingAngle(doll[i].facingObject);
      return Math.abs(getTheta(i,getVehicleInfo(i)) - theta) < .02;
   }

   boolean leftArm(int actor, double angle) {
      double L = doll[actor].getLeftArmAngle();
      return doll[actor].setLeftArmAngle(lerp(2 * dTime, L, angle));
   }

   boolean moveBy(int i, double x, double z) {
      VehicleInfo vi = getVehicleInfo(i);
      x += vi.x;
      z -= vi.y;
      Matrix m = vehicle[i].matrix;
      vi.x     = Math.max(-.45, Math.min(.45,  x));
      vi.y     = Math.max(-.45, Math.min(.45, -z));
      vi.theta = Math.atan2(m.get(2,3)-z, x-m.get(0,3));
      return isAtGoal(i);
   }

   boolean moveTo(int i, double x, double z) {
      VehicleInfo vi = getVehicleInfo(i);
      Matrix m = vehicle[i].matrix;
      vi.x     = Math.max(-.45, Math.min(.45,  x));
      vi.y     = Math.max(-.45, Math.min(.45, -z));
      vi.theta = Math.atan2(m.get(2,3)-z, x-m.get(0,3));
      return isAtGoal(i);
   }

   boolean rightArm(int actor, double angle) {
      double L = doll[actor].getRightArmAngle();
      return doll[actor].setRightArmAngle(lerp(2 * dTime, L, angle));
   }

   double lerp(double t,double a,double b) { return a + t * (b - a); }

//------- RUN-TIME EVALUATION OF ACTION ARGS

   void evalArgs(Action action) {
      switch (action.argType) {
      case Action.HAS_NO_ARG:
	 break;
      case Action.STRING_ARG:
	 break;
      case Action.VECTOR_ARG:
	 StringTokenizer st = new StringTokenizer(action.s, ",");
	 action.n = 0;
	 while (st.hasMoreTokens()) {
	    String token = st.nextToken();
	    if (token.charAt(0) == '{')
	       action.n = eval(token, action.a, action.n);
            else
	       action.a[action.n++] = Script.number(token);
         }
	 break;
      }
   }

   static final double OPCODE = 123.456;
   static final double PLUS  = OPCODE + 1;
   static final double MINUS = OPCODE + 2;
   static final double TIMES = OPCODE + 3;
   static final double OVER  = OPCODE + 4;
   static final double ME    = OPCODE + 5;

   static boolean isOp(double d) { return d >= OPCODE && d < OPCODE + 10; }

   double value[] = new double[100];

   int eval(String s, double vec[], int n) {
      s = s.substring(1,s.length()-1);  // REMOVE BRACES
      int k = 0;
      for (int i = 0 ; i < s.length() ; i++)
	 switch (s.charAt(i)) {
	 case 'm':
	    value[k++] = actors[actorI];
	    i++;
	    break;
	 case '+':
	    value[k++] = PLUS;
	    break;
	 case '*':
	    value[k++] = TIMES;
	    break;
	 case '/':
	    value[k++] = OVER;
	    break;
	 default:
	    if (k>0 && !isOp(value[k-1]) && s.charAt(i) == '-') {
	       value[k++] = MINUS;
	       break;
            }
	    String token = "";
	    int j = i;
	    while (j < s.length() && isNumeric(s.charAt(j),j-i))
	       token += s.charAt(j++);
            value[k++] = (new Double(s.substring(i,j))).doubleValue();
	    i = j-1;
	    break;
         }

      vec[n++] = eval(value, k);
      return n;
   }

   // RIGHT NOW ONLY PARSES SIMPLE EXPRESSIONS

   double eval(double value[], int k) {
      double result = value[0];
      for (int j = 1 ; j < k ; j++)
	 if (value[j] == PLUS)
	    result += value[++j];
	 else if (value[j] == MINUS)
	    result -= value[++j];
	 else if (value[j] == TIMES)
	    result *= value[++j];
	 else if (value[j] == OVER)
	    result /= value[++j];

      return result;
   }

   boolean isNumeric(int i, int loc) {
      return loc==0 && i=='-' || i=='.' || i>='0' && i<='9';
   }
}