/*
 *  CALC      Provides a calculator that opens on the active screen when
 *            you press a specific key sequence.  Otherwise, the program
 *            waits quitely in the background.
 *
 *              Copyright 1989 by Davide P. Cervone.
 *  You may use this code, provided this copyright notice is kept intact.
 */

#define INTUITION_PREFERENCES_H         /* don't need 'em */
#include <intuition/intuition.h>
#include <proto/intuition.h>
#include <proto/dos.h>
#include <proto/graphics.h>
#include <proto/exec.h>

#include "cHandler.h"
#include "cKeys.h"

/*
 *  ASCII codes for some special keys
 */

#define CTRLC       3
#define BACKSPACE   8
#define ENTER       13

/*
 *  Delay() time for keypress to invert corresponding gadget
 */

#define KEYPAUSE    4L

/*
 *  TINYVALUE is the round-off value for the display.
 *  BIGVALUE is the largest value that can be dispayed or entered.
 */

#define NODECIMAL   -1
#define TINYVALUE   0.000000000001
#define BIGVALUE    999999999999.0

/*
 *  MAXDISPLAY is the number of characters that are in the display.
 *  MAXSTACK is the size of the pending operation stack (includes
 *  parenthesies).
 */

#define MAXDISPLAY  13
#define MAXSTACK    20


/*
 *  Standard character width for Topaz 8-pt font
 */

#define CHARW       8


static struct IntuiText *DisplayIText = &KeyText[KEY_DISP];

static short DisplayPos;                /* The position in the DisplayBuffer */
static double DisplayValue;             /* The value on the display */
static double DecimalPower;             /* The power of ten for next digit */
static int DecimalPlace = NODECIMAL;    /* Decimal place for next digit */
static int NoDigit = TRUE;              /* TRUE if digits not yet entered */
static int ErrorStatus;                 /* TRUE if an error occured */

/*
 *  The codes for each of the pending operations that can appear on the stack
 */

#define OP_NONE     0
#define OP_PLUS     1
#define OP_MINUS    2
#define OP_TIMES    3
#define OP_DIVIDE   4
#define OP_LPAREN   5
#define OP_RPAREN   6
#define OP_EQUAL    7
#define OP_PAREN    8


/*
 *  The precedence values for each of the stack operations.
 */

static UBYTE Precedence[] = {0,4,4,5,5,6,2,1,3};
static UBYTE Op[MAXSTACK];          /* The pending operation stack */
static double Val[MAXSTACK];        /* The pending value stack */
static short StackTop;              /* pointer to top of stack */


/*
 *  ClearDisplay()
 *
 *  Clears the display buffer and sets all the flags to indicate that we
 *  are now entering a number from the keypad.
 */

static void ClearDisplay()
{
   NoDigit = FALSE;
   DecimalPlace = NODECIMAL;
   DecimalPower = 0.0;
   DisplayValue = 0.0;
   DisplayPos = 1;
   DisplayBuffer[DisplayPos] = '0';
   DisplayBuffer[DisplayPos+1] = 0;
   DisplayIText->IText = &DisplayBuffer[1];
   DisplayIText->LeftEdge = DISPLAYIW - CHARW;
   ErrorStatus = FALSE;
}


/*
 *  RefreshDisplay()
 *
 *  Refresh the DisplayGadget
 */

static void RefreshDisplay()
{
   RefreshGList(&CalcGadget[KEY_DISP],CalcWindow,NULL,1L);
}


/*
 *  DisplayError()
 *
 *  Put the error message in the display buffer, and set the display
 *  gadget's left edge so that the message is centered.  Refresh the
 *  display.  Set the error status flag, and clear the stack.
 */

static void DisplayError(s)
char *s;
{
   ClearDisplay();
   NoDigit = TRUE;
   strcpy(DisplayIText->IText,s);
   DisplayIText->LeftEdge = (DISPLAYIW-strlen(DisplayIText->IText)*CHARW+1) / 2;
   RefreshDisplay();
   ErrorStatus = TRUE;
   StackTop = 0;
}


/*
 *  AddToDisplay()
 *
 *  Check for buffer overflow.  If none, then add character into the display
 *  buffer and adjust the display position (but don't add non-significant
 *  zero digits).  Refresh the display on the screen and cancel any errors.
 */

