/* winNeXT.m - NextStep Window-system specific routines
 *
 * Eric P. Scott, San Francisco State University, January 1990
 * (portions adapted from Bob Brown's Sunview implementation)
 *
 * If this file doesn't look like normal C, that's because it's not.
 * It's Objective-C, and perhaps not the greatest example of it.
 * (But it demonstrates what things would be like without Interface
 * builder.)
 *
 * Some things might have to be changed if NeXT fixes a few bugs...
 *
 *
 * Copyright 1990 by Eric P. Scott.  All Rights Reserved.
 * Material herein may be used, copied, modified, or distributed
 * for any purpose as long as proper authorship credit is given.
 * This software is provided as is, without warranty, and with
 * no claims of fitness for any particular purpose.  Use at your
 * own risk.  In no event shall the author, San Francisco State
 * University, the Trustees of the California State University,
 * or the State of California be held liable for any damages or
 * losses resulting from use of this software or any derivative
 * works.
 *
 * NeXT Mazewar was developed on my own time using facilities
 * provided by the San Francisco State University Computer
 * Science Department.  Thanks to Stan Osborne as faculty
 * advisor, J. Keith Wood for sharing his NeXT programming
 * experience, Dr. Sergio Aragon for the use of a SPARCstation 1,
 * MazeWar author Christoper A. Kent for his support and
 * encouragement, and all the beta testers.
 *
 * You will get two inconsequential warnings when you compile this.
 *
 */

#import <appkit/Application.h>
#import <appkit/Button.h>
#import <appkit/ButtonCell.h>
#import <appkit/Cursor.h>
#import <appkit/Font.h>
#import <appkit/Form.h>
#import <appkit/Menu.h>
#import <appkit/MenuCell.h>
#import <appkit/Panel.h>
#import <appkit/ScrollView.h>
#import <appkit/Text.h>
#import <appkit/TextField.h>
#import <appkit/TextFieldCell.h>
#import <appkit/View.h>
#import <appkit/Window.h>
#import <appkit/defaults.h>
#import <appkit/graphics.h>
#import <appkit/tiff.h>
#import <dpsclient/psops.h>
#import <dpsclient/wraps.h>
#import <streams/streams.h>

#include <errno.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "mazewar.h"
#include "winNeXT.h"

#define VIEW_X_DIM	400
#define VIEW_Y_DIM	400
#define VIEW_X_ORIGIN	100
#define VIEW_Y_ORIGIN	50
#define	MAZE_X_DIM	(MAZEXMAX*16)
#define	MAZE_Y_DIM	(MAZEYMAX*16)
#define MAZE_X_ORIGIN	48
#define MAZE_Y_ORIGIN	451
#define	SCORE_X_DIM	192
#define SCORE_Y_DIM	96
#define	SCORE_X_ORIGIN	208
#define	SCORE_Y_ORIGIN	708

#define ICON_FLASH_PERIOD 4


@interface MazeApp:Application
{
    id  currentCursor;
    id  okButton;
}

+ new;
- setCurrentCursor:anObject;
- setOkButton:anObject;
- gotName:sender;
- gotDuke:sender;
- gotButton:sender;
- userQuit:sender;
- windowDidBecomeKey:sender;
- appDidInit:sender;
- applicationDefined:(NXEvent *)theEvent;

@end

@interface MazeWin:Window
{
}

- keyDown:(NXEvent *)theEvent;
- mouseDown:(NXEvent *)theEvent;
- windowDidBecomeKey:sender;

@end

@interface MouseView:View
{
}

+ new;
- (BOOL)acceptsFirstMouse;
- rightMouseDown:(NXEvent *)theEvent;
- rightMouseUp:(NXEvent *)theEvent;
- windowDidBecomeKey:sender;

@end

@interface HelpPanelClass:Panel
{
    NXSize maxSize;
}

- constrain;
- initialHelp;
- windowWillResize:sender toSize:(NXSize *)frameSize;
- windowDidBecomeKey:sender;

@end


static RatPacket incoming;
static MWEvent event;
static id InfoPanel, editMenu, editSubmenu, cutMenuCell, copyMenuCell,
	pasteMenuCell, deleteMenuCell, selectAllMenuCell, HelpPanel;

