/* vim: set ft=objc ts=4 nowrap: */
/*
 *  AudioCD.m
 *
 *  Copyright (c) 2002 - 2003
 *
 *  Author: Andreas Schik <andreas@schik.de>
 *
 *  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
 */


#include <unistd.h>

#include <cdaudio.h>

#include "AudioCD.h"


static BOOL exitThread = NO;
static BOOL pollThreadRunning = NO;

@interface AudioCD (Private)

- (BOOL) allocateDiscInfo;
- (void) releaseDiscInfo;
- (BOOL) deviceForCD: (NSString *) testDevice;
- (BOOL) checkAllDevicesForCD: (NSString *)customDevice;
- (void) checkDrivesThread: (id)anObject;

@end

@implementation AudioCD

- initWithHandler: (id<CDHandlerProtocol>)handler
{
	self = [super init];

	if (self != nil) {
		_fd = -1;
		discInfo = NULL;
		[self setHandler: handler];
	}

	return self;
}

- (void)dealloc
{
	[self stopPolling];

	[self releaseDiscInfo];
	if (_fd > 0)
		close(_fd);

	[foundDevice release];

	[super dealloc];
}

- (void) startPollingWithPreferredDevice: (NSString *)device
{
	exitThread = NO;
	pollThreadRunning = YES;

	[NSThread detachNewThreadSelector: @selector(checkDrivesThread:)
							 toTarget: self
						   withObject: device];
}

- (void) stopPolling
{
	exitThread = YES;

	// wait for thethread to actually end
	do {
		[NSThread sleepUntilDate: [NSDate dateWithTimeIntervalSinceNow: 0.25]];
	} while (pollThreadRunning);
}

- (NSString *)device
{
	return [[foundDevice copy] autorelease];
}

- (void)setHandler: (id<CDHandlerProtocol>)handler
{
	_handler = handler;
}

- (NSMutableDictionary *)readTOC
{
	int i;
	NSMutableDictionary	*toc = [NSMutableDictionary dictionaryWithCapacity: 6];
	NSMutableArray		*tracks;

	if (!discInfo)
		return nil;

	tracks = [NSMutableArray arrayWithCapacity: discInfo->disc_total_tracks];
	for (i = 0; i < discInfo->disc_total_tracks; i++) {
		NSMutableDictionary *track = [NSMutableDictionary dictionaryWithCapacity: 5];

		[track setObject: _(@"Unknown") forKey: @"artist"];
		[track setObject: [NSString stringWithFormat: _(@"Track%d"), i+1] forKey: @"title"];

		[track setObject: [NSString stringWithFormat: @"%d",
					cd_msf_to_frames(discInfo->disc_track[i].track_length)]
			forKey: @"length"];
		[track setObject: [NSString stringWithFormat: @"%d",
					cd_msf_to_frames(discInfo->disc_track[i].track_pos)]
			forKey: @"offset"];
		[track setObject: discInfo->disc_track[i].track_type==CDAUDIO_TRACK_AUDIO?@"audio":@"data"
			forKey: @"type"];
		[tracks addObject: track];
	}

	[toc setObject: foundDevice forKey: @"device"];
	[toc setObject: _(@"Unknown") forKey: @"artist"];
	[toc setObject: _(@"Unknown") forKey: @"title"];

	[toc setObject: [NSString stringWithFormat: @"%08X", cddb_direct_discid(*discInfo)]
		forKey: @"cddbid"];
	[toc setObject: [NSString stringWithFormat: @"%d", cd_msf_to_frames(discInfo->disc_length)]
		forKey: @"discLength"];
	[toc setObject: [NSString stringWithFormat: @"%d", discInfo->disc_total_tracks]
		forKey: @"numberOfTracks"];
	[toc setObject: tracks forKey: @"tracks"];

	return toc;
}

- (BOOL) checkForCDWithId: (NSString *)cddbId
{
	NSString *temp;

	if (!discInfo)
		return NO;

	temp = [NSString stringWithFormat: @"%08X", cddb_direct_discid(*discInfo)];
	if ([cddbId isEqual: temp]) {
		return YES;
	}

	return NO;
}

- (void) playStart: (int)start End: (int)end
{
	if (_fd < 0)
		return;

	if (!discInfo)
		return;

	/*
	 * Skip leading/trailing data tracks
	 */
	while (discInfo->disc_track[start-1].track_type != CDAUDIO_TRACK_AUDIO)
		start++;
	while (discInfo->disc_track[end-1].track_type != CDAUDIO_TRACK_AUDIO)
		end--;

	if(cd_play_track(_fd, start, end) < 0) {
		[_handler audioCD: self
			   		error: errno
			 	  message: [NSString stringWithCString: strerror(errno)]];
	}
}

- (void) pause
{
	if (_fd < 0)
		return;

	if(cd_pause(_fd) < 0) {
		[_handler audioCD: self
			   		error: errno
			 	  message: [NSString stringWithCString: strerror(errno)]];
	}
}

- (void) resume
{
	if (_fd < 0)
		return;

	if(cd_resume(_fd) < 0) {
		[_handler audioCD: self
			   		error: errno
			 	  message: [NSString stringWithCString: strerror(errno)]];
	}
}

- (void) stop
{
	if (_fd < 0)
		return;

	if(cd_stop(_fd) < 0) {
		[_handler audioCD: self
			   		error: errno
				  message: [NSString stringWithCString: strerror(errno)]];
	}
}

- (void) eject
{
	if (_fd < 0)
		return;

	if(cd_eject(_fd) < 0) {
		[_handler audioCD: self
			   		error: errno
			 	  message: [NSString stringWithCString: strerror(errno)]];
	}
}

