/*
 * libopenmpt_modplug.c
 * --------------------
 * Purpose: libopenmpt emulation of the libmodplug interface
 * Notes  : (currently none)
 * Authors: OpenMPT Devs
 * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
 */

#ifndef NO_LIBMODPLUG

#ifdef LIBOPENMPT_BUILD_DLL
#undef LIBOPENMPT_BUILD_DLL
#endif

#ifdef _MSC_VER
#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#endif
#endif /* _MSC_VER */

#include "libopenmpt.h"

#include <limits.h>
#include <math.h>
#include <memory.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MPT_PP_DEFER(m, ...) m(__VA_ARGS__)
#define MPT_PP_STRINGIFY(x) #x
#define MPT_PP_JOIN_HELPER(a, b) a ## b
#define MPT_PP_JOIN(a, b) MPT_PP_JOIN_HELPER(a, b)
#define MPT_PP_UNIQUE_IDENTIFIER(prefix) MPT_PP_JOIN(prefix , __LINE__)
#if defined(__clang__)
	#define MPT_WARNING(text) _Pragma(MPT_PP_STRINGIFY(GCC warning text))
#elif defined(__GNUC__)
	#define MPT_WARNING(text) _Pragma(MPT_PP_STRINGIFY(GCC warning text))
#elif defined(_MSC_VER)
	#define MPT_WARNING(text) __pragma(message(__FILE__ "(" MPT_PP_DEFER(MPT_PP_STRINGIFY, __LINE__) "): Warning: " text))
#elif defined(__cplusplus)
	#if (__cplusplus >= 201402L)
		#define MPT_WARNING(text) \
			static inline int MPT_PP_UNIQUE_IDENTIFIER(MPT_WARNING_NAME) () noexcept { \
				int warning [[deprecated("Warning: " text)]] = 0; \
				return warning; \
			} \
		/**/
	#else
		#define MPT_WARNING(text) /**/
	#endif
#else
	#define MPT_WARNING(text) /**/
#endif
#if !defined(MPT_BUILD_SILENCE_LIBOPENMPT_CONFIGURATION_WARNINGS)
#if !defined(LIBOPENMPT_NO_DEPRECATE)
MPT_WARNING("libopenmpt-modplug from the libopenmpt source package is deprecated. Please migrate client code to native libopenmpt C or C++ APIs, or use the separate libopenmpt-modplug source package instead.")
#endif /* !LIBOPENMPT_NO_DEPRECATE */
#endif /* !MPT_BUILD_SILENCE_LIBOPENMPT_CONFIGURATION_WARNINGS */

#define MODPLUG_BUILD
#ifdef _MSC_VER
#ifdef MPT_BUILD_MSVC_SHARED
#define DLL_EXPORT
#endif /* MPT_BUILD_MSVC_SHARED */
#ifdef MPT_BUILD_MSVC_STATIC
#define MODPLUG_STATIC
#endif /* MPT_BUILD_MSVC_STATIC */
#endif /* _MSC_VER */
#ifdef _MSC_VER
#define LIBOPENMPT_MODPLUG_API
#else /* !_MSC_VER */
#define LIBOPENMPT_MODPLUG_API LIBOPENMPT_API_HELPER_EXPORT
#endif /* _MSC_VER */
#include "libmodplug/modplug.h"

/* from libmodplug/sndfile.h */
/* header is not c clean */
#define MIXING_ATTENUATION 4
#define MOD_TYPE_NONE		0x0
#define MOD_TYPE_MOD		0x1
#define MOD_TYPE_S3M		0x2
#define MOD_TYPE_XM		0x4
#define MOD_TYPE_MED		0x8
#define MOD_TYPE_MTM		0x10
#define MOD_TYPE_IT		0x20
#define MOD_TYPE_669		0x40
#define MOD_TYPE_ULT		0x80
#define MOD_TYPE_STM		0x100
#define MOD_TYPE_FAR		0x200
#define MOD_TYPE_WAV		0x400
#define MOD_TYPE_AMF		0x800
#define MOD_TYPE_AMS		0x1000
#define MOD_TYPE_DSM		0x2000
#define MOD_TYPE_MDL		0x4000
#define MOD_TYPE_OKT		0x8000
#define MOD_TYPE_MID		0x10000
#define MOD_TYPE_DMF		0x20000
#define MOD_TYPE_PTM		0x40000
#define MOD_TYPE_DBM		0x80000
#define MOD_TYPE_MT2		0x100000
#define MOD_TYPE_AMF0		0x200000
#define MOD_TYPE_PSM		0x400000
#define MOD_TYPE_J2B		0x800000
#define MOD_TYPE_ABC		0x1000000
#define MOD_TYPE_PAT		0x2000000
#define MOD_TYPE_UMX		0x80000000 // Fake type