mazerun() {	// make menus and windows, run event loop
    extern exit();
    const NXRect infoRect = { { 285.0, 528.0 }, { 392.0, 130.0 } },
	helpRect = { { 206.0, 368.0 }, { 512.0, 440.0 } },
	appRect = { { 22.0, 50.0 }, { 64.0, 64.0 } },
	titleRect = { { 120.0, 56.0}, { 211.0, 60.0 } },
	title2Rect = { { 118.0, 57.0}, { 211.0, 60.0 } },
	authorRect = { { 119.0, 22.0}, { 176.0, 45.0 } },
	author2Rect = { { 5.0, 5.0}, { 387.0, 15.0 } },
	versRect = { { 18.0, 25.0}, { 69.0, 30.0 } };
    const NXSize maxSize = { 16000.0, 1.0e38 }; 
    id mwMenu, v, f, t;
    NXStream *s;
    static NXRect docRect;

    event.eventDetail = &incoming;
    NXApp = [MazeApp new];
    mwMenu = [Menu newTitle:"MazeWar"];
    InfoPanel = [Panel newContent:&infoRect style:NX_TITLEDSTYLE
	backing:NX_BUFFERED buttonMask:NX_CLOSEBUTTONMASK defer:YES];
    [[mwMenu addItem:"Info..." action:@selector(makeKeyAndOrderFront:)
	keyEquivalent:'\0'] setTarget:InfoPanel];
    editMenu = [mwMenu addItem:"Edit" action:0 keyEquivalent:'\0'];
    editSubmenu = [Menu newTitle:"Edit"];
    cutMenuCell = [editSubmenu addItem:"Cut" action:@selector(cut:)
	keyEquivalent:'x'];
    copyMenuCell = [editSubmenu addItem:"Copy" action:@selector(copy:)
	keyEquivalent:'c'];
    pasteMenuCell = [editSubmenu addItem:"Paste" action:@selector(paste:)
	keyEquivalent:'v'];
    deleteMenuCell = [editSubmenu addItem:"Delete" action:@selector(delete:)
	keyEquivalent:'\0'];
    selectAllMenuCell = [editSubmenu addItem:"Select All"
	action:@selector(selectAll:) keyEquivalent:'a'];
    [editSubmenu sizeToFit];    
    [mwMenu setSubmenu:editSubmenu forItem:editMenu];
    HelpPanel = [HelpPanelClass newContent:&helpRect style:NX_SIZEBARSTYLE
	backing:NX_BUFFERED buttonMask:NX_CLOSEBUTTONMASK defer:YES];
    [[mwMenu addItem:"Help..." action:@selector(makeKeyAndOrderFront:)
	keyEquivalent:'?'] setTarget:HelpPanel];
    [[mwMenu addItem:"Hide" action:@selector(hide:) keyEquivalent:'h']
	setTarget:NXApp];
    [[mwMenu addItem:"Quit" action:@selector(userQuit:) keyEquivalent:'q']
	setTarget:NXApp];
    [mwMenu sizeToFit];
    [NXApp setMainMenu:mwMenu];
    [InfoPanel setTitle:"Info" ];
    v = [InfoPanel contentView];
    [v addSubview:[[Button newFrame:&appRect icon:"app" tag:0 target:nil
	action:0 key:'\0' enabled:NO] setBordered:NO]];
    t = [[[[TextField newFrame:&titleRect] setSelectable:NO]
        setBezeled:NO] setBackgroundGray:NX_LTGRAY];
    f = [Font newFont:"Times-Roman" size:48.0];
    [[[t cell] setStringValueNoCopy:"MazeWar"] setFont:f];
    [v addSubview:t];
    t = [[[[[TextField newFrame:&title2Rect] setSelectable:NO]
        setBezeled:NO] setTextGray:NX_WHITE] setBackgroundGray:-1.0];
    [[[t cell] setStringValueNoCopy:"MazeWar"] setFont:f];
    [v addSubview:t]; // why doesn't [[t copy] ...] work?
    t = [[[[TextField newFrame:&authorRect] setSelectable:NO]
        setBezeled:NO] setBackgroundGray:NX_LTGRAY];
    f = [Font newFont:"Helvetica" size:12.0];
    [[[t cell] setStringValueNoCopy:"by Christopher A. Kent\
\nWestern Research Laboratory\nDigital Equipment Corporation"] setFont:f];
    [v addSubview:t];
    t = [[[[[TextField newFrame:&versRect] setSelectable:NO]
        setBezeled:NO] setTextGray:NX_DKGRAY] setBackgroundGray:NX_LTGRAY];
    [[[[t cell] setAlignment:NX_CENTERED] setStringValueNoCopy:
	"1/27/1990\nrelease"] setFont:f];
    [v addSubview:t];
    t = [[[[[TextField newFrame:&author2Rect] setSelectable:NO]
        setBezeled:NO] setTextGray:NX_WHITE] setBackgroundGray:NX_LTGRAY];
    f = [Font newFont:"Helvetica-Bold" size:12.0];
    [[[t cell] setStringValueNoCopy:"NeXT Interface by Eric P. Scott, \
San Francisco State University"] setFont:f];
    [v addSubview:t];
    v = [ScrollView newFrame:&helpRect];
    [v setBackgroundGray:NX_WHITE];
    [[v setVertScrollerRequired:YES] getContentSize:&docRect.size];
    t = [Text newFrame:&docRect];
    [v setDocView:t];
    [[[t setAutosizing:NX_WIDTHSIZABLE] superview] setAutoresizeSubviews:YES];
    [[[[[[[t setOpaque:YES] setMonoFont:NO] notifyAncestorWhenFrameChanged:YES]
	setVertResizable:YES] setHorizResizable:NO] setMarginLeft:6.0
	right:6.0 top:0.0 bottom:0.0] setMaxSize:&maxSize];
    s = NXOpenMemory(helpText, sizeof helpText-1, NX_READONLY);
    [t readRichText:s];
    NXClose(s);	// do I have to vm_deallocate() anything?
    [t setEditable:NO];
    [[HelpPanel setTitle:"Help"] setContentView:v];
    [HelpPanel constrain];
    [t setSel:0:0];
     
    [NXApp run];
    [NXApp free];
    exit(0);
}

