//-----------------------------------------------------------------------------
// SokoScore.M
//
//	"Top-Scores" object for SokoSave.
//
// Copyright (c), 1997, Paul McCarthy.  All rights reserved.
// Copyright (c), 1997, Eric Sunshine.  All rights reserved.
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// $Id: SokoScore.M,v 1.2 97/12/10 07:02:38 sunshine Exp $
// $Log:	SokoScore.M,v $
//  Revision 1.2  97/12/10  07:02:38  sunshine
//  v10.1: Ported to OPENSTEP 4.1 for Mach, OPENSTEP 4.2 for Mach & Windows,
//  and Rhapsody Developer Release (RDR) for Mach and Windows (Yellow Box).
//  Now allows only horizontal resizing of "New Score" panel.
//  Replaced MiscTableScroll with NSTableView.
//  
//  Revision 1.1  97/11/13  02:59:04  zarnuk
//  v9
//-----------------------------------------------------------------------------
#import "SokoScore.h"
#import "SokoDefs.h"
#import "SokoFiles.h"
extern "Objective-C" {
#import <AppKit/NSApplication.h>
#import <AppKit/NSCell.h>
#import <AppKit/NSPanel.h>
#import <AppKit/NSTableColumn.h>
#import <AppKit/NSTableView.h>
#import <AppKit/NSTextField.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSDateFormatter.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSUserDefaults.h>
}

static NSString* const MAZE_KEY = @"Maze";
static NSString* const MOVE_KEY = @"Moves";
static NSString* const PUSH_KEY = @"Pushes";
static NSString* const DATE_KEY = @"Date";
static NSString* const NAME_KEY = @"Name";
static NSString* const NOTE_KEY = @"Notes";

inline static id get_def( NSString* name )
    { return [[NSUserDefaults standardUserDefaults] objectForKey:name]; }

inline static void set_def( NSString* name, id value )
    { [[NSUserDefaults standardUserDefaults] setObject:value forKey:name]; }


//-----------------------------------------------------------------------------
// scoreFileInput
//-----------------------------------------------------------------------------
static FILE* scoreFileInput()
    {
    FILE* fp;
    char const* s = [expand( getScoresFile() ) fileSystemRepresentation];
    if ((fp = fopen( s, "r" )) == 0)
	{
	s = [expand( getFactoryScoresFile() ) fileSystemRepresentation];
	fp = fopen( s, "r" );
	}
    return fp;
    }


//-----------------------------------------------------------------------------
// scoreFileOutput
//-----------------------------------------------------------------------------
static FILE* scoreFileOutput()
    {
    FILE* fp;
    NSString* const filename = expand( getScoresFile() );
    if ((fp = fopen( [filename fileSystemRepresentation], "w" )) == 0)
	{
	NSString* const dir = directoryPart( filename );
	if (!mkdirs( dir ) ||
	   (fp = fopen( [filename fileSystemRepresentation], "w" )) == 0)
	    NSRunAlertPanel( @"Sorry",
		@"Cannot create score file.\n\"%@\"\n%s", @"OK", 0, 0,
		filename, strerror( errno ) );
	}
    return fp;
    }


//-----------------------------------------------------------------------------
// good_int
//-----------------------------------------------------------------------------
static BOOL good_int( char const* t, int* p )
    {
    BOOL ok = NO;
    if (t != 0)
	{
	unsigned char const* s = (unsigned char const*) t;
	while (*s != 0 && isspace(*s))		// Skip leading whitespace.
	    s++;
	if ('0' <= *s && *s <= '9')
	    {
	    int x = 0;
	    do  {
		x = x * 10 + *s - '0';
		s++;
		}
	    while ('0' <= *s && *s <= '9');
	    if (x >= 0)				// If no overflow.
		{
		while (*s != 0 && isspace(*s))	// Skip trailing whitespace.
		    s++;
		if (*s == 0)			// Disallow trailing garbage.
		    {
		    *p = x;
		    ok = YES;
		    }
		}
	    }
	}
    return ok;
    }


//-----------------------------------------------------------------------------
// score_cmp
//	Unnecessary and incorrect (NSString*) cast avoids compiler warning
//	since Foundation inappropriately defines different -compare prototypes.
//-----------------------------------------------------------------------------
static int score_cmp( id p1, id p2, void* context )
    {
    NSArray* order = (NSArray*)context;
    int const lim = [order count];
    for (int i = 0; i < lim; i++)
	{
	id const key = [order objectAtIndex:i];
	id const v1 = [p1 objectForKey:key];
	id const v2 = [p2 objectForKey:key];
	NSComparisonResult const rc = [(NSString*)v1 compare:v2]; // NOTE *1*
	if (rc != NSOrderedSame)
	    return rc;
	}
    return NSOrderedSame;
    }


