//
//  ptmodDecoder.m
//  playptmod
//
//  Created by Christopher Snowhill on 10/22/13.
//  Copyright 2013 __NoWork, Inc__. All rights reserved.
//

#import "ptmodDecoder.h"

#import "mo3.h"
#import "umx.h"

#import "Logging.h"

#import "PlaylistController.h"

@implementation ptmodDecoder

BOOL probe_length(void *ptmod, unsigned long *intro_length, unsigned long *loop_length, int test_vblank, const void *src, unsigned long size, unsigned int subsong) {
	playptmod_Config(ptmod, PTMOD_OPTION_VSYNC_TIMING, test_vblank);

	playptmod_Play(ptmod, subsong);

	unsigned long length_total = 0;
	unsigned long length_saved;

	const long length_safety = 44100 * 60 * 30;

	while(playptmod_LoopCounter(ptmod) < 1 && length_total < length_safety) {
		playptmod_Render(ptmod, NULL, 512);
		length_total += 512;
	}

	if(playptmod_LoopCounter(ptmod) < 1) {
		*loop_length = 0;
		*intro_length = 44100 * 60 * 3;
		playptmod_Stop(ptmod);
		return YES;
	}

	length_saved = length_total;

	while(playptmod_LoopCounter(ptmod) < 2) {
		playptmod_Render(ptmod, NULL, 512);
		length_total += 512;
	}

	playptmod_Stop(ptmod);

	*loop_length = length_total - length_saved;
	*intro_length = length_saved - *loop_length;

	return YES;
}

- (BOOL)open:(id<CogSource>)s {
	[s seek:0 whence:SEEK_END];
	size = [s tell];
	[s seek:0 whence:SEEK_SET];

	data = malloc(size);
	[s read:data amount:size];

	isMo3 = 0;
	char *try_data = unpackMo3(data, &size);
	if(try_data) {
		free(data);
		data = try_data;
		isMo3 = 1;
	} else {
		try_data = unpackUmx(data, &size);
		if(try_data) {
			free(data);
			data = try_data;
		}
	}

	if([[[s url] fragment] length] == 0)
		track_num = 0;
	else
		track_num = [[[s url] fragment] intValue];

	void *mod = playptmod_Create(44100);
	if(!mod) return NO;

	if(!playptmod_LoadMem(mod, data, size)) {
		playptmod_Free(mod);
		return NO;
	}

	int format = playptmod_GetFormat(mod);
	BOOL can_be_vblank = (format <= FORMAT_MK2);

	unsigned long normal_intro_length, normal_loop_length, vblank_intro_length, vblank_loop_length;

	if(!probe_length(mod, &normal_intro_length, &normal_loop_length, 0, data, size, track_num))
		return NO;
	if(can_be_vblank) {
		if(!probe_length(mod, &vblank_intro_length, &vblank_loop_length, 1, data, size, track_num))
			return NO;
		if(vblank_loop_length == 0)
			can_be_vblank = NO;
	} else {
		vblank_intro_length = 0;
		vblank_loop_length = 0;
	}

	playptmod_Free(mod);

	isVblank = can_be_vblank && ((vblank_intro_length + vblank_loop_length) < (normal_intro_length + normal_loop_length));

	unsigned long intro_length = isVblank ? vblank_intro_length : normal_intro_length;
	unsigned long loop_length = isVblank ? vblank_loop_length : normal_loop_length;

	framesLength = intro_length + loop_length * 2;
	totalFrames = framesLength + 44100 * 8;

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

	return YES;
}

- (BOOL)decoderInitialize {
	ptmod = playptmod_Create(44100);
	if(!ptmod)
		return NO;

	playptmod_Config(ptmod, PTMOD_OPTION_CLAMP_PERIODS, 0);
	playptmod_Config(ptmod, PTMOD_OPTION_VSYNC_TIMING, isVblank);

	if(!playptmod_LoadMem(ptmod, data, size))
		return NO;

	playptmod_Play(ptmod, track_num);

	framesRead = 0;

	return YES;
}

