/*
  File: SimApplet.java
  Basic classes for simulation applets,
  such as Canvas, Layout, Applet, Thread, Frame, etc.

  Part of the www.MyPhysicsLab.com physics simulation applet.
  Copyright (c) 2001  Erik Neumann

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

  Contact Erik Neumann at erikn@MyPhysicsLab.com or
  610 N. 65th St. Seattle WA 98103

*/


import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.net.URL;
import java.util.Enumeration;
import java.io.*;
import java.util.Iterator;

/////////////////////////////////////////////////////////////////////////////
// CoordMap class
/* Provides the coordinate mapping between screen and simulation world.
  Calculates the scaling and origin coordinates.

  QUICK GUIDE:
  new CoordMap(int y_dir, double x1, double x2, double y1, double y2,
    int align_x, int align_y)
  x1,x2,y1,y2 give the simulation coords of simulation area's left, right,
  top, bottom. This is different from the screen coords!  We assure that the
  rect x1,x2,y1,y2 fits in the screen area, but there is usually some excess
  area.

  y_dir is either CoordMap.INCREASE_UP or CoordMap.INCREASE_DOWN
  (specifies whether the y coordinate increases going up the screen or down).
  Note that always y1 < y2, so
    INCREASE_DOWN:  y1 = top,    y2 = bottom
    INCREASE_UP:    y1 = bottom, y2 = top

  There are ways to find the actual screen boundaries in simulation coords,
  but it is better to stick to using the simulation area -- because the screen
  area can change due to resizing of the window.  After a resize, we are
  guaranteed to have the same simulation area showing in the window.

  If you want to use the entire screen area, try using the expand() method to
  grow the simulation area.  Then you can use the whole screen area, but if
  there is any later resizing of the window your simulation area won't change.

  align_x and align_y determine how the map is shifted when the screen area is
  too wide or too tall. That is, x1,x2,y1,y2 define a rectangle with a certain
  aspect ratio. The screen will usually have a different aspect ratio, so we
  have to fit the x1,x2,y1,y2 rectangle into the screen. The question is
  whether the x1,x2,y1,y2 rectangle is aligned to the left, right or middle.
  This is specified for horizontal and vertical separately. The extra area is
  still available to the app (but see comments above) its just a question of
  where the origin is placed.

  If setFillScreen(true) then the aspect ratio is not maintained, and the
  given x1,x2,y1,y2 will match the screen boundaries regardless of aspect
  ratio.

  TO DO:
  Add some exceptions for divide by zero errors in recalc().  Eg. width=0 is a
  problem.
  Protect the class by hiding all the variables and only allow method access.

*/

class CoordMap
{
  // class constants for origin location
  public static final int ALIGN_MIDDLE = 0;
  public static final int ALIGN_LEFT = 1;
  public static final int ALIGN_RIGHT = 2;
  public static final int ALIGN_UPPER = 3;
  public static final int ALIGN_LOWER = 4;
  public static final int INCREASE_UP = -1;
  public static final int INCREASE_DOWN = 1;

  private int y_direction = INCREASE_DOWN;
  private int origin_x = 0;  /* CALCULATED origin in screen coords */
  private int origin_y = 0;  /* CALCULATED origin in screen coords */
  public int screen_left = 0;  /* in screen coords (pixels) */
  public int screen_top = 0;  /* in screen coords (pixels) */
  public int screen_width = 0;  /* in screen coords (pixels) */
  public int screen_height = 0;  /* in screen coords (pixels) */
  // "box" variables give simulation area in screen coords
  private int boxLeft,boxRight,boxTop,boxBottom,boxWidth,boxHeight;

