/*
 * mixer.c -- main SDL_mixer (2.0) module
 *
 * Copyright (c) 2013, 2014 David Demelier <markand@malikania.fr>
 * Copyright (c) 2014 Joseph Wallace <tangent128@gmail.com>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <common/common.h>
#include <common/rwops.h>

#include <SDL_mixer.h>

/* ---------------------------------------------------------
 * Mix_Chunk object
 * --------------------------------------------------------- */

#define MixChunk	MixChunkObject.name

static const CommonObject MixChunkObject;

/*
 * Chunk:volume(volume)
 *
 * Arguments:
 *	volume the volume
 *
 * Returns:
 *	The previous volume
 */
static int
l_chunk_volume(lua_State *L)
{
	Mix_Chunk *c	= commonGetAs(L, 1, MixChunk, Mix_Chunk *);
	int volume	= luaL_checkinteger(L, 2);

	return commonPush(L, "i", Mix_VolumeChunk(c, volume));
}

/*
 * Chunk:playChannel(channel, loops, ticks)
 *
 * Arguments:
 *	channel the channel
 *	loops the number of iterations
 *	ticks the delay in milliseconds
 *
 * Returns:
 *	True on success or nil on failure
 *	The error message
 */
static int
l_chunk_playChannel(lua_State *L)
{
	Mix_Chunk *c	= commonGetAs(L, 1, MixChunk, Mix_Chunk *);
	int channel	= luaL_checkinteger(L, 2);
	int loops	= luaL_optinteger(L, 3, -1);
	int ticks	= luaL_optinteger(L, 4, -1);

	if (Mix_PlayChannelTimed(channel, c, loops, ticks) < 0)
		return commonPushSDLError(L, 1);

	return commonPush(L, "b", 1);
}

/*
 * Chunk:fadeInChannel(channel, loops, delay, ticks)
 *
 * Arguments:
 *	channel the channel
 *	loops the number of loops
 *	delay the delay in milliseconds
 *	ticks (optional) the milliseconds limits, default: -1
 *
 * Returns:
 *	True on success or nil on failure
 *	The error message
 */
static int
l_chunk_fadeInChannel(lua_State *L)
{
	Mix_Chunk *c	= commonGetAs(L, 1, MixChunk, Mix_Chunk *);
	int channel	= luaL_checkinteger(L, 2);
	int loops	= luaL_checkinteger(L, 3);
	int ms		= luaL_checkinteger(L, 4);
	int ticks	= luaL_optinteger(L, 5, -1);

	if (Mix_FadeInChannelTimed(channel, c, loops, ms, ticks) < 0)
		return commonPushSDLError(L, 1);

	return commonPush(L, "b", 1);
}

/*
 * Chunk:__gc()
 */
static int
l_chunk_gc(lua_State *L)
{
	CommonUserdata *udata = commonGetUserdata(L, 1, MixChunk);

	if (udata->mustdelete)
		Mix_FreeChunk(udata->data);

	return 0;
}

static const luaL_Reg MixMethods[] = {
	{ "volume",		l_chunk_volume				},
	{ "playChannel",	l_chunk_playChannel			},
	{ "fadeInChannel",	l_chunk_fadeInChannel			},
	{ NULL,			NULL					}
};

static const luaL_Reg MixMetamethods[] = {
	{ "__gc",		l_chunk_gc				},
	{ NULL,			NULL					}
};

static const CommonObject MixChunkObject = {
	"Chunk",
	MixMethods,
	MixMetamethods
};

/* ---------------------------------------------------------
 * Mix_Music object
 * --------------------------------------------------------- */

#define MixMusic	MixMusicObject.name

static const CommonObject	MixMusicObject;

/*
 * mixer.type
 */
static const CommonEnum MusicType[] = {
	{ "None",		MUS_NONE				},
	{ "WAV",		MUS_WAV					},
	{ "MOD",		MUS_MOD					},
	{ "MID",		MUS_MID					},
	{ "OGG",		MUS_OGG					},
	{ "MP3",		MUS_MP3					},
	{ NULL,			-1					}
};

