//=================================================================================
// File MCalc.java jlouie 6.96 73270.1776@compuserve.com
// This file should be named MCalc.java and compiled with 'javac'/'java compiler'
// Title  ¥ MCalc v1.61 ¥
// Free if distributed as is with a functioning and unaltered about box.
// Thanks to W. Carlini, T. Ball, R. Scorer, W. Brogden & the folks at Java Support.
// Creates an GUI AWT Mortgage Calculator
// Compiled with: Sun's Mac JDK 1.02/ System 7.5.2
//             Sun's Win95 JDK 1.02
// Tested with:   Mac Netscape 2.0javab1 and 3.0b4
//                Sun's MacJDK Applet Viewer
//             Win95 Netscape 3.xx
//             Sun's Win95 Applet Viewer
//
// Known bugs and incompatabilities:
// OKbutton.requestFocus() is buggy on Mac
// This Applet does not work with Netscape 3.0b4 (Mac), see workaround in MCalc.init()
// Dialog boxes and Frame menus are not supported by Netscape 2.0jb1(Mac)
// Packed Dialog boxes do not display the last item under Netscape 3.0b4 (Mac)
// Modal Dialog boxes are non_modal under Netscape 2.01 (Win95)
// Key filter isDigit() is buggy with Netscape 3.0b4 (Win95)
// 
// Features not implemented:
// Responding to keyboard equivalents.
// Formatting numeric output (see Format() in "Core Java").
// Online help
//
// "New" Features:   Traps return key in Dialog Box (Win95)
//             Runs as an applet or application
//             Corrects an error in MyTextField initialization
//             "Supports" key filters in MyTextField  
//
// Ref: "Teach Yourself Java in 21 Days", Lemay & Perkins, Sams Net
// Ref: "Java in a Nutshell", David Flanagan, O'Reilly & Associates
// Ref: "Core Java", Cornell & Horstmann, Sunsoft Press
// Ref: "The Java Programming Language" Arnold & Gosling Addison Wesley
// General Ref: "Code Complete" Steve McConnell, Microsoft Press
//=================================================================================

import java.awt.*;

//=====================================================================
//
// Class ¥ MCalc ¥
//
// Applet class.  Adds a button to a web page.
// Will also execute as an application (requires Java Interpreter)
//
//======================================================================

public class MCalc extends java.applet.Applet
{  
   Button newCalc;
      
   public void init()  // applet entry point
   {
      //NewCalc();  // workaround Mac Netscape 3.0b4
      add(new Label("Mortgage Calculator:"));
      add(newCalc= new Button("New Calculator"));
   }
   
   public static void main(String[] input)  // application entry point
   {
      MCalc mc= new MCalc();
      System.out.println("Press Ctrl-C (Win95) or Apple-Q (Mac) to Quit the Interpreter.");
      mc.NewCalc();
   }
   
   public void NewCalc()
   {
         new MyFrame("MCalc");  
   }
      
   public boolean action(Event evt, Object obj) 
   {  
      if (evt.target == newCalc)
      {
            NewCalc();
            return true;
      }
      return super.action(evt, obj);   
   }
}


//============================================================================
//
// Class ¥ MyFrame ¥
//
// Calculator Input/Output Interface
//
//==========================================================================

class MyFrame extends Frame
{
   MyTextField[]  textArray;           // Array of four extended text boxes for input/output
   Label       messageBox;          // Calculator field for user feedback
   MyAbout     dl;                  // About Dialog Box
   MyError        err;              // Error Dialog Box
   double         dbArray[], dbResult= 0; // Array of double, stores numeric input
   Point       p;                // Stores location window        
   