//=============================================================================
// IMPLEMENTATION
//=============================================================================
@implementation SokoScore

//-----------------------------------------------------------------------------
// -scoreWithMaze:moves:pushes:timestamp:name:notes:
//-----------------------------------------------------------------------------
- (id)scoreWithMaze:(NSString*)maze moves:(int)moves pushes:(int)pushes
    timestamp:(NSDate*)ts name:(NSString*)name notes:(NSString*)notes
    {
    return [NSDictionary dictionaryWithObjectsAndKeys:
	maze, MAZE_KEY,
	[NSNumber numberWithInt:moves], MOVE_KEY,
	[NSNumber numberWithInt:pushes], PUSH_KEY,
	ts, DATE_KEY,
	name, NAME_KEY,
	notes, NOTE_KEY, 0];
    }


//-----------------------------------------------------------------------------
// -sortScores:
//-----------------------------------------------------------------------------
- (NSArray*)sortScores:(NSArray*)array
    {
    return [array sortedArrayUsingFunction:&score_cmp context:columnOrder];
    }


//-----------------------------------------------------------------------------
// -insort:score:
//-----------------------------------------------------------------------------
- (NSArray*)insort:(NSArray*)array score:(id)score
    {
    array = [array arrayByAddingObjectsFromArray:
	[NSArray arrayWithObject:score]];
    return [array sortedArrayUsingFunction:&score_cmp context:columnOrder
	hint:[array sortedArrayHint]];
    }


//-----------------------------------------------------------------------------
// -readScores
//-----------------------------------------------------------------------------
- (NSArray*)readScores
    {
    NSMutableArray* array = [NSMutableArray array];
    FILE* fp;
    time_t ts;
    int len, line, moves, pushes;
    char buff[ 2048 ];
    enum { MAZE_COL,MOVE_COL,PUSH_COL,DATE_COL,NAME_COL,NOTE_COL,MAX_COL };
    char* fld[ MAX_COL ];

    if ((fp = scoreFileInput()) != 0)
	{
	line = 0;
	while (fgets( buff, sizeof(buff), fp ) != 0)
	    {
	    line++;

	    len = strlen( buff ) - 1;
	    if (len >= 0 && buff[ len ] == '\n')
		buff[ len ] = '\0';	// Nuke trailing '\n'.

	    char* s = buff;		// Break into fields at tabs.
	    for (int i = 0; i < int(MAX_COL); i++)
		{
		fld[i] = s;
		while (*s != 0 && *s != '\t')
		    s++;
		if (*s != 0)
		    *s++ = 0;
		}

	    if (good_int( fld[MOVE_COL], &moves ) &&
		good_int( fld[PUSH_COL], &pushes ) &&
		good_int( fld[DATE_COL], (int*)&ts ))
		{
		[array addObject:[self
		    scoreWithMaze:[NSString stringWithCString:fld[MAZE_COL]]
		    moves:moves pushes:pushes
		    timestamp:[NSDate dateWithTimeIntervalSince1970:ts]
		    name:[NSString stringWithCString:fld[NAME_COL]]
		    notes:[NSString stringWithCString:fld[NOTE_COL]]]];
		}
	    else
		{
		fprintf( stderr, "SokoSave:%s:%d: "
			    "format error in scores file.\n",
			    [expand(getScoresFile()) cString], line );
		}
	    }
	fclose(fp);
	}
    return array;
    }


//-----------------------------------------------------------------------------
// -writeScores
//-----------------------------------------------------------------------------
- (void)writeScores
    {
    FILE* fp;
    if ((fp = scoreFileOutput()) != 0)
	{
	int const lim = [scores count];
	for (int i = 0; i < lim; i++)
	    {
	    NSDictionary* const score = [scores objectAtIndex:i];
	    fprintf( fp, "%s\t%d\t%d\t%ld\t%s\t%s\n",
		[[score objectForKey:MAZE_KEY] cString],
		[[score objectForKey:MOVE_KEY] intValue],
		[[score objectForKey:PUSH_KEY] intValue],
		(time_t)[[score objectForKey:DATE_KEY] timeIntervalSince1970],
		[[score objectForKey:NAME_KEY] cString],
		[[score objectForKey:NOTE_KEY] cString] );
	    }
	fclose(fp);
	dirty = NO;
	}
    }


