package eva2.tools.chart2d;

import java.awt.* ;
import javax.swing.BorderFactory;
import javax.swing.border.* ;

import eva2.tools.math.Mathematics;

import java.awt.geom.AffineTransform ;
import java.text.NumberFormat;
import java.text.DecimalFormat;

/**
 * ScaledBorder puts an border around Components
 * ( especially around DrawingAreas ) with scaled and labeled axes. 
 */
public class ScaledBorder implements Border 
{
  private boolean under_construction = false;

  /**
   * length of the distance markers on the axes in pixels
   */
  int marker_length = 2;

  /**
   * length in pixels of the arrows at the ends of the axes
   */
  int arrow_length = 10;

  /**
   * a flag if the arrows should be visible
   */
  public boolean show_arrows = true;

  /**
   * distance between the x-values in digits
   */
  int x_value2value = 2;

  /**
   * distance between y-label and y-values in digit width
   */
  int y_label2values = 1;

  /**
   * distance between y-values and y-axis markers in parts of the digit width
   */
  int y_values2marker = 2;

  /**
   * distance between values and arrows in pixels
   */
  int x_values2arrow = 10,
      y_values2arrow = 10;

  /**
   * distance between arrows and outer border
   */
  int axis2border = 4;

  /**
   * distance between labels and the border in pixels
   */
  public int x_label2border = 6,
             y_label2border = 6;

  /**
   * the size of the source rectangle
   * that means before the values are mdified by scale functions
   */
  SlimRect src_rect = null;

  /**
   * the minimal increment of the scales
   */
  public double minimal_increment;

  /**
   * the  displayed labels
   */
  public String x_label, y_label;

  /**
   * foreground and background colors
   */
  public Color foreground, background;

  /**
   * the border which is shown around the scaled border
   */
  Border outer_border;

  /**
   * flag if the outer border should be displayed
   */
  boolean show_outer_border = true;

  /**
   * scale functions if, for example, an logarithmic function is needed instead
   * of a linear.
   */
  public DFunction x_scale, y_scale;

  /**
   * formatters of the x- and y-axis numbers
   * @see java.text.NumberFormat
   */
  private NumberFormat format_x = new DecimalFormat(),
                      format_y = new DecimalFormat();

  private double src_dX = -1, src_dY = -1;

  private boolean do_refresh,
                  auto_scale_x = true,
                  auto_scale_y = true;

  private Insets old_insets;

  private DMeasures m;

  /**
   * constructor creates a default ScaledBorder inside of a lowered BevelBorder
   */
  public ScaledBorder(){
    this(
      BorderFactory.createBevelBorder(
        BevelBorder.LOWERED,
        Color.white,
        Color.lightGray,
        Color.black,
        Color.lightGray
      )
    );
  }
  /**
   * constructor creates a new <code>ScaledBorder<code/>
   * surrounded by the specified <code>Border<code/>
   */
  public ScaledBorder( Border outer ){
    outer_border = outer;
    m = new DMeasures( this );
  }

  /**
   * method tells the border to calculate the differences between displayed
   * x-values by itself
   */
  public void setAutoScaleX(){ auto_scale_x = true; }

  /**
   * method tells the border to calculate the differences between displayed
   * y-values by itself
   */
  public void setAutoScaleY(){ auto_scale_y = true; }

  /**
   * method sets the differences between source x-values of the displayed values
   * @see setAutoScaleX().
   * If scale functions are used there might be a difference between the shown values
   * and the source values
   */
  public void setSrcdX(double dX){
    auto_scale_x = false;
    src_dX = dX;
  }

  /**
   * method sets the differences between source y-values of the displayed values
   * @see setAutoScaleY().
   * If scale functions are used there might be a difference between the shown values
   * and the source values
   */
  public void setSrcdY(double dY){
    auto_scale_y = false;
    src_dY = dY;
  }

  /**
   * Returns the inner rectangle in pixel coordinates.
   * 
   * @param c
   * @return
   */
  public SlimRect getInnerRect(Component c) {
	  Insets inner_insets = getBorderInsets(c);
	  Dimension d = c.getSize();
	  int width = d.width - inner_insets.left - inner_insets.right;
	  int height = d.height - inner_insets.top - inner_insets.bottom;

	  SlimRect rect=new SlimRect(inner_insets.left, inner_insets.top, width, height);
	  return rect;
  }
  
