// Slider.java
// $Id: Slider.java,v 1.6 1997/07/28 12:50:54 bmahe Exp $
// Author: bmahe@sophia.inria.fr
// (c) COPYRIGHT MIT and INRIA, 1997.
// Please first read the full copyright statement in file COPYRIGHT.html

package w3c.tools.widgets;

import java.awt.*;
import java.awt.event.*;
import java.math.*;

/**
 * Slider : 
 * @author Benoit Mahe <bmahe@sophia.inria.fr>
 */

public class Slider extends Canvas {

  //inner classes

  class PointerClickListener extends MouseAdapter {

    Slider  slider  = null;
  
    public void mouseClicked(MouseEvent e) {
      Object target = e.getComponent();
      if (target == slider)
	slider.movePointerTo(e.getX(), e.getY());
    }

    public PointerClickListener(Slider s) {
      this.slider  = s;
    }

  }

  class PointerMotionListener implements MouseMotionListener {

    Slider  slider  = null;

    public void mouseDragged(MouseEvent e) {
      Object target = e.getComponent();
      if (target == slider)
	slider.movePointerTo(e.getX(),e.getY());
    }

    public void mouseMoved(MouseEvent e) {
    }

    PointerMotionListener(Slider s) {
      this.slider  = s;
    }

  }

  class Pointer {

    private int width       = 0;
    private int height      = 0;

    private int centerX = 0;

    Dimension size = null;
    
    int x = 0;
    int y = 0;

    public void fill3DPointer(Graphics g,
			      int x,
			      int y,
			      int width,
			      int height,
			      boolean raised)
    {
      int xPoints[] = new int[3];
      int yPoints[] = new int[3];
      int nPoints   = 3;

      Color c = g.getColor();
      Color brighter = c.brighter();
      Color darker = c.darker();

      if (!raised) g.setColor(darker);

      g.fillRect(x + 1, y + 1, width - 2, 2 * height / 3);
      xPoints[0] = x;
      yPoints[0] = y + 2 * height / 3;
      xPoints[1] = x + width / 2;
      yPoints[1] = y + height;
      xPoints[2] = x + width;
      yPoints[2] = y + 2 * height / 3;
      g.fillPolygon(xPoints,yPoints,nPoints);
      g.setColor(raised ? brighter : darker);
      g.drawLine(x, y, x, y + (2 * height / 3) -1);
      g.drawLine(x + 1, y, x + width - 2, y);
      g.drawLine(xPoints[0],yPoints[0],xPoints[1],yPoints[1]);
      g.setColor(raised ? darker : brighter);
      g.drawLine(x + width - 1, y, x + width - 1, y + (2*height/3) - 2);
      g.drawLine(xPoints[1],yPoints[1],xPoints[2] - 1,yPoints[2] - 1);
    }

    public void paint(Graphics g) {
      fill3DPointer(g,
		    getLocation().x,
		    getLocation().y,
		    width,
		    height,
		    true);
    }

    public void setSize (int width, int height) {
      this.width    = width;
      this.height   = height;
      size = new Dimension(width, height);
    }

    public int getCenterX() {
      return centerX;
    }

    public void setLocation(int x, int y) {
      this.x = x-(width/2);
      this.y = y-(height/2);
      this.centerX = x;
    }

    public void setLocation(Point p) {
      setLocation(p.x, p.y);
    }

    public Point getLocation() {
      return new Point(x,y);
    }

    public Point getNewLocation(int x, int y) {
       return new Point(getLocation().x + x,
			getLocation().y + y);
    }

    Pointer(int width, int height, Point location) {
      setSize(width,height);
      setLocation(location);
    }

  }

  class Graduation {
    public int x1;
    public int y1;
    public int x2;
    public int y2;

    public long value;
    
    public void draw(Graphics g, boolean display) {
      g.drawLine(x1,y1,x2,y2);
      if (display) {
	String svalue = String.valueOf(value);
	FontMetrics f = g.getFontMetrics();
	int height = f.getHeight();
	int width  = f.stringWidth(svalue);
	g.drawString(svalue,x1-width/2,y2+height);
      }
    }

