//
//  $Id: SPFieldEditorController.h 802 2009-06-03 20:46:57Z bibiko $
//
//  SPHistoryController.h
//  sequel-pro
//
//  Created by Rowan Beentje on July 23, 2009
//
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation; either version 2 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
//  More info at <http://code.google.com/p/sequel-pro/>

#import "TableDocument.h"
#import "TableContent.h"
#import "TablesList.h"
#import "SPHistoryController.h"


@implementation SPHistoryController

@synthesize modifyingHistoryState;

#pragma mark Setup and teardown

/**
 * Initialise by creating a blank history array
 */
- (id) init
{
	if (self = [super init]) {
		history = [[NSMutableArray alloc] init];
		historyPosition = NSNotFound;
		modifyingHistoryState = NO;
	}
	return self;	
}

- (void) awakeFromNib
{
	tableContentInstance = [theDocument valueForKey:@"tableContentInstance"];
}

- (void) dealloc
{
	[history release];
	[super dealloc];
}

#pragma mark -
#pragma mark Interface interaction

/**
 * Updates the toolbar item to reflect the current history state and position
 */
- (void) updateToolbarItem
{
	BOOL backEnabled = NO;
	BOOL forwardEnabled = NO;
	int i;
	NSMenu *navMenu;

	// Set the active state of the segments if appropriate
	if ([history count] && historyPosition > 0) backEnabled = YES;
	if ([history count] && historyPosition + 1 < [history count]) forwardEnabled = YES;
	
	[historyControl setEnabled:backEnabled forSegment:0];
	[historyControl setEnabled:forwardEnabled forSegment:1];

	// Generate back and forward menus as appropriate to reflect the new state
	if (backEnabled) {
		navMenu = [[NSMenu alloc] init];
		for (i = historyPosition - 1; i >= 0; i--) {
			[navMenu addItem:[self menuEntryForHistoryEntryAtIndex:i]];
		}
		[historyControl setMenu:navMenu forSegment:0];
		[navMenu release];
	} else {
		[historyControl setMenu:nil forSegment:0];
	}
	if (forwardEnabled) {
		navMenu = [[NSMenu alloc] init];
		for (i = historyPosition + 1; i < [history count]; i++) {
			[navMenu addItem:[self menuEntryForHistoryEntryAtIndex:i]];
		}
		[historyControl setMenu:navMenu forSegment:1];
		[navMenu release];
	} else {
		[historyControl setMenu:nil forSegment:1];
	}
}

/**
 * Trigger a navigation action in response to a click
 */
- (IBAction) historyControlClicked:(NSSegmentedControl *)theControl
{

	switch ([theControl selectedSegment]) {

		// Back button clicked:
		case 0:
			if (historyPosition == NSNotFound || !historyPosition) return;
			[self loadEntryAtPosition:historyPosition - 1];
			break;

		// Forward button clicked:
		case 1:
			if (historyPosition == NSNotFound || historyPosition + 1 >= [history count]) return;
			[self loadEntryAtPosition:historyPosition + 1];
			break;
	}
}

/**
 * Retrieve the view that is currently selected from the database
 */
- (unsigned int) currentlySelectedView
{
	unsigned int theView = NSNotFound;

	NSString *viewName = [[[theDocument valueForKey:@"tableTabView"] selectedTabViewItem] identifier];
	if ([viewName isEqualToString:@"source"]) {
		theView = SP_VIEW_STRUCTURE;
	} else if ([viewName isEqualToString:@"content"]) {
		theView = SP_VIEW_CONTENT;
	} else if ([viewName isEqualToString:@"customQuery"]) {
		theView = SP_VIEW_CUSTOMQUERY;
	} else if ([viewName isEqualToString:@"status"]) {
		theView = SP_VIEW_STATUS;
	} else if ([viewName isEqualToString:@"relations"]) {
		theView = SP_VIEW_RELATIONS;
	}

	return theView;
}

#pragma mark -
#pragma mark Adding or updating history entries

/**
 * Call to store or update a history item for the document state. Checks against
 * the latest stored details; if they match, a new history item is not created.
 * This should therefore be called without worry of duplicates.
 * Table histories are created per table/filter setting, and while view changes
 * update the current history entry, they don't replace it.
 */
