
#import "TextApp.h"

#import <appkit/ScrollView.h>
#import <appkit/Text.h>
#import <appkit/Panel.h>
#import <appkit/Font.h>
#import <appkit/defaults.h>
#import <streams/streams.h>
#import <appkit/OpenPanel.h>
#import <appkit/Bitmap.h>
#import <appkit/Control.h>

#import <sys/file.h>
#import <sys/errno.h>
#import <libc.h>
#import <stdlib.h>
#import <ctype.h>
#import <sys/param.h>
#import <string.h>

			// a short method of getting defaults.
#define def( name) NXGetDefaultValue( "Text", name)
#define idef( name) atoi( def( name))
#define fdef( name) atof( def( name))

extern int getwd( char *);

static const char *file=NULL, *command=NULL, *save=NULL;
static NXStream *st=NULL;

@implementation TextApp : Application
{
    id	text;
    int miniWinNum, winNum;
}

	/*
	  This saves the text into the file specified by save.  If
	  this filename is empty, or cannot be correctly written
	  to, NO is returned.  Otherwise, YES is returned (if the
	  save was successful.)
	*/
-(BOOL)doSave
{
  if( save && *save)										// We've got a filename
    if( !(st=NXOpenMemory( NULL, 0, NX_WRITEONLY)))						// If couldn't get a memory stream,
      NXRunAlertPanel( "Save.", "Unable to save file %s.\n", "OK", NULL, NULL, save);		// tell the user.
    else
      {
	[text writeText:st];									// write the data to the stream,
	if( NXSaveToFile( st, save)==-1)							// and try to write to the file.
	  {
	    NXRunAlertPanel( "Save.", "Unable to save file %s.\n", "OK", NULL, NULL, save);	// couldn't write to the file.
	    NXCloseMemory( st, NX_FREEBUFFER);
	  }
	else
	  {
	    NXCloseMemory( st, NX_FREEBUFFER);
	    [[text window] setDocEdited:NO];							// the document was saved, so now isn't edited
	    if( !def( "Title") || !*def( "Title"))						// change the title if appropriate.
	      [[text window] setTitle:save];
	    return YES;										// return success.
	  }
      }
  return NO;											// failure by default.
}