- (void)decoderShutdown {
	if(ptmod) {
		playptmod_Stop(ptmod);
		playptmod_Free(ptmod);
		ptmod = NULL;
	}
}

- (NSDictionary *)properties {
	return [NSDictionary dictionaryWithObjectsAndKeys:
	                     [NSNumber numberWithInt:0], @"bitrate",
	                     [NSNumber numberWithFloat:44100], @"sampleRate",
	                     [NSNumber numberWithDouble:totalFrames], @"totalFrames",
	                     [NSNumber numberWithInt:32], @"bitsPerSample",
	                     [NSNumber numberWithBool:YES], @"floatingPoint",
	                     [NSNumber numberWithInt:2], @"channels",
	                     [NSNumber numberWithBool:YES], @"seekable",
	                     @"host", @"endian",
	                     nil];
}

- (int)readAudio:(void *)buf frames:(UInt32)frames {
	BOOL repeat_one = IsRepeatOneSet();

	if(!repeat_one && framesRead >= totalFrames)
		return 0;

	if(!ptmod) {
		if(![self decoderInitialize])
			return 0;
	}

	int total = 0;
	while(total < frames) {
		int framesToRender = 512;
		if(!repeat_one && framesToRender > totalFrames - framesRead)
			framesToRender = (int)(totalFrames - framesRead);
		if(framesToRender > frames - total)
			framesToRender = frames - total;

		int32_t *sampleBuf = (int32_t *)buf + total * 2;

		playptmod_Render(ptmod, sampleBuf, framesToRender);

		if(!repeat_one && framesRead + framesToRender > framesLength) {
			long fadeStart = (framesLength > framesRead) ? framesLength : framesRead;
			long fadeEnd = (framesRead + framesToRender < totalFrames) ? framesRead + framesToRender : totalFrames;
			const long fadeTotal = totalFrames - framesLength;
			for(long fadePos = fadeStart; fadePos < fadeEnd; ++fadePos) {
				const long scale = (fadeTotal - (fadePos - framesLength));
				const long offset = fadePos - framesRead;
				int32_t *samples = sampleBuf + offset * 2;
				samples[0] = (int32_t)(samples[0] * scale / fadeTotal);
				samples[1] = (int32_t)(samples[1] * scale / fadeTotal);
			}

			framesToRender = (int)(fadeEnd - framesRead);
		}

		if(!framesToRender)
			break;

		total += framesToRender;
		framesRead += framesToRender;
	}

	for(int i = 0; i < total; ++i) {
		int32_t *sampleIn = (int32_t *)buf + i * 2;
		float *sampleOut = (float *)buf + i * 2;
		sampleOut[0] = sampleIn[0] * (1.0f / 16777216.0f);
		sampleOut[1] = sampleIn[1] * (1.0f / 16777216.0f);
	}

	return total;
}

- (long)seek:(long)frame {
	if(frame < framesRead || !ptmod) {
		[self decoderShutdown];
		if(![self decoderInitialize])
			return 0;
	}

	while(framesRead < frame) {
		int frames_todo = INT_MAX;
		if(frames_todo > frame - framesRead)
			frames_todo = (int)(frame - framesRead);
		playptmod_Render(ptmod, NULL, frames_todo);
		framesRead += frames_todo;
	}

	framesRead = frame;

	return frame;
}

- (void)close {
	[self decoderShutdown];

	if(data) {
		if(isMo3)
			freeMo3(data);
		else
			free(data);
		data = NULL;
	}
}

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

+ (NSArray *)fileTypes {
	return [NSArray arrayWithObjects:@"mod", @"mdz", @"stk", @"m15", @"fst", @"mo3", @"umx", nil];
}

+ (NSArray *)mimeTypes {
	return [NSArray arrayWithObjects:@"audio/x-mod", nil];
}

+ (float)priority {
	return 1.5;
}

@end
