/*
 * Copyright (c) 2008-2024 Jonathan Schleifer <js@nil.im>
 *
 * All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3.0 only,
 * as published by the Free Software Foundation.
 *
 * 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 Lesser General Public License
 * version 3.0 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3.0 along with this program. If not, see
 * <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <stdlib.h>

#import "OFTimer.h"
#import "OFTimer+Private.h"
#import "OFDate.h"
#import "OFRunLoop.h"
#import "OFRunLoop+Private.h"
#ifdef OF_HAVE_THREADS
# import "OFCondition.h"
#endif

#import "OFInvalidArgumentException.h"

@implementation OFTimer
@synthesize timeInterval = _interval, repeats = _repeats, valid = _valid;

+ (instancetype)scheduledTimerWithTimeInterval: (OFTimeInterval)timeInterval
					target: (id)target
				      selector: (SEL)selector
				       repeats: (bool)repeats
{
	void *pool = objc_autoreleasePoolPush();
	OFDate *fireDate = [OFDate dateWithTimeIntervalSinceNow: timeInterval];
	id timer = [[[self alloc] initWithFireDate: fireDate
					  interval: timeInterval
					    target: target
					  selector: selector
					   repeats: repeats] autorelease];

	[[OFRunLoop currentRunLoop] addTimer: timer];

	[timer retain];
	objc_autoreleasePoolPop(pool);

	return [timer autorelease];
}

+ (instancetype)scheduledTimerWithTimeInterval: (OFTimeInterval)timeInterval
					target: (id)target
				      selector: (SEL)selector
					object: (id)object
				       repeats: (bool)repeats
{
	void *pool = objc_autoreleasePoolPush();
	OFDate *fireDate = [OFDate dateWithTimeIntervalSinceNow: timeInterval];
	id timer = [[[self alloc] initWithFireDate: fireDate
					  interval: timeInterval
					    target: target
					  selector: selector
					    object: object
					   repeats: repeats] autorelease];

	[[OFRunLoop currentRunLoop] addTimer: timer];

	[timer retain];
	objc_autoreleasePoolPop(pool);

	return [timer autorelease];
}

+ (instancetype)scheduledTimerWithTimeInterval: (OFTimeInterval)timeInterval
					target: (id)target
				      selector: (SEL)selector
					object: (id)object1
					object: (id)object2
				       repeats: (bool)repeats
{
	void *pool = objc_autoreleasePoolPush();
	OFDate *fireDate = [OFDate dateWithTimeIntervalSinceNow: timeInterval];
	id timer = [[[self alloc] initWithFireDate: fireDate
					  interval: timeInterval
					    target: target
					  selector: selector
					    object: object1
					    object: object2
					   repeats: repeats] autorelease];

	[[OFRunLoop currentRunLoop] addTimer: timer];

	[timer retain];
	objc_autoreleasePoolPop(pool);

	return [timer autorelease];
}

+ (instancetype)scheduledTimerWithTimeInterval: (OFTimeInterval)timeInterval
					target: (id)target
				      selector: (SEL)selector
					object: (id)object1
					object: (id)object2
					object: (id)object3
				       repeats: (bool)repeats
{
	void *pool = objc_autoreleasePoolPush();
	OFDate *fireDate = [OFDate dateWithTimeIntervalSinceNow: timeInterval];
	id timer = [[[self alloc] initWithFireDate: fireDate
					  interval: timeInterval
					    target: target
					  selector: selector
					    object: object1
					    object: object2
					    object: object3
					   repeats: repeats] autorelease];

	[[OFRunLoop currentRunLoop] addTimer: timer];

	[timer retain];
	objc_autoreleasePoolPop(pool);

	return [timer autorelease];
}

+ (instancetype)scheduledTimerWithTimeInterval: (OFTimeInterval)timeInterval
					target: (id)target
				      selector: (SEL)selector
					object: (id)object1
					object: (id)object2
					object: (id)object3
					object: (id)object4
				       repeats: (bool)repeats
{
	void *pool = objc_autoreleasePoolPush();
	OFDate *fireDate = [OFDate dateWithTimeIntervalSinceNow: timeInterval];
	id timer = [[[self alloc] initWithFireDate: fireDate
					  interval: timeInterval
					    target: target
					  selector: selector
					    object: object1
					    object: object2
					    object: object3
					    object: object4
					   repeats: repeats] autorelease];

	[[OFRunLoop currentRunLoop] addTimer: timer];

	[timer retain];
	objc_autoreleasePoolPop(pool);

	return [timer autorelease];
}

#ifdef OF_HAVE_BLOCKS
+ (instancetype)scheduledTimerWithTimeInterval: (OFTimeInterval)timeInterval
				       repeats: (bool)repeats
					 block: (OFTimerBlock)block
{
	void *pool = objc_autoreleasePoolPush();
	OFDate *fireDate = [OFDate dateWithTimeIntervalSinceNow: timeInterval];
	id timer = [[[self alloc] initWithFireDate: fireDate
					  interval: timeInterval
					   repeats: repeats
					     block: block] autorelease];

	[[OFRunLoop currentRunLoop] addTimer: timer];

	[timer retain];
	objc_autoreleasePoolPop(pool);

	return [timer autorelease];
}
#endif

+ (instancetype)timerWithTimeInterval: (OFTimeInterval)timeInterval
			       target: (id)target
			     selector: (SEL)selector
			      repeats: (bool)repeats
{
	void *pool = objc_autoreleasePoolPush();
	OFDate *fireDate = [OFDate dateWithTimeIntervalSinceNow: timeInterval];
	id timer = [[[self alloc] initWithFireDate: fireDate
					  interval: timeInterval
					    target: target
					  selector: selector
					   repeats: repeats] autorelease];

	[timer retain];
	objc_autoreleasePoolPop(pool);

	return [timer autorelease];
}

+ (instancetype)timerWithTimeInterval: (OFTimeInterval)timeInterval
			       target: (id)target
			     selector: (SEL)selector
			       object: (id)object
			      repeats: (bool)repeats
{
	void *pool = objc_autoreleasePoolPush();
	OFDate *fireDate = [OFDate dateWithTimeIntervalSinceNow: timeInterval];
	id timer = [[[self alloc] initWithFireDate: fireDate
					  interval: timeInterval
					    target: target
					  selector: selector
					    object: object
					   repeats: repeats] autorelease];

	[timer retain];
	objc_autoreleasePoolPop(pool);

	return [timer autorelease];
}

+ (instancetype)timerWithTimeInterval: (OFTimeInterval)timeInterval
			       target: (id)target
			     selector: (SEL)selector
			       object: (id)object1
			       object: (id)object2
			      repeats: (bool)repeats
{
	void *pool = objc_autoreleasePoolPush();
	OFDate *fireDate = [OFDate dateWithTimeIntervalSinceNow: timeInterval];
	id timer = [[[self alloc] initWithFireDate: fireDate
					  interval: timeInterval
					    target: target
					  selector: selector
					    object: object1
					    object: object2
					   repeats: repeats] autorelease];

	[timer retain];
	objc_autoreleasePoolPop(pool);

	return [timer autorelease];
}

+ (instancetype)timerWithTimeInterval: (OFTimeInterval)timeInterval
			       target: (id)target
			     selector: (SEL)selector
			       object: (id)object1
			       object: (id)object2
			       object: (id)object3
			      repeats: (bool)repeats
{
	void *pool = objc_autoreleasePoolPush();
	OFDate *fireDate = [OFDate dateWithTimeIntervalSinceNow: timeInterval];
	id timer = [[[self alloc] initWithFireDate: fireDate
					  interval: timeInterval
					    target: target
					  selector: selector
					    object: object1
					    object: object2
					    object: object3
					   repeats: repeats] autorelease];

	[timer retain];
	objc_autoreleasePoolPop(pool);

	return [timer autorelease];
}

+ (instancetype)timerWithTimeInterval: (OFTimeInterval)timeInterval
			       target: (id)target
			     selector: (SEL)selector
			       object: (id)object1
			       object: (id)object2
			       object: (id)object3
			       object: (id)object4
			      repeats: (bool)repeats
{
	void *pool = objc_autoreleasePoolPush();
	OFDate *fireDate = [OFDate dateWithTimeIntervalSinceNow: timeInterval];
	id timer = [[[self alloc] initWithFireDate: fireDate
					  interval: timeInterval
					    target: target
					  selector: selector
					    object: object1
					    object: object2
					    object: object3
					    object: object4
					   repeats: repeats] autorelease];

	[timer retain];
	objc_autoreleasePoolPop(pool);

	return [timer autorelease];
}

#ifdef OF_HAVE_BLOCKS
+ (instancetype)timerWithTimeInterval: (OFTimeInterval)timeInterval
			      repeats: (bool)repeats
				block: (OFTimerBlock)block
{
	void *pool = objc_autoreleasePoolPush();
	OFDate *fireDate = [OFDate dateWithTimeIntervalSinceNow: timeInterval];
	id timer = [[[self alloc] initWithFireDate: fireDate
					  interval: timeInterval
					   repeats: repeats
					     block: block] autorelease];

	[timer retain];
	objc_autoreleasePoolPop(pool);

	return [timer autorelease];
}
#endif

- (instancetype)init
{
	OF_INVALID_INIT_METHOD
}

- (instancetype)of_initWithFireDate: (OFDate *)fireDate
			   interval: (OFTimeInterval)interval
			     target: (id)target
			   selector: (SEL)selector
			     object: (id)object1
			     object: (id)object2
			     object: (id)object3
			     object: (id)object4
			  arguments: (unsigned char)arguments
			    repeats: (bool)repeats
    OF_METHOD_FAMILY(init) OF_DIRECT
{
	self = [super init];

	@try {
		_fireDate = [fireDate retain];
		_interval = interval;
		_target = [target retain];
		_selector = selector;
		_object1 = [object1 retain];
		_object2 = [object2 retain];
		_object3 = [object3 retain];
		_object4 = [object4 retain];
		_arguments = arguments;
		_repeats = repeats;
		_valid = true;
#ifdef OF_HAVE_THREADS
		_condition = [[OFCondition alloc] init];
#endif
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (instancetype)initWithFireDate: (OFDate *)fireDate
			interval: (OFTimeInterval)interval
			  target: (id)target
			selector: (SEL)selector
			 repeats: (bool)repeats
{
	return [self of_initWithFireDate: fireDate
				interval: interval
				  target: target
				selector: selector
				  object: nil
				  object: nil
				  object: nil
				  object: nil
			       arguments: 0
				 repeats: repeats];
}

- (instancetype)initWithFireDate: (OFDate *)fireDate
			interval: (OFTimeInterval)interval
			  target: (id)target
			selector: (SEL)selector
			  object: (id)object
			 repeats: (bool)repeats
{
	return [self of_initWithFireDate: fireDate
				interval: interval
				  target: target
				selector: selector
				  object: object
				  object: nil
				  object: nil
				  object: nil
			       arguments: 1
				 repeats: repeats];
}

- (instancetype)initWithFireDate: (OFDate *)fireDate
			interval: (OFTimeInterval)interval
			  target: (id)target
			selector: (SEL)selector
			  object: (id)object1
			  object: (id)object2
			 repeats: (bool)repeats
{
	return [self of_initWithFireDate: fireDate
				interval: interval
				  target: target
				selector: selector
				  object: object1
				  object: object2
				  object: nil
				  object: nil
			       arguments: 2
				 repeats: repeats];
}

- (instancetype)initWithFireDate: (OFDate *)fireDate
			interval: (OFTimeInterval)interval
			  target: (id)target
			selector: (SEL)selector
			  object: (id)object1
			  object: (id)object2
			  object: (id)object3
			 repeats: (bool)repeats
{
	return [self of_initWithFireDate: fireDate
				interval: interval
				  target: target
				selector: selector
				  object: object1
				  object: object2
				  object: object3
				  object: nil
			       arguments: 3
				 repeats: repeats];
}

- (instancetype)initWithFireDate: (OFDate *)fireDate
			interval: (OFTimeInterval)interval
			  target: (id)target
			selector: (SEL)selector
			  object: (id)object1
			  object: (id)object2
			  object: (id)object3
			  object: (id)object4
			 repeats: (bool)repeats
{
	return [self of_initWithFireDate: fireDate
				interval: interval
				  target: target
				selector: selector
				  object: object1
				  object: object2
				  object: object3
				  object: object4
			       arguments: 4
				 repeats: repeats];
}

#ifdef OF_HAVE_BLOCKS
- (instancetype)initWithFireDate: (OFDate *)fireDate
			interval: (OFTimeInterval)interval
			 repeats: (bool)repeats
			   block: (OFTimerBlock)block
{
	self = [super init];

	@try {
		_fireDate = [fireDate retain];
		_interval = interval;
		_repeats = repeats;
		_block = [block copy];
		_valid = true;
# ifdef OF_HAVE_THREADS
		_condition = [[OFCondition alloc] init];
# endif
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}
#endif

- (void)dealloc
{
	/*
	 * The run loop references the timer, so it should never be deallocated
	 * if it is still in a run loop.
	 */
	OFAssert(_inRunLoop == nil);
	OFAssert(_inRunLoopMode == nil);

	[_fireDate release];
	[_target release];
	[_object1 release];
	[_object2 release];
	[_object3 release];
	[_object4 release];