/*
 * Music:play(loops)
 *
 * Arguments:
 *	loops (optional) the number of loops, default: -1
 *
 * Returns:
 *	True on success or nil on failure
 *	The error message
 */
static int
l_music_play(lua_State *L)
{
	Mix_Music *m = commonGetAs(L, 1, MixMusic, Mix_Music *);
	int loops = luaL_optinteger(L, 2, -1);

	if (Mix_PlayMusic(m, loops) < 0)
		return commonPushSDLError(L, 1);

	return commonPush(L, "b", 1);
}

/*
 * Music:fadeIn(loops, delay, position)
 *
 * Arguments:
 *	loops number of iterations
 *	delay the delay in milliseconds
 *	position (optional) the position
 *
 * Returns:
 *	True on success or nil on failure
 *	The error message
 */
static int
l_music_fadeIn(lua_State *L)
{
	Mix_Music *m	= commonGetAs(L, 1, MixMusic, Mix_Music *);
	int loops	= luaL_checkinteger(L, 2);
	int ms		= luaL_checkinteger(L, 3);
	double pos;

	/* if we have four arguments, we use Mix_FadeInMusicPos instead */
	if (lua_gettop(L) >= 4) {
		pos = luaL_checknumber(L, 4);

		if (Mix_FadeInMusicPos(m, loops, ms, pos) < 0)
			return commonPushSDLError(L, 1);
	} else if (Mix_FadeInMusic(m, loops, ms) < 0)
		return commonPushSDLError(L, 1);

	return commonPush(L, "b", 1);
}

/*
 * Music:volume(volume)
 *
 * Arguments:
 *	volume the volume
 *
 * Returns
 *	The previous volume
 */
static int
l_music_volume(lua_State *L)
{
	return commonPush(L, "i", Mix_VolumeMusic(luaL_checkinteger(L, 1)));
}

/*
 * Music:resume()
 */
static int
l_music_resume(lua_State *L)
{
	Mix_ResumeMusic();

	(void)L;

	return 0;
}

/*
 * Music:rewind()
 */
static int
l_music_rewind(lua_State *L)
{
	Mix_RewindMusic();

	(void)L;

	return 0;
}

/*
 * Music:setPosition(pos)
 *
 * Arguments:
 *	pos the position
 *
 * Returns:
 *	True on success or nil on failure
 *	The error message
 */
static int
l_music_setPosition(lua_State *L)
{
	if (Mix_SetMusicPosition(luaL_checknumber(L, 1)) < 0)
		return commonPushSDLError(L, 1);

	return commonPush(L, "b", 1);
}

/*
 * Music:halt()
 */
static int
l_music_halt(lua_State *L)
{
	Mix_HaltMusic();

	(void)L;

	return 0;
}

/*
 * Music:fadeOut(delay)
 *
 * Arguments:
 *	delay the delay in milliseconds
 *
 * Returns:
 *	True on success or nil on failure
 *	The error message
 */
static int
l_music_fadeOut(lua_State *L)
{
	if (Mix_FadeOutMusic(luaL_checkinteger(L, 1)) < 0)
		return commonPushSDLError(L, 1);

	return commonPush(L, "b", 1);
}

/*
 * Music:type()
 *
 * Returns:
 *	The music type (mixer.type)
 */
static int
l_music_getType(lua_State *L)
{
	return commonPush(L, "i", Mix_GetMusicType(commonGetAs(L, 1, MixMusic, Mix_Music *)));
}

/*
 * Music:playing()
 *
 * Returns:
 *	True if playing
 */
static int
l_music_playing(lua_State *L)
{
	return commonPush(L, "b", Mix_PlayingMusic());
}

/*
 * Music:paused()
 *
 * Returns:
 *	True if paused
 */
static int
l_music_paused(lua_State *L)
{
	return commonPush(L, "b", Mix_PausedMusic());
}

/*
 * Music:fading()
 *
 * Returns:
 *	The fading status
 */
static int
l_music_fading(lua_State *L)
{
	return commonPush(L, "i", Mix_FadingMusic());
}