static id mwWindow;

InitWindow(int argc, char *argv[])	// make MazeWar window
{
    const NXRect mwRect = { { 262.0, 2.0}, { 608.0, 808.0 } },	// Alto sized!
	mvRect = { { VIEW_X_ORIGIN, VIEW_Y_ORIGIN},
	{ VIEW_X_DIM, VIEW_Y_DIM } };
    id mView;

    mwWindow = [MazeWin newContent:&mwRect style:NX_TITLEDSTYLE
	backing:NX_BUFFERED buttonMask:0 defer:YES];
    [mwWindow setTitle:"MazeWar" ];
    [mwWindow setBackgroundGray:NX_WHITE];
    mView = [MouseView newFrame:&mvRect];
    [InfoPanel setDelegate:mView];
    [[[[mwWindow contentView] setOpaque:YES] setFlip:YES] addSubview:mView];
    [mwWindow setDelegate:mwWindow];
    [mwWindow makeKeyAndOrderFront:nil];
}

static id scoreFont;
#ifdef NOTDEF
StartWindow(short *ratBits, int ratWidth, int ratHeight)
#else
StartWindow()	// initialize everything properly
#endif
{
    extern void initCursors(), HourGlassCursor(), initMaze(), invertRats(),
	initRats(), drawMaze();
    extern ShowPosition(), ShowView(), ShowAllPositions(), NewScoreCard();

    initCursors();
    HourGlassCursor();
    scoreFont = [Font newFont:"Times-Roman" size:10.0];	// fits in 12 rows
    [[mwWindow contentView] lockFocus];	// this is about as dumb as X-Windows
    [scoreFont set];
    initMaze();
#ifdef NOTDEF
    initRats(ratBits, ratWidth, ratHeight);
#else
    initRats();
#endif

    drawMaze();
    ShowPosition(M.xloc, M.yloc, M.invincible, M.dir);
    PSsetlinewidth(1.0);	// ...and it still looks thick
    ShowView(M.xloc, M.yloc, M.dir);
    ShowAllPositions();

    NewScoreCard();
    [mwWindow setTitle:"MazeWar"];	// undo advise()
}

StopWindow()	// shut down the window system
{
    id v;

    v = [mwWindow contentView];
    if ([v isFocusView]) [v unlockFocus];	// don't ask
    [NXApp terminate:nil];	// no point continuing
}

static int dirty;
ClearView()	// clear out perspective view
{
    const NXRect canvas={ { VIEW_X_ORIGIN, VIEW_Y_ORIGIN},
	{ VIEW_X_DIM+1, VIEW_Y_DIM+1 } };
    NXEraseRect(&canvas);
    PSsetgray(NX_BLACK);
    dirty++;
}

FlashTop()	// hit an opponent
{
    PSsetinstance(TRUE);
    PSsetalpha(1.0/6.0);
    PScompositerect((float)VIEW_X_ORIGIN, (float)VIEW_Y_ORIGIN,
    	(float)VIEW_X_DIM, (float)VIEW_Y_DIM, NX_SOVER);
    NXPing();
    PSsetalpha(1.0);
    PSnewinstance();
    PSsetinstance(FALSE);
}

FlashScreen()	// we were hit
{
    PSsetinstance(TRUE);
    PSsetalpha(1.0/6.0);
    PScompositerect(0.0, 0.0, 607.0, 807.0, NX_SOVER);
    NXPing();
    PSsetalpha(1.0);
    PSnewinstance();
    PSsetinstance(FALSE);
}

DrawViewLine(int x1, int y1, int x2, int y2)
{
    PSmoveto((float)x1+VIEW_X_ORIGIN, (float)y1+VIEW_Y_ORIGIN);
    PSlineto((float)x2+VIEW_X_ORIGIN, (float)y2+VIEW_Y_ORIGIN);
    PSstroke();
    dirty++;
}

