//
 Copyright 2001 Ken Perlin
import java.awt.*;
import java.util.*;
import java.io.*;
import java.net.*;

public class Poetry extends BufferedApplet
{
   public String in[] = {};

   Hashtable words = null, pairs = null, trpls = null, tileMap;
   Vector tiles = new Vector();
   Vector drifts = new Vector();
   int w, h, mx, my;
   boolean isM = false, isT = false;

// START UP AND REGISTER A DRIFT BEHAVIOR

   void add(PoetryDrift d) {
      drifts.addElement(d);
      d.start();
   }

// BEFORE EXITING, STOP ALL DRIFT BEHAVIORS

   public void stop() {
      super.stop();
      for (int i = 0 ; i < drifts.size() ; i++)
         ((PoetryDrift)drifts.elementAt(i)).stop();
   }

   PoetryEntry A = null;
   int prevx, prevy;

// USER SELECTS AN ACTIVE TILE TO DRAG AROUND

   public boolean mouseDown(Event E, int x, int y) {
      if (x < 10 && y < 10) {
         isT = true;
         return true;
      }
      A = null;
      for (int i = 0 ; i < tiles.size() ; i++) {
         PoetryEntry T = (PoetryEntry)tiles.elementAt(i);
         T.isActive = false;
         if (x >= T.l() && x < T.r() && y >= T.t() && y < T.b())
            A = T;
      }
      if (A == null)
         isM = true;
      else {
         A.isActive = true;
         A.isStuck = ! A.isStuck;
         prevx = x;
         prevy = y;
      }   
      return true;
   }

// USER LIFTS MOUSE BUTTON TO RELEASE THE ACTIVE TILE

   public boolean mouseUp(Event e, int x, int y) {
      if (isT)
         isT = false;
      else if (isM)
         isM = false;
      else {
         A.isActive = false;
         A = null;
      }
      return true;
   }

// USER DRAGS THE ACTIVE TILE TO DESIRED LOCATION

   public boolean mouseDrag(Event e, int x, int y) {
      if (isM) {
         mx = x;
         my = y;
      }
      else {
         A.x += x - prevx;
         A.y += y - prevy;
         prevx = x;
         prevy = y;
      }
      return true;
   } 

   PoetryEntry add(Hashtable h, String s) {
      PoetryEntry P;

      if ((P = (PoetryEntry)h.get(s)) == null) {
         h.put(s, P = new PoetryEntry(this));
         P.s = s;
      }
      P.count++;
      return P;
   }  

   int count = 0;

   void read(String URLName) {
      Vector tmp = new Vector();

      String line;
      try {
	 URL url = new URL(getCodeBase(), URLName);
	 try {
	    DataInputStream stream = new DataInputStream(url.openStream());
	    try {
	       while ((line = stream.readLine()) != null)
		  tmp.addElement(line);
	    } catch (Exception e) {System.out.println("reading: " + e);}
	 } catch (Exception e) {System.out.println(e);}
      } catch (MalformedURLException e) {System.out.println(e);}

      in = new String[tmp.size()];
      for (int i = 0 ; i < in.length ; i++)
	 in[i] = (String)tmp.elementAt(i);
   }

