/*
 * cocos2d for iPhone: http://www.cocos2d-iphone.org
 *
 * Copyright (c) 2008-2010 Ricardo Quesada
 * Copyright (c) 2011 Zynga Inc.
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */


// cocos2d imports
#import "CCScheduler.h"
#import "ccMacros.h"
#import "Support/uthash.h"
#import "Support/utlist.h"
#import "Support/ccCArray.h"

//
// Data structures
//
#pragma mark -
#pragma mark Data Structures

// A list double-linked list used for "updates with priority"
typedef struct _listEntry
{
	struct	_listEntry *prev, *next;
	TICK_IMP	impMethod;
	id			target;				// not retained (retained by hashUpdateEntry)
	NSInteger	priority;
	BOOL		paused;	
    BOOL    markedForDeletion;		// selector will no longer be called and entry will be removed at end of the next tick
} tListEntry;

typedef struct _hashUpdateEntry
{
	tListEntry		**list;		// Which list does it belong to ?
	tListEntry		*entry;		// entry in the list
	id				target;		// hash key (retained)
	UT_hash_handle  hh;
} tHashUpdateEntry;

// Hash Element used for "selectors with interval"
typedef struct _hashSelectorEntry
{
	struct ccArray	*timers;
	id				target;		// hash key (retained)
	unsigned int	timerIndex;
	CCTimer			*currentTimer;
	BOOL			currentTimerSalvaged;
	BOOL			paused;
	UT_hash_handle  hh;
} tHashSelectorEntry;



//
// CCTimer
//
#pragma mark -
#pragma mark - CCTimer

@implementation CCTimer

@synthesize interval;

-(id) init
{
	NSAssert(NO, @"CCTimer: Init not supported.");
	[self release];
	return nil;
}

+(id) timerWithTarget:(id)t selector:(SEL)s
{
	return [[[self alloc] initWithTarget:t selector:s] autorelease];
}

+(id) timerWithTarget:(id)t selector:(SEL)s interval:(ccTime) i
{
	return [[[self alloc] initWithTarget:t selector:s interval:i] autorelease];
}

-(id) initWithTarget:(id)t selector:(SEL)s
{
	return [self initWithTarget:t selector:s interval:0];
}

-(id) initWithTarget:(id)t selector:(SEL)s interval:(ccTime) seconds
{
	if( (self=[super init]) ) {
#if COCOS2D_DEBUG
		NSMethodSignature *sig = [t methodSignatureForSelector:s];
		NSAssert(sig !=0 , @"Signature not found for selector - does it have the following form? -(void) name: (ccTime) dt");
#endif
		
		// target is not retained. It is retained in the hash structure
		target = t;
		selector = s;
		impMethod = (TICK_IMP) [t methodForSelector:s];
		elapsed = -1;
		interval = seconds;
	}
	return self;
}

- (NSString*) description
{
	return [NSString stringWithFormat:@"<%@ = %08X | target:%@ selector:(%@)>", [self class], self, [target class], NSStringFromSelector(selector)];
}

-(void) dealloc
{
	CCLOGINFO(@"cocos2d: deallocing %@", self);
	[super dealloc];
}

-(void) update: (ccTime) dt
{
	if( elapsed == - 1)
		elapsed = 0;
	else
		elapsed += dt;
	if( elapsed >= interval ) {
		impMethod(target, selector, elapsed);
		elapsed = 0;
	}
}
@end

//
// CCScheduler
//
#pragma mark -
#pragma mark - CCScheduler

@interface CCScheduler (Private)
-(void) removeHashElement:(tHashSelectorEntry*)element;
@end

@implementation CCScheduler

static CCScheduler *sharedScheduler;

@synthesize timeScale = timeScale_;

+ (CCScheduler *)sharedScheduler
{
	if (!sharedScheduler)
		sharedScheduler = [[CCScheduler alloc] init];

	return sharedScheduler;
}

+(id)alloc
{
	NSAssert(sharedScheduler == nil, @"Attempted to allocate a second instance of a singleton.");
	return [super alloc];
}

+(void)purgeSharedScheduler
{
	[sharedScheduler release];
	sharedScheduler = nil;
}

- (id) init
{
	if( (self=[super init]) ) {		
		timeScale_ = 1.0f;

		// used to trigger CCTimer#update
		updateSelector = @selector(update:);
		impMethod = (TICK_IMP) [CCTimer instanceMethodForSelector:updateSelector];

		// updates with priority
		updates0 = NULL;
		updatesNeg = NULL;
		updatesPos = NULL;
		hashForUpdates = NULL;
		
		// selectors with interval
		currentTarget = nil;
		currentTargetSalvaged = NO;
		hashForSelectors = nil;
        updateHashLocked = NO;
	}

	return self;
}