//-----------------------------------------------------------------------------
// -setColumnOrder:
//-----------------------------------------------------------------------------
- (void)setColumnOrder:(NSArray*)order
    {
    int lim = [order count];
    for (int i = 0; i < lim; i++)
	[tableView moveColumn:[tableView columnWithIdentifier:
		[order objectAtIndex:i]] toColumn:i];
    }


//-----------------------------------------------------------------------------
// -setColumnSizes:
//-----------------------------------------------------------------------------
- (void)setColumnSizes:(NSDictionary*)sizes
    {
    NSEnumerator* enumerator = [[tableView tableColumns] objectEnumerator];
    id col;
    while ((col = [enumerator nextObject]) != 0)
	{
	id size = [sizes objectForKey:[col identifier]];
	if (size != 0)
	    [col setWidth:[size floatValue]];
	}
    }


//-----------------------------------------------------------------------------
// -columnOrder
//-----------------------------------------------------------------------------
- (NSArray*)columnOrder
    {
    NSMutableArray* columns = [NSMutableArray array];
    NSEnumerator* enumerator = [[tableView tableColumns] objectEnumerator];
    id col;
    while ((col = [enumerator nextObject]) != 0)
	[columns addObject:[col identifier]];
    return columns;
    }


//-----------------------------------------------------------------------------
// -columnSizes
//-----------------------------------------------------------------------------
- (NSDictionary*)columnSizes
    {
    NSMutableDictionary* sizes = [NSMutableDictionary dictionary];
    NSEnumerator* enumerator = [[tableView tableColumns] objectEnumerator];
    id col;
    while ((col = [enumerator nextObject]) != 0)
	[sizes setObject:[[NSNumber numberWithFloat:[col width]] description]
		forKey:[col identifier]];
    return sizes;
    }


//-----------------------------------------------------------------------------
// -terminate
//-----------------------------------------------------------------------------
- (void)terminate
    {
    if (dirty)
	[self writeScores];
    if (saveOrder)
	set_def( @"ScoreColOrder", [self columnOrder] );
    if (saveSizes)
	set_def( @"ScoreColSizes", [self columnSizes] );
    }


//-----------------------------------------------------------------------------
// NSTableView delegate & data source methods
//-----------------------------------------------------------------------------
- (void)tableViewColumnDidResize:(NSNotification*)notification
    { saveSizes = YES; }

- (void)tableViewColumnDidMove:(NSNotification*)notification
    {
    saveOrder = YES;
    [columnOrder autorelease];
    columnOrder = [[self columnOrder] retain];
    [scores autorelease];
    scores = [[self sortScores:scores] retain];
    [tableView reloadData];
    }

- (int)numberOfRowsInTableView:(NSTableView*)aTableView
    { return [scores count]; }

- (id)tableView:(NSTableView*)sender
    objectValueForTableColumn:(NSTableColumn*)column row:(int)row
    { return [[scores objectAtIndex:row] objectForKey:[column identifier]]; }


//-----------------------------------------------------------------------------
// -init
//-----------------------------------------------------------------------------
- (id)init
    {
    [super init];
    [NSBundle loadNibNamed:@"SokoScore" owner:self];
    [window setFrameAutosaveName:@"SokoScore"];
    [window setMenu:0]; // Under MS-Windows Scores should not have a menu.
    [[[tableView tableColumnWithIdentifier:DATE_KEY] dataCell]
	setFormatter:[[[NSDateFormatter allocWithZone:[tableView zone]]
	initWithDateFormat:get_def(NSShortTimeDateFormatString)
	allowNaturalLanguage:NO] autorelease]];

    id def;
    if ((def = get_def( @"ScoreColSizes" )) != 0)
	[self setColumnSizes:def];
    if ((def = get_def( @"ScoreColOrder" )) != 0)
	[self setColumnOrder:def];
    columnOrder = [[self columnOrder] retain];

    scores = [[self sortScores:[self readScores]] retain];
    dirty = saveSizes = saveOrder = NO;

    [tableView reloadData];
    return self;
    }


//-----------------------------------------------------------------------------
// -dealloc
//-----------------------------------------------------------------------------
- (void)dealloc
    {
    [window close];
    [window release];
    [newPanel close];
    [newPanel release];
    [columnOrder release];
    [scores release];
    [super dealloc];
    }


//-----------------------------------------------------------------------------
// -orderFront:
//-----------------------------------------------------------------------------
- (void)orderFront:(id)sender
    {
    [window orderFront:sender];
    }