   void parseWords(String in[]) {
      String word;

      words = new Hashtable();
      pairs = new Hashtable();
      trpls = new Hashtable();
      Random R = new Random();
      int nWords = 0;

      String prev = "", prev2 = "";
      for (int lineNo = 0 ; lineNo < in.length && in[lineNo] != null ; lineNo++) {
         StringTokenizer st = new StringTokenizer(in[lineNo], " ,;./():`'?!-=\"");
         while (st.hasMoreTokens()) {
            word = st.nextToken().toLowerCase();
            if (word.charAt(0) != '<') {
               add(words, word);
               if (prev.length() > 0)
                  add(pairs, prev + " " + word);
               if (prev2.length() > 0)
                  add(trpls, prev2 + " " + prev + " " + word);
               prev2 = prev;
               prev = word; 
               nWords++;
            }
         }
      }

      tileMap = new Hashtable();
      int density = nWords / 400;
      for (Enumeration e = words.keys() ; e.hasMoreElements() ; ) {
         word = (String)e.nextElement();
         PoetryEntry W = (PoetryEntry)words.get(word);
         int n = W.count / density;
         if (n == 0)
            continue;
         n = 2 + (int)Math.sqrt(n);
         int copies = n/3 + (Math.abs(R.nextInt()) % (2*n/3));
         for (int i = 0 ; i < copies ; i++) {
            PoetryEntry T = new PoetryEntry(this);
            tiles.addElement(T);

            TileClass V = getTileClass(word);
            if (V == null)
               tileMap.put(word, V = new TileClass());
            V.addElement(T);

            T.count = 1;
            T.s = word;
            T.x = w/4 + (Math.abs(R.nextInt()) % w)/2;
            T.y = h/4 + (Math.abs(R.nextInt()) % h)/2;
            T.w = 9*word.length()+6;
            T.h = 17;
         }
      }

      Object ob;
      for (Enumeration e = pairs.keys() ; e.hasMoreElements() ; )
         if (((PoetryEntry)pairs.get(ob = e.nextElement())).count < 2)
            pairs.remove(ob);
      for (Enumeration e = trpls.keys() ; e.hasMoreElements() ; )
         if (((PoetryEntry)trpls.get(ob = e.nextElement())).count < 2)
            trpls.remove(ob);
         
      add(new WordsDrift(this, words));
      add(new PairsDrift(this, pairs));
      add(new TrplsDrift(this, trpls));
   }

// USE A FIXED WIDTH FONT FOR LETTERING ON THE TILES

   Font font = new Font("Courier", Font.BOLD, 15);

// COLOR FOR ACTIVE TILE

   Color hiliteColor = new Color(1f,.8f,.7f);
   Color bgColor     = new Color(1f,1f,1f);
   Color activeColor = new Color(1f,.5f,.5f);
   Color stuckColor  = new Color(1f,1f,.5f);
   Color tileColor   = new Color(.95f,.95f,.95f);
   Color edgeColor   = new Color(.8f,.8f,.8f);
   double time = 0, t = 0, T = 0;

   public void render(Graphics g) {
      time += .03;
      t = Math.sin(time);
      T = t > 0 ? 1 : -1;
      w = bounds().width;
      h = bounds().height;
      g.setColor(bgColor);
      g.fillRect(0, 0, w, h);
      if (words == null)
         parseWords(in);
      g.setFont(font);
      for (int i = 0 ; i < tiles.size() ; i++) {
         PoetryEntry T = (PoetryEntry)tiles.elementAt(i);
         g.setColor(T.isActive ? activeColor : T.isStuck ? stuckColor : tileColor);
         g.fillRect(T.ix(), T.iy(), T.w, T.h);
         g.setColor(Color.black);
         g.drawString(T.s, T.ix()+3, T.iy()+12);
         g.fillRect(T.ix(), T.iy()+T.h, T.w, 1);
         g.fillRect(T.ix()+T.w, T.iy(), 1, T.h+1);
         g.setColor(edgeColor);
         g.fillRect(T.ix(), T.iy(), T.w, 1);
         g.fillRect(T.ix(), T.iy(), 1, T.h+1);
      }
      g.setColor(Color.pink);
      g.fill3DRect(0,0,10,10,!isT);
   }

   TileClass getTileClass(String word) {
      return (TileClass)tileMap.get(word);
   }
}

class TileClass extends Vector
{
   Random R = new Random();

   PoetryEntry choose() {
      PoetryEntry T = null;
      int n;
      for (n = 0 ; n < 10 && (T == null || T.isActive || T.isStuck) ; n++)
         T = (PoetryEntry)elementAt(Math.abs(R.nextInt()) % size());
      return (n < 10) ? T : null;
   }
}