- (void) dealloc
{
	CCLOG(@"cocos2d: deallocing %@", self);

	[self unscheduleAllSelectors];

	sharedScheduler = nil;

	[super dealloc];
}


#pragma mark CCScheduler - Custom Selectors

-(void) removeHashElement:(tHashSelectorEntry*)element
{
	ccArrayFree(element->timers);
	[element->target release];
	HASH_DEL(hashForSelectors, element);
	free(element);
}

-(void) scheduleSelector:(SEL)selector forTarget:(id)target interval:(ccTime)interval paused:(BOOL)paused
{
	NSAssert( selector != nil, @"Argument selector must be non-nil");
	NSAssert( target != nil, @"Argument target must be non-nil");	
	
	tHashSelectorEntry *element = NULL;
	HASH_FIND_INT(hashForSelectors, &target, element);
	
	if( ! element ) {
		element = calloc( sizeof( *element ), 1 );
		element->target = [target retain];
		HASH_ADD_INT( hashForSelectors, target, element );
	
		// Is this the 1st element ? Then set the pause level to all the selectors of this target
		element->paused = paused;
	
	} else
		NSAssert( element->paused == paused, @"CCScheduler. Trying to schedule a selector with a pause value different than the target");
	
	
	if( element->timers == nil )
		element->timers = ccArrayNew(10);
	else
	{
		for( unsigned int i=0; i< element->timers->num; i++ ) {
			CCTimer *timer = element->timers->arr[i];
			if( selector == timer->selector ) {
				CCLOG(@"CCScheduler#scheduleSelector. Selector already scheduled. Updating interval from: %.2f to %.2f", timer->interval, interval);
				timer->interval = interval;
				return;
			}
		}
		ccArrayEnsureExtraCapacity(element->timers, 1);
	}
	
	CCTimer *timer = [[CCTimer alloc] initWithTarget:target selector:selector interval:interval];
	ccArrayAppendObject(element->timers, timer);
	[timer release];
}

-(void) unscheduleSelector:(SEL)selector forTarget:(id)target
{
	// explicity handle nil arguments when removing an object
	if( target==nil && selector==NULL)
		return;
	
	NSAssert( target != nil, @"Target MUST not be nil");
	NSAssert( selector != NULL, @"Selector MUST not be NULL");
	
	tHashSelectorEntry *element = NULL;
	HASH_FIND_INT(hashForSelectors, &target, element);
	
	if( element ) {
		
		for( unsigned int i=0; i< element->timers->num; i++ ) {
			CCTimer *timer = element->timers->arr[i];
			
			
			if( selector == timer->selector ) {
				
				if( timer == element->currentTimer && !element->currentTimerSalvaged ) {
					[element->currentTimer retain];
					element->currentTimerSalvaged = YES;
				}

				ccArrayRemoveObjectAtIndex(element->timers, i );
				
				// update timerIndex in case we are in tick:, looping over the actions
				if( element->timerIndex >= i )
					element->timerIndex--;

				if( element->timers->num == 0 ) {
					if( currentTarget == element )
						currentTargetSalvaged = YES;
					else
						[self removeHashElement: element];
				}
				return;
			}
		}
	}
	
	// Not Found
//	NSLog(@"CCScheduler#unscheduleSelector:forTarget: selector not found: %@", selString);

}

#pragma mark CCScheduler - Update Specific

-(void) priorityIn:(tListEntry**)list target:(id)target priority:(NSInteger)priority paused:(BOOL)paused
{
	tListEntry *listElement = malloc( sizeof(*listElement) );

	listElement->target = target;
	listElement->priority = priority;
	listElement->paused = paused;
	listElement->impMethod = (TICK_IMP) [target methodForSelector:updateSelector];
	listElement->next = listElement->prev = NULL;
    listElement->markedForDeletion = NO;
	
	// empty list ?
	if( ! *list ) {
		DL_APPEND( *list, listElement );
	
	} else {
		BOOL added = NO;		
	
		for( tListEntry *elem = *list; elem ; elem = elem->next ) {
			if( priority < elem->priority ) {
				
				if( elem == *list )
					DL_PREPEND(*list, listElement);
				else {
					listElement->next = elem;
					listElement->prev = elem->prev;

					elem->prev->next = listElement;
					elem->prev = listElement;
				}
				
				added = YES;
				break;
			}
		}
		
		// Not added? priority has the higher value. Append it.
		if( !added )
			DL_APPEND(*list, listElement);
	}
	
	// update hash entry for quicker access
	tHashUpdateEntry *hashElement = calloc( sizeof(*hashElement), 1 );
	hashElement->target = [target retain];
	hashElement->list = list;
	hashElement->entry = listElement;
	HASH_ADD_INT(hashForUpdates, target, hashElement );
}