Boolean
KBEventPending()	// defer SendLocation() if unprocessed keystrokes
{
    NXEvent ev;
    // Before I put in the check for [NXApp isActive], this routine was
    // the source of one of the strangest bugs I've seen.
    // If the application was launched from the Workspace, and you hid
    // it, double clicking on its icon would have no apparent effect;
    // after doing this a few times, Workspace would hang, and after
    // a while would put up an attention panel saying that MazeWar was
    // not responding to being unhidden.  However, if another player
    // came along and killed you, THEN the application would restore.
    // Getting killed called NXPing(), which somehow unfroze things.
    // If the application was started from a terminal, everything
    // worked fine!  I'm at a loss to explain why this should make any
    // difference.
    return([NXApp isActive] &&
	[NXApp peekNextEvent:NX_KEYDOWN into:&ev]!=(NXEvent *)NULL);
}


static id hourGlass, rat, deadRat;
HourGlassCursor()	// but it's not really
{
    if ([mwWindow isKeyWindow]) {
	[hourGlass set];
	PSrevealcursor();
    }
    [NXApp setCurrentCursor:hourGlass];
}

RatCursor()
{
    if ([mwWindow isKeyWindow]) [rat set];
    [NXApp setCurrentCursor:rat];
}

DeadRatCursor()
{
    if ([mwWindow isKeyWindow]) [deadRat set];
    [NXApp setCurrentCursor:deadRat];
}

HackMazeBitmap(int x, int y, BitCell *newBits)	// paint an arrow
{
    static NXRect where={ { 0.0, 0.0 }, { 16.0, 16.0 } };

    where.origin.x=(float)(x*16+MAZE_X_ORIGIN);
    where.origin.y=(float)(y*16+MAZE_Y_ORIGIN);
    NXImageBitmap(&where, 16, 16, 1, 1, NX_PLANAR, 0,
	(void *)newBits, (void *)NULL, (void *)NULL,
	(void *)NULL, (void *)NULL);
    dirty++;
}


static id ratBitmap;
DisplayRatBitmap(int screenX, int screenY, int width, int height,
    int srcX, int srcY)	// draw an opponent
{
    NXRect src;
    NXPoint dst;

    src.origin.x = srcX;
    src.origin.y = srcY;
    src.size.width = width;
    src.size.height = height;
    dst.x = (float)(VIEW_X_ORIGIN+screenX);
    dst.y = (float)(VIEW_Y_ORIGIN+screenY);
    [ratBitmap composite:NX_SOVER fromRect:&src toPoint:&dst];
    dirty++;
}

WriteScoreString(RatId rat)	// show score for one player
{
    char buf[64];

    PSmoveto((float)(SCORE_X_ORIGIN+1), (float)(SCORE_Y_ORIGIN+12+rat*12));
    PSshow(M.ratcb.rats[rat].name);
    sprintf(buf, "%d", (unsigned int)M.ratcb.rats[rat].score);
    PSmoveto((float)(SCORE_X_ORIGIN+SCORE_X_DIM-[scoreFont getWidthOf:buf]),
	(float)(SCORE_Y_ORIGIN+12+rat*12));
    PSshow(buf);
    dirty++;
}

ClearScoreLine(RatId rat)	// clear score line
{
    NXRect eRect;

    eRect.origin.x = SCORE_X_ORIGIN;
    eRect.origin.y = (float)(SCORE_Y_ORIGIN+3+rat*12);
    eRect.size.width = SCORE_X_DIM;
    eRect.size.height = 12.0;
    NXEraseRect(&eRect);
    dirty++;
}

WriteInvertedScoreString(RatId rat)	// clear+show+invert
{
    char buf[64];

    PSsetgray(NX_DKGRAY);
    PSrectfill((float)SCORE_X_ORIGIN, (float)(SCORE_Y_ORIGIN+3+rat*12),
	(float)SCORE_X_DIM, 12.0);
    PSsetgray(NX_WHITE);
    PSmoveto((float)(SCORE_X_ORIGIN+1), (float)(SCORE_Y_ORIGIN+12+rat*12));
    PSshow(M.ratcb.rats[rat].name);
    sprintf(buf, "%d", (unsigned int)M.ratcb.rats[rat].score);
    PSmoveto((float)(SCORE_X_ORIGIN+SCORE_X_DIM-[scoreFont getWidthOf:buf]),
	(float)(SCORE_Y_ORIGIN+12+rat*12));
    PSshow(buf);
    PSsetgray(NX_BLACK);
    dirty++;
}


void initCursors()
{
    const NXPoint ratHot={ 0.0, 6.0 }, deadRatHot={ 0.0, 9.0 },
	hourGlassHot={ 1.0, 12.0 };
    rat = [Cursor newFromMachO:"rat.tiff"];
    [rat setHotSpot:&ratHot];
    deadRat = [Cursor newFromMachO:"dRat.tiff"];
    [deadRat setHotSpot:&deadRatHot];
    hourGlass = [Cursor newFromMachO:"cup.tiff"];
    [hourGlass setHotSpot:&hourGlassHot];
}