#ifdef OF_HAVE_BLOCKS
	[_block release];
#endif
#ifdef OF_HAVE_THREADS
	[_condition release];
#endif

	[super dealloc];
}

- (OFComparisonResult)compare: (OFTimer *)timer
{
	if (![timer isKindOfClass: [OFTimer class]])
		@throw [OFInvalidArgumentException exception];

	return [_fireDate compare: timer->_fireDate];
}

- (void)of_setInRunLoop: (OFRunLoop *)runLoop mode: (OFRunLoopMode)mode
{
	OFRunLoop *oldInRunLoop = _inRunLoop;
	OFRunLoopMode oldInRunLoopMode = _inRunLoopMode;

	_inRunLoop = [runLoop retain];
	[oldInRunLoop release];

	_inRunLoopMode = [mode copy];
	[oldInRunLoopMode release];
}

- (void)of_reschedule
{
	long long missedIntervals;
	OFTimeInterval newFireDate;
	OFRunLoop *runLoop;

	if (!_repeats || !_valid)
		return;

	missedIntervals = -_fireDate.timeIntervalSinceNow / _interval;

	/* In case the clock was changed backwards */
	if (missedIntervals < 0)
		missedIntervals = 0;

	newFireDate = _fireDate.timeIntervalSince1970 +
	    (missedIntervals + 1) * _interval;

	[_fireDate release];
	_fireDate = nil;
	_fireDate = [[OFDate alloc]
	    initWithTimeIntervalSince1970: newFireDate];

	runLoop = [OFRunLoop currentRunLoop];
	[runLoop addTimer: self forMode: runLoop.currentMode];
}