/*
 * Music:__gc()
 */
static int
l_music_gc(lua_State *L)
{
	CommonUserdata *udata = commonGetUserdata(L, 1, MixMusic);

	if (udata->mustdelete)
		Mix_FreeMusic(udata->data);

	return 0;
}

static const luaL_Reg MusicMethods[] = {
	{ "play",		l_music_play				},
	{ "fadeIn",		l_music_fadeIn				},
	{ "volume",		l_music_volume				},
	{ "resume",		l_music_resume				},
	{ "rewind",		l_music_rewind				},
	{ "setPosition",	l_music_setPosition			},
	{ "halt",		l_music_halt				},
	{ "fadeOut",		l_music_fadeOut				},
	{ "getType",		l_music_getType				},
	{ "playing",		l_music_playing				},
	{ "paused",		l_music_paused				},
	{ "fading",		l_music_fading				},
	{ NULL,			NULL					}
};

static const luaL_Reg MusicMetamethods[] = {
	{ "__gc",		l_music_gc				},
	{ NULL,			NULL					}
};

static const CommonObject MixMusicObject = {
	"Music",
	MusicMethods,
	MusicMetamethods
};

/* ---------------------------------------------------------
 * Private helpers
 * --------------------------------------------------------- */

typedef int (*GroupFunction)(int);

/*
 * Some group functions have exactly the same signature. Use this function which
 * return the integer value.
 */
static int
groupFunction(lua_State *L, GroupFunction func)
{
	return commonPush(L, "i", func(luaL_optinteger(L, 1, -1)));
}

/* ---------------------------------------------------------
 * SDL_mixer functions
 * --------------------------------------------------------- */

/*
 * mixer.flags
 */
static const CommonEnum MixerFlags[] = {
	{ "FLAC",			MIX_INIT_FLAC			},
	{ "MOD",			MIX_INIT_MOD			},
	{ "MP3",			MIX_INIT_MP3			},
	{ "OGG",			MIX_INIT_OGG			},
	{ NULL,				-1				}
};

/*
 * mixer.fading
 */
static const CommonEnum MixerFading[] = {
	{ "None",			MIX_NO_FADING			},
	{ "Out",			MIX_FADING_OUT			},
	{ "In",				MIX_FADING_IN			},
	{ NULL,				-1				}
};

/*
 * mixer.init(flags)
 *
 * Arguments:
 *	flags the flags (mixer.flags)
 *
 * Returns:
 *	The same table or the new one
 *	True on success or nil on failure
 *	The error message
 */
static int
l_mixer_init(lua_State *L)
{
	int flags = commonGetEnum(L, 1);
	int ret;

	ret = Mix_Init(flags);
	commonPushEnum(L, ret, MixerFlags);

	if ((ret & flags) != flags)
		return commonPush(L, "n s", Mix_GetError()) + 1;

	return commonPush(L, "b", 1) + 1;
}

/*
 * mixer.openAudio(frequency, format, channels, chksize)
 *
 * Arguments:
 *	frequency the frequency
 *	format the format (SDL.audioFormat)
 *	channels the number of channels
 *	chksize the chunk size
 *
 * Returns:
 *	True on success or nil on failure
 *	The error message
 */
static int
l_mixer_openAudio(lua_State *L)
{
	int frequency	= luaL_checkinteger(L, 1);
	int format	= luaL_checkinteger(L, 2);
	int channels	= luaL_checkinteger(L, 3);
	int chksize	= luaL_checkinteger(L, 4);

	if (Mix_OpenAudio(frequency, format, channels, chksize) < 0)
		return commonPushSDLError(L, 1);

	return commonPush(L, "b", 1);
}

/*
 * mixer.getNumChunkDecoders()
 *
 * Returns:
 *	The number of chunk decoders
 */
static int
l_mixer_getNumChunkDecoders(lua_State *L)
{
	return commonPush(L, "i", Mix_GetNumChunkDecoders());
}

/*
 * mixer.getChunkDecoder(index)
 *
 * Arguments:
 *	index (optional) the index, default: 0
 *
 * Returns:
 *	The decoder name
 */