  public void paintBorder(Component c, Graphics g, int x, int y, int width, int height){
    if( under_construction ) System.out.println("ScaledBorder.paintBorder()");
    if( foreground == null ) foreground = c.getForeground();
    if( background == null ) background = c.getBackground();
    Color old_color = g.getColor();
    g.setColor( background );
    g.fillRect( x, y, width, height );
    g.setColor( old_color );

    Insets outer_insets = new Insets( 0, 0, 0, 0);// insets of the outer border
    if( show_outer_border ) {
      outer_border.paintBorder( c, g, x, y, width, height );
      outer_insets = outer_border.getBorderInsets( c );
    }

    do_refresh = true;
    Insets inner_insets = getBorderInsets(c);

    Dimension d = c.getSize(),
              cd = new Dimension( d.width - inner_insets.left - inner_insets.right,
                                  d.height - inner_insets.top - inner_insets.bottom );
    FontMetrics fm = g.getFontMetrics();
    int fontAsc = fm.getAscent();
    do_refresh = false;

    m.update(c, inner_insets);

    // axes
    g.setColor( foreground );
    g.drawLine( inner_insets.left, inner_insets.top,
                inner_insets.left, inner_insets.top + cd.height );
    g.drawLine( inner_insets.left, inner_insets.top + cd.height,
                inner_insets.left + cd.width, inner_insets.top + cd.height );

    if( show_arrows ){
      g.drawLine( inner_insets.left, inner_insets.top,
                  inner_insets.left, inner_insets.top - y_values2arrow );
      g.drawLine( inner_insets.left - marker_length, inner_insets.top - y_values2arrow,
                  inner_insets.left, inner_insets.top - y_values2arrow - arrow_length );
      g.drawLine( inner_insets.left + marker_length, inner_insets.top - y_values2arrow,
                  inner_insets.left, inner_insets.top - y_values2arrow - arrow_length);
      g.drawLine( inner_insets.left - marker_length, inner_insets.top - y_values2arrow,
                  inner_insets.left + marker_length, inner_insets.top - y_values2arrow );

      g.drawLine( inner_insets.left + cd.width , inner_insets.top + cd.height,
                  inner_insets.left + cd.width + x_values2arrow, inner_insets.top + cd.height );
      g.drawLine( inner_insets.left + cd.width + x_values2arrow,
                  inner_insets.top + cd.height - marker_length,
                  inner_insets.left + cd.width + x_values2arrow + arrow_length,
                  inner_insets.top + cd.height );
      g.drawLine( inner_insets.left + cd.width + x_values2arrow,
                  inner_insets.top + cd.height + marker_length,
                  inner_insets.left + cd.width + x_values2arrow + arrow_length,
                  inner_insets.top + cd.height );
      g.drawLine( inner_insets.left + cd.width + x_values2arrow,
                  inner_insets.top + cd.height - marker_length,
                  inner_insets.left + cd.width + x_values2arrow,
                  inner_insets.top + cd.height + marker_length );
    }

    if( y_label != null ) {
      Dimension yld = new Dimension(fm.getAscent()+fm.getDescent(), fm.stringWidth(y_label));
      AffineTransform T = new AffineTransform(0, -1, 1, 0, 0, 0);
      Font old = g.getFont(), f = old.deriveFont( T );
      g.setFont( f );
      g.drawString( y_label, y_label2border + fm.getAscent(), inner_insets.top + ( cd.height + yld.height )/ 2 );
      g.setFont( old );
    }

    if( x_label != null )
      g.drawString(
        x_label, inner_insets.left + ( cd.width - fm.stringWidth( x_label ) ) / 2,
        d.height - outer_insets.bottom - x_label2border - fm.getDescent() );

    if( src_rect.x == 0 && src_rect.y == 0 ){
      int v2m = fm.stringWidth("0") / y_values2marker;
      g.drawString( "0", inner_insets.left - fm.stringWidth( "0" ) - v2m - marker_length,
                         inner_insets.top + cd.height + fontAsc );
      g.drawLine( inner_insets.left, inner_insets.top + cd.height + fm.getAscent(),
                  inner_insets.left, inner_insets.top + cd.height);
      g.drawLine( inner_insets.left, inner_insets.top + cd.height,
                  inner_insets.left - fm.stringWidth( "0" ) - v2m - marker_length,
                  inner_insets.top + cd.height );
    }

    drawYValues( g, inner_insets);
    drawXValues( g, inner_insets, cd );

    g.setColor( old_color );
  }
  
