/* ***************************************************************************
 *  
 *       File:  CalculatorEngine.java
 *    Package:  ca.janeg.calc
 * 
 * References:  Object Oriented Programming and Java, 
 *              by Danny C.C. Poo and Derek B.K. Kiong, Springer, 1999 (p48-49)
 * 
 * 
 * Date         Author          Changes
 * ------------ -------------   ----------------------------------------------
 * Oct 17, 2002 Jane Griscti    Created
 * Oct 18, 2002 Jane Griscti    Added unary functions %, sqrt, reciprocal, etc
 * Oct 20, 2002 Jane Griscti    Added var display, number formatter and related
 *                              methods
 *                              Added integer binary operations: xor, or, and
 *                              leftShift, rightShift
 * Oct 21, 2002 Jane Griscti    Cleaned up comments
 * Oct 22, 2002 Jane Griscti    Added trig and log unary functions
 * *************************************************************************** */

package ca.janeg.calc;

import java.text.DecimalFormat;
import java.text.NumberFormat;

/**
 *  A class to perform standard calculator operations. 
 *  For example,
 * 
 *  <pre>
 *      CalculatorEngine c = new CalculatorEngine();
 *      c.digit( 1 );
 *      c.digit( 2 );
 *      c.add();
 *      c.digit( 1 );
 *      c.digit( 3 );
 *      c.equals();
 *      System.out.println( c.display() );
 *  </pre>
 * 
 *  Accuracy is limited to fifteen decimal places.
 * 
 *  @author     Jane Griscti    jane@janeg.ca
 *  @version    1.2             Oct 20, 2002 
 */
public class CalculatorEngine {

    private StringBuffer display    = new StringBuffer( 64 );
    private DecimalFormat df        = (DecimalFormat)NumberFormat.getInstance();
    private boolean newOp           = false;
    private boolean inDecimals      = false;
        
    private double  value;          // current digits
    private double  keep;           // previous value or operation result
    private int     toDo;           // binary operation waiting for 2nd value
    private int     decimalCount;   // number of decimal positions in current
                                    //   value

    /**
     *  Creates a new <code>CalculatorEngine</code> object.
     */
    public CalculatorEngine(){
        super();
        df.setMaximumFractionDigits( 15 );
    }

    /* -- Digits and the decimal point handler -- */
    
    /**
     *  Accept a digit or decimal as input.
     */
    public void digit(final int n ){
        
        /*
         *  Strategy:
         *      1. Start a new value if at the beginning of a new operation.
         * 
         *      2. Append the input character, setting the decimal flag if it's
         *         a decimal point or increasing the decimal count if we're
         *         already into decimals.
         * 
         *      3. Convert the revised input string to a double for use in
         *         calculations; forcing input errors to return a 0.0 value.
         */
        
        if( newOp ){
            display.delete( 0, display.length() );
            newOp = false;
        }
        
        char c = (char)n;
        
        if( c == '.' ){
            display.append( '.' );
            inDecimals = true;
        }else if( !inDecimals ){
            display.append( n );
        }else{
            if( decimalCount < 16 ){
                display.append( n );
                decimalCount++;
            }
        }
        
        try{
            value = Double.parseDouble( display.toString() );
        }catch( NumberFormatException e ){
            value = Double.parseDouble( "0.0" );
        }
    }
    
    /* -- Binary operations -- 
     * 
     *   A binary operation signals the engine to:
     *     1. store the current value
     *     2. set the 'toDo' flag with the requested operation
     *     3. accept input for a second value
     *     4. perform the 'toDo' op when '=' or another binary operation
     *        is requested
     */
    
    /**
     *  Add the next input value to the previous value
     */         
    public void add(){
        binaryOperation( "+" );
    }
    
    /**
     *  Subtract the next input value from the previous value
     */
    public void subtract(){
        binaryOperation( "-" );
    }
    
    /**
     *  Multiply the next input value by the previous value
     */
    public void multiply(){
        binaryOperation( "*" );
    }
    
    /**
     *  Divide the previous value by the next input value
     */
    public void divide(){
        binaryOperation( "/" );
    }

    /**
     *  Bitwise And ( & ) 
     */ 
    public void and(){
        binaryOperation( "&" );
    }
    
    /**
     *  Bitwise Or ( | ) 
     */
    public void or(){
        binaryOperation( "|" );     
    }
    
    /**
     *  Bitwise ( ^ )
     */
    public void xor(){
        binaryOperation( "^" );
    }
    
    /**
     *  Bitwise left shift ( < )
     */
    public void leftShift(){
        binaryOperation( "<" );
    }
    
    /**
     *  Bitwise right shift ( > )
     */
    public void rightShift(){
        binaryOperation( ">" );
    }
    
