#import "Drachma.h"
#import "DrachmaController.h"
#import "OACategories.h"
#import "OADebug.h"
#import "CompletingTextView.h"
#import "OACoreData.h"
#import "DrachmaPayeeManagement.h"
#import "ModeledObjectTypes.h"
#import "OADataWatch.h"
#import "ExpenseTransferer.h"
#import "Balancer.h"
#import "OADecimalNumberExpressionFormatter.h"
#import "OAListSerializerFormatter.h"
#import "Queries.h"
#import "TextExport.h"
#import "OAMacros.h"

// This array defines the fields of the expense
// managed object.  It indicates the names of the
// fields, and the selectors to be used for
// getting and setting them.  These same selectors
// and field names will work on the DrachmaController.
// It might be cooler if we scraped this information
// from the Entity description itself.
struct ExpenseField {
    NSString *fieldName;
    SEL getSelector;
    SEL setSelector;
	BOOL resetAfterEnter;   // after adding a new expensve, reset to nil?
    BOOL loadOnCompletion;  // after hitting esc, fill in the value of this field?
} expenseFields[22]; 

@interface DrachmaController (Private)
- (void) setExpensesToDate:value;
- (void) setTotalExpenses:value;
@end

@implementation DrachmaController

#pragma mark -
#pragma mark Internal initialization, dealloc

+ (void) initialize
{
    // Initialize the expenseFields array.
    struct ExpenseField fields[] = {
    { @"date", @selector(date), @selector(setDate:), NO, NO },
    { @"amount", @selector(amount), @selector(setAmount:), YES, YES },
    { @"checkNumber", @selector(checkNumber), @selector(setCheckNumber:), YES, NO },
    { @"payeeName", @selector(payeeName), @selector(setPayeeName:), YES, NO },
    { @"memo", @selector(memo), @selector(setMemo:), YES, NO },
    { @"category", @selector(category), @selector(setCategory:), YES, YES },
    { NULL, NULL, NULL }
    };
    memcpy (&expenseFields, &fields, sizeof (fields));
    
    // re-evaluate whether or not we can commit any time one of those fields changes
    NSMutableArray *allFields = [NSMutableArray arrayWithCapacity:20];
    for (struct ExpenseField *ef = expenseFields; ef->fieldName; ef++)
        [allFields addObject:ef->fieldName];
    [self setKeys:allFields triggerChangeNotificationsForDependentKey:@"canCommit"];
}

- initWithDrachma:(Drachma*)drachma
{
    if ((self = [super initWithWindowNibName:@"Drachma"])) {
        m_drachma = [drachma retain];
        [self clearNewExpense:self];        
    }
    return self;
}

- (void) awakeFromNib
{
    // We need to observe changes to the set of data, and to the set of expenses,
    // so we know when to update certain parts of our display:
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center addObserver:self 
               selector:@selector(objectsDidChange:) 
                   name:NSManagedObjectContextObjectsDidChangeNotification 
                 object:[m_drachma managedObjectContext]];
    [m_expensesController addObserver:self forKeyPath:@"selection" options:0 context:nil];
    
    // The amount column is an expression column:
    [[m_amountColumn dataCell] setFormatter:[[[OADecimalNumberExpressionFormatter alloc] init] autorelease]];
    
    // The "distribute into" column is a list of categories:
    OAListSerializerFormatter *oalsf = 
        [OAListSerializerFormatter formatterWithAvailableChoices:[OAKeyPath keyPathWithObject:m_categoriesController
                                                                                      keyPath:@"arrangedObjects"]
                                                        namePath:@"name"
                                                  containerClass:[NSMutableSet class]];
    [[m_distributeIntoColumn dataCell] setFormatter:oalsf];    
}

- (void) dealloc
{
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center removeObserver:self];

    [m_drachma release];
    for (struct ExpenseField *ef = expenseFields; ef->fieldName != NULL; ef++) {
        [self performSelector:ef->setSelector withObject:nil];
    }
    [m_expensesController removeObserver:self forKeyPath:@"selection"];
    [super dealloc];
}

+ (DrachmaController*) drachmaControllerWithDrachma:(Drachma*)drachma
{
    return [[[DrachmaController alloc] initWithDrachma:drachma] autorelease];
}

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    // 
    [self setExpensesToDate:nil];
}

- (void) objectsDidChange:(NSNotificationCenter*)center
{
    // when an expense changes, the total may change as well...
    [self setTotalExpenses:nil];    
}

#pragma mark -
#pragma mark Observable Values

- (NSManagedObjectContext*) managedObjectContext
{
    return [m_drachma managedObjectContext];
}

- (Drachma*) drachma
{
    return m_drachma;
}

- (NSDate*) date
{
    return [[m_date retain] autorelease];
}

- (void) setDate:(NSDate*)date
{
    [m_date autorelease];
    m_date = [date copy];
}

- (NSNumber*) amount
{
    return [[m_amount retain] autorelease];
}

- (void) setAmount:(NSNumber*)num
{
    [m_amount autorelease];
    m_amount = [num copy];
}