  private double pixel_per_unit_x = 100;  /* CALCULATED */
  private double pixel_per_unit_y = 100;  /* CALCULATED */
  private boolean fill_screen = false;
  public double sim_x1;  /* in simulation coords, left boundary of simulation area */
  public double sim_x2;  /* in simulation coords, right boundary of simulation area */
  public double sim_y1;  /* simulation area top (INCREASE_DOWN) or bottom (INCRESE_UP) */
  public double sim_y2;  /* simulation area bottom (INCRESE_UP) or top (INCREASE_DOWN) */
  public double sim_width = 0;  /* = sim_x2 - sim_x1 */
  public double sim_height = 0;  /* = sim_y2 - sim_y1 */
  // sim coords corresponding to left,right,top,bottom of screen  DO NOT USE!!!
  /* note: It is better to use the 'box' methods than sim_left, sim_top, etc.
     because when the window is resized, the screen area changes, but the 'box' stays
     the same. */
  public double sim_left, sim_top, sim_right, sim_bottom;  /* CALCULATED -- DO NOT USE */
  private int align_x = ALIGN_LEFT;
  private int align_y = ALIGN_UPPER;

  public CoordMap(int y_dir, double x1, double x2, double y1, double y2,
                  int align_x, int align_y) {
    y_direction = y_dir;
    this.align_x = align_x;
    this.align_y = align_y;
    setRange(x1, x2, y1, y2);
  }


  public void pvars(String s)  {
    System.out.println("------- map vars: "+s+" -------");
    System.out.println("sim x1="+sim_x1+",x2="+sim_x2+",y1="+sim_y1+",y2="+sim_y2);
    System.out.println("screen left="+screen_left+",top="+screen_top+",width="+screen_width+",height="+screen_height);
    System.out.println("origin x="+ origin_x+",y="+origin_y);
    System.out.println("sim left="+sim_left+",top="+sim_top+",right="+sim_right+",bottom="+sim_bottom+")");
    System.out.println("simToScreen left="+simToScreenX(sim_left)+
      ", top="+simToScreenY(sim_top)+
      ", right="+simToScreenX(sim_right)+
      ", bottom="+ simToScreenY(sim_bottom));
    System.out.println("pixel_per_unit_x="+pixel_per_unit_x+" sim width="+sim_width+", sim_height="+sim_height+")");
    System.out.println("align x="+align_x+", y="+align_y+")");
  }

  /*
  input:  must have following set:
    screen_width, screen_height, screen_left, screen_top,
    sim_width, sim_height, sim_x1, sim_y1
    y_direction, fill_screen

    additionally, if (fill_screen==false) must have:
    align_x, align_y
  */
  private void recalc() {
    if (fill_screen) {
      /* fill the screen according to chosen sim values
         calculate resulting pixel_per_unit for x & y
         calculate resulting screen coord origin location
         offsets should be zero
      */
      pixel_per_unit_x = (double)screen_width/sim_width;
      pixel_per_unit_y = (double)screen_height/sim_height;
      origin_x = screen_left - (int)(sim_x1 * pixel_per_unit_x +0.4999);
      if (y_direction == INCREASE_DOWN)
        origin_y = screen_top
              - (int)(sim_y1 * pixel_per_unit_y +0.4999);
      else
        origin_y = screen_top + screen_height
              + (int)(sim_y1 * pixel_per_unit_y +0.4999);
    } else {
      if ((sim_width == 0) || (sim_height == 0))
        return;
      if ((screen_width <= 0) || (screen_height <= 0))
        return;
      int ideal_height = (int)((double)screen_width*sim_height/sim_width);
      int ideal_width;
      int offset_x, offset_y;
      /* how to figure out location of origin:
        Imagine the rectangle (within screen) that holds the simulation, so it has width = sim_width
        and height = sim_height.  Now impose a temporary coord system over this with upperleft being
        0,0, and lowerright being 1,1. The origin location is specified in these temporary coords.
        We shift the entire rectangle according to requested alignment.  And then finally we can
        determine the screen coords of the origin.
      */
      if (screen_height < ideal_height)  {// height is limiting factor
        pixel_per_unit_y = pixel_per_unit_x = (double)screen_height/sim_height;
        offset_y = 0;
        ideal_width = (int)(sim_width*pixel_per_unit_x);
        switch (align_x) {
          case ALIGN_LEFT:  offset_x = 0; break;
          case ALIGN_RIGHT: offset_x = screen_width - ideal_width; break;
          case ALIGN_MIDDLE: offset_x = (screen_width - ideal_width)/2; break;
          default: offset_x = 0; break;
        }
      } else  {// width is limiting factor
        pixel_per_unit_y = pixel_per_unit_x = (double)screen_width/sim_width;
        offset_x = 0;
        ideal_height = (int)(sim_height*pixel_per_unit_y);
        switch (align_y) {
          case ALIGN_UPPER:  offset_y = 0; break;
          case ALIGN_MIDDLE: offset_y = (screen_height - ideal_height)/2; break;
          case ALIGN_LOWER:  offset_y = screen_height - ideal_height; break;
          default: offset_y = 0; break;
        }
      }

      origin_x = screen_left + offset_x - (int)(sim_x1*pixel_per_unit_x);
      if (y_direction == INCREASE_DOWN)
        origin_y = screen_top + offset_y - (int)(sim_y1*pixel_per_unit_y);
      else
        origin_y = screen_top + screen_height - offset_y + (int)(sim_y1*pixel_per_unit_y);

    }
    sim_left = screenToSimX(screen_left);
    sim_right = screenToSimX(screen_left + screen_width);
    sim_top = screenToSimY(screen_top);
    sim_bottom = screenToSimY(screen_top + screen_height);
    boxLeft = simToScreenX(sim_x1);
    boxRight = simToScreenX(sim_x2);
    boxTop = simToScreenY((y_direction == INCREASE_UP) ? sim_y2 : sim_y1);
    boxBottom = simToScreenY((y_direction == INCREASE_UP) ? sim_y1 : sim_y2);
    boxWidth = boxRight - boxLeft;
    boxHeight = boxBottom - boxTop;
  }