static id mazeBitmap;
void initMaze()	// construct maze Bitmap.  Sorry, I prefer LTGRAY to BLACK.
{
    register int	i, j, k, line, index;
    long mazeBits[MAZEXMAX*MAZEYMAX*16];

    for (i = 0; i < MAZEYMAX; i++) {
	line = i * MAZEXMAX * MAZEYMAX;
	for (j = 0; j < MAZEXMAX; j++) {
	    index = line + j;
	    for (k = 0; k < 16; k++) {
		if (!M.maze[j].y[i])
		    mazeBits[index] = 037777777777;	// NX_WHITE
		else
		    mazeBits[index] = 025252525252;	// NX_LTGRAY
		index += 32;
	    }
	}
    }
    mazeBitmap = [Bitmap newSize:MAZE_X_DIM:MAZE_Y_DIM type:NX_UNIQUEBITMAP];
    [mazeBitmap image:mazeBits width:MAZE_X_DIM height:MAZE_Y_DIM bps:2 spp:1];
}

void drawMaze()	// BLiT!
{
    const NXPoint mazeLoc = { MAZE_X_ORIGIN, MAZE_Y_ORIGIN };
    [mazeBitmap composite:NX_COPY toPoint:&mazeLoc];
    dirty++;
}

#ifdef NOTDEF
void initRats(short *bits, int width, int height)
#else
void initRats()	// now with alpha for SOVER compositing
#endif
{
#ifdef NOTDEF
    ratBitmap = [Bitmap newSize:width:height type:NX_UNIQUEBITMAP];
    [ratBitmap image:bits width:width height:height bps:1 spp:1];
#else
    ratBitmap = [Bitmap newFromMachO:"rats.tiff"];
#endif
}


static BOOL flashIcon;
NotifyPlayer()
{
    flashIcon = TRUE;
}

// Exceedingly NeXT-specific routines follow

flushIt() {
    PSflushgraphics();
    dirty = 0;
}

static id namePanel, nameForm;
void getPlayerName(char *buf) // char buf[128]
{	// we don't have a terminal to run on!
    const NXRect nwRect = { { 361.0, 401.0}, { 360.0, 161.0 } },
	nfRect = { { 0.0, 69.0 }, { 356.0, 45.0 } },
	okRect = { { 280.0, 8.0 }, { 72.0, 24.0 } },
	quitRect = { { 200.0, 8.0 }, { 72.0, 24.0 } },
	helpRect = { { 120.0, 8.0 }, { 72.0, 24.0 } },
	appRect = { { 8.0, 104.0 }, { 48.0, 48.0 } };
    id v, okButton, quitButton, helpButton, appButton;

    namePanel = [Panel newContent:&nwRect style:NX_TITLEDSTYLE
	backing:NX_BUFFERED buttonMask:0 defer:YES];
    [[namePanel setTitle:"Welcome to Alto MazeWar!" ] setDelegate:NXApp];
    nameForm = [Form newFrame:&nfRect];
    [nameForm addEntry:"Your Name:" tag:0 target:NXApp
	action:@selector(gotName:)];
    [nameForm addEntry:"Duke Host (Return for any game):" tag:1
	target:NXApp action:@selector(gotDuke:)];
    v = [namePanel contentView];
    [v addSubview:nameForm];
    okButton = [Button newFrame:&okRect title:"OK" tag:NX_OKTAG
	target:NXApp action:@selector(gotButton:) key:'\r' enabled:YES];
    [[okButton setIcon:"returnSign" position:NX_ICONRIGHT]
	setAltIcon:"returnSignH"];
    [v addSubview:okButton];
    [NXApp setOkButton:okButton];
    quitButton = [Button newFrame:&quitRect title:"Quit" tag:NX_CANCELTAG
	target:NXApp action:@selector(gotButton:) key:0 enabled:YES];
    [v addSubview:quitButton];
    helpButton = [Button newFrame:&helpRect title:"Help" tag:-1
	target:NXApp action:@selector(gotButton:) key:0 enabled:YES];
    [[helpButton setType:NX_TOGGLE] setAltTitle:"Hide Help"];
    [v addSubview:helpButton];
    appButton = [Button newFrame:&appRect];
    [[[[appButton setBordered:NO] setIcon:"app"] cell] setEnabled:NO];
    [v addSubview:appButton];
    [nameForm selectTextAt:0];
    for (;;) {
	switch ([NXApp runModalFor:namePanel]) {
	case NX_CANCELTAG:
	    StopWindow();	// doesn't return in this version
	/*FALL THROUGH*/
	case NX_OKTAG:
	    break;
	default:
	    [HelpPanel initialHelp];
//	    [[helpButton cell] setEnabled:NO];
	    continue;
	}
	break;
    }
    (void)strncpy(buf, [nameForm stringValueAt:0], 127);	// no gets()!!!
}

