//
// YRKSpinningProgressIndicator.m
//
// Original drawing code by Kelan Champagne; forked by Rowan Beentje
// for fixes, determinate mode, and threaded drawing.
//
// Copyright (c) 2009, Kelan Champagne (http://yeahrightkeller.com)
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//     * Neither the name of the <organization> nor the
//       names of its contributors may be used to endorse or promote products
//       derived from this software without specific prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY Kelan Champagne ''AS IS'' AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL Kelan Champagne BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#import "YRKSpinningProgressIndicator.h"


@interface YRKSpinningProgressIndicator (YRKSpinningProgressIndicatorPrivate)

- (void)updateFrame:(NSTimer *)timer;
- (void) animateInBackgroundThread;
- (void)actuallyStartAnimation;
- (void)actuallyStopAnimation;

@end


@implementation YRKSpinningProgressIndicator

- (id)initWithFrame:(NSRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        _position = 0;
        _numFins = 12;
        _isAnimating = NO;
        _isFadingOut = NO;
		_isIndeterminate = YES;
		_currentValue = 0.0;
		_maxValue = 100.0;
		_usesThreadedAnimation = NO;
    }
    return self;
}

- (void) dealloc {
	if (_foreColor) [_foreColor release];
	if (_backColor) [_backColor release];
	if (_isAnimating) [self stopAnimation:self];
    
	[super dealloc];
}

- (void)viewDidMoveToWindow
{
    [super viewDidMoveToWindow];

    if ([self window] == nil) {
        // No window?  View hierarchy may be going away.  Dispose timer to clear circular retain of timer to self to timer.
        [self actuallyStopAnimation];
    }
    else if (_isAnimating) {
        [self actuallyStartAnimation];
    }
}

- (void)drawRect:(NSRect)rect
{
	NSInteger i;
	CGFloat alpha = 1.0;

	// Determine size based on current bounds
	NSSize size = [self bounds].size;
	CGFloat maxSize;
	if(size.width >= size.height)
		maxSize = size.height;
	else
		maxSize = size.width;

	// fill the background, if set
	if(_drawBackground) {
		[_backColor set];
		[NSBezierPath fillRect:[self bounds]];
	}

	CGContextRef currentContext = (CGContextRef)[[NSGraphicsContext currentContext] graphicsPort];
	[NSGraphicsContext saveGraphicsState];

	// Move the CTM so 0,0 is at the center of our bounds
	CGContextTranslateCTM(currentContext,[self bounds].size.width/2,[self bounds].size.height/2);

	if (_isIndeterminate) {
		// do initial rotation to start place
		CGContextRotateCTM(currentContext, 3.14159*2/_numFins * _position);

		NSBezierPath *path = [[NSBezierPath alloc] init];
		CGFloat lineWidth = 0.08 * maxSize; // should be 2.75 for 32x32
		CGFloat lineStart = 0.234375 * maxSize; // should be 7.5 for 32x32
		CGFloat lineEnd = 0.421875 * maxSize;  // should be 13.5 for 32x32
		[path setLineWidth:lineWidth];
		[path setLineCapStyle:NSRoundLineCapStyle];
		[path moveToPoint:NSMakePoint(0,lineStart)];
		[path lineToPoint:NSMakePoint(0,lineEnd)];

		for (i=0; i<_numFins; i++) {
			if(_isAnimating) {
				[[_foreColor colorWithAlphaComponent:alpha] set];
			} else {
				[[_foreColor colorWithAlphaComponent:0.2] set];
			}

			[path stroke];

			// we draw all the fins by rotating the CTM, then just redraw the same segment again
			CGContextRotateCTM(currentContext, 6.282185/_numFins);
			alpha -= 1.0/_numFins;
		}
		[path release];

	} else {

		CGFloat lineWidth = 1 + (0.01 * maxSize);
		CGFloat circleRadius = (maxSize - lineWidth) / 2.1;
		NSPoint circleCenter = NSMakePoint(0, 0);
		[[_foreColor colorWithAlphaComponent:alpha] set];
		NSBezierPath *path = [[NSBezierPath alloc] init];
		[path setLineWidth:lineWidth];
		[path appendBezierPathWithOvalInRect:NSMakeRect(-circleRadius, -circleRadius, circleRadius*2, circleRadius*2)];
		[path stroke];
		[path release];
		path = [[NSBezierPath alloc] init];
		[path appendBezierPathWithArcWithCenter:circleCenter radius:circleRadius startAngle:90 endAngle:90-(360*(_currentValue/_maxValue)) clockwise:YES];
		[path lineToPoint:circleCenter] ;
		[path fill];
		[path release];
	}

	[NSGraphicsContext restoreGraphicsState];
}

# pragma mark -
# pragma mark Subclass

- (void)updateFrame:(NSTimer *)timer;
{
    if(_position > 0) {
        _position--;
    }
    else {
        _position = _numFins - 1;
    }
    
    if (_usesThreadedAnimation) {
        // draw now instead of waiting for setNeedsDisplay (that's the whole reason
        // we're animating from background thread)
        [self display];
    }
    else {
        [self setNeedsDisplay:YES];
    }
}

