//
//  SidDecoder.mm
//  sidplay
//
//  Created by Christopher Snowhill on 12/8/14.
//  Copyright 2014 __NoWork, Inc__. All rights reserved.
//

#import "SidDecoder.h"

#import <sidplayfp/residfp.h>

#import "roms.hpp"

#import "Logging.h"

#import "PlaylistController.h"

#include <vector>

static const char *extListEmpty[] = { NULL };
static const char *extListStr[] = { ".str", NULL };

@interface sid_file_object : NSObject {
	size_t refCount;
	NSString *path;
	NSData *data;
}
@property size_t refCount;
@property NSString *path;
@property NSData *data;
@end

@implementation sid_file_object
@synthesize refCount;
@synthesize path;
@synthesize data;
@end

@interface sid_file_container : NSObject {
	NSLock *lock;
	NSMutableDictionary *list;
}
+ (sid_file_container *)instance;
- (void)add_hint:(NSString *)path source:(id)source;
- (void)remove_hint:(NSString *)path;
- (BOOL)try_hint:(NSString *)path data:(NSData **)data;
@end

@implementation sid_file_container
+ (sid_file_container *)instance {
	static sid_file_container *instance;

	@synchronized(self) {
		if(!instance) {
			instance = [[self alloc] init];
		}
	}

	return instance;
}
- (sid_file_container *)init {
	if((self = [super init])) {
		lock = [[NSLock alloc] init];
		list = [[NSMutableDictionary alloc] initWithCapacity:0];
	}
	return self;
}
- (void)add_hint:(NSString *)path source:(id)source {
	[lock lock];
	sid_file_object *obj = [list objectForKey:path];
	if(obj) {
		obj.refCount += 1;
		[lock unlock];
		return;
	}
	[lock unlock];

	obj = [[sid_file_object alloc] init];

	obj.refCount = 1;

	if(![source seekable])
		return;

	[source seek:0 whence:SEEK_END];
	size_t fileSize = [source tell];

	void *dataBytes = malloc(fileSize);
	if(!dataBytes)
		return;

	[source seek:0 whence:SEEK_SET];
	[source read:dataBytes amount:fileSize];

	NSData *data = [NSData dataWithBytes:dataBytes length:fileSize];
	free(dataBytes);

	obj.path = path;
	obj.data = data;

	[lock lock];
	[list setObject:obj forKey:path];
	[lock unlock];
}
- (void)remove_hint:(NSString *)path {
	[lock lock];
	sid_file_object *obj = [list objectForKey:path];
	if(obj.refCount <= 1) {
		[list removeObjectForKey:path];
	} else {
		obj.refCount--;
	}
	[lock unlock];
}
- (BOOL)try_hint:(NSString *)path data:(NSData **)data {
	sid_file_object *obj;
	[lock lock];
	obj = [list objectForKey:path];
	[lock unlock];
	if(obj) {
		*data = obj.data;
		return YES;
	} else {
		return NO;
	}
}
@end

static void sidTuneLoader(const char *fileName, std::vector<uint8_t> &bufferRef) {
	NSData *hintData = nil;

	if(![[sid_file_container instance] try_hint:[NSString stringWithUTF8String:fileName] data:&hintData]) {
		NSString *urlString = [NSString stringWithUTF8String:fileName];
		NSURL *url = [NSURL URLWithDataRepresentation:[urlString dataUsingEncoding:NSUTF8StringEncoding] relativeToURL:nil];

		id audioSourceClass = NSClassFromString(@"AudioSource");
		id<CogSource> source = [audioSourceClass audioSourceForURL:url];

		if(![source open:url])
			return;

		if(![source seekable])
			return;

		[source seek:0 whence:SEEK_END];
		long fileSize = [source tell];
		[source seek:0 whence:SEEK_SET];

		bufferRef.resize(fileSize);

		[source read:&bufferRef[0] amount:fileSize];

		[source close];
	} else {
		bufferRef.resize([hintData length]);
		memcpy(&bufferRef[0], [hintData bytes], [hintData length]);
	}
}

@implementation SidDecoder

- (BOOL)open:(id<CogSource>)s {
	if(![s seekable])
		return NO;

	[self setSource:s];

	NSString *path = [[s url] absoluteString];
	NSRange fragmentRange = [path rangeOfString:@"#" options:NSBackwardsSearch];
	if(fragmentRange.location != NSNotFound) {
		path = [path substringToIndex:fragmentRange.location];
	}

	currentUrl = [path stringByRemovingPercentEncoding];

	[[sid_file_container instance] add_hint:currentUrl source:s];
	hintAdded = YES;

	NSString *extension = [[s url] pathExtension];

	const char **extList = [extension isEqualToString:@"mus"] ? extListStr : extListEmpty;

	tune = new SidTune(sidTuneLoader, [currentUrl UTF8String], extList, true);

	if(!tune->getStatus())
		return NO;

	NSURL *url = [s url];
	int track_num;
	if([[url fragment] length] == 0)
		track_num = 1;
	else
		track_num = [[url fragment] intValue];

	n_channels = 1;

	length = 3 * 60 * 44100;

	tune->selectSong(track_num);

	engine = new sidplayfp;

	engine->setRoms(kernel, basic, chargen);

	if(!engine->load(tune))
		return NO;

	ReSIDfpBuilder *_builder = new ReSIDfpBuilder("ReSIDfp");
	builder = _builder;

	if(_builder) {
		_builder->create((engine->info()).maxsids());
		if(_builder->getStatus()) {
			_builder->filter(true);
			_builder->filter6581Curve(0.5);
			_builder->filter8580Curve(0.5);
		}
		if(!_builder->getStatus())
			return NO;
	} else
		return NO;

	const SidTuneInfo *tuneInfo = tune->getInfo();

	SidConfig conf = engine->config();
	conf.frequency = 44100;
	conf.sidEmulation = builder;
	conf.playback = SidConfig::MONO;
	if(tuneInfo && (tuneInfo->sidChips() > 1))
		conf.playback = SidConfig::STEREO;
	if(!engine->config(conf))
		return NO;

	if(conf.playback == SidConfig::STEREO) {
		n_channels = 2;
	}

	renderedTotal = 0;
	fadeTotal = fadeRemain = 44100 * 8;

	[self willChangeValueForKey:@"properties"];
	[self didChangeValueForKey:@"properties"];

	return YES;
}