//-----------------------------------------------------------------------------
// -windowWillResize:toSize:
//-----------------------------------------------------------------------------
- (NSSize)windowWillResize:(NSWindow*)sender toSize:(NSSize)size
    {
    if (sender == newPanel)
	size.height = [newPanel frame].size.height;
    return size;
    }


//-----------------------------------------------------------------------------
// +globalInstance:
//-----------------------------------------------------------------------------
+ (SokoScore*)globalInstance:(BOOL)create
    {
    static SokoScore* instance = 0;
    if (instance == 0 && create)
	instance = [[self alloc] init];
    return instance;
    }


//-----------------------------------------------------------------------------
// +launch
//-----------------------------------------------------------------------------
+ (void)launch
    {
    [[self globalInstance:YES] orderFront:self];
    }


//-----------------------------------------------------------------------------
// -newOk:
//-----------------------------------------------------------------------------
- (void)newOk:(id)sender
    {
    [NSApp stopModal]; 
    }


//-----------------------------------------------------------------------------
// -fixField:
//	Convert all tab and newline characters into single spaces so that
//	they will not screw up the syntax when the data is written to the
//	SCORES file.
//-----------------------------------------------------------------------------
- (NSString*)fixField:(NSString*)input
    {
    NSString* output = @"";
    NSCharacterSet* ws = [NSCharacterSet whitespaceAndNewlineCharacterSet];
    NSScanner* scanner = [NSScanner scannerWithString:input];
    [scanner scanUpToCharactersFromSet:ws intoString:&output];
    while (![scanner isAtEnd])
	{
	NSString* more;
	[scanner scanCharactersFromSet:ws intoString:0];
	if ([scanner scanUpToCharactersFromSet:ws intoString:&more])
	    output = [output stringByAppendingFormat:@" %@", more];
	}
    return output;
    }


//-----------------------------------------------------------------------------
// -solved:moves:pushes:
//-----------------------------------------------------------------------------
- (void)solved:(NSString*)maze moves:(int)moves pushes:(int)pushes
    {
    dirty = YES;

    maze = [self fixField:[[maze lastPathComponent]
	stringByDeletingPathExtension]];
    NSDate* const ts = [NSDate date];
    NSString* const name = NSUserName();

    NSDictionary* score = [self scoreWithMaze:maze moves:moves pushes:pushes
	timestamp:ts name:name notes:@""];
    NSArray* oldScores = [scores autorelease];
    scores = [self insort:oldScores score:score];

    int row = [scores indexOfObject:score];
    [tableView noteNumberOfRowsChanged];
    [tableView selectRow:row byExtendingSelection:NO];
    [tableView scrollRowToVisible:row];
    [window orderFront:self];

    [newMaze setStringValue:maze];
    [newMoves setIntValue:moves];
    [newPushes setIntValue:pushes];
    [newName setStringValue:name];
    [newNotes setStringValue:@""];
    [newNotes selectText:self];

    [newPanel makeKeyAndOrderFront:self];
    [NSApp runModalForWindow:newPanel];
    [newPanel close];
    [window makeKeyAndOrderFront:self];

    NSString* const t_name = [newName stringValue];
    NSString* const t_note = [newNotes stringValue];
    if ((![t_name isEqualToString:@""] && ![t_name isEqualToString:name]) ||
	![t_note isEqualToString:@""])
	{
	score = [self scoreWithMaze:maze moves:moves pushes:pushes timestamp:ts
		name:[self fixField:t_name] notes:[self fixField:t_note]];
	scores = [self insort:oldScores score:score];
	row = [scores indexOfObject:score];
	[tableView selectRow:row byExtendingSelection:NO];
	[tableView scrollRowToVisible:row];
	[tableView reloadData];
	}
    [scores retain];
    }


//-----------------------------------------------------------------------------
// +solved:moves:pushes:
//-----------------------------------------------------------------------------
+ (void)solved:(NSString*)maze moves:(int)moves pushes:(int)pushes
    {
    [[self globalInstance:YES] solved:maze moves:moves pushes:pushes];
    }


//-----------------------------------------------------------------------------
// +terminate
//-----------------------------------------------------------------------------
+ (void)terminate
    {
    [[self globalInstance:NO] terminate];
    }


//-----------------------------------------------------------------------------
// -performPrint:
//-----------------------------------------------------------------------------
- (void)performPrint:(id)sender
    {
    [tableView print:self];
    }

@end
