// Face Paint 1.0
// December 1998
// Jonathan Blocksom, blocksom@gollygee.com
// Copyright (c) 1998 GollyGee Software, Inc.
// 
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation.
//
//  This program is distributed in the hope that it will be useful,
//  but without any warranty; without even the implied warranty of
//  merchantability or fitness for a particular purpose.  See the
//  GNU General Public License for more details, found in the file
//  license.txt which should have been distributed with this program.
//
// The skeleton of this program was based on the GNU Pilot
// SDK tutorial program tex2hex.

#pragma pack(2)
#include <Common.h>
#include <System/SysAll.h>
#include <UI/UIAll.h>

#include "app.h"
#define PILOT_APPID    'Face'   

//
// Function prototypes
//

// Event handling/control flow functions
static int initApplication(void);
static void eventLoop(void);
static void stopApplication(void);
static Boolean handleFrmEvent(EventPtr event);
void handlePenDown(EventPtr event);
int handleBtnEvent(EventPtr event);
void handlePenMove(EventPtr event);

// Face graphics stuff
void drawFace();
void drawCurrentFace();
void drawDot(int x, int y);
void eraseDot(int x, int y);

// Standalone graphics routines
void jbWinDrawPoint(int x, int y);
void drawCircle(int centerX, int centerY, int radius);
void drawCirclePoints(int centerX, int centerY, int x, int y);
void drawBezierCurves(int *curvePts, int numCurves);

void swapDrawMode();

#define FACEAREA_L 0
#define FACEAREA_U 16
#define FACEAREA_W 160
#define FACEAREA_H 129

RectangleType faceAreaRect;    // Area on screen user can draw on
RectangleType saveAreaRect;    // Offscreen area to save work while
                               // switching faces

// Bezier curve data for faces:
int face2Data[] = {
    80, 15,  90, 15,   105, 15,  105, 55,
    80, 15,  70, 15,   55, 15,   55, 55,
    105, 55, 100, 85, 90, 105,  80, 105,
    55, 55,  60, 85,  70, 105,  80, 105,
    75, 57,  70, 70,   75, 73,   80, 73,
    85, 57,  90, 70,   85, 73,   80, 73
};
#define NUMFACE2CURVES 6

int face3Data[] = {
    80, 20, 60, 20, 40, 50, 40, 80,
    40, 80, 40, 85, 40, 90, 50, 100,
    50, 100, 60, 110, 70, 110, 80, 110,
    80, 20, 100, 20, 120, 50, 120, 80,
    120, 80, 120, 85, 120, 90, 110, 100,
    110, 100, 100, 110, 90, 110, 80, 110,
    // Frown
    60, 90, 70, 85, 90, 85, 100, 90

};
#define NUMFACE3CURVES 7



#define MAX_FACES 3
int currentFace=0;
#define nextFace() \
{ currentFace++; if (currentFace>=MAX_FACES) currentFace=0; }

// Draw mode stuff
#define PENCIL_MODE 1
#define ERASER_MODE 0

int currentDrawMode = PENCIL_MODE;


WinHandle screenWindow;
WinHandle saveWindows[MAX_FACES];  
// handles to the off-screen windows used to save work


//
//  Event handling, start/stop code
//

DWord  
PilotMain(
    Word cmd, 
    Ptr cmdPBP, 
    Word launchFlags)
// Launch point.  Only runs on a standard launch,
// doesn't do anything if it's a find or other fancy thing
{
    int wasError;

    if (cmd == sysAppLaunchCmdNormalLaunch) {
        wasError = initApplication();
        if (wasError != 0) return wasError;
        eventLoop();
        stopApplication();
    }
    return 0;
}