-(BOOL)save:sender
{
  if( save && *save && access( save, F_OK)==0 && idef( "Backup"))		// if there's already a file there, and backup is specified,
    {
      char t[ MAXPATHLEN];							// make a backup filename.
      strcat( strcpy( t, save), "~");
      if( rename( save, t)==-1)							// if couldn't rename, ask user.
	switch(  NXRunAlertPanel( "Cannot make backup.", "Could not make a backup of %s.  Overwrite anyway?", "Cancel", "Overwrite", "Save Panel", save))
	  {
	    case NX_ALERTDEFAULT :
	    case NX_ALERTERROR :
	      return NO;
	    case NX_ALERTOTHER :
	      save=NULL;							// This will force a SavePanel.
	    default :
	      break;
	  }
    }
  if( !save || !*save)								// if we don't have a filename,
    {
      id savePanel=[SavePanel new];						// look at a savePanel.
      if( ![savePanel runModal])						// If cancelled,
        return NO;								// return failure.
      save=[savePanel filename];						// Otherwise get the new name.
    }
  return [self doSave];								// go try to save the file.
}
-(BOOL)saveAs:sender
{
  id savePanel=[SavePanel new];
  if( [savePanel runModal])							// if not cancelled,
    return [self doSave];							// attempt the save.
  return NO;
}
- revert:sender
{
  if( !*save)
    NXRunAlertPanel( "Revert", "No file to revert to.", "OK", NULL, NULL);
  else if( [[text window] isDocEdited] && NXRunAlertPanel( "Revert", "Do you want to revert changes to %s?", "Revert", "Cancel", NULL, save)!=NX_ALERTDEFAULT)
    return self;
  else if( !(st=NXMapFile( save, NX_READONLY)))
    NXRunAlertPanel( "Revert", "Unable to read %s.", "OK", NULL, NULL, save);
  else
    {
      [text readText:st];
      [[text window] setDocEdited:NO];
      NXCloseMemory( st, NX_FREEBUFFER);
    }
  return self;
}
- new:sender
{
  if( [[text window] isDocEdited])
    switch( NXRunAlertPanel( "New", "Save changes%s%s?", "Save", "No", "Cancel", *save ? " to " : "", save))
      {
	case NX_ALERTDEFAULT :
	  if( ![self save:self])
	default :
	    return self;
	case NX_ALERTALTERNATE :
	  break;
      }
  [text selectText:self];
  [text delete:self];
  save=NULL;
  if( !def( "Text") || !*def( "Text"))
    [[text window] setTitle:"Empty Window"];
  [[text window] setDocEdited:NO];
  return self;
}
- open:sender
{
  id openPanel;
  if( [[text window] isDocEdited])
    switch( NXRunAlertPanel( "Open", "Save changes%s%s?", "Save", "No", "Cancel", *save ? " to " : "", save))
      {
	case NX_ALERTDEFAULT :
	  if( ![self save:self])
	default :
	    return self;
	case NX_ALERTALTERNATE :
	  break;
      }
  openPanel=[OpenPanel new];
  if( [openPanel runModal])
    if( !(st=NXMapFile( save=[openPanel filename], NX_READONLY)))
      NXRunAlertPanel( "Open", "Unable to read %s.", "OK", NULL, NULL, save);
    else
      {
	[text readText:st];
	[[text window] setDocEdited:NO];
	NXCloseMemory( st, NX_FREEBUFFER);
	[[text window] setDocEdited:NO];
	if( !def( "Text") || !*def( "Text"))
	  [[text window] setTitle:save];
      }
  return self;
}
- insert:sender
{
  id openPanel=[OpenPanel new];
  const char *s;
  id title=[[openPanel contentView] findViewWithTag:NX_OPTITLEFIELD];
  [title setStringValue:"Insert"];
  if( [openPanel runModal])
    if( !(st=NXMapFile( s=[openPanel filename], NX_READONLY)))
      NXRunAlertPanel( "Open", "Unable to read %s.", "OK", NULL, NULL, s);
    else
      {
        char *buff;
	int len, maxlen;
	NXGetMemoryBuffer( st, &buff, &len, &maxlen);
	[text replaceSel:buff length:len];
	[[text window] setDocEdited:YES];
	NXCloseMemory( st, NX_FREEBUFFER);
      }
  [title setStringValue:"Open"];
  return self;
}
- New:sender
{
  [[text window] setDocEdited:NO];
  return [self new:sender];
}
- Open:sender
{
  [[text window] setDocEdited:NO];
  return [self open:sender];
}
- Quit:sender
{
  [[text window] setDocEdited:NO];
  return [self windowWillClose:sender];
}
- Revert:sender
{
  [[text window] setDocEdited:NO];
  return [self revert:sender];
}

- quit:sender
{
  if( [[text window] isDocEdited])
    switch( NXRunAlertPanel( "Quit", "Save changes%s%s?", "Save", "No", "Cancel", (*save ? " to " : ""), save))
      {
	case NX_ALERTDEFAULT :
	  if( ![self save:self])		// Only stop if saved.
	default :
	    return nil;				// don't really close
	case NX_ALERTALTERNATE :
	  break;
      }
  [self stopModal];				// just stop the loop.
  return self;
}
- windowWillClose:sender
{
  return [self quit:sender];
}
- windowDidDeminiaturize:sender
{
  id bm;
  if( [sender isDocEdited] && (bm=[Bitmap findBitmapFor:"MiniWindow"]))
    {
      NXRect r={{3, 3}, {40, 40}};
      [bm lockFocus];
      NXHighlightRect( &r);
      [bm unlockFocus];
    }
  return self;
}
- windowWillMiniaturize:sender toMiniwindow:mw
{
  miniWinNum=[mw windowNum];
  return [self windowDidDeminiaturize:sender];
}

- text:sender isEmpty:(BOOL)flag
{
  [[text window] setDocEdited:YES];
  return self;
}
- setText:anObject
{
  text= [anObject isKindOf:[ScrollView class]] ? [anObject docView] : anObject;
  [text setDelegate:self];
  [[text window] setMiniwindowIcon:"MiniWindow"];
  miniWinNum=winNum=-1;
  return self;
}

	/*
	  This captures output as it comes in, and places it in
	  the output Text object.  I had to do the last display
	  because otherwise I got a disgusting looking caret at
	  the end of the text.  I though selectNull was supposed
	  to take care of it?  I think it was a bug, because even
	  after I started selecting text, the caret stayed on.
	  The explicit redisplay kills it nicely, though.
	*/