  /**
   * The scaling of the y-axis is defined here.
   * 
   * @param g
   * @param insets
   */
  private void drawYValues( Graphics g, Insets insets){
	  if( under_construction ) System.out.println("ScaledBorder.drawYValues()");

	  FontMetrics fm = g.getFontMetrics();
	  int fontAsc = fm.getAscent(), v2m = fm.stringWidth("0") / y_values2marker;
	  
//	  double startVal,dn = (src_rect.y / src_dY );
//	  startVal = Math.round(dn-0.5)*src_dY;
//
//	  if( startVal < src_rect.y || ( src_rect.x == 0 && src_rect.y == 0 ) ) {
//		  startVal+=src_dY; 
//	  }
	  
	  double startVal = Mathematics.firstMultipleAbove(src_rect.y, src_dY);
	  
	  double v, scaledV, minx = src_rect.x;
	  if( x_scale != null ) minx = x_scale.getImageOf( minx );
	  v = startVal;
	  while (v <= src_rect.y + src_rect.height){
		  if( y_scale != null ) scaledV = y_scale.getImageOf( v );
		  else scaledV=v;
		  String text = format_y.format(scaledV);
		  try{ scaledV = format_y.parse(text).doubleValue(); }
		  catch( java.text.ParseException ex ){ }
		  Point p = m.getPoint( minx, scaledV );
		  if( p != null ){
			  g.drawString( text,
					  insets.left - fm.stringWidth( text ) - v2m - marker_length,
					  p.y  + fontAsc / 2 );
			  g.drawLine( insets.left - marker_length, p.y, insets.left, p.y );
		  }
		  if (v+src_dY<= v) {
			  System.err.println("Overflow error B in ScaledBorder! v,src_dY:" + v + ", " + src_dY);
			  v*=1.01;
		  }
		  v += src_dY;
	  }
  }
  
  public double getSrcdY( FontMetrics fm, Dimension cd ){
	  return getSrcdY(fm.getHeight(), cd.height);
  }

  public double getSrcdY( int fontMetricsHeight, int componentHeight ){
	    if( under_construction ) System.out.println("ScaledBorder.getSrcdY()");
	    if( (!do_refresh && src_dY != -1) || !auto_scale_y ) return src_dY;
	    int max = componentHeight / fontMetricsHeight;
	    if (Double.isInfinite(src_rect.height) || Double.isInfinite(src_rect.width)) {
	    	System.err.println("Error, infinite value in ScaledBorder:getSrcdY !!");
	    }
	    double minsrc_dY = 2 * src_rect.height / (double)max; // die 2 einfach mal so eingesetzt   <--------------------------
	    src_dY = aBitBigger( minsrc_dY );
	    if( src_dY < minimal_increment ) src_dY = minimal_increment;
	    if( under_construction ) System.out.println("Setting src_dY to " + src_dY + " for " + src_rect);
	    return src_dY;
	  }
  
  private void drawXValues( Graphics g, Insets insets, Dimension cd ){
    if( under_construction ) System.out.println("ScaledBorder.drawXValues()");

    FontMetrics fm = g.getFontMetrics();
    double mx = cd.width / src_rect.width;
    int n, labelX,
        xnull = insets.left + (int)( - src_rect.x * mx );

    n = (int)( src_rect.x / src_dX );
    if( n * src_dX < src_rect.x || ( src_rect.x == 0 && src_rect.y == 0 ) ) n++;

    int fontAsc = fm.getAscent(), xLineY = insets.top + cd.height;
    labelX = xnull + (int)(n * src_dX * mx);
    while( n * src_dX <= src_rect.x + src_rect.width ){
      double v = n * src_dX;
      if( x_scale != null ) v = x_scale.getImageOf(v);
      String text = format_x.format(v);
      try{ v = format_x.parse(text).doubleValue(); }
      catch( java.text.ParseException ex ){ }
      int strW = fm.stringWidth( text );
      g.drawString( text, labelX - strW / 2, xLineY + fontAsc );
      g.drawLine( labelX, xLineY, labelX, xLineY + marker_length );
      n++;
      labelX = xnull + (int)( n * src_dX * mx);
    }
  }

  public double getSrcdX( FontMetrics fm, Dimension cd ){
    if( under_construction ) System.out.println("ScaledBorder.getSrcdX()");
    if( (!do_refresh && src_dX != - 1) || !auto_scale_x ) return src_dX;
    int digit_width = fm.stringWidth("0"),
        max = cd.width / ( digit_width * ( x_value2value + 1 ) );
    src_dX = src_rect.width / (double)max;
    int n, labelX, olsrc_dX;

    boolean ok = false;
    while( !ok ){
      src_dX = aBitBigger( src_dX );

      n = (int)( src_rect.x / src_dX );
      if( n * src_dX < src_rect.x ) n++;

      olsrc_dX = 0;

      boolean suits = true, first = true;
      while( suits && n * src_dX <= src_rect.x + src_rect.width ){
        double v = n * src_dX;
        if( x_scale != null ) v = x_scale.getImageOf( v );
        String text = format_x.format(v);
        int strW = fm.stringWidth( text );
        labelX = (int)((( n * src_dX - src_rect.x ) / src_rect.width ) * cd.width ) - strW / 2;
        if( !first && labelX <= olsrc_dX + digit_width * x_value2value ) suits = false;
        else{
          olsrc_dX = labelX + strW;
          n++;
        }
        first = false;
      }
      if( !suits ) ok = false;
      else ok = true;
    }
    if( src_dX < minimal_increment ) src_dX = minimal_increment;
    return src_dX;
  }