void getDukeName(char *ratName, char *buf)
{
    static int invokations;
    char sorry[144];

    switch (invokations) {	// what a crock
    case 1:
	[nameForm setStringValue:ratName at:0];
	[[nameForm cellAt:0:0] setEnabled:NO];
	/*FALL THROUGH*/
    default:
	(void)strcat(strcpy(sorry, "Don't know host "),
	    [nameForm stringValueAt:1]);
	[namePanel setTitle:sorry];
	[nameForm setStringValue:"" at:1];	// sigh
	[nameForm selectTextAt:1];
	for (;;) {
	    switch ([NXApp runModalFor:namePanel]) {
	    case NX_CANCELTAG:
		StopWindow();
	    /*FALL THROUGH*/
	    case NX_OKTAG:
		break;
	    default:
		[HelpPanel initialHelp];
		continue;
	    }
	    break;
	}
	/*FALL THROUGH*/
    case 0:
	(void)strncpy(buf, [nameForm stringValueAt:1], 127);
	break;
    }
    invokations++;
}

void endGetName()	// more bogosity
{
    [namePanel close];
    NXPing();
}

void advise(char *s)	// we're doing something that will take a while
{
    [mwWindow setTitle:s];
    DPSFlush();
}

static int netOK;  /* -1 = idle  0 = defer  1 = active  2 = pending */
void beginNetwork()	// nothing like healthy paranoia
{
    extern DPSFDProc netHandler();
    if (netOK < 0) {
	DPSAddFD(M.theSocket, netHandler, (void *)NULL, NX_MODALRESPTHRESHOLD);
	netOK = 1;
    }
    else if (netOK == 0) netOK = 2;
}

void endNetwork()
{
    switch (netOK) {
    case 1:
	DPSRemoveFD(M.theSocket);
	netOK = -1;
	break;
    case 2:
	netOK = 0;
	break;
    default:
	break;
    }
}

static int dog;
DPSFDProc netHandler(int fd, void *userData)
{
    extern ConvertIncoming(), play();
    int fromLen = sizeof(event.eventSource);
    int cc;

    event.eventType = EVENT_NETWORK;
    cc = recvfrom(M.theSocket, event.eventDetail,
	sizeof(RatPacket), 0, (struct sockaddr *)&event.eventSource, &fromLen);
    ConvertIncoming(event.eventDetail);
    if (cc <= 0) {
	if (cc < 0 && errno != EINTR)
	    perror("event recvfrom");
	return;
    }
    dog++; play(&event); if (dirty) flushIt();
}

DPSTimedEntryProc timeHandler(DPSTimedEntry te, double now, void *userData)
{
    extern play();
    id v;
    static int icon_flash;

    if (flashIcon) {	// this WILL get your attention
	icon_flash = (++icon_flash) % ICON_FLASH_PERIOD;
	switch (icon_flash) {
	case 1:
	    v = [[NXApp appIcon] contentView];
	    [v lockFocus];
	    PSsetinstance(TRUE);
	    PScompositerect(0.0, 0.0, 64.0, 64.0, NX_HIGHLIGHT);
	    [v unlockFocus];
	    break;
	case ICON_FLASH_PERIOD/2+1:
	    v = [[NXApp appIcon] contentView];
	    [v lockFocus];
	    PSnewinstance();
	    PSsetinstance(FALSE);
	    [v unlockFocus];
	    if ([NXApp isActive]) {
		flashIcon = FALSE;
		icon_flash = 0;
	    }
	    break;
	}
    }
    if (netOK == 1) {	// let's not crash the program, ok?
        if (dog) dog = 0;
	else {
	    event.eventType = EVENT_TIMEOUT;
	    play(&event); if (dirty) flushIt();
	}
    }
}

MWError(char *s)	/* normally this is in the window-independent stuff! */
{
	extern exit();

	(void)NXRunAlertPanel("Fatal Error", s, "OK", (char *)NULL,
		(char *)NULL);
	StopWindow();
	exit(0);
}

@implementation MazeApp

+ new
{
    self = [super new];
    [self setDelegate:self];
    return self;
}

- setCurrentCursor:anObject
{
    currentCursor = anObject;
    return self;
}

- setOkButton:anObject
{
    okButton = anObject;
    return self;
}

- gotName:sender
{
    [sender selectTextAt:1];
    return self;
}

- gotDuke:sender
{
    [okButton performClick:sender];
    return self;
}

- gotButton:sender
{
    return [self stopModal:[sender tag]];
}

