#import "FlashDocument.h"
#import "FlashApp.h"
#import "Deck.h"
#import "Card.h"
#import "Random.h"
#import <appkit/Matrix.h>
#import <appkit/ScrollView.h>
#import <appkit/Text.h>
#import <appkit/Font.h>
#import <appkit/Panel.h>
#import <appkit/ButtonCell.h>
#import <appkit/nextstd.h>
#import <objc/hashtable.h>	/* for NXCopyStringBuffer() */
#import <text/pathutil.h>

#define KEY 0	// Text Views
#define RES 1

#define P_HIGH 104.0	// Cell Size
#define P_WIDE 274.0
#define P_MARG 7.0

@implementation FlashDocument
/*
 * This class is used to keep track of a Flash document.
 *

/* Private Functions */

#define ObjName(S) "Card_" #S
#define SetCell(I,T,S) [[[diff cellAt:0:I] setTitle:#T] setAction:@selector(S)]
#define SetDiffCell(I,N) SetCell(I,N, was ## N ## :)

static Text *getText(id me, STR name)
{
	Text* tv;
	tv = [[NXGetNamedObject(name, me) docView] setDelegate:me];
	return tv;
}

static void alignText(Text *text, int align)
{
	[[text setAlignment:align] calcLine];
}

static void writeToCard(Card *card, id* tv)
{
	int i=2;	// Bottom Up
	NXStream *stream;

	while(i--) {
		stream=NXOpenMemory(NULL,0,NX_READWRITE);
		[tv[i] writeRichText:stream];
		[card setFace:i stream:stream];
		NXCloseMemory(stream, NX_FREEBUFFER);
		[card setFace:i tag:[tv[i] alignment]];
	}
}

static void readFromCard(Card *card, id* tv)
{
	int i=2;
	NXStream *stream;

	while(i--) {
		stream=[card faceStream:i];
		[tv[i] readRichText:stream];
		NXCloseMemory(stream, NX_SAVEBUFFER);
		alignText(tv[i],[card faceTag:i]);
	}
}

static Text *newText(int i, int j)	// Generate a TextCell of the Printout.
{
	Text *text;
	NXRect myRect;

	NXSetRect(&myRect, i*P_WIDE, j*P_HIGH, P_WIDE, P_HIGH );
	text = [Text newFrame:&myRect]; 
	[text setMarginLeft:P_MARG right:P_MARG top:P_MARG bottom:P_MARG];
	return text;
}

/* Factory methods */

+ new
/*
 * Creates a new, empty, document.
 * Creates a deck; creates a window for that deck;
 */
{
	self = [super new];
	contents = [Deck new];
	[self getWindow];
	[self setName:NULL andDirectory:NULL];
	[self setViewMode:NO];  
	[self showTextOf:[self newCard]];
	return self;
}

+ newFromFile:(const char *)file
/*
 * Opens an existing document from the specified file.
 */
{
	if (self = [super newFromFile:file]) {
	    [self getWindow];
	    [self setName:file];
	    [self showTextOf:[contents giveCard]];
	    return self;
	}
	return nil;
}

/* Create and Destroy Methods*/

- getWindow
/*
 * set textViews and their delegates; orders the window front;
 */
{
    rand = [Random new];
    [contents setMarkerAtEnd];
    window=[NXApp locateWindow:[NXApp loadNibSection:"Card.nib" owner:self]];
    viewMode = YES;
    textView[RES]=[getText(self, ObjName(ResScroll))
		 setFont:[NXApp defaultFont:"ResFont"]];
    textView[KEY]=[getText(self, ObjName(KeyScroll))
		 setFont:[NXApp defaultFont:"KeyFont"]];
    [window makeKeyAndOrderFront:self];
    [NXApp addDocument:[self filename]];
    return self;
}

- newCard
{
	cardNew = YES;
	cardChanged = NO;
	return [[Card new] setFace:KEY tag:NX_CENTERED];
}

- free
/*
 * Free all subsisting objects
 */
{
	[contents freeObjects];
	return [super free];
}

/* State Variables */

- view
/*
 * Returns the TextView which is first responder;
 * Returns nil if firstResponder is not Text.
 */
{
	view = [window firstResponder];
	if (! [view isKindOfGivenName:"Text"] )
		view = nil;

	return view;
}