  /**
   * method returns to a certain minimal value the next higher value which can be
   * displayed, which looks a bit nicer
   * it returns values like ... 0.05, 0.1, 0.2, 0.5, 1, 2, 5, 10, ...
   *
   * @param the double value next to which the displayable value should be found
   * @return the displayable value
   */
  public static double aBitBigger( double min ){
    if( min <= 0 || Double.isInfinite(min) || Double.isNaN(min)) return 1;
    double d = 1;
    if( min < d ){
      while( d * .5 > min ) {
        d *= .5;
        if( d * .4 > min ) d *= .4;
        if( d * .5 > min ) d *= .5;
      }
    }
    else{
      while( d <= min ) {
        d *= 2;
        if( d <= min ) d *= 2.5;
        if( d <= min ) d *= 2;
      }
    }
    return d;
  }

  public boolean isBorderOpaque(){
    return outer_border.isBorderOpaque();
  }

  /**
   * Apply a decimal format pattern to x (bXorY true) or y (bXorY false) axis.
   * 
   * @see #java.text.DecimalFormat.applyPattern
   * 
   * @param bXorY
   * @param pattern
   */
  public void applyPattern(boolean bXorY, String pattern) {
	  if (bXorY) ((java.text.DecimalFormat)format_x).applyPattern(pattern);
	  else ((java.text.DecimalFormat)format_y).applyPattern(pattern);
  }

  public Insets getBorderInsets(Component c){
    if( under_construction ) System.out.println("ScaledBorder.getBorderInsets()");
    if( !do_refresh && old_insets != null ) return old_insets;

    Graphics g = c.getGraphics();

    Insets insets = new Insets(0, 0, 0, 0);
    if( show_outer_border ) insets = outer_border.getBorderInsets( c );

    if( g == null ) return insets;

    FontMetrics fm = g.getFontMetrics();
//    int fontAsc = fm.getAscent();
    int fontHeight = fm.getHeight(),
        digit_width = fm.stringWidth("0");

    if( c instanceof DArea ){
      DArea area = (DArea)c;
      DMeasures m = area.getDMeasures();
      src_rect = m.getSourceOf(area.getSlimRectangle());
      x_scale = area.getDMeasures().x_scale;
      y_scale = area.getDMeasures().y_scale;
    }

    // left:
    if( y_label != null ) insets.left += fm.getAscent() + fm.getDescent();
    insets.left += y_label2values * digit_width;
    getSrcdY( fm, c.getSize() );
    double start, n, inc;
    int maxWidth = 0;
    start = src_dY*(int)( src_rect.y / src_dY );
    n=start;
    if ( n < src_rect.y ) n+=src_dY; 
    
    if (((src_rect.y + src_rect.height)-start)>20) inc = ((src_rect.y + src_rect.height)- start)/20.;
    else inc = src_dY;
    
    for (; n <= src_rect.y + src_rect.height; n+=inc ){
    	// TODO here might be a bug for mean values
      double v = n;
      if( y_scale != null ) v = y_scale.getImageOf( v );
      int w = fm.stringWidth( format_y.format(v) );
      if( w > maxWidth ) maxWidth = w;
      // avoid nearly endless loop for large src_rect.y value and small src_dY
    }
    
    insets.left += 1 + y_label2border + maxWidth + digit_width / y_values2marker + marker_length;

    // bottom:
    insets.bottom += 1 + fontHeight + x_label2border;
    if( x_label != null ) insets.bottom += fontHeight;

    // top:
    if( show_arrows ) insets.top += y_values2arrow + arrow_length;
    insets.top += axis2border;

    // right:
    if( show_arrows ) insets.right += x_values2arrow + arrow_length;
    insets.right += axis2border;
    getSrcdX( fm, c.getSize() );
    int k = (int)( src_rect.x + src_rect.width / src_dX );
    if( k < 0 ) k ++;
    int w = fm.stringWidth( format_x.format(k * src_dX) );
    if( w / 2 > insets.right ) insets.right = w / 2;

    old_insets = insets;
    return insets;
  }
}

/****************************************************************************
 * END OF FILE
 ****************************************************************************/
