import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
import javax.swing.event.*;
import javax.swing.*;
import java.util.*;

public class ReflectionLattice extends JPanel implements MouseMotionListener, ChangeListener {
  
  GeneralPath path;
  double scale = 75;

  double r = 1.0/3;
  double len = 10;
  double theta = Math.random();
  
  Vector shapes = new Vector();
  
  JSlider radiusSlider;
  JSlider lenSlider;
  JSlider scaleSlider;
  
  public ReflectionLattice() {
    //setOpaque(true);
    addMouseMotionListener(this);
  }
  
  
  public void paintComponent(Graphics g1) {
    Graphics2D g = (Graphics2D)g1;
    
    g.setColor(getBackground());
    g.fillRect(0, 0, getWidth(), getHeight());
    
    int xcount = (int)(1+getWidth()/scale/2);
    int ycount = (int)(1+getHeight()/scale/2);
    g.translate(getWidth()/2, getHeight()/2);
    g.scale(scale, scale);
    
    g.setColor(Color.black);
    g.setStroke(new BasicStroke(1/(float)scale));
    for(int i=-xcount; i<=xcount; i++) {
      for(int j=-ycount; j<=ycount; j++) {
        g.draw(new Ellipse2D.Double(i-r, j-r, 2*r, 2*r));
      }
    }
    
    g.setColor(Color.red);
    //g.setColor(new Color(1, 0, 0, .5f));
    if (path != null) g.draw(path);
    
    g.setColor(Color.blue);
    for(int i=0; i<shapes.size(); i++) {
      if (shapes.elementAt(i) instanceof Shape) {
        g.draw((Shape)shapes.elementAt(i));
      }
      else if (shapes.elementAt(i) instanceof Point2D) {
        Point2D p = (Point2D)shapes.elementAt(i);
        g.draw(new Ellipse2D.Double(p.getX()-.05, p.getY()-.05,.1, .1));
      }
    }

    
  }
  
  public void setPath(GeneralPath path) {
    this.path = path;
  }
  
  public void setPath(double theta, double len) {
    setPath(calculatePath(theta, len));
  }
  
  public void setPath() {
    setPath(theta, len);
  }
  
  public GeneralPath calculatePath(double theta, double len) {
    shapes.clear();
    GeneralPath path = new GeneralPath();
    Point2D start = new Point2D.Double(0, 0); //new Point2D.Double(.49999, .1);
    path.moveTo((float)start.getX(), (float)start.getY());
    while(len > 0) {
      Point nextSphere = nextSphere(start, theta);
      if (nextSphere != null) {
        Point2D p = intersection(start, theta, nextSphere);
        
        if (len < p.distance(start)) {
          path.lineTo((float)(start.getX()+Math.cos(theta)*len), (float)(start.getY()+Math.sin(theta)*len));
        }
        else {
          path.lineTo((float)p.getX(), (float)p.getY());
        }        
        //path.lineTo(nextSphere.x, nextSphere.y);
        len -= p.distance(start);
        if (p.distance(start) < 1e-10) break; 

        double phi = Math.atan2(p.getY()-nextSphere.y, p.getX()-nextSphere.x);
        theta = Math.PI + 2*phi - theta;
        while(theta < -Math.PI) theta += 2*Math.PI;
        while(theta >  Math.PI) theta -= 2*Math.PI;
        start = p;
        
      }
      else {
        path.lineTo((float)(start.getX()+Math.cos(theta)*len), (float)(start.getY()+Math.sin(theta)*len));
        len = 0;
      }
    }
    return path;
  }
  
  
  public Point nextSphere(Point2D start, double theta) {
    int startx = (int)Math.round(start.getX());
    int starty = (int)Math.round(start.getY());
    if (theta==Math.PI/2) return new Point(startx, starty+1);
    else if (theta==-Math.PI/2) return new Point(startx, starty-1);
    else if (theta==0) return new Point(startx+1, starty);
    else if (theta==Math.PI || theta==-Math.PI) return new Point(startx-1, starty);
    else if (Math.PI/4 < Math.abs(theta) && Math.abs(theta) < 3*Math.PI/4) {
      Point p = nextSphere(new Point2D.Double(start.getY(), start.getX()), Math.PI/2-theta);
      if (p==null) return null;
      return new Point(p.y, p.x);
    }
    else {
      double dy = Math.tan(theta);
      int dx = Math.abs(theta) < Math.PI/2 ? 1 : -1;
      dy *= dx;
      //System.out.println();
      for(int k=0; k<150; k++) {
        
        int i = startx + k*dx;
        int j = starty + (int)(start.getY()+dx*(i-start.getX())*dy - starty);
        if (k != 0) {
          double ddx = dx*(start.getX()-i);
          double ddy = dy*(start.getY()-j);
          double a = dx*dx + dy*dy;
          double b = 2*( ddx + ddy );
          //double c = ddx*ddx + ddy*ddy;
          double t = -.5*b/a;
          double d2 = (start.getX()+dx*t - i)*(start.getX()+dx*t - i) + (start.getY()+dy*t - j)*(start.getY()+dy*t - j);
          //shapes.add(new Point2D.Double(start.getX()+dx*t, start.getY()+dy*t));
          //System.out.println("i="+i+" j="+j+" t="+t+" dist="+d2+" < "+r*r);
          if (d2 < r*r) {
            return new Point(i, j);
          }
        }
        
        {
          
          j += (dy < 0) ? -1 : 1;
          double ddx = dx*(start.getX()-i);
          double ddy = dy*(start.getY()-j);
          double a = dx*dx + dy*dy;
          double b = 2*( ddx + ddy );
          //double c = ddx*ddx + ddy*ddy;
          double t = -.5*b/a;
          double d2 = (start.getX()+dx*t - i)*(start.getX()+dx*t - i) + (start.getY()+dy*t - j)*(start.getY()+dy*t - j);
          //shapes.add(new Point2D.Double(start.getX()+dx*t, start.getY()+dy*t));
          //System.out.println("i="+i+" j="+j+" t="+t+" dist="+d2+" < "+r*r);
          if (d2 < r*r) {
            return new Point(i, j);
          }
        }
        
        
      }
      return null;
    }
  }
  