  //  If setFillScreen(true) then the aspect ratio is not maintained, and the given
  //  sim boundaries will match the screen boundaries regardless of aspect ratio.
  public void setFillScreen(boolean f)  {
    fill_screen = f;
    recalc();
  }

  // Expand the simulation boundaries to take up the entire screen
  /* Why is this useful?  When starting up, we ask for a minimum simulation boundary
     area, for example a square boundary of (-5,-5,5,5).  The recalc() procedure then figures out how
     to fit that simulation area into the current screen, which usually is rectangular with
     the width not equal to the height.  Suppose that the width is greater than the height.
     Then expand() changes the simulation area to match the maximum available
     screen area.  In our example, it might change to (-6.3, -5, 6.3, 5) where the horizontal
     boundaries got extended.
  */
  public void expand() {
    sim_x1 = screenToSimX(screen_left);
    sim_x2 = screenToSimX(screen_left + screen_width);
    sim_width = sim_x2 - sim_x1;
    sim_y1 = screenToSimY(screen_top);
    sim_y2 = screenToSimY(screen_top + screen_height);
    sim_height = sim_y2 - sim_y1;
    if (y_direction == INCREASE_UP) { // swap y1 and y2 if increase up
      double d = sim_y1;
      sim_y1 = sim_y2;
      sim_y2 = d;
      sim_height = sim_y2 - sim_y1;
    }
    recalc();
  }

  public void setRange(double xlo, double xhi, double ylo, double yhi)  {
    //System.out.println("map.setRange("+xlo+","+xhi+","+ylo+","+yhi+")");
    sim_x1 = xlo;
    sim_x2 = xhi;
    sim_y1 = ylo;
    sim_y2 = yhi;
    sim_width = xhi - xlo;
    sim_height = yhi - ylo;
    recalc();
    //pvars("setRange");
  }

  public void setScreen(int left, int top, int width, int height)  {
    //System.out.println("map.setScreen "+left+" "+top+" "+width+" "+height);
    if ((width>0) && (height>0)) {
      screen_top = top;
      screen_left = left;
      screen_width = width;
      screen_height = height;
      recalc();
      //pvars("setScreen");
    }
  }