- (NSNumber*) checkNumber
{
    return [[m_checkNumber retain] autorelease];
}

- (void) setCheckNumber:(NSNumber*)num
{
    [m_checkNumber autorelease];
    m_checkNumber = [num copy];
}

- (NSString*) payeeName
{
    return [[m_payeeName retain] autorelease];
}

- (void) setPayeeName:(NSString*)num
{
    [m_payeeName autorelease];
    m_payeeName = [num copy];
}

- (NSString*) memo
{
    return [[m_memo retain] autorelease];
}

- (void) setMemo:(NSString*)num
{
    [m_memo autorelease];
    m_memo = [num copy];
}

- (Category*) category
{
    return [[m_category retain] autorelease];
}

- (void) setCategory:(Category*)num
{
    [m_category autorelease];
    m_category = [num retain];
}

- (BOOL) isTransfer
{
	return m_isTransfer;
}

- (void) setIsTransfer:(BOOL)transfer
{
	m_isTransfer = transfer;
}

- (Category*) transferCategory
{
	return m_transferCategory;
}

- (void) setTransferCategory:(Category*)num
{
	[m_transferCategory autorelease];
	m_transferCategory = [num retain];
}

- (BOOL) canCommit
{
    // To commit, we need a payee, category, and amount, I'd say.
    return (m_payeeName != nil && m_category != nil && m_amount != nil);
}

- (NSNumber*) totalExpenses
{
    NSArray *expenses = [[m_drachma managedObjectContext] expenses];
    return [[expenses valueForKeyPath:@"@sum.amount"] decimalNumberByNegation];
}

- (void) setTotalExpenses:value
{
    // dummy routine used only to notify observers of changes
}

- (NSNumber*) expensesToDate
{
    Expense *selection = [m_expensesController selectedObject];
    if (selection == nil)
        return [NSNumber numberWithInt:0];
    
    NSDate *selectionDate = [selection date];
    NSArray *expenses = [[m_drachma managedObjectContext] expensesBefore:selectionDate];
    NSNumber *sum = [[expenses valueForKeyPath:@"@sum.amount"] decimalNumberByNegation];
    return sum;
}

- (void) setExpensesToDate:value
{
    // dummy routine used only to notify observers of changes
}

#pragma mark -
#pragma mark Miscellaneous

- (void) loadFieldsFrom:(Expense*)exp
{
    for (struct ExpenseField *ef = expenseFields; ef->fieldName; ef++) {
        if (ef->loadOnCompletion) {
            id value = [exp valueForKey:ef->fieldName];
            [self performSelector:ef->setSelector withObject:value];
        }
    }
}

#pragma mark -
#pragma mark IB Actions

- (IBAction)clearNewExpense:(id)sender
{
    // Clear some fields
    for (struct ExpenseField *ef = expenseFields; ef->fieldName != NULL; ef++) {
		if(ef->resetAfterEnter)
			[self performSelector:ef->setSelector withObject:nil];
    }
	
	[self setIsTransfer:NO]; // Note: don't clear transfer category
	
	if (sender != nil) // sent by the UI, reset the date:
		[self setDate:[NSDate date]];
}

- (void)setExpenseFields:(Expense*)newExp
{
	for (struct ExpenseField *ef = expenseFields; ef->fieldName != NULL; ef++) {
        id value = [self performSelector:ef->getSelector];
        if (value != nil)
            [newExp setValue:value forKey:ef->fieldName];
    }
}	

- (IBAction)commitNewExpense:(id)sender
{
    NSManagedObjectContext *managedObjectContext = [m_drachma managedObjectContext];
    Expense *newExp = [NSEntityDescription insertNewObjectForEntityForName:@"Expense"
													inManagedObjectContext:managedObjectContext];    
	[self setExpenseFields:newExp];
	NSMutableArray *newExps = [NSMutableArray arrayWithObject:newExp];
	
	if (m_isTransfer && m_transferCategory != nil) {
		Expense *xferExp = [NSEntityDescription insertNewObjectForEntityForName:@"Expense"
														 inManagedObjectContext:managedObjectContext];
		[self setExpenseFields:xferExp];
		[xferExp setCategory:m_transferCategory];
		[xferExp setAmount:[[newExp amount] decimalNumberByNegation]];
		[newExps addObject:xferExp];
	}
    
    [self clearNewExpense:nil]; // use nil to avoid clearing date
    [[self window] makeFirstResponder:m_payeeField];
    [m_expensesController setSelectedObjects:newExps];
    [m_expensesTable scrollRowToVisible:[m_expensesController selectionIndex]];
}

- (IBAction) debug:sender
{
    [OADataWatch displayContext:[m_drachma managedObjectContext]
                          model:[m_drachma managedObjectModel]];
}

- (NSArrayController*) deletionController
{
    if ([[[m_tab selectedTabViewItem] identifier] isEqualToString:@"expenses"])
        return m_expensesController;
    return nil;
}

- (IBAction) delete:(id)sender
{
    [[self deletionController] remove:sender];
}