    public void showValue(Graphics g) {
      String svalue = String.valueOf(value);
      FontMetrics f = g.getFontMetrics();
      int height = f.getHeight();
      int width  = f.stringWidth(svalue);
      g.drawString(svalue,x1-width/2,y2+height);
    }

    public int dx(int x) {
      int dx = x1-x;
      return ((dx < 0) ? -dx : dx);
    }

    public Graduation(int x1, int y1, int x2, int y2, long value) {
      this.x1    = x1;
      this.y1    = y1;
      this.x2    = x2;
      this.y2    = y2;
      this.value = value;
    }

  }

  //SLIDER itself

  int marginx       = 10;
  int marginy       = 5;
  int defaultWidth  = 180;
  int defaultHeight = 53;
  int width         = 0;
  int height        = 0;

  int minheight     = 53;
  int minwidth      = 150;

  int rect_margin_x = 20;
  int rect_margin_y = 10;

  int pointerWidth  = 12;
  int pointerHeight = 20;

  int pointerX = 0;
  int pointerY = 0;

  int guideHeight = 2;
  int graduationHeight = 5;

  long min  = 0;
  long max  = 0;
  long step = 0;

  int minpixelstep = 2;

  boolean border = false;

  Color color = Color.gray;

  Dimension size = null;

  Point pposition = null;

  Graduation graduations [] = null;

  Graduation currentGraduation = null;

  protected Pointer pointer = null;

  protected int getGoodX(int x) {
    if (x > (width - marginx - rect_margin_x)) {
      if (graduations[graduations.length - 1].x1 != pointer.getCenterX()) {
	currentGraduation = graduations[graduations.length - 1];
	return graduations[graduations.length - 1].x1;
      }
      else return -1;
    } else  if (x < (marginx + rect_margin_x)) {
      if (graduations[0].x1 != pointer.getCenterX()) {
	currentGraduation = graduations[0];
	return graduations[0].x1;      
      }
      else return -1;
    } else {
      // find the nearest graduation.
      int dx = -1;
      int mindx = getGraduationLength()+50;
      int i = 0;
      while (i < graduations.length) {
	dx = (graduations[i].dx(x));
	if (dx < mindx) {
	  mindx = dx;
	  i++;
	} else break;
      }
      if (graduations[i-1].x1 != pointer.getCenterX()) {
	currentGraduation = graduations[i-1];
	return graduations[i-1].x1;
      }
      return -1; //FIXME
    }
  }

  /**
   * Get the current value pointed by the slider.
   * @return the value.
   */
  public long getValue() {
    if (currentGraduation != null) {
      return currentGraduation.value;
    }
    else return min;
  }

  /**
   * Set the value pointed by the slider.
   * if the value is too high, resize the slider.
   * @param The value to point.
   */
  public void setValue(long value) {
    if (value <= graduations[0].value){
      movePointerTo(graduations[0].x1, pposition.y);
      return;
    } 
    if (value >= graduations[graduations.length -1].value)
      setMax(value + 10*step);
    for (int i = 1; i < graduations.length -2; i++) {
      if (value - graduations[i].value < step) {
	movePointerTo(graduations[i].x1, pposition.y);
	return ;
      }
    }
  }

  protected void movePointerTo(int x, int y) {
    int gx = getGoodX(x);
    if (gx != -1)
      pointer.setLocation(gx,pposition.y);
    if (getGraphics() != null)
      paint(getGraphics());
  }

  protected void movePointerTo(Point p) {
    int gx = getGoodX(p.x);
    if (gx != -1)
      pointer.setLocation(gx,pposition.y);
    if (getGraphics() != null)
      paint(getGraphics());
  }

  /**
   * paint the slider.
   */
  public void paint (Graphics g) {
    Dimension d = getSize();
    Shape s = g.getClip();
    Image dbi    = null;
    Graphics dbg = null;
    dbi = ImageCache.getImage(this, d.width, d.height);
    dbg = dbi.getGraphics();
    dbg.setClip(s);
    dbg.setColor(color);
    dbg.clearRect(0,0, d.width, d.height);
    dbg.fillRect(marginx,
		 marginy,
		 width-(2*marginx),
		 height-(2*marginy));
    if (border) {
      dbg.setColor(color.darker());
      dbg.drawRect(marginx,
		   marginy,
		   width-(2*marginx),
		   height-(2*marginy));
    }
    dbg.setColor(Color.white);
    dbg.fill3DRect(marginx + rect_margin_x,
		   marginy + rect_margin_y - guideHeight/2,
		   getGraduationLength(),
		   guideHeight,
		   false);
    paintGraduation(dbg);
    dbg.setColor(color.darker());
    pointer.paint(dbg);
    g.drawImage(dbi, 0, 0, this);
  }