static int
l_mixer_getChunkDecoder(lua_State *L)
{
	return commonPush(L, "s", Mix_GetChunkDecoder(luaL_optinteger(L, 1, 0)));
}

/*
 * mixer.loadWAV(path)
 *
 * Arguments:
 *	path the path
 *
 * Returns:
 *	The chunk object or nil on failure
 *	The error message
 */
static int
l_mixer_loadWAV(lua_State *L)
{
	const char *path = luaL_checkstring(L, 1);
	Mix_Chunk *c;

	if ((c = Mix_LoadWAV(path)) == NULL)
		return commonPushSDLError(L, 1);

	return commonPush(L, "p", MixChunk, c);
}

/*
 * mixer.loadWAV_RW(rw)
 *
 * Arguments:
 *	rw the RWops
 *
 * Returns:
 *	The chunk object or nil on failure
 *	The error message
 */
static int
l_mixer_loadWAV_RW(lua_State *L)
{
	SDL_RWops *ops = commonGetAs(L, 1, RWOpsName, SDL_RWops *);
	Mix_Chunk *c;

	if ((c = Mix_LoadWAV_RW(ops, 0)) == NULL)
		return commonPushSDLError(L, 1);

	return commonPush(L, "p", MixChunk, c);
}

/*
 * mixer.allocateChannels(num)
 *
 * Arguments:
 *	num the number to allocate
 *
 * Returns:
 *	The number allocated
 */
static int
l_mixer_allocateChannels(lua_State *L)
{
	int num = luaL_checkinteger(L, 1);

	return commonPush(L, "i", Mix_AllocateChannels(num));
}

/*
 * mixer.volume(channel, volume)
 *
 * Arguments:
 *	channel the channel
 *	volume the volume
 *
 * Returns:
 *	The current volume
 */
static int
l_mixer_volume(lua_State *L)
{
	int channel	= luaL_checkinteger(L, 1);
	int volume	= luaL_checkinteger(L, 2);

	return commonPush(L, "i", Mix_Volume(channel, volume));
}

/*
 * mixer.pause(channel)
 *
 * Arguments:
 *	channel the channel
 */
static int
l_mixer_pause(lua_State *L)
{
	Mix_Pause(luaL_checkinteger(L, 1));

	return 0;
}

/*
 * mixer.resume(channel)
 *
 * Arguments:
 *	channel the channel
 */
static int
l_mixer_resume(lua_State *L)
{
	Mix_Resume(luaL_checkinteger(L, 1));

	return 0;
}

/*
 * mixer.haltChannel(channel)
 *
 * Arguments:
 *	channel (optional) the channel, default: -1
 *
 * Returns:
 *	The number of channels affected
 */
static int
l_mixer_haltChannel(lua_State *L)
{
	return commonPush(L, "i", Mix_HaltChannel(luaL_checkinteger(L, 1)));
}

/*
 * mixer.expireChannel(channel, delay)
 *
 * Arguments:
 *	channel the channel
 *	delay the delay in milliseconds
 *
 * Returns:
 *	The number of channels affected
 */
static int
l_mixer_expireChannel(lua_State *L)
{
	int channel	= luaL_checkinteger(L, 1);
	int ticks	= luaL_checkinteger(L, 2);

	return commonPush(L, "i", Mix_ExpireChannel(channel, ticks));
}

/*
 * mixer.fadeOutChannels(channel, ms)
 *
 * Arguments:
 *	channel the channel
 *	ms the number in milliseconds
 *
 * Returns:
 *	The number of channels affected
 */
static int
l_mixer_fadeOutChannel(lua_State *L)
{
	int channel	= luaL_checkinteger(L, 1);
	int ms		= luaL_checkinteger(L, 2);

	return commonPush(L, "i", Mix_FadeOutChannel(channel, ms));
}

/*
 * mixer.playing(channel)
 *
 * Arguments:
 *	channel (optional) the channel, default: -1
 *
 * Returns:
 *	The number of channels affected
 */
static int
l_mixer_playing(lua_State *L)
{
	return commonPush(L, "i", Mix_Playing(luaL_optinteger(L, 1, -1)));
}