#define BUFFER_COUNT 1024

struct _ModPlugFile {
	openmpt_module* mod;
	signed short* buf;
	signed int* mixerbuf;
	char* name;
	char* message;
	ModPlug_Settings settings;
	ModPlugMixerProc mixerproc;
	ModPlugNote** patterns;
};

static ModPlug_Settings globalsettings = {
	MODPLUG_ENABLE_OVERSAMPLING|MODPLUG_ENABLE_NOISE_REDUCTION,
	2,
	16,
	44100,
	MODPLUG_RESAMPLE_LINEAR,
	128,
	256,
	0,
	0,
	0,
	0,
	0,
	0,
	0
};

static int32_t modplugresamplingmode_to_filterlength(int mode)
{
	if(mode<0){
		return 1;
	}
	switch(mode){
	case MODPLUG_RESAMPLE_NEAREST: return 1; break;
	case MODPLUG_RESAMPLE_LINEAR: return 2; break;
	case MODPLUG_RESAMPLE_SPLINE: return 4; break;
	case MODPLUG_RESAMPLE_FIR: return 8; break;
	}
	return 8;
}

LIBOPENMPT_MODPLUG_API ModPlugFile* ModPlug_Load(const void* data, int size)
{
	ModPlugFile* file = malloc(sizeof(ModPlugFile));
	const char* name = NULL;
	const char* message = NULL;
	if(!file) return NULL;
	memset(file,0,sizeof(ModPlugFile));
	memcpy(&file->settings,&globalsettings,sizeof(ModPlug_Settings));
	file->mod = openmpt_module_create_from_memory2(data,size,NULL,NULL,NULL,NULL,NULL,NULL,NULL);
	if(!file->mod){
		free(file);
		return NULL;
	}
	file->buf = malloc(BUFFER_COUNT*sizeof(signed short)*4);
	if(!file->buf){
		openmpt_module_destroy(file->mod);
		free(file);
		return NULL;
	}
	openmpt_module_set_repeat_count(file->mod,file->settings.mLoopCount);
	name = openmpt_module_get_metadata(file->mod,"title");
	if(name){
		file->name = malloc(strlen(name)+1);
		if(file->name){
			strcpy(file->name,name);
		}
		openmpt_free_string(name);
		name = NULL;
	}else{
		file->name = malloc(strlen("")+1);
		if(file->name){
			strcpy(file->name,"");
		}
	}
	message = openmpt_module_get_metadata(file->mod,"message");
	if(message){
		file->message = malloc(strlen(message)+1);
		if(file->message){
			strcpy(file->message,message);
		}
		openmpt_free_string(message);
		message = NULL;
	}else{
		file->message = malloc(strlen("")+1);
		if(file->message){
			strcpy(file->message,"");
		}
	}
	openmpt_module_set_render_param(file->mod,OPENMPT_MODULE_RENDER_STEREOSEPARATION_PERCENT,file->settings.mStereoSeparation*100/128);
	openmpt_module_set_render_param(file->mod,OPENMPT_MODULE_RENDER_INTERPOLATIONFILTER_LENGTH,modplugresamplingmode_to_filterlength(file->settings.mResamplingMode));
	return file;
}

LIBOPENMPT_MODPLUG_API void ModPlug_Unload(ModPlugFile* file)
{
	int p;
	if(!file) return;
	if(file->patterns){
		for(p=0;p<openmpt_module_get_num_patterns(file->mod);p++){
			if(file->patterns[p]){
				free(file->patterns[p]);
				file->patterns[p] = NULL;
			}
		}
		free(file->patterns);
		file->patterns = NULL;
	}
	if(file->mixerbuf){
		free(file->mixerbuf);
		file->mixerbuf = NULL;
	}
	openmpt_module_destroy(file->mod);
	file->mod = NULL;
	free(file->name);
	file->name = NULL;
	free(file->message);
	file->message = NULL;
	free(file->buf);
	file->buf = NULL;
	free(file);
}