- userQuit:sender
{
    extern quit();
    quit();	// try to exit cleanly
    return [self terminate:sender];	// shouldn't get here
}

- windowDidBecomeKey:sender	// used by initial panel
{
    [editSubmenu disableFlushWindow];
    if (![cutMenuCell isEnabled]) [cutMenuCell setEnabled:YES];
    if (![copyMenuCell isEnabled]) [copyMenuCell setEnabled:YES];
    if (![pasteMenuCell isEnabled]) [pasteMenuCell setEnabled:YES];
    if (![deleteMenuCell isEnabled]) [deleteMenuCell setEnabled:YES];
    if (![selectAllMenuCell isEnabled]) [selectAllMenuCell setEnabled:YES];
    [[editSubmenu reenableFlushWindow] flushWindowIfNeeded];
    return self;
}

- appDidInit:sender	// the docs lie, things are *not* ready yet
{
    static NXEvent ev = { NX_APPDEFINED };
    ev.window = [[self mainMenu] windowNum];
    ev.data.compound.subtype = 0;
    ev.ctxt = [self context];
    (void)DPSPostEvent(&ev, FALSE);  // post to tail of event queue
    return self;
}

- applicationDefined:(NXEvent *)theEvent
{
    extern MazeInit();
    DPSTimedEntryProc timeHandler();

    switch (theEvent->data.compound.subtype) {
    case 0:
	MazeInit(NXArgc, NXArgv);	// do the once-only stuff
	if (netOK == 2) {
	    netOK = -1; beginNetwork();
	}
	(void)DPSAddTimedEntry(.25, timeHandler, (void *)NULL,
	    NX_RUNMODALTHRESHOLD);
	break;
    default:
	[currentCursor set];
        break;
    }
    return self;
}


@end

static int sentPeek;

@implementation MazeWin

- keyDown:(NXEvent *)theEvent	// at last, some "real" code
{
    extern play();

    if ((theEvent->flags&(NX_CONTROLMASK|NX_ALTERNATEMASK|NX_COMMANDMASK))==0
	&& theEvent->data.key.charSet==NX_ASCIISET)
	switch (theEvent->data.key.charCode) {
    case '4':
	if ((theEvent->flags&NX_NUMERICPADMASK)==0) return self;
    case 'a':
	event.eventType = EVENT_A;
	break;
    case '5':
	if ((theEvent->flags&NX_NUMERICPADMASK)==0) return self;
    case 's':
	event.eventType = EVENT_S;
	break;
    case '6':
	if ((theEvent->flags&NX_NUMERICPADMASK)==0) return self;
    case 'd':
	event.eventType = EVENT_D;
	break;
    case '+':
	if ((theEvent->flags&NX_NUMERICPADMASK)==0) return self;
    case 'f':
	event.eventType = EVENT_F;
	break;
    case ' ':
	event.eventType = EVENT_BAR;
	break;
    case 'i':
	event.eventType = EVENT_I;
	break;
    case 'k':
	event.eventType = EVENT_K;
	break;
    case 'o':
	event.eventType = EVENT_O;
	break;
    case 'l':
	event.eventType = EVENT_L;
	break;
    case 'q':
	event.eventType = EVENT_INT;
	break;
    default:
        return self;
    }
    else if ((theEvent->flags&(NX_CONTROLMASK|NX_ALTERNATEMASK|
	NX_COMMANDMASK|NX_NUMERICPADMASK))==NX_NUMERICPADMASK &&
	theEvent->data.key.charSet==NX_SYMBOLSET &&
	theEvent->data.key.charCode == 174) event.eventType = EVENT_BAR;
    else return [super keyDown:theEvent];
    dog++; play(&event); if (dirty) flushIt();
    return self;
}

- mouseDown:(NXEvent *)theEvent	// I suppose this could be in MouseView
{
    extern play();
    const NXRect Mrect = { { VIEW_X_ORIGIN, VIEW_Y_ORIGIN},
	{ VIEW_X_DIM, VIEW_Y_DIM } };
    NXPoint vLoc;

    vLoc = theEvent->location;
    [[self contentView] convertPoint:&vLoc fromView:nil];
    if (!NXMouseInRect(&vLoc, &Mrect, YES)) return [super mouseDown:theEvent];
    event.eventType = EVENT_MIDDLE_D;
    dog++; play(&event); if (dirty) flushIt();
    return self;
}