  protected int getGraduationLength() {
    // use to recover arround error.
    int length = width - 2 * (marginx + rect_margin_x);
    int nbpart = (int)((max - min) / step);
    int pixelstep = length / nbpart;
    return pixelstep * nbpart;
  }

  protected void updateGraduationPosition() {
    int length = width - 2 * (marginx + rect_margin_x);
    int nbpart = (int) ((max - min) / step);
    int pixelstep = length / nbpart;
    int y1 = marginy  + 5 * rect_margin_y / 3;
    int y2 = y1+ graduationHeight;
    int minpixel = marginx + rect_margin_x;
    int maxpixel = length + minpixel;
    int gradx = minpixel;
    graduations = new Graduation[nbpart+1];
    int i=0;
    while (i < graduations.length){
      graduations[i] = new Graduation(gradx,y1,gradx,y2,min+i*step);
      gradx += pixelstep;
      i++;
    } 
  }

  protected void paintGraduation(Graphics g) {
    g.setColor(Color.black);
    Font font = new Font(g.getFont().getName(),
			 g.getFont().getStyle(),
			 g.getFont().getSize() - 1);
    g.setFont(font);
    for (int i=0 ; i < graduations.length; i++)
      graduations[i].draw(g, (i == 0) || (i == graduations.length-1));
    g.setColor(Color.white);
    currentGraduation.showValue(g);
  }

  /**
   * update the slider.
   */
  public void update (Graphics g){
    paint(g);
  }

  protected void setDefaultSize(long min, long max, long step) {
    int length = minwidth - 2 * (marginx + rect_margin_x);
    int nbpart = (int)((max - min) / step);
    int pixelstep = length / nbpart;
    if (pixelstep < minpixelstep) {
      minwidth = (minpixelstep * nbpart) + 2 * (marginx + rect_margin_x);
      defaultWidth = minwidth;
    }
    setSize(defaultWidth, defaultHeight);
  }

  /**
   * Resizes this component so that it has width "width" and height "height".
   * @param d - The dimension specifying the new size of this slider. 
   */
  public void setSize(Dimension d) {
    setSize(d.width, d.height);
  }

  /**
   * Resizes this component so that it has width "width" and height "height". 
   * @param width - The new width of this slider
   * @param height - The new height of this slider
   */
  public void setSize (int width, int height) {
    if (height < minheight)
      height = minheight;
    if (width < minwidth)
      width = minwidth;
    this.width    = width;
    this.height   = height;
    rect_margin_y = (height - (2* marginy)) / 3;
    pposition = new Point(pposition.x,  marginy + rect_margin_y);
    pointer.setLocation(pposition);
    size = new Dimension(width, height);
    super.setSize(width,height);
    updateGraduationPosition();
    currentGraduation = graduations[0];
  }

  /**
   * Gets the preferred size of this slider.
   * @return A dimension object indicating this slider's preferred size.
   * @see #getMinimumSize
   */
  public Dimension getPreferredSize() {
    return size;
  }

  /**
   * Gets the mininimum size of this component.
   * @return A dimension object indicating this slider's minimum size.
   * @see #getPreferredSize
   */
  public Dimension getMinimumSize() {
    return size;
  }
  
  /**
   * Set the slider's color.
   * @param color - the slider's color.
   */
  public void setColor(Color color) {
    this.color = color;
  }

  /**
   * Set the minimum bound of the slider.
   * Use initialize or SetBounds if you want to set more than one value.
   * @param min - the min bound
   * @see #setMax 
   * @see #setBounds 
   * @see #setStep 
   * @see #initialize
   */
  public void setMin(long min) {
    this.min = min;
    update();
  }