- (void) animateInBackgroundThread
{
	NSAutoreleasePool *animationPool = [[NSAutoreleasePool alloc] init];
	
	// Set up the animation speed to subtly change with size > 32.
	NSInteger animationDelay = 38000 + (2000 * ([self bounds].size.height / 32));
	NSInteger poolFlushCounter = 0;

	do {
		[self updateFrame:nil];
		usleep(animationDelay);
		poolFlushCounter++;
		if (poolFlushCounter > 256) {
			[animationPool drain];
			animationPool = [[NSAutoreleasePool alloc] init];
			poolFlushCounter = 0;
		}
	} while (![[NSThread currentThread] isCancelled]); 

	[animationPool release];
}

- (void)startAnimation:(id)sender
{
	if (!_isIndeterminate) return;
	if (_isAnimating) return;
    
    [self actuallyStartAnimation];
    _isAnimating = YES;
}

- (void)stopAnimation:(id)sender
{
    [self actuallyStopAnimation];
    _isAnimating = NO;
}

- (void)actuallyStartAnimation
{
    // Just to be safe kill any existing timer.
    [self actuallyStopAnimation];

    if ([self window]) {
        // Why animate if not visible?  viewDidMoveToWindow will re-call this method when needed.
        if (_usesThreadedAnimation) {
            _animationThread = [[NSThread alloc] initWithTarget:self selector:@selector(animateInBackgroundThread) object:nil];
            [_animationThread start];
        }
        else {
            _animationTimer = [[NSTimer timerWithTimeInterval:(NSTimeInterval)0.05
                                                       target:self
                                                     selector:@selector(updateFrame:)
                                                     userInfo:nil
                                                      repeats:YES] retain];

            [[NSRunLoop currentRunLoop] addTimer:_animationTimer forMode:NSRunLoopCommonModes];
            [[NSRunLoop currentRunLoop] addTimer:_animationTimer forMode:NSDefaultRunLoopMode];
            [[NSRunLoop currentRunLoop] addTimer:_animationTimer forMode:NSEventTrackingRunLoopMode];
        }
    }
}

- (void)actuallyStopAnimation
{
	if (_animationThread) {
        // we were using threaded animation
		[_animationThread cancel];
		if (![_animationThread isFinished]) {
			[[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
		}
		[_animationThread release];
        _animationThread = nil;
	}
    else if (_animationTimer) {
        // we were using timer-based animation
        [_animationTimer invalidate];
        [_animationTimer release];
        _animationTimer = nil;
    }
    [self setNeedsDisplay:YES];
}

# pragma mark Not Implemented

- (void)setStyle:(NSProgressIndicatorStyle)style
{
    if (NSProgressIndicatorSpinningStyle != style) {
        NSAssert(NO, @"Non-spinning styles not available.");
    }
}


# pragma mark -
# pragma mark Accessors

- (NSColor *)foreColor
{
    return [[_foreColor retain] autorelease];
}

- (void)setForeColor:(NSColor *)value
{
    if (_foreColor != value) {
        [_foreColor release];
        _foreColor = [value copy];
        [self setNeedsDisplay:YES];
    }
}

- (NSColor *)backColor
{
    return [[_backColor retain] autorelease];
}

- (void)setBackColor:(NSColor *)value
{
    if (_backColor != value) {
        [_backColor release];
        _backColor = [value copy];
        [self setNeedsDisplay:YES];
    }
}

- (BOOL)drawBackground
{
    return _drawBackground;
}

- (void)setDrawBackground:(BOOL)value
{
    if (_drawBackground != value) {
        _drawBackground = value;
    }
    [self setNeedsDisplay:YES];
}

- (BOOL)isIndeterminate
{
	return _isIndeterminate;
}

- (void)setIndeterminate:(BOOL)isIndeterminate
{
	_isIndeterminate = isIndeterminate;
	if (!_isIndeterminate && _isAnimating) [self stopAnimation:self];
    [self setNeedsDisplay:YES];
}

- (double)doubleValue
{
	return _currentValue;
}

- (void)setDoubleValue:(double)doubleValue
{
    // Automatically put it into determinate mode if it's not already.
    if (_isIndeterminate) {
        [self setIndeterminate:NO];
    }
	_currentValue = doubleValue;
	[self setNeedsDisplay:YES];
}

- (void)setNumberValue:(NSNumber *)numberValue
{
	[self setDoubleValue:[numberValue doubleValue]];
}
- (double)maxValue
{
	return _maxValue;
}

- (void)setMaxValue:(double)maxValue
{
	_maxValue = maxValue;
    [self setNeedsDisplay:YES];
}

- (void)setUsesThreadedAnimation:(BOOL)useThreaded
{
    if (_usesThreadedAnimation != useThreaded) {
        _usesThreadedAnimation = useThreaded;
        
        if (_isAnimating) {
            // restart the timer to use the new mode
            [self stopAnimation:self];
            [self startAnimation:self];
        }
    }
}

- (BOOL)usesThreadedAnimation
{
    return _usesThreadedAnimation;
}

@end