   static final int NO_KEY_FILTER= 0;        // keyFilter Constants
   static final int NUMERIC_ONLY=   1;
   static final int DIGITS_ONLY=    2;
   
   
   MyFrame(String title)            //  Main Window Constructor
   {
      super(title);
      dbArray= new double[4];
      textArray= new MyTextField[4];
      setLayout(new GridLayout(6,2,10,10));
      add(new Label("A Mortgage Calculator", Label.RIGHT));
      messageBox= new Label("One Entry Must Be 0", Label.LEFT);
      add(messageBox);
      add(new Label("Principle: $", Label.RIGHT));
      textArray[0]= new MyTextField("",20, NUMERIC_ONLY);
      add(textArray[0]);
      add(new Label("Annual Interest (0-100): %", Label.RIGHT));
      textArray[1]= new MyTextField("",20, NUMERIC_ONLY);
      add(textArray[1]);
      add(new Label("Loan Period (Months): ", Label.RIGHT));
      textArray[2]= new MyTextField("",20, DIGITS_ONLY);
      add(textArray[2]);
      add(new Label("Monthly Payments: $", Label.RIGHT));
      textArray[3]= new MyTextField("0",20, NUMERIC_ONLY);
      add(textArray[3]);
      
      // we must instance MyTextFields _before_ this assignment
      textArray[0].nextField = textArray[1];
      textArray[0].prevField = textArray[3];
      textArray[1].nextField = textArray[2];
      textArray[1].prevField = textArray[0];
      textArray[2].nextField = textArray[3];
      textArray[2].prevField = textArray[1];
      textArray[3].nextField = textArray[0];
      textArray[3].prevField = textArray[2];
      
      Button OKbutton= new Button("OK");
      add(OKbutton);
      add(new Button("Cancel"));
      MenuBar mb= new MenuBar();
      Menu m= new Menu("File");
      m.add(new MenuItem("About MCalc..."));
      m.add(new MenuItem("Close"));
      mb.add(m);
      setMenuBar(mb);   
      p= location();
      this.move(p.x + 20, p.y + 20);
      this.resize(400,400);   // workaround 
      this.pack();         // workaround Win95 & Netscape
      Font f= OKbutton.getFont();
      f= new Font(f.getName(), Font.BOLD, f.getSize());
      OKbutton.setFont(f);
      OKbutton.setBackground(Color.white);
      this.setBackground(Color.lightGray);
      this.show();
      textArray[0].requestFocus();
   }
   
   //==================================
   // ¥ ParseString ¥
   //
   // Strips the input string of commas
   //
   // Use a StringBuffer to avoid multiple 
   // memory allocations
   //
   //==================================
   
   String ParseString(String inString)
   {
      StringBuffer   outString= new StringBuffer(inString.length());

      for (int i=0; i<inString.length(); i++)
      {
         if (inString.charAt(i) != ',')
            {outString.append(inString.charAt(i));}
      }
      return outString.toString();
   }
   
   //==============================
   //
   // ¥ handleEvent ¥
   //
   // Each interactive panel must handle it's events.
   // Call super.handleEvent() to call this panels action() method
   //
   //==============================
   
   public boolean handleEvent(Event event) // MyFrame
   {  
      if (event.id == Event.WINDOW_DESTROY)
      {
         if (this.isShowing()) this.hide();
         this.dispose();
         return true;   
      }
      return super.handleEvent(event);  // calls action
   }

   //==============================
   //
   // ¥ action ¥
   //
   // called by default handleEvent()
   // final attempt to trap return key here, workaround
   //
   //==============================
            
   public boolean action(Event evt, Object obj)  // MyFrame
   {
      char ch;
      String str= (String)obj;
      
      if (evt.target instanceof MenuItem)
      {
         ch= str.charAt(0);
         switch (ch)
         {
            case 'C':         // Cancel
               if (this.isShowing()) this.hide();
               this.dispose();
               return true;
            case 'A':
               DoAboutBox();     // About
               return true;
         }  
      }
      
      if (evt.target instanceof Button)
      {
         ch= str.charAt(0);
         switch (ch)
         {
            case 'O':            // OK       
               DoCalc();
               return true;
            case 'C':            // Cancel
               if (this.isShowing()) this.hide();
               this.dispose();
               return true;
         }
      }
      
      if (evt.target instanceof MyTextField)  // return key down in TextField
      { 
            MyTextField mtf= (MyTextField)evt.target;
            if (mtf != null)
            {
               //if ((evt.modifiers & Event.SHIFT_MASK) == Event.SHIFT_MASK)
               if (evt.shiftDown()) // traps shift and option-shift etc.
               {
                  if (mtf.prevField != null)
                  {
                     mtf.prevField.requestFocus();
                     mtf.prevField.selectAll(); 
                     return true;      
                  }              
               }
               else
               { 
                  if (mtf.nextField != null) 
                  {
                     mtf.nextField.requestFocus();
                     mtf.nextField.selectAll();
                     return true;      
                  }
               }
            }
      }     
      return super.action(evt, obj); 
   }
   
   
   //==================================
   // ¥ DoAlgo ¥ CalcInt ¥
   //
   // The actual amortization algorithm:
   //
   // m= P*i/(1-(1+i)^-N)
   // i=r/1200
   //
   // solving for i is non_trivial
   // Thanks to Dr. W. Carlini (& Euclid) for this formula!
   //
   //==================================
   