static int AddToDisplay(c)
char c;
{
   if (DisplayPos == MAXDISPLAY ||
       DisplayValue > BIGVALUE || DisplayValue < -BIGVALUE)
   {
      DisplayError("Overflow");
   } else {
      if (DisplayValue != 0.0 || DisplayBuffer[DisplayPos] != '0' ||
          DecimalPlace != NODECIMAL) DisplayIText->LeftEdge -= CHARW;
      DisplayBuffer[DisplayPos++] = c;
      DisplayBuffer[DisplayPos] = 0;
      if (DisplayValue == 0.0 && c == '0' && DecimalPlace == NODECIMAL)
         DisplayPos--;
      RefreshDisplay();
      ErrorStatus = FALSE;
   }
   return(ErrorStatus == FALSE);
}


/*
 *  SetDisplay()
 *
 *  Sets the display to the given value.
 *  Check for overflow.  If none, then
 *  Set the display value and the current display position.
 *  If the value is near zero, set the display to zero,
 *  Otherwise
 *    If the value is negative, add a minus sign and make the value positive.
 *    divide the value by the power of ten that makes its integer part only
 *       one digit and add a small offset so that we round up.
 *    If the number is less than 1.0, then add '0.' and as many zeros as
 *       needed to get to the first non-zero decimal digit.
 *    Mark the last non-zero decimal place.
 *    As long as the value is still non-zero,
 *      Find the first digit of the number (TINYVALUE is for round-off errors)
 *      Multiply by ten to get next digit ready.
 *      Add a decimal place if it is time.
 *      Check if the digit is non-zero (or a non-trivial zero)
 *      Decrement the digits still before the decimal place.
 *      If the display is filled, cancel the loop.
 *  NULL-terminate the display string.
 *  Calculate the display offset, and refresh the display.
 *  Set the flags.
 */

static void SetDisplay(v)
double v;
{
   char c;
   short digits,LastNonZero;
   extern double pow(),log10();

   if (v > BIGVALUE || v < -BIGVALUE)
   {
      DisplayError("Overflow");
   } else {
      DisplayValue = v;
      DisplayPos = 1;
      DisplayIText->IText = &DisplayBuffer[1];
      if (v <= TINYVALUE && v >= -TINYVALUE)
      {
         DisplayBuffer[DisplayPos++] = '0';
         DisplayBuffer[DisplayPos++] = '.';
         LastNonZero = DisplayPos;
      } else {
         if (v < 0.0)
         {
            DisplayBuffer[DisplayPos++] = '-';
            v = -v;
         }
         digits = log10(v);
         v = v / pow(10.0,(double)digits) + (5.0*TINYVALUE);
         if (digits < 0)
         {
            DisplayBuffer[DisplayPos++] = '0';
            DisplayBuffer[DisplayPos++] = '.';
            LastNonZero = DisplayPos;
            while (++digits) DisplayBuffer[DisplayPos++] = '0';
            digits = -1;
         } else {
            LastNonZero = DisplayPos;
         }
         while (v > TINYVALUE)
         {
            c = v + TINYVALUE; v = (v-c) * 10.0;
            DisplayBuffer[DisplayPos++] = c + '0';
            if (digits == 0 && DisplayPos <= MAXDISPLAY)
               DisplayBuffer[DisplayPos++] = '.';
            if (c || digits >= 0) LastNonZero = DisplayPos;
            digits--;
            if (DisplayPos > MAXDISPLAY) v = 0.0;
         }
      }
      DisplayBuffer[LastNonZero] = 0;
      DisplayIText->LeftEdge = DISPLAYIW - (LastNonZero-1)*CHARW;
      RefreshDisplay();
      ErrorStatus = FALSE;
      NoDigit = TRUE;
      DecimalPlace = NODECIMAL;
   }
}


/*
 *  ClearEntry()
 *
 *  Clear the display.  Set the NoDigit flag to TRUE so that the zero we are
 *  now displaying will not be part of the displayed number.
 */

static void ClearEntry()
{
   ClearDisplay();
   NoDigit = TRUE;
   RefreshDisplay();
}


