//
//  PBGitHistoryList.m
//  GitX
//
//  Created by Nathan Kinsinger on 2/20/10.
//  Copyright 2010 Nathan Kinsinger. All rights reserved.
//

#import "PBGitHistoryList.h"
#import "PBGitRepository.h"
#import "PBGitRevList.h"
#import "PBGitGrapher.h"
#import "PBGitHistoryGrapher.h"



@interface PBGitHistoryList ()

- (void) resetGraphing;

- (PBGitHistoryGrapher *) grapher;
- (NSInvocationOperation *) operationForCommits:(NSArray *)newCommits;

- (void) updateProjectHistoryForRev:(PBGitRevSpecifier *)rev;
- (void) updateHistoryForRev:(PBGitRevSpecifier *)rev;

@end




@implementation PBGitHistoryList


@synthesize projectRevList;
@synthesize commits;
@synthesize isUpdating;
@synthesize updatedGraph;
@dynamic projectCommits;



#pragma mark -
#pragma mark Public

- (id) initWithRepository:(PBGitRepository *)repo
{
	commits = [NSMutableArray array];
	repository = repo;
	lastBranchFilter = -1;
	[repository addObserver:self forKeyPath:@"currentBranch" options:0 context:@"currentBranch"];
	[repository addObserver:self forKeyPath:@"currentBranchFilter" options:0 context:@"currentBranch"];
	[repository addObserver:self forKeyPath:@"hasChanged" options:0 context:@"repositoryHasChanged"];

	shouldReloadProjectHistory = YES;
	projectRevList = [[PBGitRevList alloc] initWithRepository:repository rev:[PBGitRevSpecifier allBranchesRevSpec] shouldGraph:NO];

	return self;
}


- (void) forceUpdate
{
	if ([repository.currentBranch isSimpleRef])
		shouldReloadProjectHistory = YES;

	[self updateHistory];
}


- (void) updateHistory
{
	PBGitRevSpecifier *rev = repository.currentBranch;
	if (!rev)
		return;

	if ([rev isSimpleRef])
		[self updateProjectHistoryForRev:rev];
	else
		[self updateHistoryForRev:rev];
}


- (NSArray *) projectCommits
{
	return [projectRevList.commits copy];
}



#pragma mark -
#pragma mark History Grapher delegate methods

- (void) addCommitsFromArray:(NSArray *)array
{
	if (!array || [array count] == 0)
		return;
	if (resetCommits) {
		self.commits = [NSMutableArray array];
		resetCommits = NO;
	}

	NSRange range = NSMakeRange([commits count], [array count]);
	NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:range];

	[self willChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"commits"];
	[commits addObjectsFromArray:array];
	[self didChange:NSKeyValueChangeInsertion valuesAtIndexes:indexes forKey:@"commits"];
}


- (void) finishedGraphing
{
	if (!currentRevList.isParsing && ([[graphQueue operations] count] == 0)) {
		self.isUpdating = NO;
		[self performSelector:@selector(setUpdatedGraph:) withObject:[NSDate date] afterDelay:0];
	}
}



#pragma mark -
#pragma mark Private

- (void) resetGraphing
{
	resetCommits = YES;
	self.isUpdating = YES;

	[graphQueue setSuspended:YES];
	if (graphQueue)
		[graphQueue removeObserver:self forKeyPath:@"operations"];
	graphQueue = [[NSOperationQueue alloc] init];
	[graphQueue addObserver:self forKeyPath:@"operations" options:0 context:@"operations"];
	lastOperation = nil;

	grapher = [self grapher];
}


- (NSInvocationOperation *) operationForCommits:(NSArray *)newCommits
{
	NSInvocationOperation *graphOperation = [[NSInvocationOperation alloc] initWithTarget:grapher selector:@selector(graphCommits:) object:newCommits];
	if (lastOperation)
		[graphOperation addDependency:lastOperation];
	lastOperation = graphOperation;

	return graphOperation;
}


- (NSSet *) baseCommitsForLocalRefs
{
	NSMutableSet *baseCommitSHAs = [NSMutableSet set];
	NSDictionary *refs = repository.refs;

	for (NSString *sha in refs)
		for (PBGitRef *ref in [refs objectForKey:sha])
			if ([ref isBranch] || [ref isTag])
				[baseCommitSHAs addObject:sha];

	return baseCommitSHAs;
}


- (NSSet *) baseCommitsForRemoteRefs
{
	NSMutableSet *baseCommitSHAs = [NSMutableSet set];
	NSDictionary *refs = repository.refs;

	PBGitRef *remoteRef = [[repository.currentBranch ref] remoteRef];

	for (NSString *sha in refs)
		for (PBGitRef *ref in [refs objectForKey:sha])
			if ([remoteRef isEqualToRef:[ref remoteRef]])
				[baseCommitSHAs addObject:sha];

	return baseCommitSHAs;
}


- (NSSet *) baseCommits
{
	if ((repository.currentBranchFilter == kGitXSelectedBranchFilter) || (repository.currentBranchFilter == kGitXAllBranchesFilter)) {
		if (lastSHA)
			return [NSMutableSet setWithObject:lastSHA];
		else if ([repository.currentBranch isSimpleRef]) {
			PBGitRef *currentRef = [repository.currentBranch ref];
			NSString *sha = [repository shaForRef:currentRef];
			if (sha)
				return [NSMutableSet setWithObject:sha];
		}
	}
	else if (repository.currentBranchFilter == kGitXLocalRemoteBranchesFilter) {
		if ([[repository.currentBranch ref] isRemote])
			return [self baseCommitsForRemoteRefs];
		else
			return [self baseCommitsForLocalRefs];
	}

	return [NSMutableSet set];
}