- (void) updateHistoryEntries
{

	// Don't modify anything if we're in the process of restoring an old history state
	if (modifyingHistoryState) return;

	// Work out the current document details
	NSString *theDatabase = [theDocument database];
	NSString *theTable = [theDocument table];
	unsigned int theView = [self currentlySelectedView];
	NSString *contentSortCol = [tableContentInstance sortColumnName];
	BOOL contentSortColIsAsc = [tableContentInstance sortColumnIsAscending];
	unsigned int contentLimitStartPosition = [tableContentInstance limitStart];
	NSIndexSet *contentSelectedIndexSet = [tableContentInstance selectedRowIndexes];
	NSRect contentViewport = [tableContentInstance viewport];
	NSDictionary *contentFilter = [tableContentInstance filterSettings];
	if (!theDatabase) return;

	// If there's any items after the current history position, remove them
	if (historyPosition != NSNotFound && historyPosition < [history count] - 1) {
		[history removeObjectsInRange:NSMakeRange(historyPosition + 1, [history count] - historyPosition - 1)];

	} else if (historyPosition != NSNotFound && historyPosition == [history count] - 1) {
		NSDictionary *currentHistoryEntry = [history objectAtIndex:historyPosition];

		// If the table is the same, and the filter settings haven't changed, delete the
		// last entry so it can be replaced.  This updates navigation within a table, rather than
		// creating a new entry every time detail is changed.
		if ([[currentHistoryEntry objectForKey:@"database"] isEqualToString:theDatabase]
			&& [[currentHistoryEntry objectForKey:@"table"] isEqualToString:theTable]
			&& ([[currentHistoryEntry objectForKey:@"view"] intValue] != theView
				|| ((![currentHistoryEntry objectForKey:@"contentFilter"] && !contentFilter)
					|| (![currentHistoryEntry objectForKey:@"contentFilter"]
						&& ![[contentFilter objectForKey:@"filterValue"] length]
						&& ![[contentFilter objectForKey:@"filterComparison"] isEqualToString:@"IS NULL"]
						&& ![[contentFilter objectForKey:@"filterComparison"] isEqualToString:@"IS NOT NULL"])
					|| [[currentHistoryEntry objectForKey:@"contentFilter"] isEqualToDictionary:contentFilter])))
		{
			[history removeLastObject];

		// Special case: if the last history item is currently active, and has no table,
		// but the new selection does - delete the last entry, in order to replace it.
		// This improves history flow.
		} else if ([[currentHistoryEntry objectForKey:@"database"] isEqualToString:theDatabase]
			&& ![currentHistoryEntry objectForKey:@"table"])
		{
			[history removeLastObject];
		}
	}

	// Construct and add the new history entry
	NSMutableDictionary *newEntry = [NSMutableDictionary dictionaryWithObjectsAndKeys:
										theDatabase, @"database",
										theTable, @"table",
										[NSNumber numberWithInt:theView], @"view",
										[NSNumber numberWithBool:contentSortColIsAsc], @"contentSortColIsAsc",
										[NSNumber numberWithInt:contentLimitStartPosition], @"contentLimitStartPosition",
										[NSValue valueWithRect:contentViewport], @"contentViewport",
										nil];
	if (contentSortCol) [newEntry setObject:contentSortCol forKey:@"contentSortCol"];
	if (contentSelectedIndexSet) [newEntry setObject:contentSelectedIndexSet forKey:@"contentSelectedIndexSet"];
	if (contentFilter) [newEntry setObject:contentFilter forKey:@"contentFilter"];

	[history addObject:newEntry];
	
	// If there are now more than fifty history entries, remove one from the start
	if ([history count] > 50) [history removeObjectAtIndex:0];

	historyPosition = [history count] - 1;
	[self updateToolbarItem];
}

#pragma mark -
#pragma mark Loading history entries

/**
 * Load a history entry and attempt to return the interface to that state.
 */