- windowDidBecomeKey:sender
{
    static NXEvent ev = { NX_APPDEFINED };

    [editSubmenu disableFlushWindow];
    if ([cutMenuCell isEnabled]) [cutMenuCell setEnabled:NO];
    if ([copyMenuCell isEnabled]) [copyMenuCell setEnabled:NO];
    if ([pasteMenuCell isEnabled]) [pasteMenuCell setEnabled:NO];
    if ([deleteMenuCell isEnabled]) [deleteMenuCell setEnabled:NO];
    if ([selectAllMenuCell isEnabled]) [selectAllMenuCell setEnabled:NO];
    [[editSubmenu reenableFlushWindow] flushWindowIfNeeded];
    ev.window = [sender windowNum];	// NextStep will change the
    ev.data.compound.subtype = 1;	// cursor AFTER this is called
    ev.ctxt = [NXApp context];		// so we have to cheat again
    (void)DPSPostEvent(&ev, TRUE);  // post to head of event queue
    return self;
}


@end

@implementation MouseView
{
}


+ new
{
    self = [super new];
    [self setFlip:YES];
    return self;
}

- (BOOL)acceptsFirstMouse	// catch right mouse events
{
    return YES;
}

- rightMouseDown:(NXEvent *)theEvent
{
    extern play();
    NXPoint vLoc;

    vLoc = theEvent->location;	// why only a 2-button mouse?  Grr...
    [self convertPoint:&vLoc fromView:nil];
    event.eventType = vLoc.x < VIEW_X_DIM/2.0 ? EVENT_LEFT_D : EVENT_RIGHT_D;
    sentPeek = 1;
    dog++; play(&event); if (dirty) flushIt();
    return self;
}

- rightMouseUp:(NXEvent *)theEvent
{
    extern play();
    if (!sentPeek) return [super rightMouseUp:theEvent];
    sentPeek = 0;
    event.eventType = EVENT_RIGHT_U;	// don't need to distinguish here
    dog++; play(&event); if (dirty) flushIt();
    return self;
}

- windowDidBecomeKey:sender	// used by InfoPanel
{
    [editSubmenu disableFlushWindow];
    if ([cutMenuCell isEnabled]) [cutMenuCell setEnabled:NO];
    if ([copyMenuCell isEnabled]) [copyMenuCell setEnabled:NO];
    if ([pasteMenuCell isEnabled]) [pasteMenuCell setEnabled:NO];
    if ([deleteMenuCell isEnabled]) [deleteMenuCell setEnabled:NO];
    if ([selectAllMenuCell isEnabled]) [selectAllMenuCell setEnabled:NO];
    [[editSubmenu reenableFlushWindow] flushWindowIfNeeded];
    return self;
}

@end

@implementation HelpPanelClass
{
    NXSize maxSize;
}

- constrain
{
    [NXApp getScreenSize:&maxSize];	// because it looks better this way
    maxSize.width -= 71.0, maxSize.height -= 2.0;
    [self setDelegate:self];
    return self;
}

- initialHelp	// main menu is useless during modal, this is a hack
{
    id t;
    int sPos, ePos;
#ifndef NOTDEF
    int xPos;
#endif
    NXRect pRect;

    if ([self isVisible]) return [self orderOut:nil];
    t = [[self contentView] docView];
#ifdef NOTDEF
    [t getParagraph:INIT_PARA start:&sPos end:&ePos rect:&pRect];
#else
    // getParagraph:start:end:rect: starts with the last physical line
    // of the chosen paragraph, i.e. not what a triple-click would
    // select.  Something is major-league broken here.
    [t getParagraph:INIT_PARA-1 start:&sPos end:&xPos rect:&pRect];
    sPos++;	// can't use the end position here either!
    [t getParagraph:INIT_PARA start:&xPos end:&ePos rect:&pRect];
#endif
    [[t setSel:sPos:ePos] scrollSelToVisible];
    [self moveTo:530.0:46.0];
#ifdef NOTDEF
    return [self orderWindow:NX_BELOW relativeTo:namePanel];	// rangechecks
#else
    return [self orderFront:nil];
#endif
}

- windowWillResize:sender toSize:(NXSize *)frameSize
{
    const NXSize minSize = { 280.0, 85.0 };	// be reasonable
    if (frameSize->width < minSize.width)
	frameSize->width = minSize.width;
    else if (frameSize->width > maxSize.width)
	frameSize->width = maxSize.width;
    if (frameSize->height < minSize.height)
	frameSize->height = minSize.height;
    else if (frameSize->height > maxSize.height)
	frameSize->height = maxSize.height;
    return self;
}

- windowDidBecomeKey:sender
{
    [editSubmenu disableFlushWindow];
    if ([cutMenuCell isEnabled]) [cutMenuCell setEnabled:NO];
    if (![copyMenuCell isEnabled]) [copyMenuCell setEnabled:YES];
    if ([pasteMenuCell isEnabled]) [pasteMenuCell setEnabled:NO];
    if ([deleteMenuCell isEnabled]) [deleteMenuCell setEnabled:NO];
    if (![selectAllMenuCell isEnabled]) [selectAllMenuCell setEnabled:YES];
    [[editSubmenu reenableFlushWindow] flushWindowIfNeeded];
    return self;
}

@end