- setViewMode:(BOOL)newMode
/*
 * Change between viewing and updating mode
 */
{
	BOOL tmp = viewMode;
	viewMode = newMode;
	if (viewMode) {
		if ( ![self checkCurrent] ) {
			viewMode = tmp;
			return nil;
		}
		[self cover:self];
		[[reveal setTitle:"Reveal"] setAction:@selector(reveal:)];
		[[[reveal  setAltTitle:"Cover"] setType:NX_TOGGLE] setState:0];
		SetDiffCell(0,Easy);
		SetDiffCell(1,Fair);
		SetDiffCell(2,Hard);
	} else {
		[self reveal:self];
		[[reveal setTitle:"Next"] setAction:@selector(next:)];
		[reveal setType:NX_MOMENTARYPUSH];
		SetCell(0,Add,add:);
		SetCell(1,Modify,modify:);
		SetCell(2,Delete,delete:);
		cardNew = cardChanged = NO;
//		[[window makeFirstResponder:textView[KEY]] makeKeyWindow];
	}
	return self;
}

- (BOOL) viewMode;
/*
 * Returns the view Mode associated with this document.
 */
{
    return viewMode;
}

- switchViewMode:sender
/*
 * Toggle between view and update mode
 */
{
	return [self setViewMode:(! viewMode)];
}	

/* Card Display Methods */

- showTextOf:aCard
/*
 * set to current card before displaying
 */
{
	if (! aCard) {
		Notify("Deck", "End of Deck.  Staring over.");
		aCard = [[contents setMarkerAtEnd] giveCard];
	}
	currentCard = aCard;
	return [self showText];
}
	
- showText
/*
 * Display card.  Blacken RES if in View Mode
 */
{
	if (! currentCard) currentCard = [self newCard];
	if ([reveal state]) [reveal performClick:self];

	readFromCard(currentCard,textView);
	[markCount setIntValue:[contents marker]];
	return self;
}

/* Action methods for Card View/Update */

- reveal:sender
/*
 * If I ask, display RES.  If Button asks, toggle.
 */
{
	if ((sender == self) || ([textView[RES] backgroundGray] == NX_BLACK))
		[[textView[RES] setBackgroundGray:NX_WHITE] update];
	else
		[self cover:self];
	return self;
}

- cover:sender
/*
 * Blacken RES.
 */
{
	[[textView[RES] setBackgroundGray:NX_BLACK] update];
	return self;
}

- wasEasy:sender
/*
 * Place in Back.
 */
{
	[self showTextOf:[[contents takeCard:currentCard] giveCard]];
	return self;
}

- wasFair:sender
/*
 * Place in front of marker.
 */
{
    [contents insertObject:currentCard at:
      [rand partition:[contents marker] part:2 of :2]];
    [self showTextOf:[contents giveCard]];
    return self;
}
	
- wasHard:sender;
/*
 * Place in front.
 */
{
    [contents insertObject:currentCard at:
       [rand partition:[contents marker] part:1 of :2]];
    [self showTextOf:[contents giveCard]];
    return self;
}

/* Update Mode */
- (BOOL)checkCurrent
 /*
  * Make sure current card isn't new or modified.
  * returns NO if user requests cancel.
  */ 
{
	if (cardChanged) {
		switch(NXRunAlertPanel("Update", "Card changed.  Add to deck?",
			 "YES", "NO", "Cancel")) {
			case NX_ALERTDEFAULT: 
				[self add:self];
				break;
			case NX_ALERTALTERNATE: break;
			default: return NO;
		}
	}
	if (cardNew) {
		[currentCard free];
		[self showTextOf:[contents removeLastObject]];
		cardNew = cardChanged = NO;
	}
	return YES;
}

- next:sender
/*
 * Show the next card.  If current one has been changed, ask.
 */
{
	if (! [self checkCurrent]) return nil;
	[self wasEasy:sender];
	cardNew = cardChanged = NO;
	return self;
}

- add:sender
/*
 * Add a new card to the deck.
 */
{
	if (! cardNew) {
		[contents takeCard:currentCard];
		currentCard = [self newCard];
	}
	return [self modify:sender];
}

- modify:sender
/*
 * Change the current Card.
 */
{
	writeToCard(currentCard,textView);
	[contents takeCard:currentCard];
	[self showTextOf:[self newCard]];
	[window setDocEdited:YES];
	return self;
}

- delete:sender
/*
 * Delete the current Card.
 */
{
    if (NXRunAlertPanel("Delete", "Are you sure you want to delete this card?", "YES", "NO", "Cancel") != NX_ALERTDEFAULT)
        return self;
    [window  setDocEdited:YES];
    [currentCard free];
    [self showTextOf:[contents giveCard]];
    return self;
}

/* Action Methods for manipulating the Deck */