void handleInput( int fd, struct Text *output)
{
#define buffSize 4096
  static char buffer[ buffSize];
  int ret=read( fd, buffer, buffSize);
  if( ret>0)
    {
      int len=[output textLength];
      [output setSel:len :len];				// get to the end of the text,
      [output replaceSel:buffer length:ret];		// and output our data.
    }
  else
    {
      DPSRemoveFD( fd);					// Don't call us anymore.
      if( ![output textLength] && idef( "Close"))	// if still empty,
        [NXApp abortModal];				// abort the modal loop.
      else
        {
	  [output setSel:0 :0];				// Move to the front of the text.
	  if( idef( "Edit") || def( "Save"))		// if the user can edit,
	    [output setEditable:YES];			// let them go.
	  [output setSelectable:YES];			// allow user to select text.
	  [output display];				// and force the caret away if not editable.
        }
    }
}

	/*
	  This is called when the time is up, if the Time default
	  was >0.  It just removes itself from the TimedEntry queue,
	  and calls the application to abort the modal loop.
	*/
void killIt( DPSTimedEntry te, double time, id self)
{
  DPSRemoveTimedEntry( te);
  [self abortModal];
}

	/*
	  This routine is overridden so that I can catch command-key
	  equivalents, since there is not a main menu in the .nib
	  file to catch them for me.  I also need to catch the
	  double-clicks in the miniWindow (if miniaturized) because
	  the application runs in and modal event loop.  That means
	  the regular double-clicks are ignored when not in the
	  main window.
	*/
-(NXEvent *)getNextEvent:(int)mask waitFor:(double)timeout threshold:(int)level
{
  NXEvent *e;
  static char keys[]="acpvxhmwiNnOoQqSsUuW";
  static BOOL first=YES;
  static SEL actions[ sizeof( keys)];
  
  if( first)
    {
      SEL iactions[]={
#define textActions 0
	@selector( selectAll:), @selector( copy:), @selector( print:), @selector( paste:), @selector( cut:),
#define windowActions 5
	@selector( performMiniaturize:), @selector( performMiniaturize:), @selector( performClose:),
#define selfActions 8
	@selector( insert:), @selector( New:), @selector( new:), @selector( Open:), @selector( open:),
	@selector( Quit:), @selector( quit:), @selector( saveAs:), @selector( save:), @selector( Revert:),
	@selector( revert:), @selector( Quit:),
      };
      first=NO;
      bcopy( iactions, actions, sizeof( SEL)*sizeof( keys));
    }
  if( winNum==-1)
    winNum=[[text window] windowNum];

	/*
	  I find gotos hideous, but I think this is one of those times when
	  their use may be justified.  I will have you know that this is
	  the _first_ goto I've used in c.  The first time through, I used
	  a recursive call, but I thought that looked too awkward.
	*/
loop:

				// Really go get the event.
  e=[super getNextEvent:mask waitFor:timeout threshold:level];

  if( e && e->type==NX_KEYDOWN && e->window==winNum && e->flags&NX_COMMANDMASK)
    if( rindex( keys, e->data.key.charCode))
      {
        int ret=rindex( keys, e->data.key.charCode)-keys;
	[ret<windowActions ? text : (ret<selfActions ? [text window] : self) perform:actions[ ret] with:self];
	goto loop;
      }

				// if its a double-click in the miniWindow,
  if( e && e->window==miniWinNum && e->type==NX_MOUSEDOWN && e->data.mouse.click>1)
    {				// bring up the main window.
      [[self findWindow:miniWinNum] deminiaturize:self];
      [self windowDidDeminiaturize:[text window]];
      miniWinNum=-1;
      goto loop;
    }
  return e;			// return whatever event we eventually got.
}