- (PBGitHistoryGrapher *) grapher
{
	BOOL viewAllBranches = (repository.currentBranchFilter == kGitXAllBranchesFilter);

	return [[PBGitHistoryGrapher alloc] initWithBaseCommits:[self baseCommits] viewAllBranches:viewAllBranches delegate:self];
}


- (void) setCurrentRevList:(PBGitRevList *)parser
{
	if (currentRevList == parser)
		return;

	if (currentRevList) {
		[currentRevList removeObserver:self forKeyPath:@"commits"];
		[currentRevList removeObserver:self forKeyPath:@"isParsing"];
	}

	currentRevList = parser;

	[currentRevList addObserver:self forKeyPath:@"commits" options:NSKeyValueObservingOptionNew context:@"commitsUpdated"];
	[currentRevList addObserver:self forKeyPath:@"isParsing" options:0 context:@"revListParsing"];
}


- (BOOL) isAllBranchesOnlyUpdate
{
	return (lastBranchFilter == kGitXAllBranchesFilter) && (repository.currentBranchFilter == kGitXAllBranchesFilter);
}


- (BOOL) isLocalRemoteOnlyUpdate:(PBGitRevSpecifier *)rev
{
	if ((lastBranchFilter == kGitXLocalRemoteBranchesFilter) && (repository.currentBranchFilter == kGitXLocalRemoteBranchesFilter)) {
		if (!lastRemoteRef && ![[rev ref] isRemote])
			return YES;

		if ([lastRemoteRef isEqualToRef:[[rev ref] remoteRef]])
			return YES;
	}

	return NO;
}


- (BOOL) selectedBranchNeedsNewGraph:(PBGitRevSpecifier *)rev
{
	if (![rev isSimpleRef])
		return YES;

	if ([self isAllBranchesOnlyUpdate] || [self isLocalRemoteOnlyUpdate:rev]) {
		lastRemoteRef = [[rev ref] remoteRef];
		lastSHA = nil;
		self.isUpdating = NO;
		self.updatedGraph = [NSDate date];
		return NO;
	}

	NSString *revSHA = [repository shaForRef:[rev ref]];
	if ([revSHA isEqualToString:lastSHA] && (lastBranchFilter == repository.currentBranchFilter))
		return NO;

	lastBranchFilter = repository.currentBranchFilter;
	lastRemoteRef = [[rev ref] remoteRef];
	lastSHA = revSHA;

	return YES;
}


- (BOOL) haveRefsBeenModified
{
	[repository reloadRefs];

	NSMutableSet *currentRefSHAs = [NSMutableSet setWithArray:[repository.refs allKeys]];
	[currentRefSHAs minusSet:lastRefSHAs];
	lastRefSHAs = [NSSet setWithArray:[repository.refs allKeys]];

	return [currentRefSHAs count] != 0;
}


#pragma mark updating history

- (void) updateProjectHistoryForRev:(PBGitRevSpecifier *)rev
{
	[self setCurrentRevList:projectRevList];

	if ([self haveRefsBeenModified])
		shouldReloadProjectHistory = YES;

	if (![self selectedBranchNeedsNewGraph:rev] && !shouldReloadProjectHistory)
		return;

	[self resetGraphing];

	if (shouldReloadProjectHistory) {
		shouldReloadProjectHistory = NO;
		lastBranchFilter = -1;
		lastRemoteRef = nil;
		lastSHA = nil;
		[projectRevList loadRevisons];
		return;
	}

	[graphQueue addOperation:[self operationForCommits:projectRevList.commits]];
}


- (void) updateHistoryForRev:(PBGitRevSpecifier *)rev
{
	PBGitRevList *otherRevListParser = [[PBGitRevList alloc] initWithRepository:repository rev:rev shouldGraph:YES];

	[self setCurrentRevList:otherRevListParser];
	[self resetGraphing];
	lastBranchFilter = -1;
	lastRemoteRef = nil;
	lastSHA = nil;

	[otherRevListParser loadRevisons];
}



#pragma mark -
#pragma mark Key Value Observing

- (void) removeObservers
{
	[repository removeObserver:self forKeyPath:@"currentBranch"];
	[repository removeObserver:self forKeyPath:@"hasChanged"];

	if (currentRevList) {
		[currentRevList removeObserver:self forKeyPath:@"commits"];
		[currentRevList removeObserver:self forKeyPath:@"isParsing"];
	}

	if (graphQueue)
		[graphQueue removeObserver:self forKeyPath:@"operations"];
}


- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
	if ([@"currentBranch" isEqualToString:context]) {
		[self updateHistory];
		return;
	}

	if ([@"repositoryHasChanged" isEqualToString:context]) {
		[self forceUpdate];
		return;
	}

	if ([@"commitsUpdated" isEqualToString:context]) {
		NSInteger changeKind = [(NSNumber *)[change objectForKey:NSKeyValueChangeKindKey] intValue];
		if (changeKind == NSKeyValueChangeInsertion) {
			NSArray *newCommits = [change objectForKey:NSKeyValueChangeNewKey];
			if ([repository.currentBranch isSimpleRef])
				[graphQueue addOperation:[self operationForCommits:newCommits]];
			else
				[self addCommitsFromArray:newCommits];
		}
		return;
	}

	if ([@"revListParsing" isEqualToString:context] || [@"operations" isEqualToString:context]) {
		[self finishedGraphing];
		return;
	}

	[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

@end