    /**
     *  Modulous ( % )
     */
    public void mod(){
        binaryOperation( "m" );
    }
    
    /**
     *  Raise the previous value to the 'power; of the next input value
     */
    public void pow(){
        binaryOperation( "p" );     
    }

    /**
     *  Perform any waiting binary operation and clear previous value
     */
    public void equals(){
        compute();
        toDo = 0;
        newOp = true;       
    }
    
    /*
     *  Setup registers for next input value
     */
    private void binaryOperation( final String op ){
        
        if( toDo == 0 ){
            keep = value;
        }else{
            compute();
        }
        
        value = 0;
        toDo = op.hashCode();
        resetDecimals();
        setDisplay();       
    }
    
    /*
     *  Perform a binary operation
     */     
    private void compute(){
        
        switch( toDo ){
            case '+':   value = keep + value;   break;
            case '-':   value = keep - value;   break;
            case '*':   value = keep * value;   break;
            case '/':
                if( value != 0 ){           // ignore divide by zero
                    value = keep / value;
                }
            case '&':   value = (int)keep & (int)value;     break;
            case '|':   value = (int)keep | (int)value;     break;
            case '^':   value = (int)keep ^ (int)value;     break;
            case '<':   value = (int)keep << (int)value;    break;
            case '>':   value = (int)keep >> (int)value;    break;
            case 'm':   value = keep % value;               break;
            case 'p':   value = Math.pow( keep, value );    break;                                  
        }       
                
        keep = value;
        setDisplay();       
    }
    
    /* -- Unary Operations -- */
    
    /**
     *  Compute the square of the current value
     */     
    public void sqrt(){
        value = Math.sqrt( value );
        unaryOperation();
    }
    
    /**
     *  Reverse the sign on the current value
     */
    public void sign(){
        value = value * -1;
        unaryOperation();
    }
    
    /**
     *  Convert the current value to a percent
     */
    public void percent(){
        value = value / 100;
        unaryOperation();
    }
    
    /**
     *  Convert the current value to it's reciprocal value
     */
    public void reciprocal(){
        if( value > 0 ){
            value = 1 / value;
        }else{
            value = 0;
        }
        unaryOperation();
    }       
    
    /**
     *  Compute the sine of the current value.
     */
    public void sin(){
        value = Math.sin( value );
        unaryOperation();
    }
    
    /**
     *  Compute the cosine of the current value
     */
    public void cos(){
        value = Math.cos( value );
        unaryOperation();
    }
    
    /**
     *  Compute the tan of the current value
     */
    public void tan(){
        value = Math.tan( value );
        unaryOperation();
    }
    
    /**
     *  Compute the asine of the current value
     */
    public void asin(){
        value = Math.asin( value );
        unaryOperation();
    }
    
    /**
     *  Compute the acosine of the current value
     */
    public void acos(){
        value = Math.acos( value );
        unaryOperation();
    }
    
    /**
     *  Compute the atan of the current value
     */
    public void atan(){
        value = Math.atan( value );
        unaryOperation();
    }

    /**
     *  Compute the log of the current value
     */
    public void log(){
        value = Math.log( value );
        unaryOperation();
    }

    /**
     *  Convert the current value to degrees
     */
    public void degrees(){
        value = Math.toDegrees( value );
        unaryOperation();
    }

    /**
     *  Convert the current value to radians
     */
    public void radians(){
        value = Math.toRadians( value );
        unaryOperation();
    }

    
    /*
     *  Setup flag to signal start of a new operation and
     *  set the display to match the value generated by a
     *  unary operation
     */
    private void unaryOperation(){
        newOp = true;
        setDisplay();
    }
        
    /* -- Control operations -- */  
    
    /**
     *  Delete the last entered digit
     */
    public void backspace(){
        display.deleteCharAt( display.length() - 1 );
        value = Double.parseDouble( display.toString() );
        setDisplay();
    }
    
    /**
     *  Clear all values
     */     
    public void clear(){
        display.delete( 0, display.length() );
        value = 0;
        keep = 0;
        toDo = 0;
        resetDecimals();
    }
    
    /**
     *  Clear the current value
     */
    public void clearEntry(){
        display.delete( 0, display.length() );
        value = 0;
        resetDecimals();
    }
    
    /*
     *  Reset the decimal flag and counter
     */ 
    private void resetDecimals(){
        inDecimals = false;
        decimalCount = 0;
    }
    
    /**
     *  Convert the current value to a formatted string for
     *  display
     */                 
    private void setDisplay(){
        if( value == 0 ){
            display.delete( 0, display.length() );
        }else{
            display.replace( 0, display.length(), df.format( value ) );
        }
    }
    
    /**
     *  Returns the current value as a decimal formatted string
     */         
    public String display(){        
        return display.toString();
    }   

}