   // Euclid's theorem to the rescue.
   
   String CalcInt(double P, double N, double m)
   {
      String outString;
      
      double temp=(m/P), answer=(m/P), diff=100, num=0, den=0, accuracy=.00001;
      int i, maxIterations= 1000;
      
      try
      {
         for (i=0; ((diff>accuracy) && (i<maxIterations)); i++)
         {
            temp= answer;
            num= (P*temp/m)+Math.pow((1+temp), -N)-1;
            den= (P/m)-N*Math.pow((1+temp),(-N-1));
            //if (den==0) throw new ArithmeticException(); // not needed
            answer= temp- (num/den);
            diff= answer- temp;
            if (diff<0) diff= -diff;
         }
         if ((answer < 0) || (Double.isNaN(answer)) || (i == maxIterations))
            {throw new ArithmeticException();}
         outString= Double.toString((1200*answer)); // annual interest (0-100)
      }
      catch (ArithmeticException e) {outString= "Invalid Input";}
      return outString;
   }
   
   String DoAlgo(int field)
   {
      String   outString;
      double   db, temp= 0;
      double   P= dbArray[0];       // principle
      double   i= (dbArray[1]/1200);   // monthly percentage rate
      double   N= dbArray[2];          // loan period months
      double   m= dbArray[3];       // monthly payments
      
      outString= "Error";
      if (field<4)
      {
         try
         {  
            switch (field)
            {
               case 0: // Principle
                  db= 1+i;
                  db= 1/Math.pow(db, N);
                  db= ((1-db)/i)*m;
                  outString= Double.toString(db);
                  break;
               case 1:  // Annual Interest
                  outString= CalcInt(P, N, m);
                  break;
               case 2:  // Loan Period
                  db= (1-(P*i/m));
                  db= Math.log(db);
                  temp= 1+i;
                  db= -db/Math.log(temp);
                  outString= Double.toString(db);
                  break;
               case 3:  // Monthly Payments
                  db= 1+i;
                  db= 1/Math.pow(db, N);
                  db= (P*i)/(1-db);
                  outString= Double.toString(db);
                  break;
               default:;
               }
         }
         catch (ArithmeticException e) {;} // outString= "Error"
      }
      return outString; 
   }
   
   //==================================
   // ¥ DoCalc ¥
   //
   // Check for valid input, update the
   // window fields and then DoAlgo()
   //
   //==================================
   
   void DoCalc()
   {
      String   strArray[], strResult= "", tempString="";
      Double   db;
      int      count= 0, field= 0;
      boolean  isError= false;
      
      strArray= new String[4];
      
      for (int i=0; i<4; i++)
      {  
         strArray[i]= textArray[i].getText();      
         try
         {
            tempString= ParseString(strArray[i]); // strip commas
            db= Double.valueOf(tempString);  // get Double, may throw exception
            dbArray[i]= db.doubleValue();   // convert Double to double          
            if (dbArray[i]<0) throw new NumberFormatException();  // negative #
            
            switch (i)
            {
               case 0:  // Principle
               break;
               case 1:  // Annual Interest
                  if (dbArray[i]>100) 
                     {throw new NumberFormatException();} // over 100%
               break;
               case 2:  // Months
                  if ((int)dbArray[i] != dbArray[i])
                     {throw new NumberFormatException();}  // not an integer
               break;
               case 3:  // Monthly Payment
               break;
            }
            textArray[i].setText(Double.toString(dbArray[i])); // echo changes
         }
         catch (NumberFormatException e)
         {
            dbArray[i]= 0;
            isError= true;
            textArray[i].setText("Error");
         }
            
         if (dbArray[i]==0) 
         {
            count +=1;  //count zero entries
            field= i;   //last zero text field
         }
      }
      
      
      if (isError) 
      {
         DoDialogError("Invalid Entry");
         messageBox.setText("Invalid Entry");
      }
      else
      {
         if (count>1) 
         {
            DoDialogError("Too Many Zero Entries");
            messageBox.setText("Too Many Zero Entries");
         }
         else
         {
            if (count<1) 
            {
               DoDialogError("One Entry Must Be 0");
               messageBox.setText("One Entry Must Be 0");
            }
            else
            {
               strResult= DoAlgo(field);  // finally, do the math!
               textArray[field].setText(strResult);
               messageBox.setText("Done");
            }
         }
      }
   }
   