- previous:sender
/*
 * Move back one.
 */
{
	if ([self checkCurrent]) {
		[contents setMarkerAt:([contents marker]+1)];
		[contents insertObject:currentCard at:0];
		[self showTextOf:[contents removeLastObject]];
		cardNew = cardChanged = NO;
	}
	return self;
}

- find:sender
/*
 * Find a string.  Use a panel to set parameters
 */
{
    id card;

/*    [self next:self]; */ [self checkCurrent];
    if (card = [super find:sender]) {
	[contents takeCard:currentCard];
	[contents cutAt:[contents indexOf:card]];
	[self showTextOf:[contents giveCard]];
    }
    return self;
}

- doAct:(SEL)action for:(STR)caller
 /*
  * Pass action methods on to Deck
  */
{
	if (viewMode) {
		[[contents takeCard:currentCard] perform:action with:self];
		return [self showTextOf:[contents giveCard]];
	} else {
		NotifyArg("Update", "Can't %s in update mode.",caller);
		return nil;
	}
}
  
- shuffle:sender
/*
 * Shuffle the deck.
 */
{
	return [self doAct:@selector(shuffle:) for:"shuffle"];
}

- sort:sender
/*
 * Organize in some coherent fashion.  Use a default sort on key.
 */
{
    [window setDocEdited:YES];
    return [self doAct:@selector(sort:) for:"sort"];
}

- swapFields:sender
/*
 * Exchange Key and Response.
 */
{
	return [self doAct:@selector(flipAll:) for:"swap fields"];
}

- reset:sender
{
	[markCount setIntValue:[[contents setMarkerAtEnd] marker]];
	return self;
}

/* Action Methods for File services */
	
- print:sender
/*
 * Create a view displaying the entire deck, staggered.  Then print it.
 */
{
	int row=0;
	NXRect myRect;
	View *deckView;
	Text *tv[2];
	Deck *pdeck;
	Card *pcard;
	Window *holder;

	pdeck = [contents copy];
	[[pdeck insertObject:currentCard at:0] setMarkerAtEnd];
	NXSetRect(&myRect, 0.0, 0.0, 2*P_WIDE, ([pdeck count]+1)*P_HIGH);
	deckView = [[View newFrame:&myRect] setFlip:YES];
	holder = [Window newContent:&myRect style:NX_PLAINSTYLE
		  backing: NX_NONRETAINED buttonMask:0 defer:NO];
	[holder setContentView:deckView];

	[deckView addSubview:[newText(1,row) setText:[self filename]] ];
	while (pcard = [pdeck giveCard]) {
		tv[0] = newText(0,row++);
		tv[1] = newText(1,row);
		readFromCard(pcard,tv);
		[deckView addSubview:tv[0]];
		[deckView addSubview:tv[1]];
	}
	[deckView printPSCode:sender];
	[holder close];
	return self;
}

- save:sender
/*
 * Saves the file.  If this document has never been saved to disk,
 * then a SavePanel is put up to ask the user what file name she
 * wishes to use to save the document.
 */
{
    if (! [contents count]) return self;

    [contents insertObject:currentCard at:0];
    [super save:sender];
    [contents giveCard];
    return self;
}

- revertToSaved:sender
/*
 * Revert the document back to what is on the disk.
 */ 
{
    [contents insertObject:currentCard at:0];
    [super revertToSaved:sender];
    [contents setMarkerAtEnd];
    [self showTextOf:[contents giveCard]];
    return self;
}


/* Text delegate methods. */


- (BOOL)textWillChange:sender
/*
 * Assigns the current text to where the cursor is.
 * Doesn't allow editing in view mode.
 */
{
	view = sender;
	if (viewMode) {
		Notify("Update", "Can only edit cards in Update Mode.");
		return YES;
	} else {
		cardChanged = YES;
		return NO;
	}
}

- (BOOL)textWillEnd:sender
/*
 * Nulls the current text as it resigns first responder.
 */
{
	view = nil;
	return NO;
}


/*
 * Target/Action messages to affect the current TextView in the main window.
 * This is the places where messages from the Text menu are handled.
 */


- centerText:sender
{
	alignText([self view],NX_CENTERED);
	return self;
}

- leftJustifyText:sender
{
	alignText([self view],NX_LEFTALIGNED);
	return self;
}

- rightJustifyText:sender
{
	alignText([self view],NX_RIGHTALIGNED);
	return self;
}

/* Outlet Connections */

- setReveal:anObject
{
    reveal = anObject;
    return self;
}

- setDiff:anObject
{
    diff = anObject;
    return self;
}

- setMarkCount:anObject
{
    markCount = anObject;
    return self;
}

@end