/*
 *  DoFunction()
 *
 *  Do the operation that's on the stack using the value on the top of the
 *  stack as the first operand and the display value as the second one.
 *  (Check for division by zero errors)
 */
 
static void DoFunction()
{
   switch(Op[StackTop])
   {
      case OP_PLUS:
         DisplayValue = Val[StackTop] + DisplayValue;
         break;

      case OP_MINUS:
         DisplayValue = Val[StackTop] - DisplayValue;
         break;

      case OP_TIMES:
         DisplayValue = Val[StackTop] * DisplayValue;
         break;

      case OP_DIVIDE:
         if (DisplayValue != 0.0)
            DisplayValue = Val[StackTop] / DisplayValue;
           else
            DisplayError("Zero Division");
         break;
   }
}


/*
 *  DoOperation()
 *
 *  Perform any pending operations of higher (or equal precedence) up to 
 *  an open parenthesis (the bottom of the stack is OP_NONE).
 *  Convert a left-paren to OP_PAREN (the left paren has highest precedence
 *    so it will not cause other operations to be performed, but when on
 *    the stack, we want it to have the lowest precedence, so it will not be
 *    poped until a right paren or an equal.
 *  If the operation is not an equal or right paren (precedence 1 and 2) then
 *    Push the operation on the stack (we need to get the next operand)
 *    Push the current display value onto the stack.
 *    (If there is a stack overflow, beep the screen).
 */

static void DoOperation(theOp)
int theOp;
{
   while (Precedence[theOp] <= Precedence[Op[StackTop]] && theOp != OP_NONE)
   {
      if (Op[StackTop] == OP_PAREN && theOp != OP_EQUAL) 
         theOp = OP_NONE;
        else
         DoFunction();
      if (StackTop) StackTop--;
   }

   if (theOp == OP_LPAREN) theOp = OP_PAREN;
   if (Precedence[theOp] > 2)
   {
      if (StackTop < MAXSTACK-1)
      {
         Op[++StackTop] = theOp;
         Val[StackTop]  = DisplayValue;
      } else {
         DisplayBeep(CalcScreen);
      }
   }
}


/*
 *  DoGadget()
 *
 *  The gadget ID indicates which function has been pressed.
 *  For a dot, if we don't already have a dot, then
 *    If no other digits have been entered, clear the display value.
 *    If the current value is zero, add a zero to the display.
 *    Set the decimal place counters
 *    Add a dot to the display.
 *  For one of the function keys, parens, or equal,
 *    Set NoDigit to indicate a calculated result, and clear the decimal flag.
 *    Do the operation indicated by the gadget ID.
 *    If an error did not occur, display the result.
 *  For the sign change key,
 *    If the number is a result, show it's negative,
 *    Otherwise, if the displayalue is not zero, then 
 *      If the displayed value is already negative, 
 *        Change the IText pointer to be past the negative,
 *        Fix the IText left edge,
 *      Otherwise
 *        Change the IText pointer to include the negative,
 *        Fix the IText left edge,
 *      Refresh the display.
 *  For the Root key,
 *    If the current value is negative, display an error.
 *    otherwise display the square root.
 *  For the Percent key,
 *    If the number has been entered (not caluclated)
 *      divide by 100
 *      If the pending operation is + or -, then change display to 
 *        the given percent of the pending operand.
 *    Display the result.
 *  For the Clear key,
 *    Clear the stack of all pending operations,
 *    and display the a zero.
 *  For the Clear Entry key, clear the entry.
 *  For a numeric key (the GadgetID tells which number was pressed),
 *    If this is the first digit,
 *      Clear the display area,
 *      Set the display to the number that was pressed, and
 *      Add the correct digit to the display.
 *    Otherwise
 *      If the value contains a decimal place,
 *        increase the decimal power of the current digit position,
 *        and increase the decimal place counter for the display.
 *        Set the display value to the current value plus the number pressed
 *           (shifted to the proper decimal place),
 *        Add the digit to the display.
 *      Otherwise (no decimal place yet)
 *        multiply the current value by 10 and add the number pressed
 *         Add the digit to the display.
 */