LIBOPENMPT_MODPLUG_API int ModPlug_Read(ModPlugFile* file, void* buffer, int size)
{
	int framesize;
	int framecount;
	int frames;
	int rendered;
	int frame;
	int channel;
	int totalrendered;
	signed short* in;
	signed int* mixbuf;
	unsigned char* buf8;
	signed short* buf16;
	signed int* buf32;
	if(!file) return 0;
	framesize = file->settings.mBits/8*file->settings.mChannels;
	framecount = size/framesize;
	buf8 = buffer;
	buf16 = buffer;
	buf32 = buffer;
	totalrendered = 0;
	while(framecount>0){
		frames = framecount;
		if(frames>BUFFER_COUNT){
			frames = BUFFER_COUNT;
		}
		if(file->settings.mChannels==1){
			rendered = (int)openmpt_module_read_mono(file->mod,file->settings.mFrequency,frames,&file->buf[frames*0]);
		}else if(file->settings.mChannels==2){
			rendered = (int)openmpt_module_read_stereo(file->mod,file->settings.mFrequency,frames,&file->buf[frames*0],&file->buf[frames*1]);
		}else if(file->settings.mChannels==4){
			rendered = (int)openmpt_module_read_quad(file->mod,file->settings.mFrequency,frames,&file->buf[frames*0],&file->buf[frames*1],&file->buf[frames*2],&file->buf[frames*3]);
		}else{
			return 0;
		}
		in = file->buf;
		if(file->mixerproc&&file->mixerbuf){
			mixbuf=file->mixerbuf;
			for(frame=0;frame<frames;frame++){
				for(channel=0;channel<file->settings.mChannels;channel++){
					*mixbuf = in[frames*channel+frame]<<(32-16-1-MIXING_ATTENUATION);
					mixbuf++;
				}
			}
			file->mixerproc(file->mixerbuf,file->settings.mChannels*frames,file->settings.mChannels);
			mixbuf=file->mixerbuf;
			for(frame=0;frame<frames;frame++){
				for(channel=0;channel<file->settings.mChannels;channel++){
					in[frames*channel+frame] = *mixbuf>>(32-16-1-MIXING_ATTENUATION);
					mixbuf++;
				}
			}
		}
		if(file->settings.mBits==8){
			for(frame=0;frame<frames;frame++){
				for(channel=0;channel<file->settings.mChannels;channel++){
					*buf8 = in[frames*channel+frame]/256+0x80;
					buf8++;
				}
			}
		}else if(file->settings.mBits==16){
			for(frame=0;frame<frames;frame++){
				for(channel=0;channel<file->settings.mChannels;channel++){
					*buf16 = in[frames*channel+frame];
					buf16++;
				}
			}
		}else if(file->settings.mBits==32){
			for(frame=0;frame<frames;frame++){
				for(channel=0;channel<file->settings.mChannels;channel++){
					*buf32 = in[frames*channel+frame] << (32-16-1-MIXING_ATTENUATION);
					buf32++;
				}
			}
		}else{
			return 0;
		}
		totalrendered += rendered;
		framecount -= frames;
		if(!rendered) break;
	}
	memset(((char*)buffer)+totalrendered*framesize,0,size-totalrendered*framesize);
	return totalrendered*framesize;
}

LIBOPENMPT_MODPLUG_API const char* ModPlug_GetName(ModPlugFile* file)
{
	if(!file) return NULL;
	return file->name;
}

LIBOPENMPT_MODPLUG_API int ModPlug_GetLength(ModPlugFile* file)
{
	if(!file) return 0;
	return (int)(openmpt_module_get_duration_seconds(file->mod)*1000.0);
}

LIBOPENMPT_MODPLUG_API void ModPlug_Seek(ModPlugFile* file, int millisecond)
{
	if(!file) return;
	openmpt_module_set_position_seconds(file->mod,(double)millisecond*0.001);
}