int 
initApplication(void)
// Initializes global variables
{
    int i; 
    Word error;

    // initialize window rectangles
    RctSetRectangle(&faceAreaRect, FACEAREA_L, FACEAREA_U, FACEAREA_W, FACEAREA_H);
    RctSetRectangle(&saveAreaRect, 0, 0, FACEAREA_W, FACEAREA_H);
    screenWindow = WinGetDisplayWindow();

    // Create offscreen windows and draw blank faces in them
    for (i=0; i<MAX_FACES; i++) {
        saveWindows[i] = WinCreateOffscreenWindow(
            FACEAREA_W, FACEAREA_H, screenFormat, &error);
        if (error != 0) return 1;
        WinSetDrawWindow(saveWindows[i]);
        WinEraseRectangle(&saveAreaRect, 0);
        drawFace();
        nextFace();
    }

    WinSetDrawWindow(screenWindow);
    FrmGotoForm(mainFormID);
    return 0;
}


static void 
eventLoop(void)
{
    short err;
    int formID;
    FormPtr form;
    EventType event;
    int done=0;

    while (!done) {
        EvtGetEvent(&event, 100);

        if (SysHandleEvent(&event)) continue;
        if (MenuHandleEvent((void *)0, &event, &err)) continue;

        if (event.eType == appStopEvent) done=1;

        if (event.eType == frmLoadEvent) {
            formID = event.data.frmLoad.formID;
            form = FrmInitForm(formID);
            FrmSetActiveForm(form);
            if (formID == mainFormID) {
                FrmSetEventHandler(form, (FormEventHandlerPtr) handleFrmEvent);
            }       
        }
        
        if (event.eType == penDownEvent) {
            handlePenDown(&event);
        }
        if (event.eType == penMoveEvent) {
            handlePenMove(&event);
        }

        FrmDispatchEvent(&event);
    }
}

void
handlePenDown(EventPtr event)
{
    RectangleType tmpClip;
    int x, y;
  
    x = event->screenX;
    y = event->screenY;
    if (RctPtInRectangle(x, y, &faceAreaRect)) {
        // Store the old clip rectangle
        WinGetClip(&tmpClip);
        // Set the clip rectangle
        WinSetClip(&faceAreaRect);

        if (currentDrawMode == PENCIL_MODE) {
            drawDot(x, y);
        } else {
            eraseDot(x, y);
        }

        WinSetClip(&tmpClip);
    } else {
        // See if it was in the pencil bitmap area
        // Not great that this is hardcoded, oh well.
        if ((x >= 0) && (x <= 31) &&
            (y >= BUTTONROW_TOP) && (y <= 159)) {
            swapDrawMode();
        }
    }
}

void
swapDrawMode()
// Called to swap the draw mode from pencil to eraser or
// vice versa.  Switches mode flag and updates display.
{
    RectangleType pencilRect;

    // Set the new mode
    if (currentDrawMode == PENCIL_MODE) 
        currentDrawMode = ERASER_MODE;
    else 
        currentDrawMode = PENCIL_MODE;

    // Change the mode indicator
    RctSetRectangle(&pencilRect, 0, BUTTONROW_TOP, 32, 16);
    WinInvertRectangle(&pencilRect, 0);
}

void
handlePenMove(EventPtr event)
{
    // If it's in the draw region, add a dot
    if (RctPtInRectangle(event->screenX, event->screenY, &faceAreaRect)) {
        handlePenDown(event);
    }
}

static void 
stopApplication(void)
{
    int i;

    // Delete all the windows we opened
    for (i=0; i<MAX_FACES; i++) {
        WinDeleteWindow(saveWindows[i], false);
    }
}

static Boolean 
handleFrmEvent(
    EventPtr event)
// Handle events for the main form
{
    FormPtr form;
    int handled = 0;

    switch (event->eType) {
        case frmOpenEvent:
            form = FrmGetActiveForm();
            FrmDrawForm(form);
            drawCurrentFace();
            handled = 1;
            break;
        

        case frmUpdateEvent:
            // redraw form
            form = FrmGetActiveForm();
            FrmDrawForm(form);
            drawCurrentFace();
            handled = 1;
            break;
        
        case ctlSelectEvent:
            handled = handleBtnEvent(event);
            break;

        case menuEvent:
            switch (event->data.menu.itemID) {
                case menuitemID_about:
                    FrmAlert(alertID_about);
                    break;
            }
            handled = 1;
            break;

        case nilEvent:
            handled = 1;
            break;
    }
    
    return handled;
}