static NXDefaultsVector defs=
{
  {"Title", NULL},			// Title of the Panel.
  {"Width", "512"}, {"Height", "256"},	// Size of the Panel.
  {"Time", "0"},			// Time to live for Panel.
  {"NXFont", "Helvetica"},		// Font to use in Panel,
  {"NXFontSize", "12"},			// Font's size.
  {"Command", NULL},			// Command to use as input.
  {"File", NULL},			// File to use as input.
  {"Burst", "1"},			// Read input before displaying Panel.
  {"Close", "0"},			// Close Panel if no input.
  {"Edit", "0"},			// Let user edit, and output result.
  {"Save", NULL},			// Where to put the Editted result.
  {"Backup", "1"},			// Make backup of previous file?
  {"Return", "0"},			// Return to the previously active app?
  {"Empty", "0"},			// Don't read stdin, even if nowhere else to go?
  {NULL, NULL},
};

	/*
	  I override this method to load up the defaults, and
	  display the output window.  I use runModalFor: to do
	  this, because I do not want an application icon, or a
	  menu (not that there is a menu in the .nib file . . .)
	*/
- run
{
  id font;
  float time;
  const char *title;
  int previous;
  FILE *f=stdin;		// default to stdin for input.

				// register our defaults.
  NXRegisterDefaults( "Text", defs);

  file=def( "File");
  command=def( "Command");
  save=def( "Save");
  title=def( "Title");

  if( save && !*save)			// If there is a save default w/o a value,
    if( file && *file)			// but the input is from a file,
      save=file;			// let the save name be the file name.
    else if( command && *command)	// else, if input was a command,
      {					// copy the command string.
	char *t=strcpy( malloc( strlen( command)+1), command);
	char *p;
	char brkchars[]=" &|;$";	// (may not be complete).
	for( p=t; *p; p++)		// get to the end of the command's name.
	  if( index( brkchars, *p) || iscntrl( *p))
	    {
	      *p=0;
	      break;
	    }
	if( rindex( t, '/'))			// get the basename.
	  t=rindex( t, '/');
	save=t;				// make that the savefile.
      }
				// If we are to read from a file,
  if( file && *file)
    {
      f=fopen( file, "r");	// open that file,
      if( !f)			// and error out if couldn't.
        {
	  NXRunAlertPanel( "Unable to open file.",
	  		"The file \"%s\" could not be opened.",
			"OK", NULL, NULL, file);
	  return self;
	}
    }				// If really to use a command,
  else if( command && *command)
    {
      f=popen( command, "r");	// open a file from that command,
      if( !f)			// and error out if couldn't.
        {
          NXRunAlertPanel( "Unable to execute command.",
	  		"The command \"%s\" could not be executed.",
			"OK", NULL, NULL, command);
	  return self;
	}
    }

  if( f==stdin && idef( "Empty"))	// Don't read if stdin and Empty set.
    {
      [text setEditable:YES];
      [text selectAll:self];
    }
  else if( idef( "Burst"))		// If in Burst mode,
    {				// open a Stream on the file,
      NXStream *st=NXOpenFile( fileno( f), NX_READONLY);
      [text readText:st];	// and read from there.
      NXClose( st);
				// If no text and Close, exit.
      if( ![text textLength] && idef( "Close"))
	return self;
      else if( idef( "Edit") || def( "Save"))	// If the user can edit,

	[text setEditable:YES];	// set editable on.
      [text setSel:0 :0];
    }
  else
    {
      [text setSelectable:NO];		// don't allow selection,
					// and set a routine to catch the output.
      DPSAddFD( fileno( f), (DPSFDProc)handleInput, text, NX_MODALRESPTHRESHOLD);
    }

				// size the window as indicated in defaults.
  [[text window] sizeWindow:idef( "Width") :idef( "Height")];

				// get the indicated font.
  if( font=[Font newFont:def( "NXFont") size:fdef( "NXFontSize")])
    [text setFont:font];

  if( (time=fdef( "Time"))>0)	// get time and use it if >0
    DPSAddTimedEntry( time, (DPSTimedEntryProc)killIt,
    		      self, NX_MODALRESPTHRESHOLD);

				// set the title
  if( !(title=def( "Title")) || !*title)
    if( file && *file)
      title=file;
    else if( command && *command)
      title=command;
    else
      title="Text";
  [[text window] setTitle:title];

  if( idef( "Return"))
    previous=[self activeApp];

  [self activateSelf:YES];			// make us active (really)

  [self runModalFor:[text window]];		// and get into a run loop.

  if( idef( "Edit"))		// If the user could edit,
    {
      NXStream *st;				// a stream for our writing.
			    		// Write the text to the output file.
      [text writeText:st=NXOpenFile( 1, NX_WRITEONLY)];
      NXClose( st);
    }

  if( idef( "Return"))
    [self activate:previous];

  return self;
}

@end