/*
 * mixer.paused(channel)
 *
 * Arguments:
 *	channel (optional) the channel, default: -1
 *
 * Returns:
 *	The number of channels affected
 */
static int
l_mixer_paused(lua_State *L)
{
	return commonPush(L, "i", Mix_Paused(luaL_optinteger(L, 1, -1)));
}

/*
 * mixer.fadingChannel(channel)
 *
 * Arguments:
 *	channel the channel
 *
 * Returns:
 *	The status
 */
static int
l_mixer_fadingChannel(lua_State *L)
{
	int channel = luaL_checkinteger(L, 1);

	if (channel < 0)
		return luaL_error(L, "invalid channel value: %d", channel);

	return commonPush(L, "i", Mix_FadingChannel(channel));
}

/*
 * mixer.reserveChannels(num)
 *
 * Arguments:
 *	num the number of channels to allocate
 *
 * Returns:
 *	The number allocated
 */
static int
l_mixer_reserveChannels(lua_State *L)
{
	return commonPush(L, "i", Mix_ReserveChannels(luaL_checkinteger(L, 1)));
}

/*
 * mixer.groupChannel(channel, tag)
 *
 * Arguments:
 *	channel the channel number
 *	tag (optional) the tag number, default: -1
 *
 * Returns:
 *	True on success
 */
static int
l_mixer_groupChannel(lua_State *L)
{
	int which	= luaL_checkinteger(L, 1);
	int tag		= luaL_optinteger(L, 2, -1);

	return commonPush(L, "b", Mix_GroupChannel(which, tag));
}

/*
 * mixer.groupChannels(from, to, tag)
 *
 * Arguments:
 *	from the minimum
 *	to the maximum
 *	tag the tag number
 *
 * Returns:
 *	The number affected
 */
static int
l_mixer_groupChannels(lua_State *L)
{
	int from	= luaL_checkinteger(L, 1);
	int to		= luaL_checkinteger(L, 2);
	int tag		= luaL_optinteger(L, 3, -1);

	return commonPush(L, "i", Mix_GroupChannels(from, to, tag));
}

/*
 * mixer.groupCount(tag)
 *
 * Arguments:
 *	tag (optional) the tag, default: -1
 *
 * Returns:
 *	The number of channels affected
 */
static int
l_mixer_groupCount(lua_State *L)
{
	return groupFunction(L, Mix_GroupCount);
}

/*
 * mixer.groupAvailable(tag)
 *
 * Arguments:
 *	tag (optional) the tag, default: -1
 *
 * Returns:
 *	The number of channels affected
 */
static int
l_mixer_groupAvailable(lua_State *L)
{
	return groupFunction(L, Mix_GroupAvailable);
}

/*
 * mixer.groupOldest(tag)
 *
 * Arguments:
 *	tag (optional) the tag, default: -1
 *
 * Returns:
 *	The number of channels affected
 */
static int
l_mixer_groupOldest(lua_State *L)
{
	return groupFunction(L, Mix_GroupOldest);
}

/*
 * mixer.groupNewer(tag)
 *
 * Arguments:
 *	tag (optional) the tag, default: -1
 *
 * Returns:
 *	The number of channels affected
 */
static int
l_mixer_groupNewer(lua_State *L)
{
	return groupFunction(L, Mix_GroupNewer);
}

/*
 * mixer.fadeOutGroup(tag, ms)
 *
 * Arguments:
 *	tag the group number
 *	ms the number of milliseconds
 *
 * Returns:
 *	The number of channels affected
 */
static int
l_mixer_fadeOutGroup(lua_State *L)
{
	int tag	= luaL_checkinteger(L, 1);
	int ms	= luaL_checkinteger(L, 2);

	return commonPush(L, "i", Mix_FadeOutGroup(tag, ms));
}

/*
 * mixer.haltGroup(tag)
 *
 * Arguments:
 *	tag (optional) the tag, default: -1
 *
 * Returns:
 *	The number of channels affected
 */