int
handleBtnEvent(
    EventPtr event)
{
    if (event->data.ctlEnter.controlID == buttonID_clear) {
        // Copy entire face from saved window
        WinCopyRectangle(
            saveWindows[currentFace], screenWindow,
            &saveAreaRect, FACEAREA_L, FACEAREA_U, scrCopy);
        // Draw a fresh face in the save window
        WinSetDrawWindow(saveWindows[currentFace]);
        WinEraseRectangle(&saveAreaRect, 0);
        drawFace();
        // Go back to the regular window
        WinSetDrawWindow(screenWindow);
        // Make sure we're in draw mode
        if (currentDrawMode == ERASER_MODE) swapDrawMode();
    } else if (event->data.ctlEnter.controlID == buttonID_face) {
        // Save work on current face
        WinCopyRectangle(
            screenWindow, saveWindows[currentFace],
            &faceAreaRect, 0, 0, scrCopy);
        // Go to the next face and load it from the saved area
        nextFace();
        WinCopyRectangle(
            saveWindows[currentFace], screenWindow,
            &saveAreaRect, FACEAREA_L, FACEAREA_U, scrCopy);
        // Draw a fresh face in the new current faces save area
        WinSetDrawWindow(saveWindows[currentFace]);
        WinEraseRectangle(&saveAreaRect, 0);
        drawFace();
        WinSetDrawWindow(screenWindow);
        // Make sure we're in draw mode
        if (currentDrawMode == ERASER_MODE) swapDrawMode();
    }
    return 1;
}


//
// Face drawing routine
// 

void
drawCurrentFace()
// Copies what is presumably the appropriate face in the 
// current faces save window to the screen
{
    WinCopyRectangle(
        saveWindows[currentFace], screenWindow,
        &saveAreaRect, FACEAREA_L, FACEAREA_U, scrCopy);
}
    
void
drawFace()
// Here is the actual code that draws the lines, circles,
// and curves that compose the faces.  
{
    switch(currentFace) {
        case 0:  // This is the big smiley face
            // Face border
            drawCircle(80, 65, 50);
            // eyes
            drawCircle(60, 45, 8);
            drawCircle(100, 45, 8);
            // Mouth
            WinDrawLine(50, 85, 60, 95);
            WinDrawLine(60, 95, 70, 100);
            WinDrawLine(70, 100, 90, 100);
            WinDrawLine(90, 100, 100, 95);
            WinDrawLine(100, 95, 110, 85);
            // Nose 
            WinDrawLine(80, 55, 90, 75);
            WinDrawLine(90, 75, 75, 75);
            break;
        case 1:   // The thin face
            drawBezierCurves(face2Data, NUMFACE2CURVES);
            drawCircle(70, 45, 5);
            drawCircle(90, 45, 5);
            WinDrawLine(75, 85, 85, 85);
            break;
        case 2:   // The unhappy face
            drawBezierCurves(face3Data, NUMFACE3CURVES);
            drawCircle(70, 55, 6);
            drawCircle(90, 55, 6);
            WinDrawLine(75, 70, 80, 65);
            WinDrawLine(80, 65, 85, 70);
            break;
        default:
            // Don't know what to draw!
            break;
    }
}

void 
drawDot(
    int x, int y)
// Called to draw a little dot where the stylus is.
{
    // Draw a circular dot:    
    WinDrawLine(x,   y-1, x+1, y-1);   //    **
    WinDrawLine(x-1, y,   x+2, y);     //   ****
    WinDrawLine(x-1, y+1, x+2, y+1);   //   ****
    WinDrawLine(x,   y+2, x+1, y+2);   //    **
}

void 
eraseDot(
    int x, int y)
// Called to erase a little dot where the stylus is.
// Erases an area and then recopies the blank face onto
// the erased area, so the face outline is always there.
{
    RectangleType rect;
    RectangleType rect2;

    // Draw a circular dot:    
    WinEraseLine(x,   y-1, x+1, y-1);   //    **
    WinEraseLine(x-1, y,   x+2, y);     //   ****
    WinEraseLine(x-1, y+1, x+2, y+1);   //   ****
    WinEraseLine(x,   y+2, x+1, y+2);   //    **
    // Copy over region from saved face
    RctSetRectangle(&rect, x-1, y-1, 4, 4);
    RctSetRectangle(&rect, x-1, y-1-FACEAREA_U, 4, 4);
    WinCopyRectangle(
        saveWindows[currentFace], screenWindow,
         &rect, x-1, y-1, scrOR); 
}