LIBOPENMPT_MODPLUG_API void ModPlug_GetSettings(ModPlug_Settings* settings)
{
	if(!settings) return;
	memcpy(settings,&globalsettings,sizeof(ModPlug_Settings));
}

LIBOPENMPT_MODPLUG_API void ModPlug_SetSettings(const ModPlug_Settings* settings)
{
	if(!settings) return;
	memcpy(&globalsettings,settings,sizeof(ModPlug_Settings));
}

LIBOPENMPT_MODPLUG_API unsigned int ModPlug_GetMasterVolume(ModPlugFile* file)
{
	int32_t val;
	if(!file) return 0;
	val = 0;
	if(!openmpt_module_get_render_param(file->mod,OPENMPT_MODULE_RENDER_MASTERGAIN_MILLIBEL,&val)) return 128;
	return (unsigned int)(128.0*pow(10.0,val*0.0005));
}

LIBOPENMPT_MODPLUG_API void ModPlug_SetMasterVolume(ModPlugFile* file,unsigned int cvol)
{
	if(!file) return;
	openmpt_module_set_render_param(file->mod,OPENMPT_MODULE_RENDER_MASTERGAIN_MILLIBEL,(int32_t)(2000.0*log10(cvol/128.0)));
}

LIBOPENMPT_MODPLUG_API int ModPlug_GetCurrentSpeed(ModPlugFile* file)
{
	if(!file) return 0;
	return openmpt_module_get_current_speed(file->mod);
}

LIBOPENMPT_MODPLUG_API int ModPlug_GetCurrentTempo(ModPlugFile* file)
{
	if(!file) return 0;
	return openmpt_module_get_current_tempo(file->mod);
}

LIBOPENMPT_MODPLUG_API int ModPlug_GetCurrentOrder(ModPlugFile* file)
{
	if(!file) return 0;
	return openmpt_module_get_current_order(file->mod);
}

LIBOPENMPT_MODPLUG_API int ModPlug_GetCurrentPattern(ModPlugFile* file)
{
	if(!file) return 0;
	return openmpt_module_get_current_pattern(file->mod);
}

LIBOPENMPT_MODPLUG_API int ModPlug_GetCurrentRow(ModPlugFile* file)
{
	if(!file) return 0;
	return openmpt_module_get_current_row(file->mod);
}

LIBOPENMPT_MODPLUG_API int ModPlug_GetPlayingChannels(ModPlugFile* file)
{
	if(!file) return 0;
	return openmpt_module_get_current_playing_channels(file->mod);
}

LIBOPENMPT_MODPLUG_API void ModPlug_SeekOrder(ModPlugFile* file,int order)
{
	if(!file) return;
	openmpt_module_set_position_order_row(file->mod,order,0);
}