  public int simToScreenScaleX(double x)  {
    /* does only scaling in x direction, no offsetting */
    return (int)(x*pixel_per_unit_x+0.5);
  }

  public int simToScreenX(double x)  {
    /* Returns the screen coords of x, given the various globals. */
    return origin_x + (int)(x*pixel_per_unit_x+0.5);
  }

  public int simToScreenY(double y)  {
    /* Returns the screen coords of y, given the various globals. */
    return origin_y + y_direction*(int)(y*pixel_per_unit_y+0.5);
  }

  public double screenToSimX(int scr_x)  {
    /* scr_x is in screen coords, returns simulation coords */
    return (double)(scr_x - origin_x)/pixel_per_unit_x;
  }

  public double screenToSimY(int scr_y) {
    /* scr_y is in screen coords, returns simulation coords */
    return y_direction*(double)(scr_y - origin_y)/pixel_per_unit_y;
  }

  /*  'Box' is the simulation area defined by sim_x1, sim_x2, sim_y1, sim_y2.
      The following methods return the screen coords of the box.
  */
  public int leftBox() {
    return boxLeft;
  }

  public int rightBox() {
    return boxRight;
  }

  public int topBox() {
    return boxTop;
  }

  public int bottomBox() {
    return boxBottom;
  }

  public int widthBox() {
    return boxWidth;
  }

  public int heightBox() {
    return boxHeight;
  }

  public boolean intersectRect(Rectangle r) {
    int x1, y1, x2, y2;
    x1 = r.x;
    y1 = r.y;
    x2 = x1 + r.width;
    y2 = y1 + r.height;
    int sx1 = screen_left;
    int sy1 = screen_top;
    int sx2 = screen_left + screen_width;
    int sy2 = screen_top + screen_height;
    if (sx1 >= x2)
      return false;
    if (x1 >= sx2)
      return false;
    if (sy1 >= y2)
      return false;
    if (y1 >= sy2)
      return false;
    //System.out.println(x1+" "+y1+" "+x2+" "+y2+" "+sx1+" "+sy1+" "+sx2+" "+sy2);
    return true;
  }
}

/////////////////////////////////////////////////////////////////////////
class SimCanvas extends Canvas implements MouseListener, MouseMotionListener {
  private Image offScreen;
  public SimApplet app;
  private boolean firstTime = true;

  public SimCanvas(SimApplet app) {
    this.app = app;
    addMouseListener(this);
    addMouseMotionListener(this);
    addFocusListener(new FocusListener() {
      public void focusGained(FocusEvent e) {
        //repaint();
      }
      public void focusLost(FocusEvent e) {
        //repaint();
      }
    });
    addKeyListener((KeyListener)app);
  }

  /* null out the offScreen buffer during invalidation, eg. resize */
  public synchronized void invalidate() {
    super.invalidate();
    offScreen = null;
  }

  /* override update to *not* erase the background before painting */
  public void update(Graphics g) {
    paint(g);
  }

  /* setSize needs to be synchronized with paint to ensure it doesn't change
    the offscreen buffer during the paint operation */
  public synchronized void setSize(int width, int height) {
    super.setSize(width, height);
    Point loc = getLocation();
    app.map.setScreen(loc.x, loc.y, width, height);
    offScreen = null;
    // Expand the simulation to take up the entire screen, but only first time through here
    // Cludgey, but no other good place for this.
    if (firstTime) {
      firstTime = false;
      app.map.expand();
    }
  }

  public void setLocation(int x, int y) {
    super.setLocation(x, y);
    Dimension sz = getSize();
    app.map.setScreen(x, y, sz.width, sz.height);
    offScreen = null;
  }