class PoetryEntry
{
   Poetry parent;
   int count = 0, w, h;
   double x, y, dx = 0, dy = 0;
   boolean isActive = false, isStuck = false;
   int isFirst = 0, isLast = 0;
   String s;

   PoetryEntry(Poetry parent) { this.parent = parent; }

   void move(double dx, double dy) {
      if (!isActive && !isStuck) {

	 // MOVE THE TILE WITH A LOW PASS (SMOOTHING) FILTER

         this.dx = (99*this.dx + dx)/100;
         this.dy = (99*this.dy + dy)/100;
         x += this.dx;
         y += this.dy;

	 // IF TILE GOES OFF EDGE OF SCREEN, REPLACE IT AT SCREEN CENTER

         double dX = x+w/2-parent.w/2;
         double dY = y+h/2-parent.h/2;
         if (Math.abs(dX) > parent.w/2 + w/2 || Math.abs(dY) > parent.h/2 + h/2) {
            x = parent.w/2 - w/2;
            y = parent.h/2 - h/2;
         }
      }
   }
   double round(double t) { return t < 0 ? Math.min(-.3,t) : Math.max(.3,t); }

   int ix() { return (int) x; }
   int iy() { return (int) y; }

   double l() { return x; }
   double r() { return x + w; }
   double t() { return y; }
   double b() { return y + h; }

   void attract(PoetryEntry P, int n) {
      double dx = P.l() - r();
      double dy = P.y   - y;
      n = Math.min(n, 5);
      double alpha = Math.min(1, n * n * n / (dx*dx + dy*dy + 1));
      if (P.w < 100)
	alpha *= P.w / 100.;
      move  ( alpha*dx/2,  alpha*dy/2);
      P.move(-alpha*dx/2, -alpha*dy/2);
   }
}

class TrplsDrift extends PoetryDrift
{
   Random R = new Random();

   TrplsDrift(Poetry parent, Hashtable patterns) {
      super(parent, patterns);
   }

   void move(String trpl) {
      int sep1 = trpl.indexOf(" ");
      String word1 = trpl.substring(0,sep1);
      String word2 = trpl.substring(sep1+1);
      int sep2 = word2.indexOf(" ");
      String word3 = word2.substring(sep2+1);
      word2 = word2.substring(0,sep2);

      TileClass V1 = parent.getTileClass(word1);
      TileClass V2 = parent.getTileClass(word2);
      TileClass V3 = parent.getTileClass(word3);

      if (V1 != null && V2 != null && V3 != null) {
         PoetryEntry T1 = V1.choose();
         PoetryEntry T2 = V2.choose();
         PoetryEntry T3 = V3.choose();
         if (T1 != null && T2 != null && T3 != null) {
            PoetryEntry T = (PoetryEntry)patterns.get(trpl);
            T1.attract(T2, T.count);
            T2.attract(T3, T.count);
            T3.attract(T1, T.count);
         }
      }
   }
}

class PairsDrift extends PoetryDrift
{
   Random R = new Random();

   PairsDrift(Poetry parent, Hashtable patterns) {
      super(parent, patterns);
   }

   void move(String pair) {
      int sep = pair.indexOf(" ");
      String word1 = pair.substring(0,sep);
      String word2 = pair.substring(sep+1);

      TileClass V1 = parent.getTileClass(word1);
      TileClass V2 = parent.getTileClass(word2);

      if (V1 != null && V2 != null) {
         PoetryEntry T1 = V1.choose();
         PoetryEntry T2 = V2.choose();
         if (T1 != null && T2 != null) {
            PoetryEntry P = (PoetryEntry)patterns.get(pair);
            T1.attract(T2, P.count);
            T2.attract(T1, P.count);
         }
      }
   }
}

class WordsDrift extends PoetryDrift
{
   Random R = new Random();