void DoGadget(theGadget)
struct Gadget *theGadget;
{
   if (theGadget)
   {
      switch(theGadget->GadgetID)
      {
         case KEY_DOT:
            if (DecimalPlace == NODECIMAL)
            {
               if (NoDigit || DisplayValue == 0.0)
               {
                  ClearDisplay();
                  DisplayPos++;
               }
               DecimalPlace = 0; 
               DecimalPower = 1.0;
               AddToDisplay('.');
            }
            break;

         case KEY_PLUS:
         case KEY_MINUS:
         case KEY_TIMES:
         case KEY_DIVIDE:
         case KEY_LPAREN:
         case KEY_RPAREN:
         case KEY_EQUAL:
            NoDigit = TRUE;
            DecimalPlace = NODECIMAL;
            DoOperation(theGadget->GadgetID - KEY_PLUS + 1);
            if (ErrorStatus == FALSE) SetDisplay(DisplayValue);
            break;
         
         case KEY_SIGN:
            if (NoDigit)
            {
               SetDisplay(-DisplayValue);
            } else if (DisplayValue > TINYVALUE || DisplayValue < -TINYVALUE) {
              if (DisplayIText->IText[0] == '-')
              {
                 DisplayIText->IText++;
                 DisplayIText->LeftEdge += CHARW;
              } else {
                 DisplayIText->IText--;
                 DisplayIText->LeftEdge -= CHARW;
              }
              DisplayValue = -DisplayValue;
              RefreshDisplay();
            }
            break;

         case KEY_SQRT:
            if (DisplayValue < 0.0)
               DisplayError("Imaginary");
              else
               SetDisplay(sqrt(DisplayValue));
            break;

         case KEY_PERCENT:
            if (NoDigit == FALSE)
            {
               DisplayValue /= 100.0;
               if (Op[StackTop] == OP_PLUS || Op[StackTop] == OP_MINUS)
                  DisplayValue *= Val[StackTop];
            }
            SetDisplay(DisplayValue);
            break;

         case KEY_CLEAR:
            StackTop = 0;
            ClearEntry();
            break;

         case KEY_DISP:
            if (NoDigit == FALSE) ClearEntry();
            break;
            
         default:
            if (NoDigit)
            {
               ClearDisplay();
               if (AddToDisplay(theGadget->GadgetID+'0'))
                  DisplayValue = (double)theGadget->GadgetID;
            } else {
               if (DecimalPlace != NODECIMAL)
               {
                  DecimalPlace++;
                  DecimalPower *= 10.0;
                  if (AddToDisplay(theGadget->GadgetID+'0'))
                     DisplayValue += theGadget->GadgetID / DecimalPower;
               } else {
                  if (AddToDisplay(theGadget->GadgetID+'0'))
                     DisplayValue = DisplayValue*10.0 + theGadget->GadgetID;
               }
            }
            break;
      }
   }
}


/*
 *  DoKey()
 *
 *  Check the key code for special characters:
 *  CTRL-C means close the calculator (return FALSE).
 *  ENTER key is converted to the equal sign (ENTER also equals RETURN).
 *  Look through the gadget list for a gadget that has this character
 *    in it's UserData field.  This is the gadget associated with this key code.
 *  If one is found, then
 *    complement the gadget that was "pressed"
 *    do the gadget's function
 *    delay for the key-press delay period (so that we can see the gadget
 *      while it is complemented)
 *    and un-complement the gadget.
 */

int DoKey(Code)
int Code;
{
   struct Gadget *theGadget = &CalcGadget[0];
   int x,y,w,h;
   
   if (Code == CTRLC) return(FALSE);
   if (Code == BACKSPACE && NoDigit == FALSE) ClearEntry();
   if (Code == ENTER) Code = '=';

   while (theGadget && Code != (int)theGadget->UserData)
      theGadget = theGadget->NextGadget;
   if (theGadget)
   {
      SetDrMd(rp,COMPLEMENT);
      x = theGadget->LeftEdge;
      y = theGadget->TopEdge;
      w = x + theGadget->Width - 1;
      h = y + theGadget->Height - 1;
      RectFill(rp,x,y,w,h);
      DoGadget(theGadget);
      Delay(KEYPAUSE);
      RectFill(rp,x,y,w,h);
   }
   return(TRUE);
}