//
//  Standalone graphics routines
//  Feel free to use if you'd like
//

void
drawCircle(
    int centerX, int centerY, 
    int radius)
//  Circle drawing routine
//  Bresenham midpoint circle algorithm
//  Algorithm from from Foley et al. 2nd edition, pg. 83 or so
{
    int x, y, d, deltaE, deltaSE;
 
    x = 0;
    y = radius;
    d = 1 - radius;
    deltaE = 3;
    deltaSE = 5 - (radius<<1);  
    drawCirclePoints(centerX, centerY, x, y);

    while (y>x) {
        if (d < 0) {
            d+=deltaE;  deltaE+=2;  deltaSE+=2;
            x++;  
        } else {
            d+=deltaSE;  deltaE+=2;  deltaSE+=4;
            x++; y--;
        }
        drawCirclePoints(centerX, centerY, x, y);
    }
}



void
drawCirclePoints(
    int cx, int cy, 
    int x, int y)
// drawCirclePoints -- Helper function for drawCircle 
// draws eight symmetrical points of a circle
{
    jbWinDrawPoint(cx + x, cy + y);
    jbWinDrawPoint(cx + x, cy - y);
    jbWinDrawPoint(cx - x, cy + y);
    jbWinDrawPoint(cx - x, cy - y);
    jbWinDrawPoint(cx + y, cy + x);
    jbWinDrawPoint(cx + y, cy - x);
    jbWinDrawPoint(cx - y, cy + x);
    jbWinDrawPoint(cx - y, cy - x);
}

void
jbWinDrawPoint(int x, int y)
// Handy thing to turn on single pixel.  There's
// gotta be an API function that does this somewhere!
{
    WinDrawLine(x, y, x, y);
}





void
drawBezierCurves(
    int *curveData,
    int numCurves)
// Draws a bunch of bezier curves.  Data passed in curveData
// is interlaced control points coordinates, four pairs per
// curve.  
// Integer arithmetic leads to some inaccuracies with this 
// routine, it's really not so great.
{
    int curve;
    int *data;
    long x2[4], y2[4];
    int lastx, lasty;
    long curx, cury;
    long t, tfac;

    for (curve=0; curve<numCurves; curve++) {
        data = &curveData[curve*8];
        // figure out MB * G
        x2[0] = -data[0] + 3*data[2] - 3*data[4] + data[6];
        x2[1] = 3*data[0] - 6*data[2] + 3*data[4];
        x2[2] = -3*data[0] + 3*data[2];
        x2[3] = data[0];
        y2[0] = -data[1] + 3*data[3] - 3*data[5] + data[7];
        y2[1] = 3*data[1] - 6*data[3] + 3*data[5];
        y2[2] = -3*data[1] + 3*data[3];
        y2[3] = data[1];

        // set t=0 position
        lastx = x2[3];
        lasty = y2[3];

        // Iterate through t (8 steps)
        for (t=1; t<8; t++) {
            curx = 0;       cury = 0;
            curx = x2[3];   cury = y2[3];
            tfac = t;
            curx += ((x2[2]*tfac)>>3);
            cury += ((y2[2]*tfac)>>3);
            tfac *= t;
            curx += ((x2[1]*tfac)>>6);
            cury += ((y2[1]*tfac)>>6);
            tfac *= t;
            curx += ((x2[0]*tfac)>>9);
            cury += ((y2[0]*tfac)>>9);
            WinDrawLine(lastx, lasty, (int)curx, (int)cury);
            lastx = (int)curx;
            lasty = (int)cury;
        }

        // draw last segment, t=1
        WinDrawLine(lastx, lasty, 
            (int)(x2[0]+x2[1]+x2[2]+x2[3]), 
            (int)(y2[0]+y2[1]+y2[2]+y2[3]));
    }
        
}