- (IBAction)copy:(id)sender
{
	NSIndexSet *selectedIndices = [m_expensesController selectionIndexes];
	NSMutableString *result = [NSMutableString string];
	
	for (int idx = [selectedIndices firstIndex]; 
		 idx != NSNotFound; 
		 idx = [selectedIndices indexGreaterThanIndex:idx])
	{
		// TODO-- Allow more control over this format
		Expense *expense = [[m_expensesController arrangedObjects] objectAtIndex:idx];
		
		[result appendString:[[expense date] description]]; 
		
		NSArray *keys = [NSArray arrayWithObjects:@"amount", @"payeeName", @"category.name", @"memo", nil];
		for (int i = 0; i < [keys count]; i++) {
			NSString *key = [keys objectAtIndex:i];
			[result appendString:@"\t"];
			id item = [expense valueForKeyPath:key];
			if (item != nil)
				[result appendString:[item description]]; 
		}
		
		[result appendString:@"\n"];
	}
	
	NSPasteboard *pb = [NSPasteboard generalPasteboard];
	NSArray *types = [NSArray arrayWithObjects:NSStringPboardType, nil];
	[pb declareTypes:types owner:nil]; // TODO--- a richer, internal rep
	[pb setString:result forType:NSStringPboardType];
}

- (BOOL) validateMenuItem:(id)menuItem
{
    if ([menuItem action] == @selector(delete:)) {
        NSArrayController *cont = [self deletionController];
        return cont != nil && [cont canRemove];
    } else if ([menuItem action] == @selector(copy:)) {
		if ([[[m_tab selectedTabViewItem] identifier] isEqual:@"expenses"]) {
			return [[m_expensesController selectionIndexes] count] != 0;
		}
		return NO;
	}
    return YES;
}

- (IBAction) importQIF:(id)sender
{
    [m_QIFImporter importQIF:sender];
}

- (IBAction) exportText:(id)sender
{
    [m_TextExporter textExport:sender];
}

- (IBAction)addBudget:(id)sender
{
    NSManagedObjectContext *mctx = [m_drachma managedObjectContext];
    NSArray *oldBudgets = [m_drachma sortedBudgets];

    // Create a new budget starting with the current month/year
    Budget *newBudget = [NSEntityDescription insertNewObjectForEntityForName:@"Budget"
                                                      inManagedObjectContext:mctx];
    NSCalendarDate *now = [NSCalendarDate calendarDate];
    NSCalendarDate *firstOfMonth = [NSCalendarDate dateWithYear:[now yearOfCommonEra]
                                                          month:[now monthOfYear]
                                                            day:1
                                                           hour:0
                                                         minute:0
                                                         second:0
                                                       timeZone:[now timeZone]];
    [newBudget setStartDate:firstOfMonth];
    
    // Copy any line items from the most recent existing budget
    if ([oldBudgets count] != 0) {
        Budget *mostRecent = [oldBudgets lastObject];
        FOREACH(LineItem *, li, [mostRecent getTheLineItems]) {
            LineItem *copy = [NSEntityDescription insertNewObjectForEntityForName:@"LineItem"
                                                           inManagedObjectContext:mctx];
            [copy setCategory:[li category]];
            [copy setAmount:[li amount]];
            [copy setBudget:newBudget];
        } ENDFOREACH;
    } 
    
    // Ensure a line item exists for all categories
    FOREACH(Category *, c, [mctx objectsOfEntityType:@"Category"
                             matchingPredicateFormat:@"distributeInto[SIZE] == 0"])
    {
        if ([[newBudget getTheLineItems] containsObjectMatchingPredicate:
             [NSPredicate predicateWithFormat:@"category == %@", c]])
            continue;
        LineItem *copy = [NSEntityDescription insertNewObjectForEntityForName:@"LineItem"
                                                       inManagedObjectContext:mctx];
        [copy setCategory:c];
        [copy setAmount:[NSDecimalNumber zero]];
        [copy setBudget:newBudget];
    } ENDFOREACH;
}

- (IBAction)reassign:(id)sender
{
	NSArray *expenses = [m_expensesController selectedObjects];
    [m_transferer initiateTransferOfSelectedExpenses:expenses
											delegate:nil 
											selector:nil 
											  window:[self window]];
}

#pragma mark -
#pragma mark Text completion code

- (id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)anObject
{
    //id fieldEditor = [sender fieldEditor:YES forObject:self];
    //[fieldEditor setDelegate:self];
    //return fieldEditor;
    if (anObject == m_payeeField) {
        OALog(@"Payee Field field editor requested, returning special text view");
        [m_completingTextView setCompletionsEnabled:YES];
        return m_completingTextView;
    }
    else 
        return nil;
}

#pragma mark -
#pragma mark Tab view delegation: balance computation

- (void)tabView:(NSTabView *)tabView didSelectTabViewItem:(NSTabViewItem *)tabViewItem
{
    if ([[tabViewItem identifier] isEqualToString:@"balance"]) {
        [m_balancer computeBalances];
    }
}

@end