- (void) loadEntryAtPosition:(unsigned int)position
{

	// Sanity check the input
	if (position == NSNotFound || position < 0 || position >= [history count]) {
		NSBeep();
		return;
	}

	modifyingHistoryState = YES;

	// Update the position and extract the history entry
	historyPosition = position;
	NSDictionary *historyEntry = [history objectAtIndex:historyPosition];

	// Set table content details for restore
	[tableContentInstance setSortColumnNameToRestore:[historyEntry objectForKey:@"contentSortCol"] isAscending:[[historyEntry objectForKey:@"contentSortCol"] boolValue]];
	[tableContentInstance setLimitStartToRestore:[[historyEntry objectForKey:@"contentLimitStartPosition"] intValue]];
	[tableContentInstance setSelectedRowIndexesToRestore:[historyEntry objectForKey:@"contentSelectedIndexSet"]];
	[tableContentInstance setViewportToRestore:[[historyEntry objectForKey:@"contentViewport"] rectValue]];
	[tableContentInstance setFiltersToRestore:[historyEntry objectForKey:@"contentFilter"]];

	// If the database, table, and view are the same and content - just trigger a table reload (filters)
	if ([[theDocument database] isEqualToString:[historyEntry objectForKey:@"database"]]
		&& [historyEntry objectForKey:@"table"] && [[theDocument table] isEqualToString:[historyEntry objectForKey:@"table"]]
		&& [[historyEntry objectForKey:@"view"] intValue] == [self currentlySelectedView] == SP_VIEW_CONTENT)
	{
		[tableContentInstance loadTable:[historyEntry objectForKey:@"table"]];
		modifyingHistoryState = NO;
		[self updateToolbarItem];
		return;
	}

	// Check and set the database
	if (![[theDocument database] isEqualToString:[historyEntry objectForKey:@"database"]]) {
		NSPopUpButton *chooseDatabaseButton = [theDocument valueForKey:@"chooseDatabaseButton"];
		[chooseDatabaseButton selectItemWithTitle:[historyEntry objectForKey:@"database"]];
		[theDocument chooseDatabase:self];
		if (![[theDocument database] isEqualToString:[historyEntry objectForKey:@"database"]]) {
			return [self abortEntryLoad];
		}
	}

	// Check and set the table
	if ([historyEntry objectForKey:@"table"] && ![[theDocument table] isEqualToString:[historyEntry objectForKey:@"table"]]) {
		TablesList *tablesListInstance = [theDocument valueForKey:@"tablesListInstance"];
		NSArray *tables = [tablesListInstance tables];
		if ([tables indexOfObject:[historyEntry objectForKey:@"table"]] == NSNotFound) {
			return [self abortEntryLoad];
		}
		[[tablesListInstance valueForKey:@"tablesListView"] selectRowIndexes:[NSIndexSet indexSetWithIndex:[tables indexOfObject:[historyEntry objectForKey:@"table"]]] byExtendingSelection:NO];
		if (![[theDocument table] isEqualToString:[historyEntry objectForKey:@"table"]]) {
			return [self abortEntryLoad];
		}
	} else if (![historyEntry objectForKey:@"table"] && [theDocument table]) {
		TablesList *tablesListInstance = [theDocument valueForKey:@"tablesListInstance"];
		[[tablesListInstance valueForKey:@"tablesListView"] deselectAll:self];		
	} else {
		[[theDocument valueForKey:@"tablesListInstance"] setContentRequiresReload:YES];	
	}

	// Check and set the view
	if ([self currentlySelectedView] != [[historyEntry objectForKey:@"view"] intValue]) {
		switch ([[historyEntry objectForKey:@"view"] intValue]) {
			case SP_VIEW_STRUCTURE:
				[theDocument viewStructure:self];
				break;
			case SP_VIEW_CONTENT:
				[theDocument viewContent:self];
				break;
			case SP_VIEW_CUSTOMQUERY:
				[theDocument viewQuery:self];
				break;
			case SP_VIEW_STATUS:
				[theDocument viewStatus:self];
				break;
			case SP_VIEW_RELATIONS:
				[theDocument viewRelations:self];
				break;
		}
		if ([self currentlySelectedView] != [[historyEntry objectForKey:@"view"] intValue]) {
			return [self abortEntryLoad];
		}
	}

	modifyingHistoryState = NO;
	[self updateToolbarItem];
}

/**
 * Convenience method for aborting history load - could at some point
 * clean up the history list, show an alert, etc
 */
- (void) abortEntryLoad
{
	NSBeep();
	modifyingHistoryState = NO;
}

/**
 * Load a history entry from an associated menu item
 */
- (void) loadEntryFromMenuItem:(id)theMenuItem
{
	[self loadEntryAtPosition:[theMenuItem tag]];
}

#pragma mark -
#pragma mark History entry details and description

/**
 * Returns a menuitem for a history entry at a supplied index
 */
- (NSMenuItem *) menuEntryForHistoryEntryAtIndex:(int)theIndex
{
	NSMenuItem *theMenuItem = [[NSMenuItem alloc] init];
	NSDictionary *theHistoryEntry = [history objectAtIndex:theIndex];

	[theMenuItem setTag:theIndex];
	[theMenuItem setTitle:[self nameForHistoryEntryDetails:theHistoryEntry]];
	[theMenuItem setTarget:self];
	[theMenuItem setAction:@selector(loadEntryFromMenuItem:)];
	
	return [theMenuItem autorelease];
}

/**
 * Returns a descriptive name for a history item dictionary
 */
- (NSString *) nameForHistoryEntryDetails:(NSDictionary *)theEntry
{
	if (![theEntry objectForKey:@"database"]) return NSLocalizedString(@"(no selection)", @"History item title with nothing selected");

	NSMutableString *theName = [NSMutableString stringWithString:[theEntry objectForKey:@"database"]];
	if (![theEntry objectForKey:@"table"] || ![[theEntry objectForKey:@"table"] length]) return theName;

	[theName appendFormat:@"/%@", [theEntry objectForKey:@"table"]];
	if (![theEntry objectForKey:@"contentFilter"]) return theName;

	NSDictionary *filterSettings = [theEntry objectForKey:@"contentFilter"];
	if (![filterSettings objectForKey:@"filterField"]) return theName;

	if ([[filterSettings objectForKey:@"filterComparison"] isEqualToString:@"IS NULL"]
		|| [[filterSettings objectForKey:@"filterComparison"] isEqualToString:@"IS NOT NULL"])
	{
		[theName appendFormat:@" (%@ %@ %@)", NSLocalizedString(@"Filtered by", @"History item filtered by label"),
			[filterSettings objectForKey:@"filterField"], [filterSettings objectForKey:@"filterComparison"]];
	} else if ([filterSettings objectForKey:@"filterValue"] && [[filterSettings objectForKey:@"filterValue"] length]) {
		[theName appendFormat:@" (%@ %@ %@ %@)", NSLocalizedString(@"Filtered by", @"History item filtered by label"),
			[filterSettings objectForKey:@"filterField"], [filterSettings objectForKey:@"filterComparison"],
			[filterSettings objectForKey:@"filterValue"]];
	}

	return theName;
}

@end