package org.freehep.graphicsbase.util;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.util.Locale;


/**
 * This code formats numbers in Scientific Notation. The input Number object is returned
 * as a ScientificFormated string. There are two output styles: Pure and Standard scientific
 * notation. Pure formatted numbers have precisely the number of digits specified by the
 * significant digits (sigDig) parameter and always specify a Base 10 Exponential(E).
 * Standard formated numbers have the number of digits specified by the significant
 * digits (sigDig) parameter but will not have a Base 10 Exponential(E) if the number of digits
 * in the mantissa <= maxWidth.
 *
 * @author Paul Spence
 * @author Mark Donszelmann
 * @version $Id: ScientificFormat.java 8584 2006-08-10 23:06:37Z duns $
 */

public class ScientificFormat extends Format
{
   /**
    * The number of significant digits the number is formatted to is recorded by sigDigit.
    * The maximum width allowed for the returned String is recorded by MaxWidth
    */
   private int sigDigit = 5;
   private int maxWidth = 8;
   private boolean sciNote = false; //set to true for pure Scientific Notation
   private DecimalFormat decimalFormat;
   private static final long serialVersionUID = -1182686857248711235L;
   
   public ScientificFormat()
   {
      
   }
   
   /**
    * Sets the significant digits, maximum allowable width and number formatting style
    * (SciNote == true for Pure formatting).
    */
   public ScientificFormat(int sigDigit, int maxWidth, boolean SciNote)
   {
      setSigDigits(sigDigit);
      setMaxWidth(maxWidth);
      setScientificNotationStyle(SciNote);
   }
   