- (void)fire
{
	OFEnsure(_arguments <= 4);

	if (!_valid)
		return;

#ifdef OF_HAVE_BLOCKS
	if (_block != NULL)
		_block(self);
	else {
#endif
		switch (_arguments) {
		case 0:
			[_target performSelector: _selector];
			break;
		case 1:
			[_target performSelector: _selector
				      withObject: _object1];
			break;
		case 2:
			[_target performSelector: _selector
				      withObject: _object1
				      withObject: _object2];
			break;
		case 3:
			[_target performSelector: _selector
				      withObject: _object1
				      withObject: _object2
				      withObject: _object3];
			break;
		case 4:
			[_target performSelector: _selector
				      withObject: _object1
				      withObject: _object2
				      withObject: _object3
				      withObject: _object4];
			break;
		}
#ifdef OF_HAVE_BLOCKS
	}
#endif

	if  (!_repeats)
		[self invalidate];

#ifdef OF_HAVE_THREADS
	[_condition lock];
	@try {
		_done = true;
		[_condition signal];
	} @finally {
		[_condition unlock];
	}
#endif
}

- (OFDate *)fireDate
{
	return _fireDate;
}

- (void)setFireDate: (OFDate *)fireDate
{
	[self retain];
	@try {
		@synchronized (self) {
			OFDate *old;

			[_inRunLoop of_removeTimer: self
					   forMode: _inRunLoopMode];

			old = _fireDate;
			_fireDate = [fireDate copy];
			[old release];

			[_inRunLoop addTimer: self forMode: _inRunLoopMode];
		}
	} @finally {
		[self release];
	}
}