  public Point2D intersection(Point2D start, double theta, Point sphere) {
    int dx;
    double dy;
    if (theta==Math.PI/2) {
      dx = 0;
      dy = 1;
    }
    else if (theta==-Math.PI/2) {
      dx = 0;
      dy = -1;
    }
    else {
      dx = Math.abs(theta) < Math.PI/2 ? 1 : -1;
      dy = dx * Math.tan(theta);
    }
    double ddx = dx*(start.getX()-sphere.x);
    double ddy = dy*(start.getY()-sphere.y);
    double a = dx*dx + dy*dy;
    double b = 2*( ddx + ddy );
    double c = (start.getX()-sphere.x)*(start.getX()-sphere.x) + (start.getY()-sphere.y)*(start.getY()-sphere.y) - r*r;
    
    double t = (-b-Math.sqrt(b*b-4*a*c))/(2*a);
    //System.out.println("t="+t);
    shapes.add(new Point2D.Double(start.getX()+dx*t, start.getY()+dy*t));
    return new Point2D.Double(start.getX()+dx*t, start.getY()+dy*t);
    
  }
  
  public void mouseMoved(MouseEvent e) {  }
  
  public void mouseDragged(MouseEvent e) {
    theta = Math.atan2(e.getY()-getHeight()/2, e.getX()-getWidth()/2);
    setPath(theta, len);
    repaint();
  }
  
  

  public JSlider getRadiusSlider(int orientation) {
    if (radiusSlider==null) {
      radiusSlider = new JSlider(orientation, 0, 7*6*5*4*3*2, (int)(7*6*5*4*3*2*r/.5));
      radiusSlider.addChangeListener(this);
    }
    return radiusSlider;
  }
  
  public JSlider getLenSlider(int orientation) {
    if (lenSlider==null) {
      lenSlider = new JSlider(orientation, -200, 1000, (int)(1000*Math.log(len)/Math.log(1000)));
      lenSlider.addChangeListener(this);
    }
    return lenSlider;
  }
  
  public JSlider getScaleSlider(int orientation) {
    if (scaleSlider==null) {
      scaleSlider = new JSlider(orientation, 10, 500, (int)(scale));
      scaleSlider.addChangeListener(this);
    }
    return scaleSlider;
  }
  
  
  public void stateChanged(ChangeEvent e) {
    if (e.getSource() == radiusSlider) {
      r = .5*radiusSlider.getValue()/radiusSlider.getMaximum();
    }
    else if (e.getSource() == lenSlider) {
      len = Math.exp(Math.log(1000)*lenSlider.getValue()/lenSlider.getMaximum());
    }
    else if (e.getSource() == scaleSlider) {
      scale = scaleSlider.getValue();
    }
    setPath(theta, len);
    repaint();
  }
  
  public void start() {
    // called if an applet
    // addSliders(getContentPane());
    setPath();
  }
  
  public void addSliders(Container c) {
    c.setLayout(new BorderLayout());
    c.add(getLenSlider(JSlider.HORIZONTAL), "South");
    c.add(getRadiusSlider(JSlider.VERTICAL), "East");
    c.add(getScaleSlider(JSlider.VERTICAL), "West");
  }
  
  
  public static void main(String[] args) {
    JFrame f = new JFrame();
    ReflectionLattice r = new ReflectionLattice();
    r.addSliders(f.getContentPane());
    f.getContentPane().add(r);
    r.setPath(r.theta, r.len);
    f.setSize(500, 500);
    f.show();
  }
  
}