  /* sychronized to prevent anyone changing the offscreen buffer */
  public synchronized void paint (Graphics g) {
    // createImage doesn't work during "init()"... probably because
    // applet is zero width & height during init.
    ////System.out.println("----- paint -------------");
    if (offScreen == null) {
      //System.out.println("createImage");
      offScreen = createImage(getSize().width, getSize().height);
    }

    if (offScreen != null) {
      //System.out.println("image w="+offScreen.getWidth(null)+" h="+
      //  offScreen.getHeight(null));
      Graphics og = offScreen.getGraphics();
      Dimension size = getSize();
      og.setClip(0,0,size.width, size.height);
      // clear offScreen to white
      og.setColor(Color.white);
      og.fillRect(0,0,size.width, size.height);

      /*
      Component focus = getFrame().getFocusOwner();
      // draw indicator that we have focus (not really needed)
      if ((focus == this) || (focus == app)) {
        og.setColor(Color.lightGray);
        og.drawRect(1,1,size.width-3, size.height-3);
      }
      */
      app.drawObjects(og);
      //og.setColor(Color.black);
      //og.drawRect(0,0,size.width-1, size.height-1);
      /*  //test code
      og.setColor(Color.lightGray);
      og.draw3DRect(1,1,size.width-3,size.height-3,true);

      og.setColor(Color.blue);
      og.drawLine(0, 0, size.width-1, size.height-1);
      og.drawLine(0, size.height-1, size.width-1, 0);

      og.setColor(Color.red);
      p = (p>100) ? 0 : p+1;
      og.fillRect(p+100, 100, 100, 100);
      */

      //if (offScreen == null)
      //  System.out.println("offscreen is null!!!!");
      g.drawImage(offScreen, 0, 0, null);
      og.dispose();
    }
    ////System.out.println("      end paint");
  }

  public Frame getFrame() {
    Component c = this;
    while ((c = c.getParent()) != null) {
      if (c instanceof Frame)
        return (Frame)c;
    }
    return null;
  }

  public Dimension getPreferredSize() {
    return new Dimension(200, 200);
  }

  public void mousePressed(MouseEvent evt) {
    //System.out.println("canvas mousePressed "+evt.getX() + " "+evt.getY());
    requestFocus();
  }

  public void mouseReleased(MouseEvent evt) {
  }

  public void mouseClicked(MouseEvent evt) {
    //System.out.println("canvas mouseClicked");
  }

  public void mouseEntered(MouseEvent evt) {
  }

  public void mouseExited(MouseEvent evt) {
  }

  public void mouseDragged(MouseEvent evt) {
  }

  public void mouseMoved(MouseEvent evt) {
  }

  /* NOTE: we need the Java 1.1 version also!!!  Otherwise, this component
     will not be focusable under Java 1.1
     So ignore any warning that this method has been deprecated and leave it here. */
  public boolean isFocusTraversable()   { // Java 1.1 version
    return true;
  }
  public boolean isFocusable()  { // Java 1.4 version
    return true;
  }


}


///////////////////////////////////////////////////////////////////////////
class SimLayout1 implements LayoutManager {
    public SimLayout1() {}
    public void addLayoutComponent(String name, Component c) {}
    public void removeLayoutComponent(Component c) {}
    public Dimension preferredLayoutSize(Container target) {
        return new Dimension(500, 500);
    }
    public Dimension minimumLayoutSize(Container target) {
        return new Dimension(100,100);
    }
    public void layoutContainer(Container target) {
        int cw = target.getSize().width * 3/4;
        target.getComponent(0).setLocation(0, 0);
        target.getComponent(0).setSize(cw, target.getSize().height);
        int i;
        int h = 0;
        for (i = 1; i < target.getComponentCount(); i++) {
            Component m = target.getComponent(i);
            if (m.isVisible()) {
            Dimension d = m.getPreferredSize();
            if (m instanceof Scrollbar)
                d.width = target.getSize().width - cw;
            int c = 0;
            if (m instanceof Label) {
                h += d.height/3;
                c = (target.getSize().width-cw-d.width)/2;
            }
            m.setLocation(cw+c, h);
            m.setSize(d.width, d.height);
            h += d.height;
            }
        }
    }
};