LIBOPENMPT_MODPLUG_API int ModPlug_GetModuleType(ModPlugFile* file)
{
	const char* type;
	int retval;
	if(!file) return 0;
	type = openmpt_module_get_metadata(file->mod,"type");
	retval = MOD_TYPE_NONE;
	if(!type){
		return retval;
	}
	if(!strcmp(type,"mod")){
		retval = MOD_TYPE_MOD;
	}else if(!strcmp(type,"s3m")){
		retval = MOD_TYPE_S3M;
	}else if(!strcmp(type,"xm")){
		retval = MOD_TYPE_XM;
	}else if(!strcmp(type,"med")){
		retval = MOD_TYPE_MED;
	}else if(!strcmp(type,"mtm")){
		retval = MOD_TYPE_MTM;
	}else if(!strcmp(type,"it")){
		retval = MOD_TYPE_IT;
	}else if(!strcmp(type,"669")){
		retval = MOD_TYPE_669;
	}else if(!strcmp(type,"ult")){
		retval = MOD_TYPE_ULT;
	}else if(!strcmp(type,"stm")){
		retval = MOD_TYPE_STM;
	}else if(!strcmp(type,"far")){
		retval = MOD_TYPE_FAR;
	}else if(!strcmp(type,"s3m")){
		retval = MOD_TYPE_WAV;
	}else if(!strcmp(type,"amf")){
		retval = MOD_TYPE_AMF;
	}else if(!strcmp(type,"ams")){
		retval = MOD_TYPE_AMS;
	}else if(!strcmp(type,"dsm")){
		retval = MOD_TYPE_DSM;
	}else if(!strcmp(type,"mdl")){
		retval = MOD_TYPE_MDL;
	}else if(!strcmp(type,"okt")){
		retval = MOD_TYPE_OKT;
	}else if(!strcmp(type,"mid")){
		retval = MOD_TYPE_MID;
	}else if(!strcmp(type,"dmf")){
		retval = MOD_TYPE_DMF;
	}else if(!strcmp(type,"ptm")){
		retval = MOD_TYPE_PTM;
	}else if(!strcmp(type,"dbm")){
		retval = MOD_TYPE_DBM;
	}else if(!strcmp(type,"mt2")){
		retval = MOD_TYPE_MT2;
	}else if(!strcmp(type,"amf0")){
		retval = MOD_TYPE_AMF0;
	}else if(!strcmp(type,"psm")){
		retval = MOD_TYPE_PSM;
	}else if(!strcmp(type,"j2b")){
		retval = MOD_TYPE_J2B;
	}else if(!strcmp(type,"abc")){
		retval = MOD_TYPE_ABC;
	}else if(!strcmp(type,"pat")){
		retval = MOD_TYPE_PAT;
	}else if(!strcmp(type,"umx")){
		retval = MOD_TYPE_UMX;
	}else{
		retval = MOD_TYPE_IT; /* fallback, most complex type */
	}
	openmpt_free_string(type);
	return retval;
}

LIBOPENMPT_MODPLUG_API char* ModPlug_GetMessage(ModPlugFile* file)
{
	if(!file) return NULL;
	return file->message;
}

LIBOPENMPT_MODPLUG_API unsigned int ModPlug_NumInstruments(ModPlugFile* file)
{
	if(!file) return 0;
	return openmpt_module_get_num_instruments(file->mod);
}

LIBOPENMPT_MODPLUG_API unsigned int ModPlug_NumSamples(ModPlugFile* file)
{
	if(!file) return 0;
	return openmpt_module_get_num_samples(file->mod);
}

LIBOPENMPT_MODPLUG_API unsigned int ModPlug_NumPatterns(ModPlugFile* file)
{
	if(!file) return 0;
	return openmpt_module_get_num_patterns(file->mod);
}

LIBOPENMPT_MODPLUG_API unsigned int ModPlug_NumChannels(ModPlugFile* file)
{
	if(!file) return 0;
	return openmpt_module_get_num_channels(file->mod);
}

LIBOPENMPT_MODPLUG_API unsigned int ModPlug_SampleName(ModPlugFile* file, unsigned int qual, char* buff)
{
	const char* str;
	char buf[32];
	if(!file) return 0;
	str = openmpt_module_get_sample_name(file->mod,qual-1);
	memset(buf,0,32);
	if(str){
		strncpy(buf,str,31);
		openmpt_free_string(str);
	}
	if(buff){
		strncpy(buff,buf,32);
	}
	return (unsigned int)strlen(buf);
}

LIBOPENMPT_MODPLUG_API unsigned int ModPlug_InstrumentName(ModPlugFile* file, unsigned int qual, char* buff)
{
	const char* str;
	char buf[32];
	if(!file) return 0;
	str = openmpt_module_get_instrument_name(file->mod,qual-1);
	memset(buf,0,32);
	if(str){
		strncpy(buf,str,31);
		openmpt_free_string(str);
	}
	if(buff){
		strncpy(buff,buf,32);
	}
	return (unsigned int)strlen(buf);
}