   //==========================
   //
   // ¥ DoAboutBox ¥
   //
   // Create an about box
   // We create a reference to the about box to avoid multiple instances
   // This workaround is necessary since Netscape 2.xx does not support modal dialogs
   //
   //==========================
   
   void DoAboutBox()
   {
      if (dl != null) // workaround for non-modal dialog Win95 netscape 2.01
            {
               if (dl.isShowing()) dl.hide();
               dl.dispose();
               dl= null;
            }
      dl= new MyAbout(this, "About Box", true); 
   }
   
   //==========================
   //
   // ¥ DoDialogError ¥
   //
   // Create an error dialog
   //
   //==========================
   
   void DoDialogError(String inString)
   {
      messageBox.setText("Error");  // respond to user while dialog loads
      if (err != null) //workaround for non-modal Win95 netscape
               {
                  if (err.isShowing()) err.hide();
                  err.dispose();
                  err= null;
               }
      err= new MyError(this,"Error Box", true, inString);
   }
}
   
   
//=====================================================================
//
// class ¥ MyTextField ¥
//
// we extend TextField to implement proper tab key behavior
// implements NO_KEY_FILTER, DIGITS_ONLY or NUMERIC_ONLY key filters
// isDigit() is buggy with Netscape 3.0b4 (Win95)
//
//=====================================================================

class MyTextField extends TextField
{
   MyTextField nextField= null, prevField= null;
   private int keyFilter;
   
   static final int NO_KEY_FILTER= 0;        // keyFilter Constants
   static final int NUMERIC_ONLY=   1;
   static final int DIGITS_ONLY=    2;
   static final int MIN_KEY=     0;
   static final int MAX_KEY=     DIGITS_ONLY;
   
   public MyTextField(String str, int col, int keyFilter)
   {
      super(str, col);           
      if ((keyFilter<MIN_KEY) || (keyFilter>MAX_KEY))
         {this.keyFilter = NO_KEY_FILTER;}
      else this.keyFilter= keyFilter;
   }
   
   
   //  overloaded constructor, default NO_KEY_FILTER
   
   public MyTextField(String str, int col)
   {
      super(str, col);
      this.keyFilter= NO_KEY_FILTER;   
   }

   //================================
   //
   // ¥ keyDown ¥
   //
   // override keyDown to trap tabs
   //
   //================================
   
   public boolean keyDown(Event evt, int key)
   {
      switch (key)
      {
         case '\b':     // backspace
            break;      // pass to super.keyDown()
         case '\n':     // ** fall through **
         case '\r':     // ** fall through **
         case '\t':                 
            //if ((evt.modifiers & Event.SHIFT_MASK) == Event.SHIFT_MASK)
            if (evt.shiftDown()) // traps shift and option-shift etc.
            {
               if (prevField != null)
               {
                  prevField.requestFocus();
                  prevField.selectAll(); 
                  return true;      
               }              
            }
            else
            {
               if (nextField != null) 
               {
                  nextField.requestFocus();
                  nextField.selectAll();
                  return true;      
               }
            }
            break;  // not handled, pass to super
         default: // not tab, return or backspace
            switch(keyFilter)
            {
               case DIGITS_ONLY:
                  if (! IsDigit(key)) return true;    // handle & ignore key input
                  break;                        // pass to super
               case NUMERIC_ONLY:
                  if (! IsNumeric(key)) return true;  // handle & ignore key input
                  break;                              
               // NO_KEY_FILTER, pass to super
            } 
      }
      return super.keyDown(evt, key);
   }
   
   //===============================
   //
   // ¥ isDigit ¥
   //
   // returns true if key is digits 0-9
   //    buggy with Netscape 3.0b4 (Win95)
   //
   //
   //===============================
   
   public boolean IsDigit(int key)
   {
      if (((key < '0') || (key >'9')) ) return false;
      else return true;
   }
   
