/*
 *  $Id: AudioScrobbler.m 238 2007-01-26 22:55:20Z stephen_booth $
 *
 *  Copyright (C) 2006 - 2007 Stephen F. Booth <me@sbooth.org>
 *
 *  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
 */

#import "AudioScrobbler.h"

#import "AudioScrobblerClient.h"
#import "PlaylistEntry.h"

#import "Logging.h"

// ========================================
// Symbolic Constants
// ========================================
NSString *const AudioScrobblerRunLoopMode = @"org.cogx.Cog.AudioScrobbler.RunLoopMode";

// ========================================
// Helpers
// ========================================
static NSString *
escapeForLastFM(NSString *string) {
	NSMutableString *result = [string mutableCopy];

	[result replaceOccurrencesOfString:@"&"
	                        withString:@"&&"
	                           options:NSLiteralSearch
	                             range:NSMakeRange(0, [result length])];

	return (nil == result ? @"" : result);
}

@interface AudioScrobbler (Private)

- (NSMutableArray *)queue;
- (NSString *)pluginID;

- (void)sendCommand:(NSString *)command;

- (BOOL)keepProcessingAudioScrobblerCommands;
- (void)setKeepProcessingAudioScrobblerCommands:(BOOL)keepProcessingAudioScrobblerCommands;

- (BOOL)audioScrobblerThreadCompleted;
- (void)setAudioScrobblerThreadCompleted:(BOOL)audioScrobblerThreadCompleted;

- (semaphore_t)semaphore;

- (void)processAudioScrobblerCommands:(id)unused;

@end

@implementation AudioScrobbler

+ (BOOL)isRunning {
	NSArray *launchedApps = [[NSWorkspace sharedWorkspace] runningApplications];
	BOOL running = NO;
	for(NSRunningApplication *app in launchedApps) {
		if([[app bundleIdentifier] isEqualToString:@"fm.last.Last.fm"] ||
		   [[app bundleIdentifier] isEqualToString:@"fm.last.Scrobbler"]) {
			running = YES;
			break;
		}
	}
	return running;
}

- (id)init {
	if((self = [super init])) {
		_pluginID = @"cog";

		if([[NSUserDefaults standardUserDefaults] boolForKey:@"automaticallyLaunchLastFM"]) {
			if(![AudioScrobbler isRunning]) {
				[[NSWorkspace sharedWorkspace] launchApplication:@"Last.fm.app"];
			}
		}

		_keepProcessingAudioScrobblerCommands = YES;

		kern_return_t result = semaphore_create(mach_task_self(), &_semaphore, SYNC_POLICY_FIFO, 0);

		if(KERN_SUCCESS != result) {
			ALog(@"Couldn't create semaphore (%s).", mach_error_type(result));

			self = nil;
			return nil;
		}

		[NSThread detachNewThreadSelector:@selector(processAudioScrobblerCommands:) toTarget:self withObject:nil];
	}
	return self;
}

- (void)dealloc {
	if([self keepProcessingAudioScrobblerCommands] || NO == [self audioScrobblerThreadCompleted])
		[self shutdown];

	_queue = nil;

	semaphore_destroy(mach_task_self(), _semaphore);
	_semaphore = 0;
}

- (void)start:(PlaylistEntry *)pe {
	[self sendCommand:[NSString stringWithFormat:@"START c=%@&a=%@&t=%@&b=%@&m=%@&l=%i&p=%@\n",
	                                             [self pluginID],
	                                             escapeForLastFM([pe artist]),
	                                             escapeForLastFM([pe title]),
	                                             escapeForLastFM([pe album]),
	                                             @"", // TODO: MusicBrainz support
	                                             [[pe length] intValue],
	                                             escapeForLastFM([pe.url path])]];
}

- (void)stop {
	[self sendCommand:[NSString stringWithFormat:@"STOP c=%@\n", [self pluginID]]];
}

- (void)pause {
	[self sendCommand:[NSString stringWithFormat:@"PAUSE c=%@\n", [self pluginID]]];
}

- (void)resume {
	[self sendCommand:[NSString stringWithFormat:@"RESUME c=%@\n", [self pluginID]]];
}

- (void)shutdown {
	[self setKeepProcessingAudioScrobblerCommands:NO];
	semaphore_signal([self semaphore]);

	// Wait for the thread to terminate
	while(NO == [self audioScrobblerThreadCompleted])
		[[NSRunLoop currentRunLoop] runMode:AudioScrobblerRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.2]];
}

@end

@implementation AudioScrobbler (Private)

- (NSMutableArray *)queue {
	if(nil == _queue)
		_queue = [[NSMutableArray alloc] init];

	return _queue;
}

- (NSString *)pluginID {
	return _pluginID;
}

- (void)sendCommand:(NSString *)command {
	@synchronized([self queue]) {
		[[self queue] addObject:command];
	}
	semaphore_signal([self semaphore]);
}

- (BOOL)keepProcessingAudioScrobblerCommands {
	return _keepProcessingAudioScrobblerCommands;
}

- (void)setKeepProcessingAudioScrobblerCommands:(BOOL)keepProcessingAudioScrobblerCommands {
	_keepProcessingAudioScrobblerCommands = keepProcessingAudioScrobblerCommands;
}

- (BOOL)audioScrobblerThreadCompleted {
	return _audioScrobblerThreadCompleted;
}

- (void)setAudioScrobblerThreadCompleted:(BOOL)audioScrobblerThreadCompleted {
	_audioScrobblerThreadCompleted = audioScrobblerThreadCompleted;
}

- (semaphore_t)semaphore {
	return _semaphore;
}

- (void)processAudioScrobblerCommands:(id)unused {
	@autoreleasepool {
		AudioScrobblerClient *client = [[AudioScrobblerClient alloc] init];
		mach_timespec_t timeout = { 5, 0 };
		NSString *command = nil;
		NSString *response = nil;
		in_port_t port = 33367;

		while([self keepProcessingAudioScrobblerCommands]) {
			@autoreleasepool {
				// Get the first command to be sent
				@synchronized([self queue]) {
					if([[self queue] count]) {
						command = [[self queue] objectAtIndex:0];
						[[self queue] removeObjectAtIndex:0];
					}
				}

				if(nil != command) {
					@try {
						if([client connectToHost:@"localhost" port:port]) {
							port = [client connectedPort];
							[client send:command];
							command = nil;

							response = [client receive];
							if(2 > [response length] || NSOrderedSame != [response compare:@"OK" options:NSLiteralSearch range:NSMakeRange(0, 2)])
								ALog(@"AudioScrobbler error: %@", response);

							[client shutdown];
						}
					}

					@catch(NSException *exception) {
						command = nil;

						[client shutdown];
						//                      ALog(@"Exception: %@",exception);
						continue;
					}
				}

				semaphore_timedwait([self semaphore], timeout);
			}
		}

		// Send a final stop command to cleanup
		@try {
			if([client connectToHost:@"localhost" port:port]) {
				[client send:[NSString stringWithFormat:@"STOP c=%@\n", [self pluginID]]];

				response = [client receive];
				if(2 > [response length] || NSOrderedSame != [response compare:@"OK" options:NSLiteralSearch range:NSMakeRange(0, 2)])
					ALog(@"AudioScrobbler error: %@", response);

				[client shutdown];
			}
		}

		@catch(NSException *exception) {
			[client shutdown];
		}

		[self setAudioScrobblerThreadCompleted:YES];
	}
}

@end