//////////////////////////////////////////////////////////////////////////
abstract class SimApplet extends Applet {
  public CoordMap map;
  protected SimTimerThread timer = null;
  protected long delay = 33;

  public abstract void threadUpdate();

  public abstract void drawObjects(Graphics g);

  // called when user returns to browser page containing applet
  public void start() {
    if (timer == null) {
      timer = new SimTimerThread(this);
      timer.setDelay(this.delay);
      timer.start();
    }
  }

  // called when user leaves browser page containing applet
  public void stop() {
    if (timer != null) {
      timer.interrupt();
      timer = null;  // destroys the thread
    }
  }

}


/////////////////////////////////////////////////////////////////////////////
class SimTimerThread extends Thread {
private SimApplet myApplet;
private long delay = 33;
private boolean suspendRequested = false;

  SimTimerThread(SimApplet myApp) {
    super("SimTimerThread Thread");
    myApplet = myApp;
  }

  public void setDelay(long delay) {
    this.delay = delay;
  }

  public void run() {
    try {
      while (!interrupted()) { // loop until interrupted
        checkSuspended();
        myApplet.threadUpdate();
        sleep(delay);  // milliseconds
      }
    }
    catch(InterruptedException e) {
      System.out.println("SimTimerThread thread interrupted.");
    }
  }
  public void requestSuspend()
  {
    suspendRequested = true;
  }

  private synchronized void checkSuspended()
    throws InterruptedException
  {
    while (suspendRequested)
      wait();
  }

  public synchronized void requestResume()
  {
    suspendRequested = false;
    notify();
  }
}



/////////////////////////////////////////////////////////////////////////////
class SimWindowAdapter extends WindowAdapter {
  Applet app = null;
  public SimWindowAdapter(Applet app) {
    this.app = app;
  }

  public void windowIconified(WindowEvent e) {
    System.out.println("windowIconified()");
    app.stop();
  }

  public void windowDeiconified(WindowEvent e) {
    System.out.println("windowDeiconified()");
    app.start();
  }

  public void windowDeactivated(WindowEvent e) {
    System.out.println("windowDeactivated()");
    //app.stop();  // disable this line when using jdb
  }

  public void windowActivated(WindowEvent e) {
    System.out.println("windowActivated()");
    app.start();
  }

  public void windowClosing(WindowEvent e) {
    app.stop();
    System.exit(0);
  }
}


/////////////////////////////////////////////////////////////////////////////
class SimFrame extends Frame implements AppletStub, AppletContext {
  public SimFrame(Applet applet)  {
    setTitle("MyPhysicsLab Simulation");
    Toolkit tk = Toolkit.getDefaultToolkit();
    Dimension d = tk.getScreenSize();
    setSize(500, 550);
    setLocation(d.width/10, d.height/10);
    applet.setStub(this);
    addWindowListener(new SimWindowAdapter(applet));
    add(applet);
    applet.init();
    applet.start();
    //addComponentListener(applet);  // applet listens for resize events
   }


   // AppletStub methods
   public boolean isActive() { return true; }
   public URL getDocumentBase() { return null; }
   public URL getCodeBase() { return null; }
   public String getParameter(String name) { return ""; }
   public AppletContext getAppletContext() { return this; }
   public void appletResize(int width, int height) {}

   // AppletContext methods
   public AudioClip getAudioClip(URL url) { return null; }
   public Image getImage(URL url) { return null; }
   public Applet getApplet(String name) { return null; }
   public Enumeration getApplets() { return null; }
   public void showDocument(URL url) {}
   public void showDocument(URL url, String target) {}
   public void showStatus(String status) {}
  // setStream, getStream, getStreamKeys are for Java 1.4 compatibility
  // comment out these methods to compile with Java 1.3
  public void setStream(String key, InputStream stream) throws IOException {}
  public InputStream getStream(String key) { return null; }
  public Iterator getStreamKeys() { return null; }

}