   //===============================
   //
   // ¥ isNumeric ¥
   //
   // returns true if key is period, comma, or digits 0-9
   //
   //===============================
   
   public boolean IsNumeric(int key)
   {  
      if ((key=='.') || (key==',') || ((key>='0') & (key<='9')) )
          {return true;}
      else return false;
   }

}

//=====================================================================
//
// class ¥ MyAbout ¥
//
// Create our About Dialog Box
//
//======================================================================

class MyAbout extends Dialog
{
   Button   OKbutton;
   
   MyAbout(Frame inFrame, String inString, boolean inBoolean)
   {
      super(inFrame, inString, inBoolean);      
      setLayout(new GridLayout(3,1));
      add(new Label("A Java AWT Applet! jlouie", Label.CENTER));
      OKbutton= new Button("OK");
      Panel pl= new Panel();
      pl.setLayout(new FlowLayout(FlowLayout.CENTER,15,15));
      pl.add(OKbutton);
      this.add(pl);
      this.add(new Label("73270.1776@compuserve.com", Label.CENTER));
      // will not display under Netscape 3.0b4 (Mac)
      Point p=location();
      this.move(p.x +20, p.y +20);
      this.resize(200,200); // workaround Win95 & Netscape
      this.pack();  // workaround Win95 & Netscape
      Font f= OKbutton.getFont();
      f= new Font(f.getName(), Font.BOLD, f.getSize());
      OKbutton.setFont(f);
      OKbutton.setBackground(Color.white);
      this.setBackground(Color.lightGray);
      this.show();
      OKbutton.requestFocus();   
   }
   
   public boolean handleEvent(Event event)
   {
      switch (event.id)
      {
         case Event.WINDOW_DESTROY:
            this.hide(); // work around Win95 non_modal dialog
            return true;
         case Event.KEY_PRESS:   
            if (event.key == '\n')  // return key
            {
               this.hide();
               return true;
            }  
      }
      return super.handleEvent(event);  // calls action
   }
   
   public boolean action(Event evt, Object obj)
   {
      if (evt.target == OKbutton)
      {
         if (this.isShowing()) this.hide();
         // DoDialogError will this.dispose()
      }
      return true;  // trap all actions
   }
}

//=====================================================================
//
// class ¥ MyError ¥
//
// Create our error dialog box
// We create a reference to the error box to avoid multiple instances
// This workaround is necessary since Netscape 2.xx does not support modal dialogs
//
//======================================================================

class MyError extends Dialog
{
   Button   OKbutton;
   Font  f;
   
   MyError(Frame inFrame, String inString, boolean inBoolean, String errString)
   {
      super(inFrame, inString, inBoolean);      
      setLayout(new GridLayout(3,1));
      //setBackground(Color.blue);
      this.add(new Label(errString,Label.CENTER));
      OKbutton= new Button("OK");
      Panel pl= new Panel();
      pl.setLayout(new FlowLayout(FlowLayout.CENTER,15,15));
      pl.add(OKbutton);
      this.add(pl);
      this.add(new Label("ERROR", Label.CENTER));  
      // will not display under Netscape 3.0b4 (Mac)
      Point p=location();
      this.move(p.x +20, p.y +20);
      this.resize(200,200);  // workaround
      this.pack();  // workaround Win95 & Netscape
      Font f= OKbutton.getFont();
      f= new Font(f.getName(), Font.BOLD, f.getSize());
      OKbutton.setFont(f);
      OKbutton.setBackground(Color.white);
      this.setBackground(Color.lightGray);
      this.show();
      OKbutton.requestFocus();   
   }
   
   public boolean handleEvent(Event event)
   {
      switch (event.id)
      {
         case Event.WINDOW_DESTROY:
            this.hide(); // work around Win95 non_model dialog
            // DoError will this.dispose()
            return true;
         case Event.KEY_PRESS:   
            if (event.key == '\n')  // return key
            {
               this.hide();
               return true;
            }  
      }
      return super.handleEvent(event); // calls action
   }
   
   public boolean action(Event evt, Object obj)
   {
      if (evt.target == OKbutton)
      {
         if (this.isShowing()) this.hide();
         // DoError will this.dispose()   
      }
      return true;  // modal dialog traps all actions
   }  
}