- (void)invalidate
{
	_valid = false;

#ifdef OF_HAVE_BLOCKS
	[_block release];
#endif
	[_target release];
	[_object1 release];
	[_object2 release];
	[_object3 release];
	[_object4 release];

	_target = nil;
	_object1 = nil;
	_object2 = nil;
	_object3 = nil;
	_object4 = nil;
}

#ifdef OF_HAVE_THREADS
- (void)waitUntilDone
{
	[_condition lock];
	@try {
		if (_done) {
			_done = false;
			return;
		}

		[_condition wait];
	} @finally {
		[_condition unlock];
	}
}
#endif

- (OFString *)description
{
#ifdef OF_HAVE_BLOCKS
	if (_block != NULL)
		return [OFString stringWithFormat:
		    @"<%@:\n"
		    @"\tFire date: %@\n"
		    @"\tInterval: %lf\n"
		    @"\tRepeats: %s\n"
		    @"\tBlock: %@\n"
		    @"\tValid: %s\n"
		    @">",
		    self.class, _fireDate, _interval, (_repeats ? "yes" : "no"),
		    _block, (_valid ? "yes" : "no")];
	else {
#endif
		void *pool = objc_autoreleasePoolPush();
		OFString *objects = @"", *ret;

		if (_arguments >= 1)
			objects = [objects stringByAppendingFormat:
			    @"\tObject: %@\n", _object1];
		if (_arguments >= 2)
			objects = [objects stringByAppendingFormat:
			    @"\tObject: %@\n", _object2];
		if (_arguments >= 3)
			objects = [objects stringByAppendingFormat:
			    @"\tObject: %@\n", _object3];
		if (_arguments >= 4)
			objects = [objects stringByAppendingFormat:
			    @"\tObject: %@\n", _object4];

		ret = [[OFString alloc] initWithFormat:
		    @"<%@:\n"
		    @"\tFire date: %@\n"
		    @"\tInterval: %lf\n"
		    @"\tRepeats: %s\n"
		    @"\tTarget: %@\n"
		    @"\tSelector: %s\n"
		    @"%@"
		    @"\tValid: %s\n"
		    @">",
		    self.class, _fireDate, _interval, (_repeats ? "yes" : "no"),
		    _target, sel_getName(_selector), objects,
		    (_valid ? "yes" : "no")];

		objc_autoreleasePoolPop(pool);

		return [ret autorelease];
#ifdef OF_HAVE_BLOCKS
	}
#endif
}
@end