-(void) appendIn:(tListEntry**)list target:(id)target paused:(BOOL)paused
{
	tListEntry *listElement = malloc( sizeof( * listElement ) );
	
	listElement->target = target;
	listElement->paused = paused;
    listElement->markedForDeletion = NO;
	listElement->impMethod = (TICK_IMP) [target methodForSelector:updateSelector];
	
	DL_APPEND(*list, listElement);

	
	// update hash entry for quicker access
	tHashUpdateEntry *hashElement = calloc( sizeof(*hashElement), 1 );
	hashElement->target = [target retain];
	hashElement->list = list;
	hashElement->entry = listElement;
	HASH_ADD_INT(hashForUpdates, target, hashElement );	
}

-(void) scheduleUpdateForTarget:(id)target priority:(NSInteger)priority paused:(BOOL)paused
{
	tHashUpdateEntry * hashElement = NULL;
	HASH_FIND_INT(hashForUpdates, &target, hashElement);
    if(hashElement)
    {
#if COCOS2D_DEBUG >= 1        
        NSAssert( hashElement->entry->markedForDeletion, @"CCScheduler: You can't re-schedule an 'update' selector'. Unschedule it first");
#endif	
        // TODO : check if priority has changed!
        
        hashElement->entry->markedForDeletion = NO;
        return;
    }
		
	// most of the updates are going to be 0, that's way there
	// is an special list for updates with priority 0
	if( priority == 0 )
		[self appendIn:&updates0 target:target paused:paused];

	else if( priority < 0 )
		[self priorityIn:&updatesNeg target:target priority:priority paused:paused];

	else // priority > 0
		[self priorityIn:&updatesPos target:target priority:priority paused:paused];
}

- (void) removeUpdateFromHash:(tListEntry*)entry
{
    tHashUpdateEntry * element = NULL;
    
    HASH_FIND_INT(hashForUpdates, &entry->target, element);
    if( element ) {
        // list entry
        DL_DELETE( *element->list, element->entry );
        free( element->entry );
        
        // hash entry
        [element->target release];
        HASH_DEL( hashForUpdates, element);
        free(element);
    }
}

-(void) unscheduleUpdateForTarget:(id)target
{
	if( target == nil )
		return;
	
	tHashUpdateEntry * element = NULL;
	HASH_FIND_INT(hashForUpdates, &target, element);
	if( element ) {    
        if(updateHashLocked)
            element->entry->markedForDeletion = YES;
        else
            [self removeUpdateFromHash:element->entry];
        
//		// list entry
//		DL_DELETE( *element->list, element->entry );
//		free( element->entry );
//	
//		// hash entry
//		[element->target release];
//		HASH_DEL( hashForUpdates, element);
//		free(element);
	}
}

#pragma mark CCScheduler - Common for Update selector & Custom Selectors

-(void) unscheduleAllSelectors
{
	// Custom Selectors
	for(tHashSelectorEntry *element=hashForSelectors; element != NULL; ) {	
		id target = element->target;
		element=element->hh.next;
		[self unscheduleAllSelectorsForTarget:target];
	}

	// Updates selectors
	tListEntry *entry, *tmp;
	DL_FOREACH_SAFE( updates0, entry, tmp ) {
		[self unscheduleUpdateForTarget:entry->target];
	}
	DL_FOREACH_SAFE( updatesNeg, entry, tmp ) {
		[self unscheduleUpdateForTarget:entry->target];
	}
	DL_FOREACH_SAFE( updatesPos, entry, tmp ) {
		[self unscheduleUpdateForTarget:entry->target];
	}
	
}

-(void) unscheduleAllSelectorsForTarget:(id)target
{
	// explicit nil handling
	if( target == nil )
		return;
	
	// Custom Selectors
	tHashSelectorEntry *element = NULL;
	HASH_FIND_INT(hashForSelectors, &target, element);
	
	if( element ) {
		if( ccArrayContainsObject(element->timers, element->currentTimer) && !element->currentTimerSalvaged ) {
			[element->currentTimer retain];
			element->currentTimerSalvaged = YES;
		}
		ccArrayRemoveAllObjects(element->timers);
		if( currentTarget == element )
			currentTargetSalvaged = YES;
		else
			[self removeHashElement:element];
	}
	
	// Update Selector
	[self unscheduleUpdateForTarget:target];
}