   /**
    * Implementation of inherited abstract method. Checks to see if object to be formatted
    * is of type Number. If so casts the Number object to double and calls the format method.
    * Returns the result.
    */
   public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos)
   {
      if (obj instanceof Number)
      {
         String result = format(((Number) obj).doubleValue());
         return toAppendTo.append(result);
      }
      else if (obj instanceof DoubleWithError)
      {
         DoubleWithError dwe = (DoubleWithError) obj;
         toAppendTo.append(format(dwe.getValue()));
         if (dwe.hasAsymmetricError())
         {
            toAppendTo.append(DoubleWithError.plus);
            int errorSigDigit = resolveErrorSigDigit(dwe.getValue(),dwe.getPlusError());
            toAppendTo.append(format(dwe.getPlusError(),errorSigDigit));
            
            toAppendTo.append(DoubleWithError.minus);
            errorSigDigit = resolveErrorSigDigit(dwe.getValue(),dwe.getMinError());
            toAppendTo.append(format(dwe.getMinError(),errorSigDigit));
         }
         else
         {
            toAppendTo.append(DoubleWithError.plusorminus);
            int errorSigDigit = resolveErrorSigDigit(dwe.getValue(),dwe.getError());
            toAppendTo.append(format(dwe.getError(),errorSigDigit));
         }
         return toAppendTo;
      }
      else throw new IllegalArgumentException("Cannot format given Object as a Number");
   }
   
   /**Dummy implementation of inherited abstract method.
    */
   public Object parseObject(String source, ParsePosition pos)
   {
      return null;
   }
   
   /**
    * Returns the number of significant digits
    */
   public int getSigDigits()
   {
      return sigDigit;
   }
   
   /**
    * Returns the maximum allowable width of formatted number excluding any exponentials
    */
   public int getMaxWidth()
   {
      return maxWidth;
   }
   
   /**
    * Returns the formatting style: True means Pure scientific formatting, False means standard.
    */
   public boolean getScientificNotationStyle()
   {
      return sciNote;
   }
   
   /**
    * Sets the number of significant digits for the formatted number
    */
   public void setSigDigits(int SigDigit)
   {
      if (SigDigit < 1) throw new IllegalArgumentException("sigDigit");
      sigDigit = SigDigit;
      decimalFormat = null;
   }
   
   /**
    * Sets the maximum allowable length of the formattted number mantissa before exponential notation
    * is used.
    */
   public void setMaxWidth(int mWidth)
   {
      if (mWidth < 3) throw new IllegalArgumentException("maxWidth");
      maxWidth = mWidth;
   }
   /**
    * Sets the format style used.
    * There are two output styles: Pure and Standard scientific
    * notation. Pure formatted numbers have precisely the number of digits specified by the
    * significant digits (sigDig) parameter and always specify a Base 10 Exponential(E).
    * Standard formated numbers have the number of digits specified by the significant
    * digits (sigDig) parameter but will not have a Base 10 Exponential(E) if the number of digits
    * in the mantissa <= maxWidth.
    */
   public void setScientificNotationStyle(boolean sciNote)
   {
      this.sciNote = sciNote;
   }
   
   
   //simplify method for taking log base 10 of x
   private final static double k = 1/Math.log(10);
   private double Log10(double x)
   {
      if (x==0) return 0;
      else return Math.log(x)*k;
   }
   
   private int resolveErrorSigDigit(double x, double dx)
   {
      //dx should never be negative
      dx = Math.abs(dx);
      //make x +ve cause negative doesn't effect sigdigits
      x=Math.abs(x);
      
      //these circumstances errorsigdit does equal sigdigit, excluding infinity and Nan which are handled by format
      if(dx == 0 || Double.isInfinite(dx) || Double.isNaN(dx) || dx >= x) return sigDigit;
      
      //fail cases for log, method fails to handle
      if(x==0||Double.isInfinite(x) || Double.isNaN(x))return sigDigit;
      
      //otherwise solve for cases when dx<x
      int log =(int)Math.round(Log10(dx/x));//always will return negative number
      int errorsigdigit = sigDigit+log;
      if(errorsigdigit <1) return 1;
      return errorsigdigit;
   }
   private DecimalFormat getDecimalFormat(int sigDig)
   {
      StringBuffer buffer = new StringBuffer("0.");
      for (int i=1; i<sigDig; i++) buffer.append('0');
      buffer.append("E0");
      return new DecimalFormat(buffer.toString(),new DecimalFormatSymbols(Locale.US));
   }
   /**
    * Format the number using scientific notation
    */
   public String format(double d)
   {
      return format(d,sigDigit);
   }
   private String format(double d, int sigDig)
   {
      // Deal with a few special values first
      if (Double.isInfinite(d)) return maxWidth < 8 ? "INF" : "Infinite";
      if (Double.isNaN(d)) return "NaN";
      
      // Delegate the hard part to decimalFormat
      if (decimalFormat == null) decimalFormat = getDecimalFormat(sigDigit);
      DecimalFormat format = (sigDig == sigDigit) ? decimalFormat : getDecimalFormat(sigDig);
      
      String preliminaryResult = format.format(d);
      if (sciNote) return preliminaryResult;
      
      int ePos = preliminaryResult.indexOf('E');      
      int exponent = Integer.parseInt(preliminaryResult.substring(ePos+1))+1;
      if (exponent>maxWidth) return preliminaryResult;
      if (exponent<-maxWidth+sigDig+1) return preliminaryResult;
      
      // We need to fix up the result
      
      int sign = preliminaryResult.charAt(0)=='-' ? 1 : 0;
      StringBuffer result = new StringBuffer(preliminaryResult.substring(sign,sign+1)+preliminaryResult.substring(sign+2,ePos));
      
      if (exponent >= sigDig)
      {
         for (int i=sigDig; i<exponent; i++) result.append('0');
      }
      else if (exponent < 0)
      {
         result.insert(0,".");
         for (int i=exponent; i<0; i++) result.insert(1,'0');
      }
      else 
      {
         result.insert(exponent,'.');
      }
      if (sign > 0) result.insert(0,'-');
      return result.toString();
   }
//   /**
//    * Format a number plus error using scientific notation
//    */
//   public String formatError(double d,double dx)
//   {
//      return format(dx, resolveErrorSigDigit(d, dx));
//   }

}