- (void) close
{
	if (_fd < 0)
		return;

	if(cd_close(_fd) < 0) {
		[_handler audioCD: self
			   		error: errno
			 	  message: [NSString stringWithCString: strerror(errno)]];
	}
}


- (BOOL) cdPresent
{
	// We must query the drive here and must not use the static
	// disc inormation.
	struct disc_info info;

	if (_fd < 0)
		return NO;

	if(cd_stat(_fd, &info) < 0) {
		[_handler audioCD: self
					error: errno
				  message: [NSString stringWithCString: strerror(errno)]];
		return NO;
	}

	return info.disc_present;
}


- (int) currentState
{
	struct disc_info        info;

	if (_fd < 0)
		return -1;

	if(cd_stat(_fd, &info) < 0) {
		[_handler audioCD: self
					error: errno
				  message: [NSString stringWithCString: strerror(errno)]];
		return -1;
	}

	return info.disc_mode;
}

- (int) currentTrack
{
	struct disc_info        info;

	if (_fd < 0)
		return -1;

	if(cd_stat(_fd, &info) < 0) {
		[_handler audioCD: self
					error: errno
				  message: [NSString stringWithCString: strerror(errno)]];
		return -1;
	}

	return info.disc_current_track;
}

- (int) currentMin
{
	struct disc_info        info;

	if (_fd < 0)
		return 0;

	if(cd_stat(_fd, &info) < 0) {
		[_handler audioCD: self
					error: errno
				  message: [NSString stringWithCString: strerror(errno)]];
		return -1;
	}

	return info.disc_track_time.minutes;
}

- (int) currentSec
{
	struct disc_info        info;

	if (_fd < 0)
		return 0;

	if(cd_stat(_fd, &info) < 0) {
		[_handler audioCD: self
					error: errno
				  message: [NSString stringWithCString: strerror(errno)]];
		return -1;
	}

	return info.disc_track_time.seconds;
}


- (int) firstTrack
{
	if (!discInfo)
		return -1;

	return discInfo->disc_first_track;
}

- (int) totalTrack
{
	if (!discInfo)
		return -1;

	return discInfo->disc_total_tracks;
}

- (int) trackLength: (int)track
{
	if (!discInfo)
		return -1;

	return (discInfo->disc_track[track].track_length.minutes * 60) +
			discInfo->disc_track[track].track_length.seconds;
}

@end


//
// private methods
//

@implementation AudioCD (Private)

- (BOOL) allocateDiscInfo
{
	if (_fd < 0)
		return NO;

	// We do not reallocate the disc info. If we already have
	// such a struct we reuse it. This means that the disc info,
	// if present, must be explicitly release to get a new one.
	if (discInfo != NULL)
		return YES;

	discInfo = (struct disc_info*)malloc(sizeof(struct disc_info));

	if(cd_stat(_fd, discInfo) < 0) {
		[_handler audioCD: self
			   		error: errno
			 	  message: [NSString stringWithCString: strerror(errno)]];
		[self releaseDiscInfo];
		return NO;
	}
	return YES;
}


- (void) releaseDiscInfo
{
	if (discInfo) {
		free(discInfo);
		discInfo = NULL;
	}
}

- (BOOL) checkAllDevicesForCD: (NSString *)customDevice
{
	int i, count;
	char *pos;
	const char *dev;
 	BOOL found = NO;
	NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
	NSDictionary *domain = [defaults persistentDomainForName: @"AudioCD"];
	NSArray *devices = [domain objectForKey: @"Devices"];

	// iterate over all known, potential cdrom devices
	count = [devices count];
	for (i = -1; i < count; i++) {
		/*
		 * Let's check whether the user defined a certain device
		 * to be read from. This will precede the predefined ones.
		 * Nevertheless, we check the predefined ones, if the user
		 * user defined device has no audio cd in it.
		 */
		if (i == -1) {
			if (!customDevice || ![customDevice length]) {
				continue;
			}
			dev = [customDevice cString];
		} else {
			dev = [[devices objectAtIndex: i] cString];
		}
		if(dev && (pos = strchr(dev, '?'))) {
			char j;

			/* try first eight of each device */
			for(j = 0; (j < 4) && !found; j++) {
				char *temp = strdup(dev);

				/* number, then letter */
				temp[pos - dev] = j + 48;
				found = [self deviceForCD: [NSString stringWithCString: temp]];
				if (!found) {
					temp[pos - dev] = j + 97;
					found = [self deviceForCD: [NSString stringWithCString: temp]];
				}
				free(temp);
			}
		} else {
			/* Name.  Go for it. */
			if ([self deviceForCD: [NSString stringWithCString: dev]]) {
				found = YES;
			}
		}
	}
	return found;
}

- (BOOL) deviceForCD: (NSString *) testDevice
{
	_fd = cd_init_device((char *)[testDevice cString]);

	// could we open the device?
	if (_fd < 0)
		return NO;

	// stop here, we found a CD
	foundDevice = [testDevice copy];
	return YES;
}

- (void) checkDrivesThread: (id)anObject
{
	id pool;
	NSString *device;
	BOOL present = NO;

	pool = [NSAutoreleasePool new];
	device = [[(NSString *)anObject copy] autorelease];

	do {
		if(_fd < 0) {
			[self checkAllDevicesForCD: device];
		}

		// is a disc present in the drive?
		if([self cdPresent] != present) {
			present = !present;
			// release the old disc information
			[self releaseDiscInfo];

			if(!present) {
				close(_fd);
				_fd = -1;
			} else {
				[self allocateDiscInfo];
			}
			// send a message to owner
			[_handler audioCDChanged: self];
		}

		// wait a second for the next check
		[NSThread sleepUntilDate: [NSDate dateWithTimeIntervalSinceNow: 1.]];
	} while (!exitThread);

	RELEASE(pool);
	pollThreadRunning = NO;
}


@end