   WordsDrift(Poetry parent, Hashtable patterns) {
      super(parent, patterns);
   }

   void repel(PoetryEntry W, PoetryEntry V) {        
      if (W.l()-2 < V.r()+2 && V.l()-2 < W.r()+2 &&
          W.t()-6 < V.b()+6 && V.t()-6 < W.b()+6) {
         double ox = Math.min(V.r()-W.l()+ 4,W.r()-V.l()+ 4) / (W.w+V.w+4)*2;
         double oy = Math.min(V.b()-W.t()+12,W.b()-V.t()+12) / (V.h+12);
         double dx = 3*(W.x - V.x) * ox * ox * oy * oy;
         double dy = 3*(W.y - V.y) * ox * ox * oy * oy;
	 if (W.w < 50) {
	    dx *= W.w / 50.;
	    dy *= W.w / 50.;
	 }
         W.move(dx,dy);
         V.move(-dx,-dy);
      }
   }

   void snapTo(PoetryEntry W, PoetryEntry V) {
      double Wx = W.r(), Wy = W.t(), Vx = V.l(), Vy = V.t();
      double dx = Vx - Wx;
      double dy = Vy - Wy;
      double rr = dx*dx + dy*dy;
      if (rr < 7) {
         PoetryEntry P = (PoetryEntry)parent.pairs.get(W.s + " " + V.s);
         if (P != null) {
            dx *= Math.min(2,P.count);
            dy *= Math.min(2,P.count);
            W.move(dx/2,dy/2);
            V.move(-dx/2,-dy/2);
         }
      }
   }

   void swirl(PoetryEntry W) {
      double dx = (W.l() + W.r())/2 - w/2;
      double dy = (W.t() + W.b())/2 - h/2;
      double r = 10000 * Math.sqrt(dx * dx + dy * dy)/h + 5;
      W.move(-4 * dx / r, -4 * dy / r);
      int mx = parent.isM ? parent.mx : w/2;
      int my = parent.isM ? parent.my : h/2;
      dx = (W.l() + W.r())/2 - mx;
      dy = (W.t() + W.b())/2 - my;
      r = 1000 * Math.sqrt(dx * dx + dy * dy)/h;
      if (parent.isM)
         r /= 10;
      W.move(-(dx/50 + dy/2)/r, -(dy/50 - dx/2)/r);
      if (parent.isT)
         W.move(10*dx/r, 10*dy/r);
   }

   void move(String word) {
      TileClass V = parent.getTileClass(word);
      PoetryEntry T1, T2;
      if (V != null) {
         if ((T1 = V.choose()) != null) {
            swirl(T1);
            for (int j = 0 ; j < parent.tiles.size() ; j++) {
               T2 = (PoetryEntry)parent.tiles.elementAt(j);
               repel(T1, T2);
               if (! word.equals(T2.s))
                  snapTo(T1, T2);
               else if (T1 != T2) {
                  double dx = T1.x-T2.x, dy = T1.y-T2.y, rr = dx*dx + dy*dy;
		  dx = 200 * dx / rr;
		  dy = 200 * dy / rr;
                  T1.move(dx, dy);
               }
            }
         }
      }
   }
}

abstract class PoetryDrift extends Thread
{
   Hashtable patterns;
   Poetry parent;
   int w, h;

   PoetryDrift(Poetry parent, Hashtable patterns) {
      this.parent = parent;
      this.patterns = patterns;
   }

   abstract void move(String pattern);

   void moveBegin() { }
   void moveEnd() { }

   public void run() {
      try {
         while (true) {
            w = parent.bounds().width;
            h = parent.bounds().height;
            moveBegin();
            for (Enumeration e = patterns.keys() ; e.hasMoreElements() ; )
               move((String)e.nextElement());
            moveEnd();
            sleep(30);
         }
      }
      catch(InterruptedException e){};
   }
}