-(void) resumeTarget:(id)target
{
	NSAssert( target != nil, @"target must be non nil" );
	
	// Custom Selectors
	tHashSelectorEntry *element = NULL;
	HASH_FIND_INT(hashForSelectors, &target, element);
	if( element )
		element->paused = NO;
	
	// Update selector
	tHashUpdateEntry * elementUpdate = NULL;
	HASH_FIND_INT(hashForUpdates, &target, elementUpdate);
	if( elementUpdate ) {
		NSAssert( elementUpdate->entry != NULL, @"resumeTarget: unknown error");
		elementUpdate->entry->paused = NO;
	}	
}

-(void) pauseTarget:(id)target
{
	NSAssert( target != nil, @"target must be non nil" );
	
	// Custom selectors
	tHashSelectorEntry *element = NULL;
	HASH_FIND_INT(hashForSelectors, &target, element);
	if( element )
		element->paused = YES;
	
	// Update selector
	tHashUpdateEntry * elementUpdate = NULL;
	HASH_FIND_INT(hashForUpdates, &target, elementUpdate);
	if( elementUpdate ) {
		NSAssert( elementUpdate->entry != NULL, @"pauseTarget: unknown error");
		elementUpdate->entry->paused = YES;
	}
	
}

-(BOOL) isTargetPaused:(id)target
{
	NSAssert( target != nil, @"target must be non nil" );
	
	// Custom selectors
	tHashSelectorEntry *element = NULL;
	HASH_FIND_INT(hashForSelectors, &target, element);
	if( element )
    {
		return element->paused;
    }
    return NO;  // should never get here
	
}

#pragma mark CCScheduler - Main Loop

-(void) tick: (ccTime) dt
{
    updateHashLocked = YES;
    
	if( timeScale_ != 1.0f )
		dt *= timeScale_;
	
	// Iterate all over the Updates selectors
	tListEntry *entry, *tmp;

	// updates with priority < 0
	DL_FOREACH_SAFE( updatesNeg, entry, tmp ) {
		if( ! entry->paused && !entry->markedForDeletion )
			entry->impMethod( entry->target, updateSelector, dt );
	}

	// updates with priority == 0
	DL_FOREACH_SAFE( updates0, entry, tmp ) {
		if( ! entry->paused && !entry->markedForDeletion )
        {
			entry->impMethod( entry->target, updateSelector, dt );
        }
	}
	
	// updates with priority > 0
	DL_FOREACH_SAFE( updatesPos, entry, tmp ) {
		if( ! entry->paused  && !entry->markedForDeletion )
			entry->impMethod( entry->target, updateSelector, dt );
	}
	
	// Iterate all over the  custome selectors
	for(tHashSelectorEntry *elt=hashForSelectors; elt != NULL; ) {	
		
		currentTarget = elt;
		currentTargetSalvaged = NO;

		if( ! currentTarget->paused ) {
			
			// The 'timers' ccArray may change while inside this loop.
			for( elt->timerIndex = 0; elt->timerIndex < elt->timers->num; elt->timerIndex++) {
				elt->currentTimer = elt->timers->arr[elt->timerIndex];
				elt->currentTimerSalvaged = NO;

				impMethod( elt->currentTimer, updateSelector, dt);
				
				if( elt->currentTimerSalvaged ) {
					// The currentTimer told the remove itself. To prevent the timer from
					// accidentally deallocating itself before finishing its step, we retained
					// it. Now that step is done, it's safe to release it.
					[elt->currentTimer release];
				}
				
				elt->currentTimer = nil;
			}			
		}
		
		// elt, at this moment, is still valid
		// so it is safe to ask this here (issue #490)
		elt = elt->hh.next;
		
		// only delete currentTarget if no actions were scheduled during the cycle (issue #481)
		if( currentTargetSalvaged && currentTarget->timers->num == 0 )
			[self removeHashElement:currentTarget];		
	}
	
    // delete all updates that are morked for deletion
    // updates with priority < 0
	DL_FOREACH_SAFE( updatesNeg, entry, tmp ) {
		if(entry->markedForDeletion )
        {
            [self removeUpdateFromHash:entry];
        }
	}
    
	// updates with priority == 0
	DL_FOREACH_SAFE( updates0, entry, tmp ) {
		if(entry->markedForDeletion )
        {
            [self removeUpdateFromHash:entry];
        }
	}
	
	// updates with priority > 0
	DL_FOREACH_SAFE( updatesPos, entry, tmp ) {
		if(entry->markedForDeletion )
        {
            [self removeUpdateFromHash:entry];
        }
	}

    updateHashLocked = NO;
	currentTarget = nil;
}
@end