LIBOPENMPT_MODPLUG_API ModPlugNote* ModPlug_GetPattern(ModPlugFile* file, int pattern, unsigned int* numrows)
{
	int c;
	int r;
	int numr;
	int numc;
	ModPlugNote note;
	if(!file) return NULL;
	if(numrows){
		*numrows = openmpt_module_get_pattern_num_rows(file->mod,pattern);
	}
	if(pattern<0||pattern>=openmpt_module_get_num_patterns(file->mod)){
		return NULL;
	}
	if(!file->patterns){
		file->patterns = malloc(sizeof(ModPlugNote*)*openmpt_module_get_pattern_num_rows(file->mod,pattern));
		if(!file->patterns) return NULL;
		memset(file->patterns,0,sizeof(ModPlugNote*)*openmpt_module_get_pattern_num_rows(file->mod,pattern));
	}
	if(!file->patterns[pattern]){
		file->patterns[pattern] = malloc(sizeof(ModPlugNote)*openmpt_module_get_pattern_num_rows(file->mod,pattern)*openmpt_module_get_num_channels(file->mod));
		if(!file->patterns[pattern]) return NULL;
		memset(file->patterns[pattern],0,sizeof(ModPlugNote)*openmpt_module_get_pattern_num_rows(file->mod,pattern)*openmpt_module_get_num_channels(file->mod));
	}
	numr = openmpt_module_get_pattern_num_rows(file->mod,pattern);
	numc = openmpt_module_get_num_channels(file->mod);
	for(r=0;r<numr;r++){
		for(c=0;c<numc;c++){
			memset(&note,0,sizeof(ModPlugNote));
			note.Note = openmpt_module_get_pattern_row_channel_command(file->mod,pattern,r,c,OPENMPT_MODULE_COMMAND_NOTE);
			note.Instrument = openmpt_module_get_pattern_row_channel_command(file->mod,pattern,r,c,OPENMPT_MODULE_COMMAND_INSTRUMENT);
			note.VolumeEffect = openmpt_module_get_pattern_row_channel_command(file->mod,pattern,r,c,OPENMPT_MODULE_COMMAND_VOLUMEEFFECT);
			note.Effect = openmpt_module_get_pattern_row_channel_command(file->mod,pattern,r,c,OPENMPT_MODULE_COMMAND_EFFECT);
			note.Volume = openmpt_module_get_pattern_row_channel_command(file->mod,pattern,r,c,OPENMPT_MODULE_COMMAND_VOLUME);
			note.Parameter = openmpt_module_get_pattern_row_channel_command(file->mod,pattern,r,c,OPENMPT_MODULE_COMMAND_PARAMETER);
			memcpy(&file->patterns[pattern][r*numc+c],&note,sizeof(ModPlugNote));
		}
	}
	return file->patterns[pattern];
}

LIBOPENMPT_MODPLUG_API void ModPlug_InitMixerCallback(ModPlugFile* file,ModPlugMixerProc proc)
{
	if(!file) return;
	if(!file->mixerbuf){
		file->mixerbuf = malloc(BUFFER_COUNT*sizeof(signed int)*4);
	}
	file->mixerproc = proc;
}

LIBOPENMPT_MODPLUG_API void ModPlug_UnloadMixerCallback(ModPlugFile* file)
{
	if(!file) return;
	file->mixerproc = NULL;
	if(file->mixerbuf){
		free(file->mixerbuf);
		file->mixerbuf = NULL;
	}
}

LIBOPENMPT_MODPLUG_API char ModPlug_ExportS3M(ModPlugFile* file, const char* filepath)
{
	(void)file;
	/* not implemented */
	fprintf(stderr,"libopenmpt-modplug: error: ModPlug_ExportS3M(%s) not implemented.\n",filepath);
	return 0;
}

LIBOPENMPT_MODPLUG_API char ModPlug_ExportXM(ModPlugFile* file, const char* filepath)
{
	(void)file;
	/* not implemented */
	fprintf(stderr,"libopenmpt-modplug: error: ModPlug_ExportXM(%s) not implemented.\n",filepath);
	return 0;
}

LIBOPENMPT_MODPLUG_API char ModPlug_ExportMOD(ModPlugFile* file, const char* filepath)
{
	(void)file;
	/* not implemented */
	fprintf(stderr,"libopenmpt-modplug: error: ModPlug_ExportMOD(%s) not implemented.\n",filepath);
	return 0;
}

LIBOPENMPT_MODPLUG_API char ModPlug_ExportIT(ModPlugFile* file, const char* filepath)
{
	(void)file;
	/* not implemented */
	fprintf(stderr,"libopenmpt-modplug: error: ModPlug_ExportIT(%s) not implemented.\n",filepath);
	return 0;
}

#endif /* NO_LIBMODPLUG */