- (NSDictionary *)properties {
	return @{ @"bitrate": @(0),
		      @"sampleRate": @(44100),
		      @"totalFrames": @(length),
		      @"bitsPerSample": @(16),
		      @"floatingPoint": @(NO),
		      @"channels": @(n_channels),
		      @"seekable": @(YES),
		      @"endian": @"host",
		      @"encoding": @"synthesized" };
}

- (NSDictionary *)metadata {
	return @{};
}

- (int)readAudio:(void *)buf frames:(UInt32)frames {
	int total = 0;
	int16_t *sampleBuffer = (int16_t *)buf;
	while(total < frames) {
		int framesToRender = 1024;
		if(framesToRender > frames)
			framesToRender = frames;
		int rendered = engine->play(sampleBuffer + total * n_channels, framesToRender * n_channels) / n_channels;

		if(rendered <= 0)
			break;

		if(n_channels == 2) {
			for(int i = 0, j = rendered * 2; i < j; i += 2) {
				int16_t *sample = sampleBuffer + total * 2 + i;
				int mid = (int)(sample[0] + sample[1]) / 2;
				int side = (int)(sample[0] - sample[1]) / 4;
				sample[0] = mid + side;
				sample[1] = mid - side;
			}
		}

		renderedTotal += rendered;

		if(!IsRepeatOneSet() && renderedTotal >= length) {
			int16_t *sampleBuf = (int16_t *)buf + total * n_channels;
			long fadeEnd = fadeRemain - rendered;
			if(fadeEnd < 0)
				fadeEnd = 0;
			float fadePosf = (float)fadeRemain / (float)fadeTotal;
			const float fadeStep = 1.0f / (float)fadeTotal;
			for(long fadePos = fadeRemain; fadePos > fadeEnd; --fadePos, fadePosf -= fadeStep) {
				long offset = (fadeRemain - fadePos) * n_channels;
				float sampleLeft = sampleBuf[offset + 0];
				sampleLeft *= fadePosf;
				sampleBuf[offset + 0] = (int16_t)sampleLeft;
				if(n_channels == 2) {
					float sampleRight = sampleBuf[offset + 1];
					sampleRight *= fadePosf;
					sampleBuf[offset + 1] = (int16_t)sampleRight;
				}
			}
			rendered = (int)(fadeRemain - fadeEnd);
			fadeRemain = fadeEnd;
		}

		total += rendered;

		if(rendered < framesToRender)
			break;
	}

	return total;
}

- (long)seek:(long)frame {
	if(frame < renderedTotal) {
		engine->load(tune);
		renderedTotal = 0;
	}

	int16_t sampleBuffer[1024 * 2];

	long remain = (frame - renderedTotal) % 32;
	frame /= 32;
	renderedTotal /= 32;
	engine->fastForward(100 * 32);

	while(renderedTotal < frame) {
		long todo = frame - renderedTotal;
		if(todo > 1024)
			todo = 1024;
		int done = engine->play(sampleBuffer, (uint_least32_t)(todo * n_channels)) / n_channels;

		if(done < todo) {
			if(engine->error())
				return -1;

			renderedTotal = length;
			break;
		}

		renderedTotal += todo;
	}

	renderedTotal *= 32;
	engine->fastForward(100);

	if(remain)
		renderedTotal += engine->play(sampleBuffer, (uint_least32_t)(remain * n_channels)) / n_channels;

	return renderedTotal;
}

- (void)cleanUp {
	if(builder) {
		delete builder;
		builder = NULL;
	}

	if(engine) {
		delete engine;
		engine = NULL;
	}

	if(tune) {
		delete tune;
		tune = NULL;
	}

	source = nil;
	if(hintAdded) {
		[[sid_file_container instance] remove_hint:currentUrl];
		hintAdded = NO;
	}
	currentUrl = nil;
}

- (void)close {
	[self cleanUp];
}

- (void)dealloc {
	[self close];
}

- (void)setSource:(id<CogSource>)s {
	source = s;
}

- (id<CogSource>)source {
	return source;
}

+ (NSArray *)fileTypes {
	return @[@"sid", @"mus"];
}

+ (NSArray *)mimeTypes {
	return nil;
}

+ (float)priority {
	return 0.5;
}

+ (NSArray *)fileTypeAssociations {
	return @[
		@[@"SID File", @"vg.icns", @"sid"],
		@[@"SID MUS File", @"song.icns", @"mus"]
	];
}

@end