  /**
   * Set the maximum bound of the slider.
   * Use initialize or SetBounds if you want to set more than one value.
   * @param max - the max bound
   * @see #setMin 
   * @see #setBounds 
   * @see #setStep 
   * @see #initialize
   */
  public void setMax(long max) {
    this.max = max;
    update();
  }

  /**
   * Set the bounds of the slider.
   * @param min - the min bound
   * @param max - the max bound
   * @see #setMin 
   * @see #setMax
   * @see #setStep 
   * @see #initialize
   */
  public void setBounds(long min, long max) {
    this.min = min;
    this.max = max;
    update();    
  }

  /**
   * Set the step of the slider.
   * Use initialize or SetBounds if you want to set more than one value.
   * @param step - the step between two position
   * @see #setMin 
   * @see #setMax
   * @see #setBounds 
   * @see #initialize
   */
  public void setStep(long step) {
    this.step = step;
    update();
  }

  /**
   * Initialize the slider's bounds and Step
   * @param min - the min bound
   * @param max - the max bound
   * @param step - the step between two position
   * @see #setMin 
   * @see #setMax
   * @see #setBounds 
   * @see #initialize
   */
  public void initialize(long min, long max, long step) {
    this.min = min;
    this.max = max;
    this.step = step;
    update();
  }

  protected void update() {
    setDefaultSize(min,max,step);
    updateGraduationPosition();
    if (graduations != null)
      currentGraduation = graduations[0];
    setVisible(true);
    Component parent = getParent();
    if (parent != null)
      parent.validate();
  }

  /**
   * Constructs a new Slider.
   * @param min - the min bound
   * @param max - the max bound
   * @param step - the step between two position
   * @param minPixelStep - the min step (in pixels) between two positions.
   * @param border - if true draw a border arround the slider.
   * @param color - the slider's color.
   */
  public Slider(long min, long max, long step,  
		int minPixelStep, boolean border, Color color) {
    this.border = border;
    this.minpixelstep = minPixelStep;
    if (color != null) this.color = color;
    this.min = min;
    this.max = max;
    this.step = step;
    pposition = new Point(marginx + rect_margin_x, marginy + rect_margin_y);
    pointer = new Pointer(pointerWidth,pointerHeight, pposition);
    PointerClickListener clickListener = 
      new PointerClickListener(this);
    PointerMotionListener motionListener = 
      new PointerMotionListener(this);
    addMouseListener( clickListener );
    addMouseMotionListener( motionListener );
    setDefaultSize(min,max,step);
    updateGraduationPosition();
    if (graduations != null)
      currentGraduation = graduations[0];
  }

  /**
   * Constructs a new Slider.
   * @param min - the min bound
   * @param max - the max bound
   * @param step - the step between two position
   * @param minPixelStep - the min step (in pixels) between two positions.
   * @param border - if true draw a border arround the slider.
   */
  public Slider(long min, long max, long step, 
		int minPixelStep, boolean border) {
    this(min, max, step, minPixelStep, border, null);
  }

  /**
   * Constructs an invisible Slider.
   * use initialize to define it.
   * @param minPixelStep - the min step (in pixels) between two positions.
   * @param border - if true draw a border arround the slider.
   */
  public Slider(int minPixelStep, boolean border) {
    this();
    this.minpixelstep = minPixelStep;
    this.border = border;
  }

  /**
   * Constructs an invisible Slider.
   * use initialize to define it.
   */
  public Slider() {
    pposition = new Point(marginx + rect_margin_x, marginy + rect_margin_y);
    pointer = new Pointer(pointerWidth,pointerHeight, pposition);
    PointerClickListener clickListener = 
      new PointerClickListener(this);
    PointerMotionListener motionListener = 
      new PointerMotionListener(this);
    addMouseListener( clickListener );
    addMouseMotionListener( motionListener );
    setVisible(false);
  }


  // TEST
  public static void main(String argv[]) {
    Frame f = new Frame ("Slider");
    Slider slider = new Slider ();
    slider.initialize(10,150, 10);
    slider.setColor(Color.lightGray);
    f.add ("Center", slider);
    if (argv.length > 1) {
      int width = Integer.parseInt(argv[0]);
      int height = Integer.parseInt(argv[1]);
      slider.setSize(width, height);
    }
    f.pack();
    f.show ();
  }

}