static int
l_mixer_haltGroup(lua_State *L)
{
	return groupFunction(L, Mix_HaltGroup);
}

/*
 * mixer.getNumMusicDecoders()
 *
 * Returns:
 *	The number of music decoders
 */
static int
l_mixer_getNumMusicDecoders(lua_State *L)
{
	return commonPush(L, "i", Mix_GetNumMusicDecoders());
}

/*
 * mixer.getMusicDecoder(index)
 *
 * Arguments:
 *	index the decoder index
 *
 * Returns:
 *	The music decoder name
 */
static int
l_mixer_getMusicDecoder(lua_State *L)
{
	return commonPush(L, "s", Mix_GetMusicDecoder(luaL_checkinteger(L, 1)));
}

/*
 * mixer.loadMUS(path)
 *
 * Arguments:
 *	path the path to the sound
 *
 * Returns:
 *	The music object or nil on failure
 *	The error message
 */
static int
l_mixer_loadMUS(lua_State *L)
{
	const char *path = luaL_checkstring(L, 1);
	Mix_Music *music;

	if ((music = Mix_LoadMUS(path)) == NULL)
		return commonPushSDLError(L, 1);

	(void)L;

	return commonPush(L, "p", MixMusic, music);
}

/*
 * mixer.closeAudio()
 */
static int
l_mixer_closeAudio(lua_State *L)
{
	Mix_CloseAudio();

	(void)L;

	return 0;
}

/*
 * mixer.quit()
 */
static int
l_mixer_quit(lua_State *L)
{
	Mix_Quit();

	(void)L;

	return 0;
}

static const luaL_Reg MixerFunctions[] = {
	{ "init",			l_mixer_init			},
	{ "openAudio",			l_mixer_openAudio		},
	{ "getNumChunkDecoders",	l_mixer_getNumChunkDecoders	},
	{ "getChunkDecoder",		l_mixer_getChunkDecoder		},
	{ "loadWAV",			l_mixer_loadWAV			},
	{ "loadWAV_RW",			l_mixer_loadWAV_RW		},
	{ "allocateChannels",		l_mixer_allocateChannels	},
	{ "volume",			l_mixer_volume			},
	{ "pause",			l_mixer_pause			},
	{ "resume",			l_mixer_resume			},
	{ "haltChannel",		l_mixer_haltChannel		},
	{ "expireChannel",		l_mixer_expireChannel		},
	{ "fadeOutChannel",		l_mixer_fadeOutChannel		},
	{ "playing",			l_mixer_playing			},
	{ "paused",			l_mixer_paused			},
	{ "fadingChannel",		l_mixer_fadingChannel		},
	{ "reserveChannels",		l_mixer_reserveChannels		},
	{ "groupChannel",		l_mixer_groupChannel		},
	{ "groupChannels",		l_mixer_groupChannels		},
	{ "groupCount",			l_mixer_groupCount		},
	{ "groupAvailable",		l_mixer_groupAvailable		},
	{ "groupOldest",		l_mixer_groupOldest		},
	{ "groupNewer",			l_mixer_groupNewer		},
	{ "fadeOutGroup",		l_mixer_fadeOutGroup		},
	{ "haltGroup",			l_mixer_haltGroup		},
	{ "getNumMusicDecoders",	l_mixer_getNumMusicDecoders	},
	{ "getMusicDecoder",		l_mixer_getMusicDecoder		},
	{ "loadMUS",			l_mixer_loadMUS			},
	{ "closeAudio",			l_mixer_closeAudio		},
	{ "quit",			l_mixer_quit			},
	{ NULL,				NULL				}
};

int EXPORT
luaopen_SDL_mixer(lua_State *L)
{
	commonNewLibrary(L, MixerFunctions);

	commonBindEnum(L, -1, "flags", MixerFlags);
	commonBindEnum(L, -1, "fading", MixerFading);
	commonBindEnum(L, -1, "type", MusicType);

	/* Mix_Chunk object */
	commonBindObject(L, &MixChunkObject);

	/* Mix_Music object */
	commonBindObject(L, &MixMusicObject);

	return 1;
}
